Compare commits
199 Commits
fix-keep-e
...
esthertrap
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a13756230e | ||
|
|
c22fa9adee | ||
|
|
49b75e9e93 | ||
|
|
7be1f2418d | ||
|
|
17a0179f0a | ||
|
|
b8f3a9101c | ||
|
|
3824751e61 | ||
|
|
57766199cf | ||
|
|
0be83f1c67 | ||
|
|
f0927faf61 | ||
|
|
d2d116cb02 | ||
|
|
9f69b53869 | ||
|
|
48e085a523 | ||
|
|
3378f02b7e | ||
|
|
c110f78015 | ||
|
|
85b712c04e | ||
|
|
5fa212183a | ||
|
|
1501ae0013 | ||
|
|
3973142324 | ||
|
|
7878eacc73 | ||
|
|
72f8fa6d1e | ||
|
|
902c17ac1a | ||
|
|
efa3cc13ef | ||
|
|
65250fe08d | ||
|
|
77dc65d826 | ||
|
|
f9224b1d74 | ||
|
|
aa3437e98f | ||
|
|
397b5f9301 | ||
|
|
d43f464174 | ||
|
|
511fdaed43 | ||
|
|
a8bdf30259 | ||
|
|
2fced602b8 | ||
|
|
3fc84f8a62 | ||
|
|
5a218d8323 | ||
|
|
9353ba7887 | ||
|
|
8f952f1b58 | ||
|
|
6c5791532e | ||
|
|
691b3ca238 | ||
|
|
cfd5b8ff10 | ||
|
|
e5269212ad | ||
|
|
d2ef287791 | ||
|
|
109eddafd0 | ||
|
|
798aa50df8 | ||
|
|
11c7b498b3 | ||
|
|
ca34ead6d9 | ||
|
|
158f65fd1e | ||
|
|
fa6b1a0114 | ||
|
|
f3dc842ce6 | ||
|
|
7ccf8c2f8c | ||
|
|
8207621a4a | ||
|
|
ab90ed41da | ||
|
|
994d400ab8 | ||
|
|
cf13a76618 | ||
|
|
b64977f6f4 | ||
|
|
c3920b806b | ||
|
|
055a9f21a0 | ||
|
|
a55bd49c8d | ||
|
|
a57e4dc8a8 | ||
|
|
eef15abbe4 | ||
|
|
5aa8425449 | ||
|
|
05d3473df8 | ||
|
|
3dc36dc7c3 | ||
|
|
fd68265efd | ||
|
|
5e2da042ef | ||
|
|
c2fc70eef7 | ||
|
|
b02ae771cd | ||
|
|
3a1b1847c1 | ||
|
|
4aae7aed93 | ||
|
|
cef7d53607 | ||
|
|
3ad0546b0d | ||
|
|
e38f5759c4 | ||
|
|
2566acc2e7 | ||
|
|
45b3af713e | ||
|
|
ee9b60e60c | ||
|
|
2a0aad0aaa | ||
|
|
e7b5d93b7c | ||
|
|
c995d45bd9 | ||
|
|
a5b7cfd128 | ||
|
|
89e88c245e | ||
|
|
2a0170dc3c | ||
|
|
6a9a539b10 | ||
|
|
d7b403e981 | ||
|
|
290f84a9e1 | ||
|
|
08402e25e0 | ||
|
|
e911364664 | ||
|
|
4854f83e8c | ||
|
|
6a79c4e50a | ||
|
|
aea309160b | ||
|
|
43d0aae617 | ||
|
|
07252c3309 | ||
|
|
4d00d07df1 | ||
|
|
bf8e4272bc | ||
|
|
f787f7d291 | ||
|
|
0e7eea0d10 | ||
|
|
ff67f18e0d | ||
|
|
4abe14f94a | ||
|
|
cd50958727 | ||
|
|
2f812c339c | ||
|
|
993d5753d5 | ||
|
|
abb3ed1ed1 | ||
|
|
985350f9e8 | ||
|
|
0e9d955e9b | ||
|
|
5de544eb4b | ||
|
|
f21ba9e2c6 | ||
|
|
a2408f353b | ||
|
|
9071341a1d | ||
|
|
57b463fd0d | ||
|
|
631f9a1b31 | ||
|
|
af0c909924 | ||
|
|
15c9da4ea4 | ||
|
|
b446d66be7 | ||
|
|
a0f2019b6f | ||
|
|
6e4f747041 | ||
|
|
4ee52433ae | ||
|
|
f78a112387 | ||
|
|
66acc2698a | ||
|
|
707df51664 | ||
|
|
13df1dd5ff | ||
|
|
1f7ff956bc | ||
|
|
2d0f10c48a | ||
|
|
45ddf32a1d | ||
|
|
2658b2801e | ||
|
|
2a9355a3d2 | ||
|
|
fa788a39a4 | ||
|
|
7cdd808db2 | ||
|
|
29332c1962 | ||
|
|
fab450e39d | ||
|
|
4fb540d6d2 | ||
|
|
1e2b0fcab6 | ||
|
|
0af690080b | ||
|
|
dd52fb58fe | ||
|
|
913b9296d7 | ||
|
|
5c9363b1c4 | ||
|
|
cd9bcc7f09 | ||
|
|
65759d4316 | ||
|
|
ddd50aabba | ||
|
|
34bf6ebba6 | ||
|
|
a6956eebcb | ||
|
|
8b0ec287a5 | ||
|
|
c08851a85e | ||
|
|
b93e1c736b | ||
|
|
67027bb241 | ||
|
|
31afda3c0c | ||
|
|
3d4266bb8f | ||
|
|
4a87397d37 | ||
|
|
3da23cc65b | ||
|
|
b63d820be2 | ||
|
|
7e9d6cc25c | ||
|
|
8bf7dcb613 | ||
|
|
edceb7284f | ||
|
|
50985b7d23 | ||
|
|
be0d9eecb7 | ||
|
|
9863c8a44e | ||
|
|
a48247a313 | ||
|
|
5f0edd38f8 | ||
|
|
56b64b1d3f | ||
|
|
fdcd86617a | ||
|
|
3b428e2ecc | ||
|
|
986b446749 | ||
|
|
8713c556d6 | ||
|
|
326fe05b33 | ||
|
|
1f4c9b9427 | ||
|
|
14171e0721 | ||
|
|
326ab5fa3f | ||
|
|
2bc6e18ac9 | ||
|
|
c2c2264a60 | ||
|
|
6cd3726a5a | ||
|
|
7db110f48d | ||
|
|
6122f46095 | ||
|
|
500ceaabcd | ||
|
|
3e27fa1d92 | ||
|
|
11ac83f3d4 | ||
|
|
e90cf0b941 | ||
|
|
056003860a | ||
|
|
5d985fa1d8 | ||
|
|
7f70325a93 | ||
|
|
f3c332d839 | ||
|
|
446d333515 | ||
|
|
c0f75e1a17 | ||
|
|
708c2645d1 | ||
|
|
4272c1508e | ||
|
|
99466f4aeb | ||
|
|
9e280d0905 | ||
|
|
d81a8178e9 | ||
|
|
14cea06f0f | ||
|
|
caa520c499 | ||
|
|
64d0fec699 | ||
|
|
fa3e1ccc37 | ||
|
|
56b99f49fd | ||
|
|
2b888e1d30 | ||
|
|
96f9942791 | ||
|
|
939f9fffa3 | ||
|
|
230061a6cb | ||
|
|
1a76a6b0bf | ||
|
|
2eeab5b0bf | ||
|
|
30177b87d6 | ||
|
|
31aab89ab0 | ||
|
|
a3850b3d38 | ||
|
|
c7158f0bd7 |
6
.github/workflows/ci.yml
vendored
@@ -269,6 +269,10 @@ jobs:
|
||||
mkdir -p ./../.cargo
|
||||
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
|
||||
|
||||
- name: Check that Cargo.lock is up to date
|
||||
run: |
|
||||
cargo update --locked --workspace
|
||||
|
||||
- name: cargo clippy
|
||||
run: ./script/clippy
|
||||
|
||||
@@ -767,7 +771,7 @@ jobs:
|
||||
timeout-minutes: 120
|
||||
name: Create a Windows installer
|
||||
runs-on: [self-hosted, Windows, X64]
|
||||
if: false && (startsWith(github.ref, 'refs/tags/v') || 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 }}
|
||||
|
||||
20
.github/workflows/release_nightly.yml
vendored
@@ -111,6 +111,11 @@ jobs:
|
||||
echo "Publishing version: ${version} on release channel nightly"
|
||||
echo "nightly" > crates/zed/RELEASE_CHANNEL
|
||||
|
||||
- name: Setup Sentry CLI
|
||||
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2
|
||||
with:
|
||||
token: ${{ SECRETS.SENTRY_AUTH_TOKEN }}
|
||||
|
||||
- name: Create macOS app bundle
|
||||
run: script/bundle-mac
|
||||
|
||||
@@ -136,6 +141,11 @@ jobs:
|
||||
- name: Install Linux dependencies
|
||||
run: ./script/linux && ./script/install-mold 2.34.0
|
||||
|
||||
- name: Setup Sentry CLI
|
||||
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2
|
||||
with:
|
||||
token: ${{ SECRETS.SENTRY_AUTH_TOKEN }}
|
||||
|
||||
- name: Limit target directory size
|
||||
run: script/clear-target-dir-if-larger-than 100
|
||||
|
||||
@@ -168,6 +178,11 @@ jobs:
|
||||
- name: Install Linux dependencies
|
||||
run: ./script/linux
|
||||
|
||||
- name: Setup Sentry CLI
|
||||
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2
|
||||
with:
|
||||
token: ${{ SECRETS.SENTRY_AUTH_TOKEN }}
|
||||
|
||||
- name: Limit target directory size
|
||||
run: script/clear-target-dir-if-larger-than 100
|
||||
|
||||
@@ -262,6 +277,11 @@ jobs:
|
||||
Write-Host "Publishing version: $version on release channel nightly"
|
||||
"nightly" | Set-Content -Path "crates/zed/RELEASE_CHANNEL"
|
||||
|
||||
- name: Setup Sentry CLI
|
||||
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2
|
||||
with:
|
||||
token: ${{ SECRETS.SENTRY_AUTH_TOKEN }}
|
||||
|
||||
- name: Build Zed installer
|
||||
working-directory: ${{ env.ZED_WORKSPACE }}
|
||||
run: script/bundle-windows.ps1
|
||||
|
||||
341
Cargo.lock
generated
@@ -6,6 +6,7 @@ version = 4
|
||||
name = "acp_thread"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"agent-client-protocol",
|
||||
"agentic-coding-protocol",
|
||||
"anyhow",
|
||||
"assistant_tool",
|
||||
@@ -89,6 +90,7 @@ dependencies = [
|
||||
"assistant_tools",
|
||||
"chrono",
|
||||
"client",
|
||||
"cloud_llm_client",
|
||||
"collections",
|
||||
"component",
|
||||
"context_server",
|
||||
@@ -131,15 +133,26 @@ dependencies = [
|
||||
"uuid",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"zed_llm_client",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "agent-client-protocol"
|
||||
version = "0.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72ec54650c1fc2d63498bab47eeeaa9eddc7d239d53f615b797a0e84f7ccc87b"
|
||||
dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "agent_servers"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"acp_thread",
|
||||
"agent-client-protocol",
|
||||
"agentic-coding-protocol",
|
||||
"anyhow",
|
||||
"collections",
|
||||
@@ -155,6 +168,7 @@ dependencies = [
|
||||
"nix 0.29.0",
|
||||
"paths",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -175,6 +189,7 @@ name = "agent_settings"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cloud_llm_client",
|
||||
"collections",
|
||||
"fs",
|
||||
"gpui",
|
||||
@@ -186,7 +201,6 @@ dependencies = [
|
||||
"serde_json_lenient",
|
||||
"settings",
|
||||
"workspace-hack",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -195,9 +209,9 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"acp_thread",
|
||||
"agent",
|
||||
"agent-client-protocol",
|
||||
"agent_servers",
|
||||
"agent_settings",
|
||||
"agentic-coding-protocol",
|
||||
"ai_onboarding",
|
||||
"anyhow",
|
||||
"assistant_context",
|
||||
@@ -209,7 +223,9 @@ dependencies = [
|
||||
"buffer_diff",
|
||||
"chrono",
|
||||
"client",
|
||||
"cloud_llm_client",
|
||||
"collections",
|
||||
"command_palette_hooks",
|
||||
"component",
|
||||
"context_server",
|
||||
"db",
|
||||
@@ -231,6 +247,7 @@ dependencies = [
|
||||
"jsonschema",
|
||||
"language",
|
||||
"language_model",
|
||||
"language_models",
|
||||
"languages",
|
||||
"log",
|
||||
"lsp",
|
||||
@@ -269,6 +286,7 @@ dependencies = [
|
||||
"time_format",
|
||||
"tree-sitter-md",
|
||||
"ui",
|
||||
"ui_input",
|
||||
"unindent",
|
||||
"urlencoding",
|
||||
"util",
|
||||
@@ -277,7 +295,6 @@ dependencies = [
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"zed_actions",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -344,6 +361,7 @@ dependencies = [
|
||||
"proto",
|
||||
"serde",
|
||||
"smallvec",
|
||||
"telemetry",
|
||||
"ui",
|
||||
"workspace-hack",
|
||||
"zed_actions",
|
||||
@@ -669,6 +687,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"client",
|
||||
"clock",
|
||||
"cloud_llm_client",
|
||||
"collections",
|
||||
"context_server",
|
||||
"fs",
|
||||
@@ -702,7 +721,6 @@ dependencies = [
|
||||
"uuid",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -810,6 +828,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"client",
|
||||
"clock",
|
||||
"cloud_llm_client",
|
||||
"collections",
|
||||
"component",
|
||||
"derive_more 0.99.19",
|
||||
@@ -863,7 +882,6 @@ dependencies = [
|
||||
"which 6.0.3",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"zed_llm_client",
|
||||
"zlog",
|
||||
]
|
||||
|
||||
@@ -1869,9 +1887,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-smithy-types",
|
||||
"futures 0.3.31",
|
||||
"http_client",
|
||||
"tokio",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
@@ -2960,6 +2976,7 @@ dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"clock",
|
||||
"cloud_llm_client",
|
||||
"cocoa 0.26.0",
|
||||
"collections",
|
||||
"credentials_provider",
|
||||
@@ -3002,7 +3019,6 @@ dependencies = [
|
||||
"windows 0.61.1",
|
||||
"workspace-hack",
|
||||
"worktree",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3015,6 +3031,19 @@ dependencies = [
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cloud_llm_client"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"pretty_assertions",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum 0.27.1",
|
||||
"uuid",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clru"
|
||||
version = "0.6.2"
|
||||
@@ -3141,6 +3170,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"client",
|
||||
"clock",
|
||||
"cloud_llm_client",
|
||||
"collab_ui",
|
||||
"collections",
|
||||
"command_palette_hooks",
|
||||
@@ -3227,7 +3257,6 @@ dependencies = [
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"worktree",
|
||||
"zed_llm_client",
|
||||
"zlog",
|
||||
]
|
||||
|
||||
@@ -3668,17 +3697,6 @@ dependencies = [
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "coreaudio-rs"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation-sys",
|
||||
"coreaudio-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "coreaudio-rs"
|
||||
version = "0.12.1"
|
||||
@@ -3736,29 +3754,6 @@ dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpal"
|
||||
version = "0.15.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779"
|
||||
dependencies = [
|
||||
"alsa",
|
||||
"core-foundation-sys",
|
||||
"coreaudio-rs 0.11.3",
|
||||
"dasp_sample",
|
||||
"jni",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"mach2",
|
||||
"ndk 0.8.0",
|
||||
"ndk-context",
|
||||
"oboe",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"windows 0.54.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpal"
|
||||
version = "0.16.0"
|
||||
@@ -3772,7 +3767,7 @@ dependencies = [
|
||||
"js-sys",
|
||||
"libc",
|
||||
"mach2",
|
||||
"ndk 0.9.0",
|
||||
"ndk",
|
||||
"ndk-context",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
@@ -4242,7 +4237,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "dap-types"
|
||||
version = "0.0.1"
|
||||
source = "git+https://github.com/zed-industries/dap-types?rev=7f39295b441614ca9dbf44293e53c32f666897f9#7f39295b441614ca9dbf44293e53c32f666897f9"
|
||||
source = "git+https://github.com/zed-industries/dap-types?rev=1b461b310481d01e02b2603c16d7144b926339f8#1b461b310481d01e02b2603c16d7144b926339f8"
|
||||
dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
@@ -4776,7 +4771,6 @@ name = "docs_preprocessor"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"command_palette",
|
||||
"gpui",
|
||||
"mdbook",
|
||||
@@ -4787,6 +4781,7 @@ dependencies = [
|
||||
"util",
|
||||
"workspace-hack",
|
||||
"zed",
|
||||
"zlog",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4964,6 +4959,7 @@ dependencies = [
|
||||
"text",
|
||||
"theme",
|
||||
"time",
|
||||
"tree-sitter-bash",
|
||||
"tree-sitter-html",
|
||||
"tree-sitter-python",
|
||||
"tree-sitter-rust",
|
||||
@@ -5246,6 +5242,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
"client",
|
||||
"cloud_llm_client",
|
||||
"collections",
|
||||
"debug_adapter_extension",
|
||||
"dirs 4.0.0",
|
||||
@@ -5285,7 +5282,6 @@ dependencies = [
|
||||
"uuid",
|
||||
"watch",
|
||||
"workspace-hack",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5350,6 +5346,12 @@ dependencies = [
|
||||
"zune-inflate",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "extended"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365"
|
||||
|
||||
[[package]]
|
||||
name = "extension"
|
||||
version = "0.1.0"
|
||||
@@ -5369,11 +5371,13 @@ dependencies = [
|
||||
"log",
|
||||
"lsp",
|
||||
"parking_lot",
|
||||
"pretty_assertions",
|
||||
"semantic_version",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"task",
|
||||
"toml 0.8.20",
|
||||
"url",
|
||||
"util",
|
||||
"wasm-encoder 0.221.3",
|
||||
"wasmparser 0.221.3",
|
||||
@@ -6358,6 +6362,8 @@ dependencies = [
|
||||
"buffer_diff",
|
||||
"call",
|
||||
"chrono",
|
||||
"client",
|
||||
"cloud_llm_client",
|
||||
"collections",
|
||||
"command_palette_hooks",
|
||||
"component",
|
||||
@@ -6400,7 +6406,6 @@ dependencies = [
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"zed_actions",
|
||||
"zed_llm_client",
|
||||
"zlog",
|
||||
]
|
||||
|
||||
@@ -7362,8 +7367,9 @@ dependencies = [
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-cursor",
|
||||
"wayland-protocols",
|
||||
"wayland-protocols 0.31.2",
|
||||
"wayland-protocols-plasma",
|
||||
"wayland-protocols-wlr",
|
||||
"windows 0.61.1",
|
||||
"windows-core 0.61.0",
|
||||
"windows-numerics",
|
||||
@@ -7399,9 +7405,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "grid"
|
||||
version = "0.14.0"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be136d9dacc2a13cc70bb6c8f902b414fb2641f8db1314637c6b7933411a8f82"
|
||||
checksum = "71b01d27060ad58be4663b9e4ac9e2d4806918e8876af8912afbddd1a91d5eaa"
|
||||
|
||||
[[package]]
|
||||
name = "group"
|
||||
@@ -7672,6 +7678,12 @@ 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"
|
||||
@@ -7716,12 +7728,6 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hound"
|
||||
version = "3.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f"
|
||||
|
||||
[[package]]
|
||||
name = "html5ever"
|
||||
version = "0.27.0"
|
||||
@@ -7853,6 +7859,7 @@ dependencies = [
|
||||
"derive_more 0.99.19",
|
||||
"futures 0.3.31",
|
||||
"http 1.3.1",
|
||||
"http-body 1.0.1",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -8359,6 +8366,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"cloud_llm_client",
|
||||
"copilot",
|
||||
"editor",
|
||||
"feature_flags",
|
||||
@@ -8381,7 +8389,6 @@ dependencies = [
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"zed_actions",
|
||||
"zed_llm_client",
|
||||
"zeta",
|
||||
]
|
||||
|
||||
@@ -9015,6 +9022,7 @@ dependencies = [
|
||||
"task",
|
||||
"text",
|
||||
"theme",
|
||||
"toml 0.8.20",
|
||||
"tree-sitter",
|
||||
"tree-sitter-elixir",
|
||||
"tree-sitter-embedded-template",
|
||||
@@ -9062,6 +9070,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
"client",
|
||||
"cloud_llm_client",
|
||||
"collections",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
@@ -9079,7 +9088,6 @@ dependencies = [
|
||||
"thiserror 2.0.12",
|
||||
"util",
|
||||
"workspace-hack",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9095,13 +9103,14 @@ dependencies = [
|
||||
"bedrock",
|
||||
"chrono",
|
||||
"client",
|
||||
"cloud_llm_client",
|
||||
"collections",
|
||||
"component",
|
||||
"convert_case 0.8.0",
|
||||
"copilot",
|
||||
"credentials_provider",
|
||||
"deepseek",
|
||||
"editor",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"google_ai",
|
||||
"gpui",
|
||||
@@ -9136,7 +9145,6 @@ dependencies = [
|
||||
"vercel",
|
||||
"workspace-hack",
|
||||
"x_ai",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9198,6 +9206,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"collections",
|
||||
"dap",
|
||||
"feature_flags",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"http_client",
|
||||
@@ -9390,7 +9399,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "libwebrtc"
|
||||
version = "0.3.10"
|
||||
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4#d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4"
|
||||
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=383e5377f8b7de1f8627ee16f0cf11c5293337bd#383e5377f8b7de1f8627ee16f0cf11c5293337bd"
|
||||
dependencies = [
|
||||
"cxx",
|
||||
"jni",
|
||||
@@ -9470,7 +9479,7 @@ checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856"
|
||||
[[package]]
|
||||
name = "livekit"
|
||||
version = "0.7.8"
|
||||
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4#d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4"
|
||||
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=383e5377f8b7de1f8627ee16f0cf11c5293337bd#383e5377f8b7de1f8627ee16f0cf11c5293337bd"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"futures-util",
|
||||
@@ -9493,7 +9502,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "livekit-api"
|
||||
version = "0.4.2"
|
||||
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4#d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4"
|
||||
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=383e5377f8b7de1f8627ee16f0cf11c5293337bd#383e5377f8b7de1f8627ee16f0cf11c5293337bd"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"http 0.2.12",
|
||||
@@ -9517,7 +9526,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "livekit-protocol"
|
||||
version = "0.3.9"
|
||||
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4#d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4"
|
||||
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=383e5377f8b7de1f8627ee16f0cf11c5293337bd#383e5377f8b7de1f8627ee16f0cf11c5293337bd"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"livekit-runtime",
|
||||
@@ -9534,7 +9543,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "livekit-runtime"
|
||||
version = "0.4.0"
|
||||
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4#d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4"
|
||||
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=383e5377f8b7de1f8627ee16f0cf11c5293337bd#383e5377f8b7de1f8627ee16f0cf11c5293337bd"
|
||||
dependencies = [
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
@@ -9566,7 +9575,7 @@ dependencies = [
|
||||
"core-foundation 0.10.0",
|
||||
"core-video",
|
||||
"coreaudio-rs 0.12.1",
|
||||
"cpal 0.16.0",
|
||||
"cpal",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"gpui_tokio",
|
||||
@@ -10337,20 +10346,6 @@ dependencies = [
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"jni-sys",
|
||||
"log",
|
||||
"ndk-sys 0.5.0+25.2.9519653",
|
||||
"num_enum",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk"
|
||||
version = "0.9.0"
|
||||
@@ -10360,7 +10355,7 @@ dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"jni-sys",
|
||||
"log",
|
||||
"ndk-sys 0.6.0+11769913",
|
||||
"ndk-sys",
|
||||
"num_enum",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
@@ -10371,15 +10366,6 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
|
||||
|
||||
[[package]]
|
||||
name = "ndk-sys"
|
||||
version = "0.5.0+25.2.9519653"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691"
|
||||
dependencies = [
|
||||
"jni-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk-sys"
|
||||
version = "0.6.0+11769913"
|
||||
@@ -10949,29 +10935,6 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oboe"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb"
|
||||
dependencies = [
|
||||
"jni",
|
||||
"ndk 0.8.0",
|
||||
"ndk-context",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"oboe-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oboe-sys"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ollama"
|
||||
version = "0.1.0"
|
||||
@@ -10992,14 +10955,18 @@ dependencies = [
|
||||
"anyhow",
|
||||
"command_palette_hooks",
|
||||
"db",
|
||||
"editor",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"gpui",
|
||||
"language",
|
||||
"project",
|
||||
"settings",
|
||||
"theme",
|
||||
"ui",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"zed_actions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -13750,12 +13717,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rodio"
|
||||
version = "0.20.1"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7ceb6607dd738c99bc8cb28eff249b7cd5c8ec88b9db96c0608c1480d140fb1"
|
||||
checksum = "e40ecf59e742e03336be6a3d53755e789fd05a059fa22dfa0ed624722319e183"
|
||||
dependencies = [
|
||||
"cpal 0.15.3",
|
||||
"hound",
|
||||
"cpal",
|
||||
"dasp_sample",
|
||||
"num-rational",
|
||||
"symphonia",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -14775,6 +14745,7 @@ dependencies = [
|
||||
"fs",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"itertools 0.14.0",
|
||||
"language",
|
||||
"log",
|
||||
"menu",
|
||||
@@ -15775,6 +15746,66 @@ dependencies = [
|
||||
"zeno",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "815c942ae7ee74737bb00f965fa5b5a2ac2ce7b6c01c0cc169bbeaf7abd5f5a9"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"symphonia-codec-pcm",
|
||||
"symphonia-core",
|
||||
"symphonia-format-riff",
|
||||
"symphonia-metadata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia-codec-pcm"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f395a67057c2ebc5e84d7bb1be71cce1a7ba99f64e0f0f0e303a03f79116f89b"
|
||||
dependencies = [
|
||||
"log",
|
||||
"symphonia-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia-core"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "798306779e3dc7d5231bd5691f5a813496dc79d3f56bf82e25789f2094e022c3"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"bitflags 1.3.2",
|
||||
"bytemuck",
|
||||
"lazy_static",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia-format-riff"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05f7be232f962f937f4b7115cbe62c330929345434c834359425e043bfd15f50"
|
||||
dependencies = [
|
||||
"extended",
|
||||
"log",
|
||||
"symphonia-core",
|
||||
"symphonia-metadata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia-metadata"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc622b9841a10089c5b18e99eb904f4341615d5aa55bbf4eedde1be721a4023c"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"symphonia-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
@@ -15955,13 +15986,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "taffy"
|
||||
version = "0.5.1"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8b61630cba2afd2c851821add2e1bb1b7851a2436e839ab73b56558b009035e"
|
||||
checksum = "7aaef0ac998e6527d6d0d5582f7e43953bb17221ac75bb8eb2fcc2db3396db1c"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"grid",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"slotmap",
|
||||
]
|
||||
@@ -18356,9 +18386,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wayland-backend"
|
||||
version = "0.3.8"
|
||||
version = "0.3.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7208998eaa3870dad37ec8836979581506e0c5c64c20c9e79e9d2a10d6f47bf"
|
||||
checksum = "fe770181423e5fc79d3e2a7f4410b7799d5aab1de4372853de3c6aa13ca24121"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"downcast-rs",
|
||||
@@ -18370,9 +18400,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wayland-client"
|
||||
version = "0.31.8"
|
||||
version = "0.31.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2120de3d33638aaef5b9f4472bff75f07c56379cf76ea320bd3a3d65ecaf73f"
|
||||
checksum = "978fa7c67b0847dbd6a9f350ca2569174974cd4082737054dbb7fbb79d7d9a61"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"rustix 0.38.44",
|
||||
@@ -18403,6 +18433,18 @@ 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"
|
||||
@@ -18412,7 +18454,20 @@ dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
"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-scanner",
|
||||
]
|
||||
|
||||
@@ -18476,11 +18531,11 @@ name = "web_search"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cloud_llm_client",
|
||||
"collections",
|
||||
"gpui",
|
||||
"serde",
|
||||
"workspace-hack",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -18489,6 +18544,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"cloud_llm_client",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"http_client",
|
||||
@@ -18497,7 +18553,6 @@ dependencies = [
|
||||
"serde_json",
|
||||
"web_search",
|
||||
"workspace-hack",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -18521,7 +18576,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "webrtc-sys"
|
||||
version = "0.3.7"
|
||||
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4#d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4"
|
||||
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=383e5377f8b7de1f8627ee16f0cf11c5293337bd#383e5377f8b7de1f8627ee16f0cf11c5293337bd"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cxx",
|
||||
@@ -18534,13 +18589,15 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "webrtc-sys-build"
|
||||
version = "0.3.6"
|
||||
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4#d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4"
|
||||
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=383e5377f8b7de1f8627ee16f0cf11c5293337bd#383e5377f8b7de1f8627ee16f0cf11c5293337bd"
|
||||
dependencies = [
|
||||
"fs2",
|
||||
"hex-literal",
|
||||
"regex",
|
||||
"reqwest 0.11.27",
|
||||
"scratch",
|
||||
"semver",
|
||||
"sha2",
|
||||
"zip",
|
||||
]
|
||||
|
||||
@@ -19661,14 +19718,12 @@ dependencies = [
|
||||
"cc",
|
||||
"chrono",
|
||||
"cipher",
|
||||
"clang-sys",
|
||||
"clap",
|
||||
"clap_builder",
|
||||
"codespan-reporting 0.12.0",
|
||||
"concurrent-queue",
|
||||
"core-foundation 0.9.4",
|
||||
"core-foundation-sys",
|
||||
"coreaudio-sys",
|
||||
"cranelift-codegen",
|
||||
"crc32fast",
|
||||
"crossbeam-epoch",
|
||||
@@ -20164,7 +20219,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.197.0"
|
||||
version = "0.198.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"agent",
|
||||
@@ -20205,6 +20260,7 @@ dependencies = [
|
||||
"extension",
|
||||
"extension_host",
|
||||
"extensions_ui",
|
||||
"feature_flags",
|
||||
"feedback",
|
||||
"file_finder",
|
||||
"fs",
|
||||
@@ -20324,7 +20380,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_emmet"
|
||||
version = "0.0.3"
|
||||
version = "0.0.4"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.1.0",
|
||||
]
|
||||
@@ -20363,19 +20419,6 @@ dependencies = [
|
||||
"zed_extension_api 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_llm_client"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6607f74dee2a18a9ce0f091844944a0e59881359ab62e0768fb0618f55d4c1dc"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum 0.27.1",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_proto"
|
||||
version = "0.2.2"
|
||||
@@ -20555,6 +20598,7 @@ dependencies = [
|
||||
"call",
|
||||
"client",
|
||||
"clock",
|
||||
"cloud_llm_client",
|
||||
"collections",
|
||||
"command_palette_hooks",
|
||||
"copilot",
|
||||
@@ -20596,7 +20640,6 @@ dependencies = [
|
||||
"workspace-hack",
|
||||
"worktree",
|
||||
"zed_actions",
|
||||
"zed_llm_client",
|
||||
"zlog",
|
||||
]
|
||||
|
||||
|
||||
13
Cargo.toml
@@ -29,6 +29,7 @@ members = [
|
||||
"crates/cli",
|
||||
"crates/client",
|
||||
"crates/clock",
|
||||
"crates/cloud_llm_client",
|
||||
"crates/collab",
|
||||
"crates/collab_ui",
|
||||
"crates/collections",
|
||||
@@ -70,7 +71,6 @@ members = [
|
||||
"crates/gpui",
|
||||
"crates/gpui_macros",
|
||||
"crates/gpui_tokio",
|
||||
|
||||
"crates/html_to_markdown",
|
||||
"crates/http_client",
|
||||
"crates/http_client_tls",
|
||||
@@ -251,6 +251,7 @@ channel = { path = "crates/channel" }
|
||||
cli = { path = "crates/cli" }
|
||||
client = { path = "crates/client" }
|
||||
clock = { path = "crates/clock" }
|
||||
cloud_llm_client = { path = "crates/cloud_llm_client" }
|
||||
collab = { path = "crates/collab" }
|
||||
collab_ui = { path = "crates/collab_ui" }
|
||||
collections = { path = "crates/collections" }
|
||||
@@ -413,6 +414,7 @@ zlog_settings = { path = "crates/zlog_settings" }
|
||||
#
|
||||
|
||||
agentic-coding-protocol = "0.0.10"
|
||||
agent-client-protocol = "0.0.11"
|
||||
aho-corasick = "1.1"
|
||||
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
|
||||
any_vec = "0.14"
|
||||
@@ -459,7 +461,7 @@ core-video = { version = "0.4.3", features = ["metal"] }
|
||||
cpal = "0.16"
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
ctor = "0.4.0"
|
||||
dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "7f39295b441614ca9dbf44293e53c32f666897f9" }
|
||||
dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "1b461b310481d01e02b2603c16d7144b926339f8" }
|
||||
dashmap = "6.0"
|
||||
derive_more = "0.99.17"
|
||||
dirs = "4.0"
|
||||
@@ -482,6 +484,7 @@ heed = { version = "0.21.0", features = ["read-txn-no-tls"] }
|
||||
hex = "0.4.3"
|
||||
html5ever = "0.27.0"
|
||||
http = "1.1"
|
||||
http-body = "1.0"
|
||||
hyper = "0.14"
|
||||
ignore = "0.4.22"
|
||||
image = "0.25.1"
|
||||
@@ -643,7 +646,6 @@ which = "6.0.0"
|
||||
windows-core = "0.61"
|
||||
wit-component = "0.221"
|
||||
workspace-hack = "0.1.0"
|
||||
zed_llm_client = "= 0.8.6"
|
||||
zstd = "0.11"
|
||||
|
||||
[workspace.dependencies.async-stripe]
|
||||
@@ -718,6 +720,11 @@ workspace-hack = { path = "tooling/workspace-hack" }
|
||||
split-debuginfo = "unpacked"
|
||||
codegen-units = 16
|
||||
|
||||
# mirror configuration for crates compiled for the build platform
|
||||
# (without this cargo will compile ~400 crates twice)
|
||||
[profile.dev.build-override]
|
||||
codegen-units = 16
|
||||
|
||||
[profile.dev.package]
|
||||
taffy = { opt-level = 3 }
|
||||
cranelift-codegen = { opt-level = 3 }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Zed
|
||||
|
||||
[](https://zed.dev)
|
||||
[](https://github.com/zed-industries/zed/actions/workflows/ci.yml)
|
||||
|
||||
Welcome to Zed, a high-performance, multiplayer code editor from the creators of [Atom](https://github.com/atom/atom) and [Tree-sitter](https://github.com/tree-sitter/tree-sitter).
|
||||
@@ -37,3 +38,5 @@ We use [`cargo-about`](https://github.com/EmbarkStudios/cargo-about) to automati
|
||||
- Is it showing a `no license specified` error for a crate you've created? If so, add `publish = false` under `[package]` in your crate's Cargo.toml.
|
||||
- Is the error `failed to satisfy license requirements` for a dependency? If so, first determine what license the project has and whether this system is sufficient to comply with this license's requirements. If you're unsure, ask a lawyer. Once you've verified that this system is acceptable add the license's SPDX identifier to the `accepted` array in `script/licenses/zed-licenses.toml`.
|
||||
- Is `cargo-about` unable to find the license for a dependency? If so, add a clarification field at the end of `script/licenses/zed-licenses.toml`, as specified in the [cargo-about book](https://embarkstudios.github.io/cargo-about/cli/generate/config.html#crate-configuration).
|
||||
|
||||
<!-- Thank you! -->
|
||||
|
||||
8
assets/badge/v0.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"label": "",
|
||||
"message": "Zed",
|
||||
"logoSvg": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 96 96\"><rect width=\"96\" height=\"96\" fill=\"#000\"/><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M9 6C7.34315 6 6 7.34315 6 9V75H0V9C0 4.02944 4.02944 0 9 0H89.3787C93.3878 0 95.3955 4.84715 92.5607 7.68198L43.0551 57.1875H57V51H63V58.6875C63 61.1728 60.9853 63.1875 58.5 63.1875H37.0551L26.7426 73.5H73.5V36H79.5V73.5C79.5 76.8137 76.8137 79.5 73.5 79.5H20.7426L10.2426 90H87C88.6569 90 90 88.6569 90 87V21H96V87C96 91.9706 91.9706 96 87 96H6.62132C2.61224 96 0.604504 91.1529 3.43934 88.318L52.7574 39H39V45H33V37.5C33 35.0147 35.0147 33 37.5 33H58.7574L69.2574 22.5H22.5V60H16.5V22.5C16.5 19.1863 19.1863 16.5 22.5 16.5H75.2574L85.7574 6H9Z\" fill=\"#fff\"/></svg>",
|
||||
"logoWidth": 16,
|
||||
"labelColor": "black",
|
||||
"color": "white"
|
||||
}
|
||||
4
assets/icons/ai_open_ai_compat.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.25669 0.999943C8.27509 0.993825 9.24655 1.42125 9.9227 2.17279C11.4427 1.85079 12.9991 2.53518 13.7733 3.86518C14.159 4.5149 14.3171 5.26409 14.2372 5.99994H13.2967C13.3789 5.42185 13.265 4.8321 12.9686 4.32514C12.2353 3.06961 10.6088 2.63919 9.33676 3.36322L6.48032 4.98822C6.46926 4.99697 6.46284 5.01135 6.46372 5.02533V6.38568L9.91294 4.42084C10.0565 4.33818 10.2336 4.33823 10.3768 4.42084L13.1502 5.99994H11.2948L9.88364 5.19623C9.87034 5.19054 9.85459 5.19128 9.84262 5.19916L8.64926 5.87983L8.8602 5.99994H7.99985C6.89539 6.00004 5.99988 6.89547 5.99985 7.99994V9.34955L3.90219 8.15522C3.75815 8.07431 3.66897 7.92228 3.66977 7.75873V4.53803C3.66977 4.50828 3.67172 4.4654 3.67172 4.44135C3.08836 4.65262 2.59832 5.0599 2.28794 5.59174C1.55635 6.84647 1.99122 8.44936 3.26059 9.17475L5.99985 10.7363V11.6162C5.87564 11.6568 5.73827 11.6456 5.6229 11.579L2.7977 9.96869C2.77156 9.95382 2.73449 9.9311 2.71372 9.91889C2.60687 10.5231 2.7194 11.1466 3.0311 11.6777C3.6435 12.7209 4.87159 13.1902 5.99985 12.9023V13.8398C4.50443 14.1233 2.98758 13.4424 2.22641 12.1347C1.71174 11.2677 1.60096 10.2237 1.9227 9.27045C0.880739 8.13295 0.703328 6.46023 1.48325 5.13373C1.98739 4.26024 2.84863 3.64401 3.84653 3.44233C4.3245 1.9837 5.70306 0.996447 7.25669 0.999943ZM7.25766 1.91498C5.78932 1.9143 4.59839 3.08914 4.59751 4.53803V7.79193C4.59926 7.80578 4.60735 7.81796 4.61997 7.82416L5.8143 8.50483L5.81626 4.57611C5.81537 4.41216 5.90431 4.2606 6.04868 4.17963L8.87387 2.56928C8.89868 2.55441 8.93612 2.53379 8.95786 2.5224C8.48035 2.13046 7.8788 1.91498 7.25766 1.91498Z" fill="black"/>
|
||||
<path d="M13.5 6C14.6046 6 15.5 6.89543 15.5 8V13.5C15.5 14.6046 14.6046 15.5 13.5 15.5H8C6.89543 15.5 6 14.6046 6 13.5V8C6 6.89543 6.89543 6 8 6H13.5ZM10.8916 8.02539C10.0563 8.02539 9.33453 8.27982 8.81934 8.76562C8.30213 9.25335 8.02547 9.94371 8.02539 10.748C8.02539 11.557 8.29852 12.2492 8.81543 12.7373C9.33013 13.2232 10.0521 13.4746 10.8916 13.4746C11.9865 13.4745 12.8545 13.1022 13.3076 12.3525C13.3894 12.2176 13.4521 12.0693 13.4521 11.8857C13.4521 11.4795 13.0933 11.2773 12.7842 11.2773C12.6604 11.2774 12.5292 11.3025 12.4072 11.3779C12.2862 11.4529 12.2058 11.5586 12.1494 11.666L12.1475 11.6689C11.9677 12.0213 11.5535 12.246 10.8955 12.2461C10.4219 12.2461 10.0667 12.0932 9.83008 11.8506C9.59255 11.607 9.44141 11.2389 9.44141 10.748C9.44148 10.264 9.59319 9.89628 9.83203 9.65137C10.0702 9.40725 10.4255 9.25391 10.8916 9.25391C11.4912 9.25399 11.9415 9.50614 12.1289 9.8916V9.89062C12.1888 10.0157 12.276 10.1311 12.4023 10.2129C12.5303 10.2956 12.6724 10.3271 12.8115 10.3271C12.9661 10.3271 13.1303 10.2857 13.2627 10.1758C13.4018 10.0603 13.4746 9.89383 13.4746 9.71582C13.4746 9.61857 13.4542 9.52036 13.4199 9.42773L13.3818 9.33691C12.9749 8.49175 11.9927 8.02548 10.8916 8.02539ZM10.3203 8.97852L10.1494 9.03516C10.2095 9.01178 10.2716 8.99089 10.3359 8.97363C10.3307 8.97505 10.3256 8.97706 10.3203 8.97852ZM10.4814 8.94141C10.4969 8.9385 10.5126 8.93616 10.5283 8.93359C10.5126 8.93617 10.4969 8.9385 10.4814 8.94141ZM10.6709 8.91504C10.6819 8.91399 10.693 8.913 10.7041 8.91211C10.693 8.913 10.6819 8.91399 10.6709 8.91504Z" fill="black" fill-opacity="0.95"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
@@ -1 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-volume-off"><path d="M16 9a5 5 0 0 1 .95 2.293"/><path d="M19.364 5.636a9 9 0 0 1 1.889 9.96"/><path d="m2 2 20 20"/><path d="m7 7-.587.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298V11"/><path d="M9.828 4.172A.686.686 0 0 1 11 4.657v.686"/></svg>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.6667 6C11.003 6.44823 11.2208 6.97398 11.3001 7.52867" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12.9094 3.75732C13.7621 4.6095 14.3383 5.69876 14.5629 6.88315C14.7875 8.06754 14.6502 9.29213 14.1688 10.3973" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2.66675 2L13.6667 13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5.33333 4.66669L4.942 5.05802C4.85494 5.1456 4.75136 5.21504 4.63726 5.2623C4.52317 5.30957 4.40083 5.33372 4.27733 5.33335H2.66667C2.48986 5.33335 2.32029 5.40359 2.19526 5.52862C2.07024 5.65364 2 5.82321 2 6.00002V10C2 10.1768 2.07024 10.3464 2.19526 10.4714C2.32029 10.5964 2.48986 10.6667 2.66667 10.6667H4.27733C4.40083 10.6663 4.52317 10.6905 4.63726 10.7377C4.75136 10.785 4.85494 10.8544 4.942 10.942L7.19733 13.198C7.26307 13.2639 7.34687 13.3088 7.43813 13.3269C7.52939 13.3451 7.62399 13.3358 7.70995 13.3002C7.79591 13.2646 7.86936 13.2042 7.921 13.1268C7.97263 13.0494 8.00013 12.9584 8 12.8654V7.33335" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7.21875 2.78136C7.28267 2.71719 7.36421 2.67345 7.45303 2.65568C7.54184 2.63791 7.63393 2.64691 7.71762 2.68154C7.80132 2.71618 7.87284 2.77488 7.92312 2.85022C7.97341 2.92555 8.0002 3.01412 8.00008 3.10469V3.56202" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 527 B After Width: | Height: | Size: 1.6 KiB |
@@ -1 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-volume-2"><path d="M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z"/><path d="M16 9a5 5 0 0 1 0 6"/><path d="M19.364 18.364a9 9 0 0 0 0-12.728"/></svg>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 3.13467C7.99987 3.04181 7.97223 2.95107 7.92057 2.8739C7.86892 2.79674 7.79557 2.7366 7.70977 2.70108C7.62397 2.66557 7.52958 2.65626 7.43849 2.67434C7.34741 2.69242 7.26373 2.73707 7.198 2.80266L4.942 5.058C4.85494 5.14558 4.75136 5.21502 4.63726 5.26228C4.52317 5.30954 4.40083 5.33369 4.27733 5.33333H2.66667C2.48986 5.33333 2.32029 5.40357 2.19526 5.52859C2.07024 5.65362 2 5.82319 2 6V10C2 10.1768 2.07024 10.3464 2.19526 10.4714C2.32029 10.5964 2.48986 10.6667 2.66667 10.6667H4.27733C4.40083 10.6663 4.52317 10.6905 4.63726 10.7377C4.75136 10.785 4.85494 10.8544 4.942 10.942L7.19733 13.198C7.26307 13.2639 7.34687 13.3087 7.43813 13.3269C7.52939 13.3451 7.62399 13.3358 7.70995 13.3002C7.79591 13.2645 7.86936 13.2042 7.921 13.1268C7.97263 13.0494 8.00013 12.9584 8 12.8653V3.13467Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10.6667 6C11.0995 6.57699 11.3334 7.27877 11.3334 8C11.3334 8.72123 11.0995 9.42301 10.6667 10" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12.9094 12.2427C13.4666 11.6855 13.9085 11.0241 14.2101 10.2961C14.5116 9.56815 14.6668 8.78793 14.6668 7.99999C14.6668 7.21205 14.5116 6.43183 14.2101 5.70387C13.9085 4.97591 13.4666 4.31448 12.9094 3.75732" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 475 B After Width: | Height: | Size: 1.4 KiB |
1
assets/icons/cloud_download.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-cloud-download-icon lucide-cloud-download"><path d="M12 13v8l-4-4"/><path d="m12 21 4-4"/><path d="M4.393 15.269A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.436 8.284"/></svg>
|
||||
|
After Width: | Height: | Size: 372 B |
@@ -1,8 +1,5 @@
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M3 1C2.44771 1 2 1.44772 2 2V13C2 13.5523 2.44772 14 3 14H10.5C10.7761 14 11 13.7761 11 13.5C11 13.2239 10.7761 13 10.5 13H3V2L10.5 2C10.7761 2 11 1.77614 11 1.5C11 1.22386 10.7761 1 10.5 1H3ZM12.6036 4.89645C12.4083 4.70118 12.0917 4.70118 11.8964 4.89645C11.7012 5.09171 11.7012 5.40829 11.8964 5.60355L13.2929 7H6.5C6.22386 7 6 7.22386 6 7.5C6 7.77614 6.22386 8 6.5 8H13.2929L11.8964 9.39645C11.7012 9.59171 11.7012 9.90829 11.8964 10.1036C12.0917 10.2988 12.4083 10.2988 12.6036 10.1036L14.8536 7.85355C15.0488 7.65829 15.0488 7.34171 14.8536 7.14645L12.6036 4.89645Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.437 11.0461L13.4831 8L10.437 4.95392" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13 8L8 8" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6.6553 13.4659H4.21843C3.89528 13.4659 3.58537 13.3375 3.35687 13.109C3.12837 12.8805 3 12.5706 3 12.2475V3.71843C3 3.39528 3.12837 3.08537 3.35687 2.85687C3.58537 2.62837 3.89528 2.5 4.21843 2.5H6.6553" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 768 B After Width: | Height: | Size: 637 B |
3
assets/icons/file_icons/surrealql.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.628 11.0743V10.4575H8.45562L8.65084 10.2445C8.75911 10.1264 8.96952 9.79454 9.11862 9.50789C9.52153 8.73047 9.51798 7.25107 9.11862 6.43992C8.58614 5.35722 7.49453 4.56381 6.24942 4.35703C4.59252 4.08192 2.86196 5.00312 2.14045 6.54287C1.77038 7.33182 1.77038 8.64437 2.14045 9.43333C2.45905 10.1122 3.11309 10.8204 3.73609 11.1595C4.51439 11.5828 5.18264 11.676 7.51312 11.6848L9.62627 11.6928L9.628 11.0743ZM5.30605 10.169C4.24109 10.0111 3.45215 9.07124 3.45659 7.96813C3.45659 7.33004 3.70064 6.80022 4.18697 6.36182C4.67685 5.91986 5.1312 5.77344 5.86602 5.82048C7.00287 5.89236 7.82382 6.79845 7.82382 7.98056C7.82382 8.61332 7.71996 8.91682 7.33036 9.42534C6.90172 9.98444 6.08345 10.2853 5.30692 10.1699M15.1374 10.9802V10.2684H11.8138V4.47509H10.1986V11.6928H15.1374V10.9802Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 916 B |
@@ -1,3 +1,5 @@
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.72742 8.83338C3.63539 8.57302 3.34973 8.43656 3.08937 8.52858C2.82901 8.6206 2.69255 8.90626 2.78458 9.16662C2.86101 9.38288 2.95188 9.59228 3.056 9.79364C3.81427 11.2601 5.27842 12.3044 7.00014 12.4753L7.00014 14L5.50014 14C5.22399 14 5.00014 14.2239 5.00014 14.5C5.00014 14.7761 5.22399 15 5.50014 15L7.50014 15L9.50014 15C9.77628 15 10.0001 14.7761 10.0001 14.5C10.0001 14.2239 9.77628 14 9.50014 14L8.00014 14L8.00014 12.4753C9.72168 12.3043 11.1857 11.26 11.9439 9.79364C12.048 9.59228 12.1389 9.38288 12.2153 9.16662C12.3073 8.90626 12.1709 8.6206 11.9105 8.52858C11.6501 8.43656 11.3645 8.57302 11.2725 8.83338C11.2114 9.00607 11.1388 9.17337 11.0556 9.33433C10.3899 10.6218 9.04706 11.5 7.49994 11.5C5.95282 11.5 4.60997 10.6218 3.94428 9.33433C3.86104 9.17337 3.78845 9.00607 3.72742 8.83338ZM5.5 3.5L5.5 7.5C5.5 8.60457 6.39543 9.5 7.5 9.5C8.60457 9.5 9.5 8.60457 9.5 7.5L9.5 3.5C9.5 2.39543 8.60457 1.5 7.5 1.5C6.39543 1.5 5.5 2.39543 5.5 3.5ZM4.5 7.5C4.5 9.15685 5.84315 10.5 7.5 10.5C9.15685 10.5 10.5 9.15685 10.5 7.5L10.5 3.5C10.5 1.84315 9.15685 0.5 7.5 0.5C5.84315 0.5 4.5 1.84315 4.5 3.5L4.5 7.5Z" fill="black"/>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 12.2028V14.3042" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12.2027 6.94928V8.11672C12.2027 9.20041 11.7599 10.2397 10.9717 11.006C10.1836 11.7723 9.11457 12.2028 7.99992 12.2028C6.88527 12.2028 5.81627 11.7723 5.02809 11.006C4.23991 10.2397 3.79712 9.20041 3.79712 8.11672V6.94928" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10.1015 3.63555C10.1015 2.56426 9.16065 1.6958 8.00008 1.6958C6.83951 1.6958 5.89868 2.56426 5.89868 3.63555V8.16165C5.89868 9.23294 6.83951 10.1014 8.00008 10.1014C9.16065 10.1014 10.1015 9.23294 10.1015 8.16165V3.63555Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 847 B |
@@ -1,3 +1,8 @@
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.87 1.83637C13.0557 1.63204 13.0407 1.31581 12.8363 1.13006C12.632 0.944307 12.3158 0.959365 12.13 1.16369L10.4589 3.00199C10.2216 1.58215 8.98719 0.5 7.5 0.5C5.84315 0.5 4.5 1.84315 4.5 3.5L4.5 7.5C4.5 8.0754 4.66199 8.61297 4.94286 9.06958L4.24966 9.8321C4.1363 9.6744 4.03412 9.5081 3.94428 9.33433C3.86104 9.17337 3.78845 9.00607 3.72742 8.83338C3.63539 8.57302 3.34973 8.43656 3.08937 8.52858C2.82901 8.6206 2.69255 8.90626 2.78458 9.16662C2.86101 9.38288 2.95188 9.59228 3.056 9.79364C3.20094 10.074 3.37167 10.3388 3.56506 10.5852L2.13003 12.1637C1.94428 12.368 1.95933 12.6842 2.16366 12.87C2.36799 13.0558 2.68422 13.0407 2.86997 12.8364L4.25951 11.3079C5.01297 11.9497 5.95951 12.372 7.00014 12.4753L7.00014 14L5.50014 14C5.22399 14 5.00014 14.2239 5.00014 14.5C5.00014 14.7761 5.22399 15 5.50014 15L7.50014 15L9.50014 15C9.77628 15 10.0001 14.7761 10.0001 14.5C10.0001 14.2239 9.77628 14 9.50014 14L8.00014 14L8.00014 12.4753C9.72168 12.3043 11.1857 11.26 11.9439 9.79364C12.048 9.59228 12.1389 9.38288 12.2153 9.16662C12.3073 8.90626 12.1709 8.6206 11.9105 8.52858C11.6501 8.43656 11.3645 8.57302 11.2725 8.83338C11.2114 9.00607 11.1388 9.17337 11.0556 9.33433C10.3899 10.6218 9.04706 11.5 7.49994 11.5C6.523 11.5 5.62751 11.1498 4.93254 10.5675L5.60604 9.82669C6.12251 10.2476 6.78178 10.5 7.5 10.5C9.15685 10.5 10.5 9.15685 10.5 7.5L10.5 4.44333L12.87 1.83637ZM9.5 4.05673L9.5 3.5C9.5 2.39543 8.60457 1.5 7.5 1.5C6.39543 1.5 5.5 2.39543 5.5 3.5L5.5 7.5C5.5 7.77755 5.55653 8.04189 5.65872 8.28214L9.5 4.05673ZM6.28022 9.08509L9.5 5.54333L9.5 7.5C9.5 8.60457 8.60457 9.5 7.5 9.5C7.04083 9.5 6.6178 9.34527 6.28022 9.08509Z" fill="black"/>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 3L13 13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12 9C12 8.74858 12 8.49375 12 8.23839V7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4.00043 7V8.09869C3.98856 8.86731 4.22157 9.62164 4.66938 10.2643C5.11718 10.907 5.75924 11.4085 6.51267 11.7042C7.2661 11.9999 8.09632 12.0761 8.89619 11.923C9.47851 11.8115 10.0253 11.5823 10.5 11.2539" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10 6V3.62904C9.99714 3.26103 9.8347 2.90448 9.53885 2.6168C9.24299 2.32913 8.83093 2.12707 8.36903 2.04316C7.90713 1.95926 7.42226 1.9984 6.99252 2.15427C6.56278 2.31015 6.21317 2.57369 6 2.90245" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6 6V8.00088C6.00031 8.39636 6.10356 8.78287 6.29674 9.11159C6.48991 9.44031 6.76433 9.69649 7.08534 9.84779C7.40634 9.99909 7.75954 10.0387 8.10032 9.96165C8.4411 9.88459 8.75417 9.69431 9 9.41483" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8 12V14" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -1,8 +1,5 @@
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M1 3.25C1 3.11193 1.11193 3 1.25 3H13.75C13.8881 3 14 3.11193 14 3.25V10.75C14 10.8881 13.8881 11 13.75 11H1.25C1.11193 11 1 10.8881 1 10.75V3.25ZM1.25 2C0.559643 2 0 2.55964 0 3.25V10.75C0 11.4404 0.559644 12 1.25 12H5.07341L4.82991 13.2986C4.76645 13.6371 5.02612 13.95 5.37049 13.95H9.62951C9.97389 13.95 10.2336 13.6371 10.1701 13.2986L9.92659 12H13.75C14.4404 12 15 11.4404 15 10.75V3.25C15 2.55964 14.4404 2 13.75 2H1.25ZM9.01091 12H5.98909L5.79222 13.05H9.20778L9.01091 12Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.8 3H3.2C2.53726 3 2 3.51167 2 4.14286V9.85714C2 10.4883 2.53726 11 3.2 11H12.8C13.4627 11 14 10.4883 14 9.85714V4.14286C14 3.51167 13.4627 3 12.8 3Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5.33325 14H10.6666" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8 11.3333V14" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 677 B After Width: | Height: | Size: 569 B |
@@ -495,7 +495,7 @@
|
||||
"shift-f12": "editor::GoToImplementation",
|
||||
"alt-ctrl-f12": "editor::GoToTypeDefinitionSplit",
|
||||
"alt-shift-f12": "editor::FindAllReferences",
|
||||
"ctrl-m": "editor::MoveToEnclosingBracket",
|
||||
"ctrl-m": "editor::MoveToEnclosingBracket", // from jetbrains
|
||||
"ctrl-|": "editor::MoveToEnclosingBracket",
|
||||
"ctrl-{": "editor::Fold",
|
||||
"ctrl-}": "editor::UnfoldLines",
|
||||
@@ -872,8 +872,6 @@
|
||||
"tab": "git_panel::FocusEditor",
|
||||
"shift-tab": "git_panel::FocusEditor",
|
||||
"escape": "git_panel::ToggleFocus",
|
||||
"ctrl-enter": "git::Commit",
|
||||
"ctrl-shift-enter": "git::Amend",
|
||||
"alt-enter": "menu::SecondaryConfirm",
|
||||
"delete": ["git::RestoreFile", { "skip_prompt": false }],
|
||||
"backspace": ["git::RestoreFile", { "skip_prompt": false }],
|
||||
@@ -910,7 +908,9 @@
|
||||
"ctrl-g backspace": "git::RestoreTrackedFiles",
|
||||
"ctrl-g shift-backspace": "git::TrashUntrackedFiles",
|
||||
"ctrl-space": "git::StageAll",
|
||||
"ctrl-shift-space": "git::UnstageAll"
|
||||
"ctrl-shift-space": "git::UnstageAll",
|
||||
"ctrl-enter": "git::Commit",
|
||||
"ctrl-shift-enter": "git::Amend"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -975,9 +975,14 @@
|
||||
"context": "CollabPanel && not_editing",
|
||||
"bindings": {
|
||||
"ctrl-backspace": "collab_panel::Remove",
|
||||
"space": "menu::Confirm",
|
||||
"ctrl-up": "collab_panel::MoveChannelUp",
|
||||
"ctrl-down": "collab_panel::MoveChannelDown"
|
||||
"space": "menu::Confirm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "CollabPanel",
|
||||
"bindings": {
|
||||
"alt-up": "collab_panel::MoveChannelUp",
|
||||
"alt-down": "collab_panel::MoveChannelDown"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1132,7 +1137,10 @@
|
||||
"alt-ctrl-f": "keymap_editor::ToggleKeystrokeSearch",
|
||||
"alt-c": "keymap_editor::ToggleConflictFilter",
|
||||
"enter": "keymap_editor::EditBinding",
|
||||
"alt-enter": "keymap_editor::CreateBinding"
|
||||
"alt-enter": "keymap_editor::CreateBinding",
|
||||
"ctrl-c": "keymap_editor::CopyAction",
|
||||
"ctrl-shift-c": "keymap_editor::CopyContext",
|
||||
"ctrl-t": "keymap_editor::ShowMatchingKeybinds"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -549,7 +549,7 @@
|
||||
"alt-cmd-f12": "editor::GoToTypeDefinitionSplit",
|
||||
"alt-shift-f12": "editor::FindAllReferences",
|
||||
"cmd-|": "editor::MoveToEnclosingBracket",
|
||||
"ctrl-m": "editor::MoveToEnclosingBracket",
|
||||
"ctrl-m": "editor::MoveToEnclosingBracket", // From Jetbrains
|
||||
"alt-cmd-[": "editor::Fold",
|
||||
"alt-cmd-]": "editor::UnfoldLines",
|
||||
"cmd-k cmd-l": "editor::ToggleFold",
|
||||
@@ -950,8 +950,6 @@
|
||||
"tab": "git_panel::FocusEditor",
|
||||
"shift-tab": "git_panel::FocusEditor",
|
||||
"escape": "git_panel::ToggleFocus",
|
||||
"cmd-enter": "git::Commit",
|
||||
"cmd-shift-enter": "git::Amend",
|
||||
"backspace": ["git::RestoreFile", { "skip_prompt": false }],
|
||||
"delete": ["git::RestoreFile", { "skip_prompt": false }],
|
||||
"cmd-backspace": ["git::RestoreFile", { "skip_prompt": true }],
|
||||
@@ -1001,7 +999,9 @@
|
||||
"ctrl-g backspace": "git::RestoreTrackedFiles",
|
||||
"ctrl-g shift-backspace": "git::TrashUntrackedFiles",
|
||||
"cmd-ctrl-y": "git::StageAll",
|
||||
"cmd-ctrl-shift-y": "git::UnstageAll"
|
||||
"cmd-ctrl-shift-y": "git::UnstageAll",
|
||||
"cmd-enter": "git::Commit",
|
||||
"cmd-shift-enter": "git::Amend"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1037,9 +1037,15 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-backspace": "collab_panel::Remove",
|
||||
"space": "menu::Confirm",
|
||||
"cmd-up": "collab_panel::MoveChannelUp",
|
||||
"cmd-down": "collab_panel::MoveChannelDown"
|
||||
"space": "menu::Confirm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "CollabPanel",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"alt-up": "collab_panel::MoveChannelUp",
|
||||
"alt-down": "collab_panel::MoveChannelDown"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1233,7 +1239,10 @@
|
||||
"cmd-alt-f": "keymap_editor::ToggleKeystrokeSearch",
|
||||
"cmd-alt-c": "keymap_editor::ToggleConflictFilter",
|
||||
"enter": "keymap_editor::EditBinding",
|
||||
"alt-enter": "keymap_editor::CreateBinding"
|
||||
"alt-enter": "keymap_editor::CreateBinding",
|
||||
"cmd-c": "keymap_editor::CopyAction",
|
||||
"cmd-shift-c": "keymap_editor::CopyContext",
|
||||
"cmd-t": "keymap_editor::ShowMatchingKeybinds"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == insert && !menu",
|
||||
"context": "Editor && vim_mode == insert",
|
||||
"bindings": {
|
||||
// "j k": "vim::SwitchToNormalMode"
|
||||
// "j k": "vim::NormalBefore"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"ctrl-alt-s": "zed::OpenSettings",
|
||||
"ctrl-{": "pane::ActivatePreviousItem",
|
||||
"ctrl-}": "pane::ActivateNextItem",
|
||||
"shift-escape": null, // Unmap workspace::zoom
|
||||
"ctrl-f2": "debugger::Stop",
|
||||
"f6": "debugger::Pause",
|
||||
"f7": "debugger::StepInto",
|
||||
@@ -44,8 +45,8 @@
|
||||
"ctrl-alt-right": "pane::GoForward",
|
||||
"alt-f7": "editor::FindAllReferences",
|
||||
"ctrl-alt-f7": "editor::FindAllReferences",
|
||||
// "ctrl-b": "editor::GoToDefinition", // Conflicts with workspace::ToggleLeftDock
|
||||
// "ctrl-alt-b": "editor::GoToDefinitionSplit", // Conflicts with workspace::ToggleLeftDock
|
||||
"ctrl-b": "editor::GoToDefinition", // Conflicts with workspace::ToggleLeftDock
|
||||
"ctrl-alt-b": "editor::GoToDefinitionSplit", // Conflicts with workspace::ToggleRightDock
|
||||
"ctrl-shift-b": "editor::GoToTypeDefinition",
|
||||
"ctrl-alt-shift-b": "editor::GoToTypeDefinitionSplit",
|
||||
"f2": "editor::GoToDiagnostic",
|
||||
@@ -100,12 +101,27 @@
|
||||
"shift shift": "command_palette::Toggle",
|
||||
"ctrl-alt-shift-n": "project_symbols::Toggle",
|
||||
"alt-0": "git_panel::ToggleFocus",
|
||||
"alt-1": "workspace::ToggleLeftDock",
|
||||
"alt-1": "project_panel::ToggleFocus",
|
||||
"alt-5": "debug_panel::ToggleFocus",
|
||||
"alt-6": "diagnostics::Deploy",
|
||||
"alt-7": "outline_panel::ToggleFocus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Pane", // this is to override the default Pane mappings to switch tabs
|
||||
"bindings": {
|
||||
"alt-1": "project_panel::ToggleFocus",
|
||||
"alt-2": null, // Bookmarks (left dock)
|
||||
"alt-3": null, // Find Panel (bottom dock)
|
||||
"alt-4": null, // Run Panel (bottom dock)
|
||||
"alt-5": "debug_panel::ToggleFocus",
|
||||
"alt-6": "diagnostics::Deploy",
|
||||
"alt-7": "outline_panel::ToggleFocus",
|
||||
"alt-8": null, // Services (bottom dock)
|
||||
"alt-9": null, // Git History (bottom dock)
|
||||
"alt-0": "git_panel::ToggleFocus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Workspace || Editor",
|
||||
"bindings": {
|
||||
@@ -151,6 +167,9 @@
|
||||
{ "context": "OutlinePanel", "bindings": { "alt-7": "workspace::CloseActiveDock" } },
|
||||
{
|
||||
"context": "Dock || Workspace || Terminal || OutlinePanel || ProjectPanel || CollabPanel || (Editor && mode == auto_height)",
|
||||
"bindings": { "escape": "editor::ToggleFocus" }
|
||||
"bindings": {
|
||||
"escape": "editor::ToggleFocus",
|
||||
"shift-escape": "workspace::CloseActiveDock"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"cmd-{": "pane::ActivatePreviousItem",
|
||||
"cmd-}": "pane::ActivateNextItem",
|
||||
"cmd-0": "git_panel::ToggleFocus", // overrides `cmd-0` zoom reset
|
||||
"shift-escape": null, // Unmap workspace::zoom
|
||||
"ctrl-f2": "debugger::Stop",
|
||||
"f6": "debugger::Pause",
|
||||
"f7": "debugger::StepInto",
|
||||
@@ -108,6 +109,21 @@
|
||||
"cmd-7": "outline_panel::ToggleFocus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Pane", // this is to override the default Pane mappings to switch tabs
|
||||
"bindings": {
|
||||
"cmd-1": "project_panel::ToggleFocus",
|
||||
"cmd-2": null, // Bookmarks (left dock)
|
||||
"cmd-3": null, // Find Panel (bottom dock)
|
||||
"cmd-4": null, // Run Panel (bottom dock)
|
||||
"cmd-5": "debug_panel::ToggleFocus",
|
||||
"cmd-6": "diagnostics::Deploy",
|
||||
"cmd-7": "outline_panel::ToggleFocus",
|
||||
"cmd-8": null, // Services (bottom dock)
|
||||
"cmd-9": null, // Git History (bottom dock)
|
||||
"cmd-0": "git_panel::ToggleFocus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Workspace || Editor",
|
||||
"bindings": {
|
||||
@@ -146,11 +162,15 @@
|
||||
}
|
||||
},
|
||||
{ "context": "GitPanel", "bindings": { "cmd-0": "workspace::CloseActiveDock" } },
|
||||
{ "context": "ProjectPanel", "bindings": { "cmd-1": "workspace::CloseActiveDock" } },
|
||||
{ "context": "DebugPanel", "bindings": { "cmd-5": "workspace::CloseActiveDock" } },
|
||||
{ "context": "Diagnostics > Editor", "bindings": { "cmd-6": "pane::CloseActiveItem" } },
|
||||
{ "context": "OutlinePanel", "bindings": { "cmd-7": "workspace::CloseActiveDock" } },
|
||||
{
|
||||
"context": "Dock || Workspace || Terminal || OutlinePanel || ProjectPanel || CollabPanel || (Editor && mode == auto_height)",
|
||||
"bindings": { "escape": "editor::ToggleFocus" }
|
||||
"bindings": {
|
||||
"escape": "editor::ToggleFocus",
|
||||
"shift-escape": "workspace::CloseActiveDock"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
"cmd-l": "go_to_line::Toggle",
|
||||
"ctrl-shift-d": "editor::DuplicateLineDown",
|
||||
@@ -15,7 +15,12 @@
|
||||
"cmd-enter": "editor::NewlineBelow",
|
||||
"cmd-alt-enter": "editor::NewlineAbove",
|
||||
"cmd-shift-l": "editor::SelectLine",
|
||||
"cmd-shift-t": "outline::Toggle",
|
||||
"cmd-shift-t": "outline::Toggle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"alt-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"alt-shift-backspace": "editor::DeleteToNextWordEnd",
|
||||
"alt-delete": "editor::DeleteToNextWordEnd",
|
||||
@@ -39,10 +44,6 @@
|
||||
"ctrl-_": "editor::ConvertToSnakeCase"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {}
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar",
|
||||
"bindings": {
|
||||
|
||||
@@ -220,6 +220,8 @@
|
||||
{
|
||||
"context": "vim_mode == normal",
|
||||
"bindings": {
|
||||
"i": "vim::InsertBefore",
|
||||
"a": "vim::InsertAfter",
|
||||
"ctrl-[": "editor::Cancel",
|
||||
":": "command_palette::Toggle",
|
||||
"c": "vim::PushChange",
|
||||
@@ -353,9 +355,7 @@
|
||||
"shift-d": "vim::DeleteToEndOfLine",
|
||||
"shift-j": "vim::JoinLines",
|
||||
"shift-y": "vim::YankLine",
|
||||
"i": "vim::InsertBefore",
|
||||
"shift-i": "vim::InsertFirstNonWhitespace",
|
||||
"a": "vim::InsertAfter",
|
||||
"shift-a": "vim::InsertEndOfLine",
|
||||
"o": "vim::InsertLineBelow",
|
||||
"shift-o": "vim::InsertLineAbove",
|
||||
@@ -377,6 +377,8 @@
|
||||
{
|
||||
"context": "vim_mode == helix_normal && !menu",
|
||||
"bindings": {
|
||||
"i": "vim::HelixInsert",
|
||||
"a": "vim::HelixAppend",
|
||||
"ctrl-[": "editor::Cancel",
|
||||
";": "vim::HelixCollapseSelection",
|
||||
":": "command_palette::Toggle",
|
||||
|
||||
@@ -691,7 +691,10 @@
|
||||
// 5. Never show the scrollbar:
|
||||
// "never"
|
||||
"show": null
|
||||
}
|
||||
},
|
||||
// Default depth to expand outline items in the current file.
|
||||
// Set to 0 to collapse all items that have children, 1 or higher to collapse items at that depth or deeper.
|
||||
"expand_outlines_with_depth": 100
|
||||
},
|
||||
"collaboration_panel": {
|
||||
// Whether to show the collaboration panel button in the status bar.
|
||||
@@ -1076,6 +1079,10 @@
|
||||
// Send anonymized usage data like what languages you're using Zed with.
|
||||
"metrics": true
|
||||
},
|
||||
// Whether to disable all AI features in Zed.
|
||||
//
|
||||
// Default: false
|
||||
"disable_ai": false,
|
||||
// Automatically update Zed. This setting may be ignored on Linux if
|
||||
// installed through a package manager.
|
||||
"auto_update": true,
|
||||
@@ -1712,6 +1719,7 @@
|
||||
"openai": {
|
||||
"api_url": "https://api.openai.com/v1"
|
||||
},
|
||||
"openai_compatible": {},
|
||||
"open_router": {
|
||||
"api_url": "https://openrouter.ai/api/v1"
|
||||
},
|
||||
|
||||
@@ -15,13 +15,15 @@
|
||||
"adapter": "JavaScript",
|
||||
"program": "$ZED_FILE",
|
||||
"request": "launch",
|
||||
"cwd": "$ZED_WORKTREE_ROOT"
|
||||
"cwd": "$ZED_WORKTREE_ROOT",
|
||||
"type": "pwa-node"
|
||||
},
|
||||
{
|
||||
"label": "JavaScript debug terminal",
|
||||
"adapter": "JavaScript",
|
||||
"request": "launch",
|
||||
"cwd": "$ZED_WORKTREE_ROOT",
|
||||
"console": "integratedTerminal"
|
||||
"console": "integratedTerminal",
|
||||
"type": "pwa-node"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -16,6 +16,7 @@ doctest = false
|
||||
test-support = ["gpui/test-support", "project/test-support"]
|
||||
|
||||
[dependencies]
|
||||
agent-client-protocol.workspace = true
|
||||
agentic-coding-protocol.workspace = true
|
||||
anyhow.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
use agentic_coding_protocol as acp;
|
||||
use std::{path::Path, rc::Rc};
|
||||
|
||||
use agent_client_protocol as acp;
|
||||
use anyhow::Result;
|
||||
use futures::future::{FutureExt as _, LocalBoxFuture};
|
||||
use gpui::{AsyncApp, Entity, Task};
|
||||
use project::Project;
|
||||
use ui::App;
|
||||
|
||||
use crate::AcpThread;
|
||||
|
||||
pub trait AgentConnection {
|
||||
fn request_any(
|
||||
&self,
|
||||
params: acp::AnyAgentRequest,
|
||||
) -> LocalBoxFuture<'static, Result<acp::AnyAgentResult>>;
|
||||
}
|
||||
fn name(&self) -> &'static str;
|
||||
|
||||
impl AgentConnection for acp::AgentConnection {
|
||||
fn request_any(
|
||||
&self,
|
||||
params: acp::AnyAgentRequest,
|
||||
) -> LocalBoxFuture<'static, Result<acp::AnyAgentResult>> {
|
||||
let task = self.request_any(params);
|
||||
async move { Ok(task.await?) }.boxed_local()
|
||||
}
|
||||
fn new_thread(
|
||||
self: Rc<Self>,
|
||||
project: Entity<Project>,
|
||||
cwd: &Path,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Task<Result<Entity<AcpThread>>>;
|
||||
|
||||
fn authenticate(&self, cx: &mut App) -> Task<Result<()>>;
|
||||
|
||||
fn prompt(&self, params: acp::PromptArguments, cx: &mut App) -> Task<Result<()>>;
|
||||
|
||||
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App);
|
||||
}
|
||||
|
||||
453
crates/acp_thread/src/old_acp_support.rs
Normal file
@@ -0,0 +1,453 @@
|
||||
// Translates old acp agents into the new schema
|
||||
use agent_client_protocol as acp;
|
||||
use agentic_coding_protocol::{self as acp_old, AgentRequest as _};
|
||||
use anyhow::{Context as _, Result};
|
||||
use futures::channel::oneshot;
|
||||
use gpui::{AppContext as _, AsyncApp, Entity, Task, WeakEntity};
|
||||
use project::Project;
|
||||
use std::{cell::RefCell, error::Error, fmt, path::Path, rc::Rc};
|
||||
use ui::App;
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::{AcpThread, AgentConnection};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct OldAcpClientDelegate {
|
||||
thread: Rc<RefCell<WeakEntity<AcpThread>>>,
|
||||
cx: AsyncApp,
|
||||
next_tool_call_id: Rc<RefCell<u64>>,
|
||||
// sent_buffer_versions: HashMap<Entity<Buffer>, HashMap<u64, BufferSnapshot>>,
|
||||
}
|
||||
|
||||
impl OldAcpClientDelegate {
|
||||
pub fn new(thread: Rc<RefCell<WeakEntity<AcpThread>>>, cx: AsyncApp) -> Self {
|
||||
Self {
|
||||
thread,
|
||||
cx,
|
||||
next_tool_call_id: Rc::new(RefCell::new(0)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl acp_old::Client for OldAcpClientDelegate {
|
||||
async fn stream_assistant_message_chunk(
|
||||
&self,
|
||||
params: acp_old::StreamAssistantMessageChunkParams,
|
||||
) -> Result<(), acp_old::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
|
||||
cx.update(|cx| {
|
||||
self.thread
|
||||
.borrow()
|
||||
.update(cx, |thread, cx| match params.chunk {
|
||||
acp_old::AssistantMessageChunk::Text { text } => {
|
||||
thread.push_assistant_content_block(text.into(), false, cx)
|
||||
}
|
||||
acp_old::AssistantMessageChunk::Thought { thought } => {
|
||||
thread.push_assistant_content_block(thought.into(), true, cx)
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn request_tool_call_confirmation(
|
||||
&self,
|
||||
request: acp_old::RequestToolCallConfirmationParams,
|
||||
) -> Result<acp_old::RequestToolCallConfirmationResponse, acp_old::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
|
||||
let old_acp_id = *self.next_tool_call_id.borrow() + 1;
|
||||
self.next_tool_call_id.replace(old_acp_id);
|
||||
|
||||
let tool_call = into_new_tool_call(
|
||||
acp::ToolCallId(old_acp_id.to_string().into()),
|
||||
request.tool_call,
|
||||
);
|
||||
|
||||
let mut options = match request.confirmation {
|
||||
acp_old::ToolCallConfirmation::Edit { .. } => vec![(
|
||||
acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
|
||||
acp::PermissionOptionKind::AllowAlways,
|
||||
"Always Allow Edits".to_string(),
|
||||
)],
|
||||
acp_old::ToolCallConfirmation::Execute { root_command, .. } => vec![(
|
||||
acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
|
||||
acp::PermissionOptionKind::AllowAlways,
|
||||
format!("Always Allow {}", root_command),
|
||||
)],
|
||||
acp_old::ToolCallConfirmation::Mcp {
|
||||
server_name,
|
||||
tool_name,
|
||||
..
|
||||
} => vec![
|
||||
(
|
||||
acp_old::ToolCallConfirmationOutcome::AlwaysAllowMcpServer,
|
||||
acp::PermissionOptionKind::AllowAlways,
|
||||
format!("Always Allow {}", server_name),
|
||||
),
|
||||
(
|
||||
acp_old::ToolCallConfirmationOutcome::AlwaysAllowTool,
|
||||
acp::PermissionOptionKind::AllowAlways,
|
||||
format!("Always Allow {}", tool_name),
|
||||
),
|
||||
],
|
||||
acp_old::ToolCallConfirmation::Fetch { .. } => vec![(
|
||||
acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
|
||||
acp::PermissionOptionKind::AllowAlways,
|
||||
"Always Allow".to_string(),
|
||||
)],
|
||||
acp_old::ToolCallConfirmation::Other { .. } => vec![(
|
||||
acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
|
||||
acp::PermissionOptionKind::AllowAlways,
|
||||
"Always Allow".to_string(),
|
||||
)],
|
||||
};
|
||||
|
||||
options.extend([
|
||||
(
|
||||
acp_old::ToolCallConfirmationOutcome::Allow,
|
||||
acp::PermissionOptionKind::AllowOnce,
|
||||
"Allow".to_string(),
|
||||
),
|
||||
(
|
||||
acp_old::ToolCallConfirmationOutcome::Reject,
|
||||
acp::PermissionOptionKind::RejectOnce,
|
||||
"Reject".to_string(),
|
||||
),
|
||||
]);
|
||||
|
||||
let mut outcomes = Vec::with_capacity(options.len());
|
||||
let mut acp_options = Vec::with_capacity(options.len());
|
||||
|
||||
for (index, (outcome, kind, label)) in options.into_iter().enumerate() {
|
||||
outcomes.push(outcome);
|
||||
acp_options.push(acp::PermissionOption {
|
||||
id: acp::PermissionOptionId(index.to_string().into()),
|
||||
label,
|
||||
kind,
|
||||
})
|
||||
}
|
||||
|
||||
let response = cx
|
||||
.update(|cx| {
|
||||
self.thread.borrow().update(cx, |thread, cx| {
|
||||
thread.request_tool_call_permission(tool_call, acp_options, cx)
|
||||
})
|
||||
})?
|
||||
.context("Failed to update thread")?
|
||||
.await;
|
||||
|
||||
let outcome = match response {
|
||||
Ok(option_id) => outcomes[option_id.0.parse::<usize>().unwrap_or(0)],
|
||||
Err(oneshot::Canceled) => acp_old::ToolCallConfirmationOutcome::Cancel,
|
||||
};
|
||||
|
||||
Ok(acp_old::RequestToolCallConfirmationResponse {
|
||||
id: acp_old::ToolCallId(old_acp_id),
|
||||
outcome: outcome,
|
||||
})
|
||||
}
|
||||
|
||||
async fn push_tool_call(
|
||||
&self,
|
||||
request: acp_old::PushToolCallParams,
|
||||
) -> Result<acp_old::PushToolCallResponse, acp_old::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
|
||||
let old_acp_id = *self.next_tool_call_id.borrow() + 1;
|
||||
self.next_tool_call_id.replace(old_acp_id);
|
||||
|
||||
cx.update(|cx| {
|
||||
self.thread.borrow().update(cx, |thread, cx| {
|
||||
thread.upsert_tool_call(
|
||||
into_new_tool_call(acp::ToolCallId(old_acp_id.to_string().into()), request),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})?
|
||||
.context("Failed to update thread")?;
|
||||
|
||||
Ok(acp_old::PushToolCallResponse {
|
||||
id: acp_old::ToolCallId(old_acp_id),
|
||||
})
|
||||
}
|
||||
|
||||
async fn update_tool_call(
|
||||
&self,
|
||||
request: acp_old::UpdateToolCallParams,
|
||||
) -> Result<(), acp_old::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
|
||||
cx.update(|cx| {
|
||||
self.thread.borrow().update(cx, |thread, cx| {
|
||||
thread.update_tool_call(
|
||||
acp::ToolCallUpdate {
|
||||
id: acp::ToolCallId(request.tool_call_id.0.to_string().into()),
|
||||
fields: acp::ToolCallUpdateFields {
|
||||
status: Some(into_new_tool_call_status(request.status)),
|
||||
content: Some(
|
||||
request
|
||||
.content
|
||||
.into_iter()
|
||||
.map(into_new_tool_call_content)
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})?
|
||||
.context("Failed to update thread")??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_plan(&self, request: acp_old::UpdatePlanParams) -> Result<(), acp_old::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
|
||||
cx.update(|cx| {
|
||||
self.thread.borrow().update(cx, |thread, cx| {
|
||||
thread.update_plan(
|
||||
acp::Plan {
|
||||
entries: request
|
||||
.entries
|
||||
.into_iter()
|
||||
.map(into_new_plan_entry)
|
||||
.collect(),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})?
|
||||
.context("Failed to update thread")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn read_text_file(
|
||||
&self,
|
||||
acp_old::ReadTextFileParams { path, line, limit }: acp_old::ReadTextFileParams,
|
||||
) -> Result<acp_old::ReadTextFileResponse, acp_old::Error> {
|
||||
let content = self
|
||||
.cx
|
||||
.update(|cx| {
|
||||
self.thread.borrow().update(cx, |thread, cx| {
|
||||
thread.read_text_file(path, line, limit, false, cx)
|
||||
})
|
||||
})?
|
||||
.context("Failed to update thread")?
|
||||
.await?;
|
||||
Ok(acp_old::ReadTextFileResponse { content })
|
||||
}
|
||||
|
||||
async fn write_text_file(
|
||||
&self,
|
||||
acp_old::WriteTextFileParams { path, content }: acp_old::WriteTextFileParams,
|
||||
) -> Result<(), acp_old::Error> {
|
||||
self.cx
|
||||
.update(|cx| {
|
||||
self.thread
|
||||
.borrow()
|
||||
.update(cx, |thread, cx| thread.write_text_file(path, content, cx))
|
||||
})?
|
||||
.context("Failed to update thread")?
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn into_new_tool_call(id: acp::ToolCallId, request: acp_old::PushToolCallParams) -> acp::ToolCall {
|
||||
acp::ToolCall {
|
||||
id: id,
|
||||
label: request.label,
|
||||
kind: acp_kind_from_old_icon(request.icon),
|
||||
status: acp::ToolCallStatus::InProgress,
|
||||
content: request
|
||||
.content
|
||||
.into_iter()
|
||||
.map(into_new_tool_call_content)
|
||||
.collect(),
|
||||
locations: request
|
||||
.locations
|
||||
.into_iter()
|
||||
.map(into_new_tool_call_location)
|
||||
.collect(),
|
||||
raw_input: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn acp_kind_from_old_icon(icon: acp_old::Icon) -> acp::ToolKind {
|
||||
match icon {
|
||||
acp_old::Icon::FileSearch => acp::ToolKind::Search,
|
||||
acp_old::Icon::Folder => acp::ToolKind::Search,
|
||||
acp_old::Icon::Globe => acp::ToolKind::Search,
|
||||
acp_old::Icon::Hammer => acp::ToolKind::Other,
|
||||
acp_old::Icon::LightBulb => acp::ToolKind::Think,
|
||||
acp_old::Icon::Pencil => acp::ToolKind::Edit,
|
||||
acp_old::Icon::Regex => acp::ToolKind::Search,
|
||||
acp_old::Icon::Terminal => acp::ToolKind::Execute,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_new_tool_call_status(status: acp_old::ToolCallStatus) -> acp::ToolCallStatus {
|
||||
match status {
|
||||
acp_old::ToolCallStatus::Running => acp::ToolCallStatus::InProgress,
|
||||
acp_old::ToolCallStatus::Finished => acp::ToolCallStatus::Completed,
|
||||
acp_old::ToolCallStatus::Error => acp::ToolCallStatus::Failed,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_new_tool_call_content(content: acp_old::ToolCallContent) -> acp::ToolCallContent {
|
||||
match content {
|
||||
acp_old::ToolCallContent::Markdown { markdown } => markdown.into(),
|
||||
acp_old::ToolCallContent::Diff { diff } => acp::ToolCallContent::Diff {
|
||||
diff: into_new_diff(diff),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn into_new_diff(diff: acp_old::Diff) -> acp::Diff {
|
||||
acp::Diff {
|
||||
path: diff.path,
|
||||
old_text: diff.old_text,
|
||||
new_text: diff.new_text,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_new_tool_call_location(location: acp_old::ToolCallLocation) -> acp::ToolCallLocation {
|
||||
acp::ToolCallLocation {
|
||||
path: location.path,
|
||||
line: location.line,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_new_plan_entry(entry: acp_old::PlanEntry) -> acp::PlanEntry {
|
||||
acp::PlanEntry {
|
||||
content: entry.content,
|
||||
priority: into_new_plan_priority(entry.priority),
|
||||
status: into_new_plan_status(entry.status),
|
||||
}
|
||||
}
|
||||
|
||||
fn into_new_plan_priority(priority: acp_old::PlanEntryPriority) -> acp::PlanEntryPriority {
|
||||
match priority {
|
||||
acp_old::PlanEntryPriority::Low => acp::PlanEntryPriority::Low,
|
||||
acp_old::PlanEntryPriority::Medium => acp::PlanEntryPriority::Medium,
|
||||
acp_old::PlanEntryPriority::High => acp::PlanEntryPriority::High,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_new_plan_status(status: acp_old::PlanEntryStatus) -> acp::PlanEntryStatus {
|
||||
match status {
|
||||
acp_old::PlanEntryStatus::Pending => acp::PlanEntryStatus::Pending,
|
||||
acp_old::PlanEntryStatus::InProgress => acp::PlanEntryStatus::InProgress,
|
||||
acp_old::PlanEntryStatus::Completed => acp::PlanEntryStatus::Completed,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Unauthenticated;
|
||||
|
||||
impl Error for Unauthenticated {}
|
||||
impl fmt::Display for Unauthenticated {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "Unauthenticated")
|
||||
}
|
||||
}
|
||||
|
||||
pub struct OldAcpAgentConnection {
|
||||
pub name: &'static str,
|
||||
pub connection: acp_old::AgentConnection,
|
||||
pub child_status: Task<Result<()>>,
|
||||
pub current_thread: Rc<RefCell<WeakEntity<AcpThread>>>,
|
||||
}
|
||||
|
||||
impl AgentConnection for OldAcpAgentConnection {
|
||||
fn name(&self) -> &'static str {
|
||||
self.name
|
||||
}
|
||||
|
||||
fn new_thread(
|
||||
self: Rc<Self>,
|
||||
project: Entity<Project>,
|
||||
_cwd: &Path,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Task<Result<Entity<AcpThread>>> {
|
||||
let task = self.connection.request_any(
|
||||
acp_old::InitializeParams {
|
||||
protocol_version: acp_old::ProtocolVersion::latest(),
|
||||
}
|
||||
.into_any(),
|
||||
);
|
||||
let current_thread = self.current_thread.clone();
|
||||
cx.spawn(async move |cx| {
|
||||
let result = task.await?;
|
||||
let result = acp_old::InitializeParams::response_from_any(result)?;
|
||||
|
||||
if !result.is_authenticated {
|
||||
anyhow::bail!(Unauthenticated)
|
||||
}
|
||||
|
||||
cx.update(|cx| {
|
||||
let thread = cx.new(|cx| {
|
||||
let session_id = acp::SessionId("acp-old-no-id".into());
|
||||
AcpThread::new(self.clone(), project, session_id, cx)
|
||||
});
|
||||
current_thread.replace(thread.downgrade());
|
||||
thread
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn authenticate(&self, cx: &mut App) -> Task<Result<()>> {
|
||||
let task = self
|
||||
.connection
|
||||
.request_any(acp_old::AuthenticateParams.into_any());
|
||||
cx.foreground_executor().spawn(async move {
|
||||
task.await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn prompt(&self, params: acp::PromptArguments, cx: &mut App) -> Task<Result<()>> {
|
||||
let chunks = params
|
||||
.prompt
|
||||
.into_iter()
|
||||
.filter_map(|block| match block {
|
||||
acp::ContentBlock::Text(text) => {
|
||||
Some(acp_old::UserMessageChunk::Text { text: text.text })
|
||||
}
|
||||
acp::ContentBlock::ResourceLink(link) => Some(acp_old::UserMessageChunk::Path {
|
||||
path: link.uri.into(),
|
||||
}),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let task = self
|
||||
.connection
|
||||
.request_any(acp_old::SendUserMessageParams { chunks }.into_any());
|
||||
cx.foreground_executor().spawn(async move {
|
||||
task.await?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn cancel(&self, _session_id: &acp::SessionId, cx: &mut App) {
|
||||
let task = self
|
||||
.connection
|
||||
.request_any(acp_old::CancelSendMessageParams.into_any());
|
||||
cx.foreground_executor()
|
||||
.spawn(async move {
|
||||
task.await?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx)
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ assistant_context.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
component.workspace = true
|
||||
context_server.workspace = true
|
||||
@@ -35,9 +36,9 @@ futures.workspace = true
|
||||
git.workspace = true
|
||||
gpui.workspace = true
|
||||
heed.workspace = true
|
||||
http_client.workspace = true
|
||||
icons.workspace = true
|
||||
indoc.workspace = true
|
||||
http_client.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
@@ -63,7 +64,6 @@ time.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
zstd.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -308,7 +308,12 @@ mod tests {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _input: &serde_json::Value, _cx: &App) -> bool {
|
||||
fn needs_confirmation(
|
||||
&self,
|
||||
_input: &serde_json::Value,
|
||||
_project: &Entity<Project>,
|
||||
_cx: &App,
|
||||
) -> bool {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ impl Tool for ContextServerTool {
|
||||
}
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +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 collections::HashMap;
|
||||
use feature_flags::{self, FeatureFlagAppExt};
|
||||
use futures::{FutureExt, StreamExt as _, future::Shared};
|
||||
@@ -47,9 +48,8 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use thiserror::Error;
|
||||
use util::{ResultExt as _, debug_panic, post_inc};
|
||||
use util::{ResultExt as _, post_inc};
|
||||
use uuid::Uuid;
|
||||
use zed_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit};
|
||||
|
||||
const MAX_RETRY_ATTEMPTS: u8 = 4;
|
||||
const BASE_RETRY_DELAY: Duration = Duration::from_secs(5);
|
||||
@@ -942,7 +942,7 @@ impl Thread {
|
||||
}
|
||||
|
||||
pub fn tool_uses_for_message(&self, id: MessageId, cx: &App) -> Vec<ToolUse> {
|
||||
self.tool_use.tool_uses_for_message(id, cx)
|
||||
self.tool_use.tool_uses_for_message(id, &self.project, cx)
|
||||
}
|
||||
|
||||
pub fn tool_results_for_message(
|
||||
@@ -1582,20 +1582,18 @@ impl Thread {
|
||||
model: Arc<dyn LanguageModel>,
|
||||
cx: &mut App,
|
||||
) -> Option<PendingToolUse> {
|
||||
let action_log = self.action_log.read(cx);
|
||||
// Represent notification as a simulated `project_notifications` tool call
|
||||
let tool_name = Arc::from("project_notifications");
|
||||
let tool = self.tools.read(cx).tool(&tool_name, cx)?;
|
||||
|
||||
if !action_log.has_unnotified_user_edits() {
|
||||
if !self.profile.is_tool_enabled(tool.source(), tool.name(), cx) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Represent notification as a simulated `project_notifications` tool call
|
||||
let tool_name = Arc::from("project_notifications");
|
||||
let Some(tool) = self.tools.read(cx).tool(&tool_name, cx) else {
|
||||
debug_panic!("`project_notifications` tool not found");
|
||||
return None;
|
||||
};
|
||||
|
||||
if !self.profile.is_tool_enabled(tool.source(), tool.name(), cx) {
|
||||
if self
|
||||
.action_log
|
||||
.update(cx, |log, cx| log.unnotified_user_edits(cx).is_none())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -1683,7 +1681,7 @@ impl Thread {
|
||||
|
||||
let completion_mode = request
|
||||
.mode
|
||||
.unwrap_or(zed_llm_client::CompletionMode::Normal);
|
||||
.unwrap_or(cloud_llm_client::CompletionMode::Normal);
|
||||
|
||||
self.last_received_chunk_at = Some(Instant::now());
|
||||
|
||||
@@ -2039,6 +2037,12 @@ impl Thread {
|
||||
if let Some(retry_strategy) =
|
||||
Thread::get_retry_strategy(completion_error)
|
||||
{
|
||||
log::info!(
|
||||
"Retrying with {:?} for language model completion error {:?}",
|
||||
retry_strategy,
|
||||
completion_error
|
||||
);
|
||||
|
||||
retry_scheduled = thread
|
||||
.handle_retryable_error_with_delay(
|
||||
&completion_error,
|
||||
@@ -2248,15 +2252,14 @@ impl Thread {
|
||||
..
|
||||
}
|
||||
| AuthenticationError { .. }
|
||||
| PermissionError { .. } => None,
|
||||
// These errors might be transient, so retry them
|
||||
SerializeRequest { .. }
|
||||
| BuildRequestBody { .. }
|
||||
| PromptTooLarge { .. }
|
||||
| PermissionError { .. }
|
||||
| NoApiKey { .. }
|
||||
| ApiEndpointNotFound { .. }
|
||||
| NoApiKey { .. } => Some(RetryStrategy::Fixed {
|
||||
| PromptTooLarge { .. } => None,
|
||||
// These errors might be transient, so retry them
|
||||
SerializeRequest { .. } | BuildRequestBody { .. } => Some(RetryStrategy::Fixed {
|
||||
delay: BASE_RETRY_DELAY,
|
||||
max_attempts: 2,
|
||||
max_attempts: 1,
|
||||
}),
|
||||
// Retry all other 4xx and 5xx errors once.
|
||||
HttpResponseError { status_code, .. }
|
||||
@@ -2554,7 +2557,7 @@ impl Thread {
|
||||
return self.handle_hallucinated_tool_use(tool_use.id, tool_use.name, window, cx);
|
||||
}
|
||||
|
||||
if tool.needs_confirmation(&tool_use.input, cx)
|
||||
if tool.needs_confirmation(&tool_use.input, &self.project, cx)
|
||||
&& !AgentSettings::get_global(cx).always_allow_tool_actions
|
||||
{
|
||||
self.tool_use.confirm_tool_use(
|
||||
@@ -5492,7 +5495,7 @@ fn main() {{
|
||||
let thread = thread_store.update(cx, |store, cx| store.create_thread(cx));
|
||||
let context_store = cx.new(|_cx| ContextStore::new(project.downgrade(), None));
|
||||
|
||||
let provider = Arc::new(FakeLanguageModelProvider);
|
||||
let provider = Arc::new(FakeLanguageModelProvider::default());
|
||||
let model = provider.test_model();
|
||||
let model: Arc<dyn LanguageModel> = Arc::new(model);
|
||||
|
||||
|
||||
@@ -41,6 +41,9 @@ use std::{
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
|
||||
pub static ZED_STATELESS: std::sync::LazyLock<bool> =
|
||||
std::sync::LazyLock::new(|| std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty()));
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum DataType {
|
||||
#[serde(rename = "json")]
|
||||
@@ -874,7 +877,11 @@ impl ThreadsDatabase {
|
||||
|
||||
let needs_migration_from_heed = mdb_path.exists();
|
||||
|
||||
let connection = Connection::open_file(&sqlite_path.to_string_lossy());
|
||||
let connection = if *ZED_STATELESS {
|
||||
Connection::open_memory(Some("THREAD_FALLBACK_DB"))
|
||||
} else {
|
||||
Connection::open_file(&sqlite_path.to_string_lossy())
|
||||
};
|
||||
|
||||
connection.exec(indoc! {"
|
||||
CREATE TABLE IF NOT EXISTS threads (
|
||||
|
||||
@@ -165,7 +165,12 @@ impl ToolUseState {
|
||||
self.pending_tool_uses_by_id.values().collect()
|
||||
}
|
||||
|
||||
pub fn tool_uses_for_message(&self, id: MessageId, cx: &App) -> Vec<ToolUse> {
|
||||
pub fn tool_uses_for_message(
|
||||
&self,
|
||||
id: MessageId,
|
||||
project: &Entity<Project>,
|
||||
cx: &App,
|
||||
) -> Vec<ToolUse> {
|
||||
let Some(tool_uses_for_message) = &self.tool_uses_by_assistant_message.get(&id) else {
|
||||
return Vec::new();
|
||||
};
|
||||
@@ -211,7 +216,10 @@ impl ToolUseState {
|
||||
|
||||
let (icon, needs_confirmation) =
|
||||
if let Some(tool) = self.tools.read(cx).tool(&tool_use.name, cx) {
|
||||
(tool.icon(), tool.needs_confirmation(&tool_use.input, cx))
|
||||
(
|
||||
tool.icon(),
|
||||
tool.needs_confirmation(&tool_use.input, project, cx),
|
||||
)
|
||||
} else {
|
||||
(IconName::Cog, false)
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@ doctest = false
|
||||
|
||||
[dependencies]
|
||||
acp_thread.workspace = true
|
||||
agent-client-protocol.workspace = true
|
||||
agentic-coding-protocol.workspace = true
|
||||
anyhow.workspace = true
|
||||
collections.workspace = true
|
||||
@@ -28,6 +29,7 @@ itertools.workspace = true
|
||||
log.workspace = true
|
||||
paths.workspace = true
|
||||
project.workspace = true
|
||||
rand.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
@@ -39,6 +41,7 @@ ui.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
watch.workspace = true
|
||||
indoc.workspace = true
|
||||
which.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
mod claude;
|
||||
mod codex;
|
||||
mod gemini;
|
||||
mod mcp_server;
|
||||
mod settings;
|
||||
mod stdio_agent_server;
|
||||
|
||||
#[cfg(test)]
|
||||
mod e2e_tests;
|
||||
|
||||
pub use claude::*;
|
||||
pub use codex::*;
|
||||
pub use gemini::*;
|
||||
pub use settings::*;
|
||||
pub use stdio_agent_server::*;
|
||||
|
||||
use acp_thread::AcpThread;
|
||||
use acp_thread::AgentConnection;
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use gpui::{App, AsyncApp, Entity, SharedString, Task};
|
||||
@@ -20,6 +21,7 @@ use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
@@ -33,14 +35,14 @@ pub trait AgentServer: Send {
|
||||
fn name(&self) -> &'static str;
|
||||
fn empty_state_headline(&self) -> &'static str;
|
||||
fn empty_state_message(&self) -> &'static str;
|
||||
fn supports_always_allow(&self) -> bool;
|
||||
|
||||
fn new_thread(
|
||||
fn connect(
|
||||
&self,
|
||||
// these will go away when old_acp is fully removed
|
||||
root_dir: &Path,
|
||||
project: &Entity<Project>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Entity<AcpThread>>>;
|
||||
) -> Task<Result<Rc<dyn AgentConnection>>>;
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for AgentServerCommand {
|
||||
|
||||
@@ -1,39 +1,35 @@
|
||||
mod mcp_server;
|
||||
mod tools;
|
||||
pub mod tools;
|
||||
|
||||
use collections::HashMap;
|
||||
use context_server::listener::McpServerTool;
|
||||
use project::Project;
|
||||
use settings::SettingsStore;
|
||||
use smol::process::Child;
|
||||
use std::cell::RefCell;
|
||||
use std::fmt::Display;
|
||||
use std::path::Path;
|
||||
use std::pin::pin;
|
||||
use std::rc::Rc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use agentic_coding_protocol::{
|
||||
self as acp, AnyAgentRequest, AnyAgentResult, Client, ProtocolVersion,
|
||||
StreamAssistantMessageChunkParams, ToolCallContent, UpdateToolCallParams,
|
||||
};
|
||||
use agent_client_protocol as acp;
|
||||
use anyhow::{Result, anyhow};
|
||||
use futures::channel::oneshot;
|
||||
use futures::future::LocalBoxFuture;
|
||||
use futures::{AsyncBufReadExt, AsyncWriteExt, SinkExt};
|
||||
use futures::{AsyncBufReadExt, AsyncWriteExt};
|
||||
use futures::{
|
||||
AsyncRead, AsyncWrite, FutureExt, StreamExt,
|
||||
channel::mpsc::{self, UnboundedReceiver, UnboundedSender},
|
||||
io::BufReader,
|
||||
select_biased,
|
||||
};
|
||||
use gpui::{App, AppContext, Entity, Task};
|
||||
use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::claude::mcp_server::ClaudeMcpServer;
|
||||
use crate::claude::mcp_server::{ClaudeZedMcpServer, McpConfig};
|
||||
use crate::claude::tools::ClaudeTool;
|
||||
use crate::{AgentServer, AgentServerCommand, AllAgentServersSettings};
|
||||
use acp_thread::{AcpClientDelegate, AcpThread, AgentConnection};
|
||||
use acp_thread::{AcpThread, AgentConnection};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ClaudeCode;
|
||||
@@ -48,36 +44,51 @@ impl AgentServer for ClaudeCode {
|
||||
}
|
||||
|
||||
fn empty_state_message(&self) -> &'static str {
|
||||
""
|
||||
"How can I help you today?"
|
||||
}
|
||||
|
||||
fn logo(&self) -> ui::IconName {
|
||||
ui::IconName::AiClaude
|
||||
}
|
||||
|
||||
fn supports_always_allow(&self) -> bool {
|
||||
false
|
||||
fn connect(
|
||||
&self,
|
||||
_root_dir: &Path,
|
||||
_project: &Entity<Project>,
|
||||
_cx: &mut App,
|
||||
) -> Task<Result<Rc<dyn AgentConnection>>> {
|
||||
let connection = ClaudeAgentConnection {
|
||||
sessions: Default::default(),
|
||||
};
|
||||
|
||||
Task::ready(Ok(Rc::new(connection) as _))
|
||||
}
|
||||
}
|
||||
|
||||
struct ClaudeAgentConnection {
|
||||
sessions: Rc<RefCell<HashMap<acp::SessionId, ClaudeAgentSession>>>,
|
||||
}
|
||||
|
||||
impl AgentConnection for ClaudeAgentConnection {
|
||||
fn name(&self) -> &'static str {
|
||||
ClaudeCode.name()
|
||||
}
|
||||
|
||||
fn new_thread(
|
||||
&self,
|
||||
root_dir: &Path,
|
||||
project: &Entity<Project>,
|
||||
cx: &mut App,
|
||||
self: Rc<Self>,
|
||||
project: Entity<Project>,
|
||||
cwd: &Path,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Task<Result<Entity<AcpThread>>> {
|
||||
let project = project.clone();
|
||||
let root_dir = root_dir.to_path_buf();
|
||||
let title = self.name().into();
|
||||
let cwd = cwd.to_owned();
|
||||
cx.spawn(async move |cx| {
|
||||
let (mut delegate_tx, delegate_rx) = watch::channel(None);
|
||||
let tool_id_map = Rc::new(RefCell::new(HashMap::default()));
|
||||
|
||||
let mcp_server = ClaudeMcpServer::new(delegate_rx, tool_id_map.clone(), cx).await?;
|
||||
let (mut thread_tx, thread_rx) = watch::channel(WeakEntity::new_invalid());
|
||||
let permission_mcp_server = ClaudeZedMcpServer::new(thread_rx.clone(), cx).await?;
|
||||
|
||||
let mut mcp_servers = HashMap::default();
|
||||
mcp_servers.insert(
|
||||
mcp_server::SERVER_NAME.to_string(),
|
||||
mcp_server.server_config()?,
|
||||
permission_mcp_server.server_config()?,
|
||||
);
|
||||
let mcp_config = McpConfig { mcp_servers };
|
||||
|
||||
@@ -102,192 +113,158 @@ impl AgentServer for ClaudeCode {
|
||||
|
||||
let (incoming_message_tx, mut incoming_message_rx) = mpsc::unbounded();
|
||||
let (outgoing_tx, outgoing_rx) = mpsc::unbounded();
|
||||
let (cancel_tx, mut cancel_rx) = mpsc::unbounded::<oneshot::Sender<Result<()>>>();
|
||||
|
||||
let session_id = Uuid::new_v4();
|
||||
let session_id = acp::SessionId(Uuid::new_v4().to_string().into());
|
||||
|
||||
log::trace!("Starting session with id: {}", session_id);
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let mut outgoing_rx = Some(outgoing_rx);
|
||||
let mut mode = ClaudeSessionMode::Start;
|
||||
cx.background_spawn({
|
||||
let session_id = session_id.clone();
|
||||
async move {
|
||||
let mut outgoing_rx = Some(outgoing_rx);
|
||||
|
||||
loop {
|
||||
let mut child =
|
||||
spawn_claude(&command, mode, session_id, &mcp_config_path, &root_dir)
|
||||
.await?;
|
||||
mode = ClaudeSessionMode::Resume;
|
||||
let mut child = spawn_claude(
|
||||
&command,
|
||||
ClaudeSessionMode::Start,
|
||||
session_id.clone(),
|
||||
&mcp_config_path,
|
||||
&cwd,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let pid = child.id();
|
||||
log::trace!("Spawned (pid: {})", pid);
|
||||
|
||||
let mut io_fut = pin!(
|
||||
ClaudeAgentConnection::handle_io(
|
||||
outgoing_rx.take().unwrap(),
|
||||
incoming_message_tx.clone(),
|
||||
child.stdin.take().unwrap(),
|
||||
child.stdout.take().unwrap(),
|
||||
)
|
||||
.fuse()
|
||||
);
|
||||
|
||||
select_biased! {
|
||||
done_tx = cancel_rx.next() => {
|
||||
if let Some(done_tx) = done_tx {
|
||||
log::trace!("Interrupted (pid: {})", pid);
|
||||
let result = send_interrupt(pid as i32);
|
||||
outgoing_rx.replace(io_fut.await?);
|
||||
done_tx.send(result).log_err();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
result = io_fut => {
|
||||
result?;
|
||||
}
|
||||
}
|
||||
ClaudeAgentSession::handle_io(
|
||||
outgoing_rx.take().unwrap(),
|
||||
incoming_message_tx.clone(),
|
||||
child.stdin.take().unwrap(),
|
||||
child.stdout.take().unwrap(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
log::trace!("Stopped (pid: {})", pid);
|
||||
break;
|
||||
}
|
||||
|
||||
drop(mcp_config_path);
|
||||
anyhow::Ok(())
|
||||
drop(mcp_config_path);
|
||||
anyhow::Ok(())
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.new(|cx| {
|
||||
let end_turn_tx = Rc::new(RefCell::new(None));
|
||||
let delegate = AcpClientDelegate::new(cx.entity().downgrade(), cx.to_async());
|
||||
delegate_tx.send(Some(delegate.clone())).log_err();
|
||||
|
||||
let handler_task = cx.foreground_executor().spawn({
|
||||
let end_turn_tx = end_turn_tx.clone();
|
||||
let tool_id_map = tool_id_map.clone();
|
||||
let delegate = delegate.clone();
|
||||
async move {
|
||||
while let Some(message) = incoming_message_rx.next().await {
|
||||
ClaudeAgentConnection::handle_message(
|
||||
delegate.clone(),
|
||||
message,
|
||||
end_turn_tx.clone(),
|
||||
tool_id_map.clone(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
let end_turn_tx = Rc::new(RefCell::new(None));
|
||||
let handler_task = cx.spawn({
|
||||
let end_turn_tx = end_turn_tx.clone();
|
||||
let thread_rx = thread_rx.clone();
|
||||
async move |cx| {
|
||||
while let Some(message) = incoming_message_rx.next().await {
|
||||
ClaudeAgentSession::handle_message(
|
||||
thread_rx.clone(),
|
||||
message,
|
||||
end_turn_tx.clone(),
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let mut connection = ClaudeAgentConnection {
|
||||
delegate,
|
||||
outgoing_tx,
|
||||
end_turn_tx,
|
||||
cancel_tx,
|
||||
session_id,
|
||||
_handler_task: handler_task,
|
||||
_mcp_server: None,
|
||||
};
|
||||
let thread =
|
||||
cx.new(|cx| AcpThread::new(self.clone(), project, session_id.clone(), cx))?;
|
||||
|
||||
connection._mcp_server = Some(mcp_server);
|
||||
acp_thread::AcpThread::new(connection, title, None, project.clone(), cx)
|
||||
})
|
||||
thread_tx.send(thread.downgrade())?;
|
||||
|
||||
let session = ClaudeAgentSession {
|
||||
outgoing_tx,
|
||||
end_turn_tx,
|
||||
_handler_task: handler_task,
|
||||
_mcp_server: Some(permission_mcp_server),
|
||||
};
|
||||
|
||||
self.sessions.borrow_mut().insert(session_id, session);
|
||||
|
||||
Ok(thread)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn send_interrupt(pid: libc::pid_t) -> anyhow::Result<()> {
|
||||
let pid = nix::unistd::Pid::from_raw(pid);
|
||||
fn authenticate(&self, _cx: &mut App) -> Task<Result<()>> {
|
||||
Task::ready(Err(anyhow!("Authentication not supported")))
|
||||
}
|
||||
|
||||
nix::sys::signal::kill(pid, nix::sys::signal::SIGINT)
|
||||
.map_err(|e| anyhow!("Failed to interrupt process: {}", e))
|
||||
}
|
||||
fn prompt(&self, params: acp::PromptArguments, cx: &mut App) -> Task<Result<()>> {
|
||||
let sessions = self.sessions.borrow();
|
||||
let Some(session) = sessions.get(¶ms.session_id) else {
|
||||
return Task::ready(Err(anyhow!(
|
||||
"Attempted to send message to nonexistent session {}",
|
||||
params.session_id
|
||||
)));
|
||||
};
|
||||
|
||||
#[cfg(windows)]
|
||||
fn send_interrupt(_pid: i32) -> anyhow::Result<()> {
|
||||
panic!("Cancel not implemented on Windows")
|
||||
}
|
||||
let (tx, rx) = oneshot::channel();
|
||||
session.end_turn_tx.borrow_mut().replace(tx);
|
||||
|
||||
impl AgentConnection for ClaudeAgentConnection {
|
||||
/// Send a request to the agent and wait for a response.
|
||||
fn request_any(
|
||||
&self,
|
||||
params: AnyAgentRequest,
|
||||
) -> LocalBoxFuture<'static, Result<acp::AnyAgentResult>> {
|
||||
let delegate = self.delegate.clone();
|
||||
let end_turn_tx = self.end_turn_tx.clone();
|
||||
let outgoing_tx = self.outgoing_tx.clone();
|
||||
let mut cancel_tx = self.cancel_tx.clone();
|
||||
let session_id = self.session_id;
|
||||
async move {
|
||||
match params {
|
||||
// todo: consider sending an empty request so we get the init response?
|
||||
AnyAgentRequest::InitializeParams(_) => Ok(AnyAgentResult::InitializeResponse(
|
||||
acp::InitializeResponse {
|
||||
is_authenticated: true,
|
||||
protocol_version: ProtocolVersion::latest(),
|
||||
},
|
||||
)),
|
||||
AnyAgentRequest::AuthenticateParams(_) => {
|
||||
Err(anyhow!("Authentication not supported"))
|
||||
let mut content = String::new();
|
||||
for chunk in params.prompt {
|
||||
match chunk {
|
||||
acp::ContentBlock::Text(text_content) => {
|
||||
content.push_str(&text_content.text);
|
||||
}
|
||||
AnyAgentRequest::SendUserMessageParams(message) => {
|
||||
delegate.clear_completed_plan_entries().await?;
|
||||
|
||||
let (tx, rx) = oneshot::channel();
|
||||
end_turn_tx.borrow_mut().replace(tx);
|
||||
let mut content = String::new();
|
||||
for chunk in message.chunks {
|
||||
match chunk {
|
||||
agentic_coding_protocol::UserMessageChunk::Text { text } => {
|
||||
content.push_str(&text)
|
||||
}
|
||||
agentic_coding_protocol::UserMessageChunk::Path { path } => {
|
||||
content.push_str(&format!("@{path:?}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
outgoing_tx.unbounded_send(SdkMessage::User {
|
||||
message: Message {
|
||||
role: Role::User,
|
||||
content: Content::UntaggedText(content),
|
||||
id: None,
|
||||
model: None,
|
||||
stop_reason: None,
|
||||
stop_sequence: None,
|
||||
usage: None,
|
||||
},
|
||||
session_id: Some(session_id),
|
||||
})?;
|
||||
rx.await??;
|
||||
Ok(AnyAgentResult::SendUserMessageResponse(
|
||||
acp::SendUserMessageResponse,
|
||||
))
|
||||
acp::ContentBlock::ResourceLink(resource_link) => {
|
||||
content.push_str(&format!("@{}", resource_link.uri));
|
||||
}
|
||||
AnyAgentRequest::CancelSendMessageParams(_) => {
|
||||
let (done_tx, done_rx) = oneshot::channel();
|
||||
cancel_tx.send(done_tx).await?;
|
||||
done_rx.await??;
|
||||
|
||||
Ok(AnyAgentResult::CancelSendMessageResponse(
|
||||
acp::CancelSendMessageResponse,
|
||||
))
|
||||
acp::ContentBlock::Audio(_)
|
||||
| acp::ContentBlock::Image(_)
|
||||
| acp::ContentBlock::Resource(_) => {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
}
|
||||
.boxed_local()
|
||||
|
||||
if let Err(err) = session.outgoing_tx.unbounded_send(SdkMessage::User {
|
||||
message: Message {
|
||||
role: Role::User,
|
||||
content: Content::UntaggedText(content),
|
||||
id: None,
|
||||
model: None,
|
||||
stop_reason: None,
|
||||
stop_sequence: None,
|
||||
usage: None,
|
||||
},
|
||||
session_id: Some(params.session_id.to_string()),
|
||||
}) {
|
||||
return Task::ready(Err(anyhow!(err)));
|
||||
}
|
||||
|
||||
cx.foreground_executor().spawn(async move {
|
||||
rx.await??;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) {
|
||||
let sessions = self.sessions.borrow();
|
||||
let Some(session) = sessions.get(&session_id) else {
|
||||
log::warn!("Attempted to cancel nonexistent session {}", session_id);
|
||||
return;
|
||||
};
|
||||
|
||||
session
|
||||
.outgoing_tx
|
||||
.unbounded_send(SdkMessage::new_interrupt_message())
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum ClaudeSessionMode {
|
||||
Start,
|
||||
#[expect(dead_code)]
|
||||
Resume,
|
||||
}
|
||||
|
||||
async fn spawn_claude(
|
||||
command: &AgentServerCommand,
|
||||
mode: ClaudeSessionMode,
|
||||
session_id: Uuid,
|
||||
session_id: acp::SessionId,
|
||||
mcp_config_path: &Path,
|
||||
root_dir: &Path,
|
||||
) -> Result<Child> {
|
||||
@@ -305,10 +282,16 @@ async fn spawn_claude(
|
||||
&format!(
|
||||
"mcp__{}__{}",
|
||||
mcp_server::SERVER_NAME,
|
||||
mcp_server::PERMISSION_TOOL
|
||||
mcp_server::PermissionTool::NAME,
|
||||
),
|
||||
"--allowedTools",
|
||||
"mcp__zed__Read,mcp__zed__Edit",
|
||||
&format!(
|
||||
"mcp__{}__{},mcp__{}__{}",
|
||||
mcp_server::SERVER_NAME,
|
||||
mcp_server::EditTool::NAME,
|
||||
mcp_server::SERVER_NAME,
|
||||
mcp_server::ReadTool::NAME
|
||||
),
|
||||
"--disallowedTools",
|
||||
"Read,Edit",
|
||||
])
|
||||
@@ -327,105 +310,135 @@ async fn spawn_claude(
|
||||
Ok(child)
|
||||
}
|
||||
|
||||
struct ClaudeAgentConnection {
|
||||
delegate: AcpClientDelegate,
|
||||
session_id: Uuid,
|
||||
struct ClaudeAgentSession {
|
||||
outgoing_tx: UnboundedSender<SdkMessage>,
|
||||
end_turn_tx: Rc<RefCell<Option<oneshot::Sender<Result<()>>>>>,
|
||||
cancel_tx: UnboundedSender<oneshot::Sender<Result<()>>>,
|
||||
_mcp_server: Option<ClaudeMcpServer>,
|
||||
_mcp_server: Option<ClaudeZedMcpServer>,
|
||||
_handler_task: Task<()>,
|
||||
}
|
||||
|
||||
impl ClaudeAgentConnection {
|
||||
impl ClaudeAgentSession {
|
||||
async fn handle_message(
|
||||
delegate: AcpClientDelegate,
|
||||
mut thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
|
||||
message: SdkMessage,
|
||||
end_turn_tx: Rc<RefCell<Option<oneshot::Sender<Result<()>>>>>,
|
||||
tool_id_map: Rc<RefCell<HashMap<String, acp::ToolCallId>>>,
|
||||
cx: &mut AsyncApp,
|
||||
) {
|
||||
match message {
|
||||
SdkMessage::Assistant { message, .. } | SdkMessage::User { message, .. } => {
|
||||
// we should only be sending these out, they don't need to be in the thread
|
||||
SdkMessage::ControlRequest { .. } => {}
|
||||
SdkMessage::Assistant {
|
||||
message,
|
||||
session_id: _,
|
||||
}
|
||||
| SdkMessage::User {
|
||||
message,
|
||||
session_id: _,
|
||||
} => {
|
||||
let Some(thread) = thread_rx
|
||||
.recv()
|
||||
.await
|
||||
.log_err()
|
||||
.and_then(|entity| entity.upgrade())
|
||||
else {
|
||||
log::error!("Received an SDK message but thread is gone");
|
||||
return;
|
||||
};
|
||||
|
||||
for chunk in message.content.chunks() {
|
||||
match chunk {
|
||||
ContentChunk::Text { text } | ContentChunk::UntaggedText(text) => {
|
||||
delegate
|
||||
.stream_assistant_message_chunk(StreamAssistantMessageChunkParams {
|
||||
chunk: acp::AssistantMessageChunk::Text { text },
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.push_assistant_content_block(text.into(), false, cx)
|
||||
})
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
ContentChunk::ToolUse { id, name, input } => {
|
||||
let claude_tool = ClaudeTool::infer(&name, input);
|
||||
|
||||
if let ClaudeTool::TodoWrite(Some(params)) = claude_tool {
|
||||
delegate
|
||||
.update_plan(acp::UpdatePlanParams {
|
||||
entries: params.todos.into_iter().map(Into::into).collect(),
|
||||
})
|
||||
.await
|
||||
.log_err();
|
||||
} else if let Some(resp) = delegate
|
||||
.push_tool_call(claude_tool.as_acp())
|
||||
.await
|
||||
.log_err()
|
||||
{
|
||||
tool_id_map.borrow_mut().insert(id, resp.id);
|
||||
}
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
if let ClaudeTool::TodoWrite(Some(params)) = claude_tool {
|
||||
thread.update_plan(
|
||||
acp::Plan {
|
||||
entries: params
|
||||
.todos
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect(),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
thread.upsert_tool_call(
|
||||
claude_tool.as_acp(acp::ToolCallId(id.into())),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
ContentChunk::ToolResult {
|
||||
content,
|
||||
tool_use_id,
|
||||
} => {
|
||||
let id = tool_id_map.borrow_mut().remove(&tool_use_id);
|
||||
if let Some(id) = id {
|
||||
let content = content.to_string();
|
||||
delegate
|
||||
.update_tool_call(UpdateToolCallParams {
|
||||
tool_call_id: id,
|
||||
status: acp::ToolCallStatus::Finished,
|
||||
// Don't unset existing content
|
||||
content: (!content.is_empty()).then_some(
|
||||
ToolCallContent::Markdown {
|
||||
// For now we only include text content
|
||||
markdown: content,
|
||||
let content = content.to_string();
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.update_tool_call(
|
||||
acp::ToolCallUpdate {
|
||||
id: acp::ToolCallId(tool_use_id.into()),
|
||||
fields: acp::ToolCallUpdateFields {
|
||||
status: Some(acp::ToolCallStatus::Completed),
|
||||
content: (!content.is_empty())
|
||||
.then(|| vec![content.into()]),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
})
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
ContentChunk::Image
|
||||
| ContentChunk::Document
|
||||
| ContentChunk::Thinking
|
||||
| ContentChunk::RedactedThinking
|
||||
| ContentChunk::WebSearchToolResult => {
|
||||
delegate
|
||||
.stream_assistant_message_chunk(StreamAssistantMessageChunkParams {
|
||||
chunk: acp::AssistantMessageChunk::Text {
|
||||
text: format!("Unsupported content: {:?}", chunk),
|
||||
},
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.push_assistant_content_block(
|
||||
format!("Unsupported content: {:?}", chunk).into(),
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SdkMessage::Result {
|
||||
is_error, subtype, ..
|
||||
is_error,
|
||||
subtype,
|
||||
result,
|
||||
..
|
||||
} => {
|
||||
if let Some(end_turn_tx) = end_turn_tx.borrow_mut().take() {
|
||||
if is_error {
|
||||
end_turn_tx.send(Err(anyhow!("Error: {subtype}"))).ok();
|
||||
end_turn_tx
|
||||
.send(Err(anyhow!(
|
||||
"Error: {}",
|
||||
result.unwrap_or_else(|| subtype.to_string())
|
||||
)))
|
||||
.ok();
|
||||
} else {
|
||||
end_turn_tx.send(Ok(())).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
SdkMessage::System { .. } => {}
|
||||
SdkMessage::System { .. } | SdkMessage::ControlResponse { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -592,16 +605,14 @@ enum SdkMessage {
|
||||
Assistant {
|
||||
message: Message, // from Anthropic SDK
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
session_id: Option<Uuid>,
|
||||
session_id: Option<String>,
|
||||
},
|
||||
|
||||
// A user message
|
||||
User {
|
||||
message: Message, // from Anthropic SDK
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
session_id: Option<Uuid>,
|
||||
session_id: Option<String>,
|
||||
},
|
||||
|
||||
// Emitted as the last message in a conversation
|
||||
Result {
|
||||
subtype: ResultErrorType,
|
||||
@@ -626,6 +637,26 @@ enum SdkMessage {
|
||||
#[serde(rename = "permissionMode")]
|
||||
permission_mode: PermissionMode,
|
||||
},
|
||||
/// Messages used to control the conversation, outside of chat messages to the model
|
||||
ControlRequest {
|
||||
request_id: String,
|
||||
request: ControlRequest,
|
||||
},
|
||||
/// Response to a control request
|
||||
ControlResponse { response: ControlResponse },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "subtype", rename_all = "snake_case")]
|
||||
enum ControlRequest {
|
||||
/// Cancel the current conversation
|
||||
Interrupt,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct ControlResponse {
|
||||
request_id: String,
|
||||
subtype: ResultErrorType,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -646,6 +677,24 @@ impl Display for ResultErrorType {
|
||||
}
|
||||
}
|
||||
|
||||
impl SdkMessage {
|
||||
fn new_interrupt_message() -> Self {
|
||||
use rand::Rng;
|
||||
// In the Claude Code TS SDK they just generate a random 12 character string,
|
||||
// `Math.random().toString(36).substring(2, 15)`
|
||||
let request_id = rand::thread_rng()
|
||||
.sample_iter(&rand::distributions::Alphanumeric)
|
||||
.take(12)
|
||||
.map(char::from)
|
||||
.collect();
|
||||
|
||||
Self::ControlRequest {
|
||||
request_id,
|
||||
request: ControlRequest::Interrupt,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct McpServer {
|
||||
name: String,
|
||||
@@ -661,27 +710,12 @@ enum PermissionMode {
|
||||
Plan,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct McpConfig {
|
||||
mcp_servers: HashMap<String, McpServerConfig>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct McpServerConfig {
|
||||
command: String,
|
||||
args: Vec<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
env: Option<HashMap<String, String>>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
crate::common_e2e_tests!(ClaudeCode);
|
||||
crate::common_e2e_tests!(ClaudeCode, allow_option_id = "allow");
|
||||
|
||||
pub fn local_command() -> AgentServerCommand {
|
||||
AgentServerCommand {
|
||||
|
||||
@@ -1,78 +1,53 @@
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use acp_thread::AcpClientDelegate;
|
||||
use agentic_coding_protocol::{self as acp, Client, ReadTextFileParams, WriteTextFileParams};
|
||||
use crate::claude::tools::{ClaudeTool, EditToolParams, ReadToolParams};
|
||||
use acp_thread::AcpThread;
|
||||
use agent_client_protocol as acp;
|
||||
use anyhow::{Context, Result};
|
||||
use collections::HashMap;
|
||||
use context_server::{
|
||||
listener::McpServer,
|
||||
types::{
|
||||
CallToolParams, CallToolResponse, Implementation, InitializeParams, InitializeResponse,
|
||||
ListToolsResponse, ProtocolVersion, ServerCapabilities, Tool, ToolAnnotations,
|
||||
ToolResponseContent, ToolsCapabilities, requests,
|
||||
},
|
||||
use context_server::listener::{McpServerTool, ToolResponse};
|
||||
use context_server::types::{
|
||||
Implementation, InitializeParams, InitializeResponse, ProtocolVersion, ServerCapabilities,
|
||||
ToolAnnotations, ToolResponseContent, ToolsCapabilities, requests,
|
||||
};
|
||||
use gpui::{App, AsyncApp, Task};
|
||||
use gpui::{App, AsyncApp, Task, WeakEntity};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use util::debug_panic;
|
||||
|
||||
use crate::claude::{
|
||||
McpServerConfig,
|
||||
tools::{ClaudeTool, EditToolParams, EditToolResponse, ReadToolParams, ReadToolResponse},
|
||||
};
|
||||
|
||||
pub struct ClaudeMcpServer {
|
||||
server: McpServer,
|
||||
pub struct ClaudeZedMcpServer {
|
||||
server: context_server::listener::McpServer,
|
||||
}
|
||||
|
||||
pub const SERVER_NAME: &str = "zed";
|
||||
pub const READ_TOOL: &str = "Read";
|
||||
pub const EDIT_TOOL: &str = "Edit";
|
||||
pub const PERMISSION_TOOL: &str = "Confirmation";
|
||||
|
||||
#[derive(Deserialize, JsonSchema, Debug)]
|
||||
struct PermissionToolParams {
|
||||
tool_name: String,
|
||||
input: serde_json::Value,
|
||||
tool_use_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct PermissionToolResponse {
|
||||
behavior: PermissionToolBehavior,
|
||||
updated_input: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum PermissionToolBehavior {
|
||||
Allow,
|
||||
Deny,
|
||||
}
|
||||
|
||||
impl ClaudeMcpServer {
|
||||
impl ClaudeZedMcpServer {
|
||||
pub async fn new(
|
||||
delegate: watch::Receiver<Option<AcpClientDelegate>>,
|
||||
tool_id_map: Rc<RefCell<HashMap<String, acp::ToolCallId>>>,
|
||||
thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
|
||||
cx: &AsyncApp,
|
||||
) -> Result<Self> {
|
||||
let mut mcp_server = McpServer::new(cx).await?;
|
||||
let mut mcp_server = context_server::listener::McpServer::new(cx).await?;
|
||||
mcp_server.handle_request::<requests::Initialize>(Self::handle_initialize);
|
||||
mcp_server.handle_request::<requests::ListTools>(Self::handle_list_tools);
|
||||
mcp_server.handle_request::<requests::CallTool>(move |request, cx| {
|
||||
Self::handle_call_tool(request, delegate.clone(), tool_id_map.clone(), cx)
|
||||
|
||||
mcp_server.add_tool(PermissionTool {
|
||||
thread_rx: thread_rx.clone(),
|
||||
});
|
||||
mcp_server.add_tool(ReadTool {
|
||||
thread_rx: thread_rx.clone(),
|
||||
});
|
||||
mcp_server.add_tool(EditTool {
|
||||
thread_rx: thread_rx.clone(),
|
||||
});
|
||||
|
||||
Ok(Self { server: mcp_server })
|
||||
}
|
||||
|
||||
pub fn server_config(&self) -> Result<McpServerConfig> {
|
||||
#[cfg(not(test))]
|
||||
let zed_path = std::env::current_exe()
|
||||
.context("finding current executable path for use in mcp_server")?
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
.context("finding current executable path for use in mcp_server")?;
|
||||
|
||||
#[cfg(test)]
|
||||
let zed_path = crate::e2e_tests::get_zed_path();
|
||||
|
||||
Ok(McpServerConfig {
|
||||
command: zed_path,
|
||||
@@ -106,195 +81,222 @@ impl ClaudeMcpServer {
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_list_tools(_: (), cx: &App) -> Task<Result<ListToolsResponse>> {
|
||||
cx.foreground_executor().spawn(async move {
|
||||
Ok(ListToolsResponse {
|
||||
tools: vec![
|
||||
Tool {
|
||||
name: PERMISSION_TOOL.into(),
|
||||
input_schema: schemars::schema_for!(PermissionToolParams).into(),
|
||||
description: None,
|
||||
annotations: None,
|
||||
},
|
||||
Tool {
|
||||
name: READ_TOOL.into(),
|
||||
input_schema: schemars::schema_for!(ReadToolParams).into(),
|
||||
description: Some("Read the contents of a file. In sessions with mcp__zed__Read always use it instead of Read as it contains the most up-to-date contents.".to_string()),
|
||||
annotations: Some(ToolAnnotations {
|
||||
title: Some("Read file".to_string()),
|
||||
read_only_hint: Some(true),
|
||||
destructive_hint: Some(false),
|
||||
open_world_hint: Some(false),
|
||||
// if time passes the contents might change, but it's not going to do anything different
|
||||
// true or false seem too strong, let's try a none.
|
||||
idempotent_hint: None,
|
||||
}),
|
||||
},
|
||||
Tool {
|
||||
name: EDIT_TOOL.into(),
|
||||
input_schema: schemars::schema_for!(EditToolParams).into(),
|
||||
description: Some("Edits a file. In sessions with mcp__zed__Edit always use it instead of Edit as it will show the diff to the user better.".to_string()),
|
||||
annotations: Some(ToolAnnotations {
|
||||
title: Some("Edit file".to_string()),
|
||||
read_only_hint: Some(false),
|
||||
destructive_hint: Some(false),
|
||||
open_world_hint: Some(false),
|
||||
idempotent_hint: Some(false),
|
||||
}),
|
||||
},
|
||||
],
|
||||
next_cursor: None,
|
||||
meta: None,
|
||||
})
|
||||
})
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct McpConfig {
|
||||
pub mcp_servers: HashMap<String, McpServerConfig>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct McpServerConfig {
|
||||
pub command: PathBuf,
|
||||
pub args: Vec<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub env: Option<HashMap<String, String>>,
|
||||
}
|
||||
|
||||
// Tools
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PermissionTool {
|
||||
thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, JsonSchema, Debug)]
|
||||
pub struct PermissionToolParams {
|
||||
tool_name: String,
|
||||
input: serde_json::Value,
|
||||
tool_use_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PermissionToolResponse {
|
||||
behavior: PermissionToolBehavior,
|
||||
updated_input: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum PermissionToolBehavior {
|
||||
Allow,
|
||||
Deny,
|
||||
}
|
||||
|
||||
impl McpServerTool for PermissionTool {
|
||||
type Input = PermissionToolParams;
|
||||
type Output = ();
|
||||
|
||||
const NAME: &'static str = "Confirmation";
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Request permission for tool calls"
|
||||
}
|
||||
|
||||
fn handle_call_tool(
|
||||
request: CallToolParams,
|
||||
mut delegate_watch: watch::Receiver<Option<AcpClientDelegate>>,
|
||||
tool_id_map: Rc<RefCell<HashMap<String, acp::ToolCallId>>>,
|
||||
cx: &App,
|
||||
) -> Task<Result<CallToolResponse>> {
|
||||
cx.spawn(async move |cx| {
|
||||
let Some(delegate) = delegate_watch.recv().await? else {
|
||||
debug_panic!("Sent None delegate");
|
||||
anyhow::bail!("Server not available");
|
||||
};
|
||||
async fn run(
|
||||
&self,
|
||||
input: Self::Input,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<ToolResponse<Self::Output>> {
|
||||
let mut thread_rx = self.thread_rx.clone();
|
||||
let Some(thread) = thread_rx.recv().await?.upgrade() else {
|
||||
anyhow::bail!("Thread closed");
|
||||
};
|
||||
|
||||
if request.name.as_str() == PERMISSION_TOOL {
|
||||
let input =
|
||||
serde_json::from_value(request.arguments.context("Arguments required")?)?;
|
||||
let claude_tool = ClaudeTool::infer(&input.tool_name, input.input.clone());
|
||||
let tool_call_id = acp::ToolCallId(input.tool_use_id.context("Tool ID required")?.into());
|
||||
let allow_option_id = acp::PermissionOptionId("allow".into());
|
||||
let reject_option_id = acp::PermissionOptionId("reject".into());
|
||||
|
||||
let result =
|
||||
Self::handle_permissions_tool_call(input, delegate, tool_id_map, cx).await?;
|
||||
Ok(CallToolResponse {
|
||||
content: vec![ToolResponseContent::Text {
|
||||
text: serde_json::to_string(&result)?,
|
||||
}],
|
||||
is_error: None,
|
||||
meta: None,
|
||||
})
|
||||
} else if request.name.as_str() == READ_TOOL {
|
||||
let input =
|
||||
serde_json::from_value(request.arguments.context("Arguments required")?)?;
|
||||
|
||||
let result = Self::handle_read_tool_call(input, delegate, cx).await?;
|
||||
Ok(CallToolResponse {
|
||||
content: vec![ToolResponseContent::Text {
|
||||
text: serde_json::to_string(&result)?,
|
||||
}],
|
||||
is_error: None,
|
||||
meta: None,
|
||||
})
|
||||
} else if request.name.as_str() == EDIT_TOOL {
|
||||
let input =
|
||||
serde_json::from_value(request.arguments.context("Arguments required")?)?;
|
||||
|
||||
let result = Self::handle_edit_tool_call(input, delegate, cx).await?;
|
||||
Ok(CallToolResponse {
|
||||
content: vec![ToolResponseContent::Text {
|
||||
text: serde_json::to_string(&result)?,
|
||||
}],
|
||||
is_error: None,
|
||||
meta: None,
|
||||
})
|
||||
} else {
|
||||
anyhow::bail!("Unsupported tool");
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_read_tool_call(
|
||||
params: ReadToolParams,
|
||||
delegate: AcpClientDelegate,
|
||||
cx: &AsyncApp,
|
||||
) -> Task<Result<ReadToolResponse>> {
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let response = delegate
|
||||
.read_text_file(ReadTextFileParams {
|
||||
path: params.abs_path,
|
||||
line: params.offset,
|
||||
limit: params.limit,
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(ReadToolResponse {
|
||||
content: response.content,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_edit_tool_call(
|
||||
params: EditToolParams,
|
||||
delegate: AcpClientDelegate,
|
||||
cx: &AsyncApp,
|
||||
) -> Task<Result<EditToolResponse>> {
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let response = delegate
|
||||
.read_text_file_reusing_snapshot(ReadTextFileParams {
|
||||
path: params.abs_path.clone(),
|
||||
line: None,
|
||||
limit: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let new_content = response.content.replace(¶ms.old_text, ¶ms.new_text);
|
||||
if new_content == response.content {
|
||||
return Err(anyhow::anyhow!("The old_text was not found in the content"));
|
||||
}
|
||||
|
||||
delegate
|
||||
.write_text_file(WriteTextFileParams {
|
||||
path: params.abs_path,
|
||||
content: new_content,
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(EditToolResponse)
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_permissions_tool_call(
|
||||
params: PermissionToolParams,
|
||||
delegate: AcpClientDelegate,
|
||||
tool_id_map: Rc<RefCell<HashMap<String, acp::ToolCallId>>>,
|
||||
cx: &AsyncApp,
|
||||
) -> Task<Result<PermissionToolResponse>> {
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let claude_tool = ClaudeTool::infer(¶ms.tool_name, params.input.clone());
|
||||
|
||||
let tool_call_id = match params.tool_use_id {
|
||||
Some(tool_use_id) => tool_id_map
|
||||
.borrow()
|
||||
.get(&tool_use_id)
|
||||
.cloned()
|
||||
.context("Tool call ID not found")?,
|
||||
|
||||
None => delegate.push_tool_call(claude_tool.as_acp()).await?.id,
|
||||
};
|
||||
|
||||
let outcome = delegate
|
||||
.request_existing_tool_call_confirmation(
|
||||
tool_call_id,
|
||||
claude_tool.confirmation(None),
|
||||
let chosen_option = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.request_tool_call_permission(
|
||||
claude_tool.as_acp(tool_call_id),
|
||||
vec![
|
||||
acp::PermissionOption {
|
||||
id: allow_option_id.clone(),
|
||||
label: "Allow".into(),
|
||||
kind: acp::PermissionOptionKind::AllowOnce,
|
||||
},
|
||||
acp::PermissionOption {
|
||||
id: reject_option_id.clone(),
|
||||
label: "Reject".into(),
|
||||
kind: acp::PermissionOptionKind::RejectOnce,
|
||||
},
|
||||
],
|
||||
cx,
|
||||
)
|
||||
.await?;
|
||||
})?
|
||||
.await?;
|
||||
|
||||
match outcome {
|
||||
acp::ToolCallConfirmationOutcome::Allow
|
||||
| acp::ToolCallConfirmationOutcome::AlwaysAllow
|
||||
| acp::ToolCallConfirmationOutcome::AlwaysAllowMcpServer
|
||||
| acp::ToolCallConfirmationOutcome::AlwaysAllowTool => Ok(PermissionToolResponse {
|
||||
behavior: PermissionToolBehavior::Allow,
|
||||
updated_input: params.input,
|
||||
}),
|
||||
acp::ToolCallConfirmationOutcome::Reject
|
||||
| acp::ToolCallConfirmationOutcome::Cancel => Ok(PermissionToolResponse {
|
||||
behavior: PermissionToolBehavior::Deny,
|
||||
updated_input: params.input,
|
||||
}),
|
||||
let response = if chosen_option == allow_option_id {
|
||||
PermissionToolResponse {
|
||||
behavior: PermissionToolBehavior::Allow,
|
||||
updated_input: input.input,
|
||||
}
|
||||
} else {
|
||||
debug_assert_eq!(chosen_option, reject_option_id);
|
||||
PermissionToolResponse {
|
||||
behavior: PermissionToolBehavior::Deny,
|
||||
updated_input: input.input,
|
||||
}
|
||||
};
|
||||
|
||||
Ok(ToolResponse {
|
||||
content: vec![ToolResponseContent::Text {
|
||||
text: serde_json::to_string(&response)?,
|
||||
}],
|
||||
structured_content: (),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ReadTool {
|
||||
thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
|
||||
}
|
||||
|
||||
impl McpServerTool for ReadTool {
|
||||
type Input = ReadToolParams;
|
||||
type Output = ();
|
||||
|
||||
const NAME: &'static str = "Read";
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Read the contents of a file. In sessions with mcp__zed__Read always use it instead of Read as it contains the most up-to-date contents."
|
||||
}
|
||||
|
||||
fn annotations(&self) -> ToolAnnotations {
|
||||
ToolAnnotations {
|
||||
title: Some("Read file".to_string()),
|
||||
read_only_hint: Some(true),
|
||||
destructive_hint: Some(false),
|
||||
open_world_hint: Some(false),
|
||||
idempotent_hint: None,
|
||||
}
|
||||
}
|
||||
|
||||
async fn run(
|
||||
&self,
|
||||
input: Self::Input,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<ToolResponse<Self::Output>> {
|
||||
let mut thread_rx = self.thread_rx.clone();
|
||||
let Some(thread) = thread_rx.recv().await?.upgrade() else {
|
||||
anyhow::bail!("Thread closed");
|
||||
};
|
||||
|
||||
let content = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.read_text_file(input.abs_path, input.offset, input.limit, false, cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
Ok(ToolResponse {
|
||||
content: vec![ToolResponseContent::Text { text: content }],
|
||||
structured_content: (),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct EditTool {
|
||||
thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
|
||||
}
|
||||
|
||||
impl McpServerTool for EditTool {
|
||||
type Input = EditToolParams;
|
||||
type Output = ();
|
||||
|
||||
const NAME: &'static str = "Edit";
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Edits a file. In sessions with mcp__zed__Edit always use it instead of Edit as it will show the diff to the user better."
|
||||
}
|
||||
|
||||
fn annotations(&self) -> ToolAnnotations {
|
||||
ToolAnnotations {
|
||||
title: Some("Edit file".to_string()),
|
||||
read_only_hint: Some(false),
|
||||
destructive_hint: Some(false),
|
||||
open_world_hint: Some(false),
|
||||
idempotent_hint: Some(false),
|
||||
}
|
||||
}
|
||||
|
||||
async fn run(
|
||||
&self,
|
||||
input: Self::Input,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<ToolResponse<Self::Output>> {
|
||||
let mut thread_rx = self.thread_rx.clone();
|
||||
let Some(thread) = thread_rx.recv().await?.upgrade() else {
|
||||
anyhow::bail!("Thread closed");
|
||||
};
|
||||
|
||||
let content = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.read_text_file(input.abs_path.clone(), None, None, true, cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
let new_content = content.replace(&input.old_text, &input.new_text);
|
||||
if new_content == content {
|
||||
return Err(anyhow::anyhow!("The old_text was not found in the content"));
|
||||
}
|
||||
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.write_text_file(input.abs_path, new_content, cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
Ok(ToolResponse {
|
||||
content: vec![],
|
||||
structured_content: (),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use agentic_coding_protocol::{self as acp, PushToolCallParams, ToolCallLocation};
|
||||
use agent_client_protocol as acp;
|
||||
use itertools::Itertools;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -115,51 +115,36 @@ impl ClaudeTool {
|
||||
Self::Other { name, .. } => name.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn content(&self) -> Option<acp::ToolCallContent> {
|
||||
pub fn content(&self) -> Vec<acp::ToolCallContent> {
|
||||
match &self {
|
||||
Self::Other { input, .. } => Some(acp::ToolCallContent::Markdown {
|
||||
markdown: format!(
|
||||
Self::Other { input, .. } => vec![
|
||||
format!(
|
||||
"```json\n{}```",
|
||||
serde_json::to_string_pretty(&input).unwrap_or("{}".to_string())
|
||||
),
|
||||
}),
|
||||
Self::Task(Some(params)) => Some(acp::ToolCallContent::Markdown {
|
||||
markdown: params.prompt.clone(),
|
||||
}),
|
||||
Self::NotebookRead(Some(params)) => Some(acp::ToolCallContent::Markdown {
|
||||
markdown: params.notebook_path.display().to_string(),
|
||||
}),
|
||||
Self::NotebookEdit(Some(params)) => Some(acp::ToolCallContent::Markdown {
|
||||
markdown: params.new_source.clone(),
|
||||
}),
|
||||
Self::Terminal(Some(params)) => Some(acp::ToolCallContent::Markdown {
|
||||
markdown: format!(
|
||||
)
|
||||
.into(),
|
||||
],
|
||||
Self::Task(Some(params)) => vec![params.prompt.clone().into()],
|
||||
Self::NotebookRead(Some(params)) => {
|
||||
vec![params.notebook_path.display().to_string().into()]
|
||||
}
|
||||
Self::NotebookEdit(Some(params)) => vec![params.new_source.clone().into()],
|
||||
Self::Terminal(Some(params)) => vec![
|
||||
format!(
|
||||
"`{}`\n\n{}",
|
||||
params.command,
|
||||
params.description.as_deref().unwrap_or_default()
|
||||
),
|
||||
}),
|
||||
Self::ReadFile(Some(params)) => Some(acp::ToolCallContent::Markdown {
|
||||
markdown: params.abs_path.display().to_string(),
|
||||
}),
|
||||
Self::Ls(Some(params)) => Some(acp::ToolCallContent::Markdown {
|
||||
markdown: params.path.display().to_string(),
|
||||
}),
|
||||
Self::Glob(Some(params)) => Some(acp::ToolCallContent::Markdown {
|
||||
markdown: params.to_string(),
|
||||
}),
|
||||
Self::Grep(Some(params)) => Some(acp::ToolCallContent::Markdown {
|
||||
markdown: format!("`{params}`"),
|
||||
}),
|
||||
Self::WebFetch(Some(params)) => Some(acp::ToolCallContent::Markdown {
|
||||
markdown: params.prompt.clone(),
|
||||
}),
|
||||
Self::WebSearch(Some(params)) => Some(acp::ToolCallContent::Markdown {
|
||||
markdown: params.to_string(),
|
||||
}),
|
||||
Self::TodoWrite(Some(params)) => Some(acp::ToolCallContent::Markdown {
|
||||
markdown: params
|
||||
)
|
||||
.into(),
|
||||
],
|
||||
Self::ReadFile(Some(params)) => vec![params.abs_path.display().to_string().into()],
|
||||
Self::Ls(Some(params)) => vec![params.path.display().to_string().into()],
|
||||
Self::Glob(Some(params)) => vec![params.to_string().into()],
|
||||
Self::Grep(Some(params)) => vec![format!("`{params}`").into()],
|
||||
Self::WebFetch(Some(params)) => vec![params.prompt.clone().into()],
|
||||
Self::WebSearch(Some(params)) => vec![params.to_string().into()],
|
||||
Self::TodoWrite(Some(params)) => vec![
|
||||
params
|
||||
.todos
|
||||
.iter()
|
||||
.map(|todo| {
|
||||
@@ -174,34 +159,39 @@ impl ClaudeTool {
|
||||
todo.content
|
||||
)
|
||||
})
|
||||
.join("\n"),
|
||||
}),
|
||||
Self::ExitPlanMode(Some(params)) => Some(acp::ToolCallContent::Markdown {
|
||||
markdown: params.plan.clone(),
|
||||
}),
|
||||
Self::Edit(Some(params)) => Some(acp::ToolCallContent::Diff {
|
||||
.join("\n")
|
||||
.into(),
|
||||
],
|
||||
Self::ExitPlanMode(Some(params)) => vec![params.plan.clone().into()],
|
||||
Self::Edit(Some(params)) => vec![acp::ToolCallContent::Diff {
|
||||
diff: acp::Diff {
|
||||
path: params.abs_path.clone(),
|
||||
old_text: Some(params.old_text.clone()),
|
||||
new_text: params.new_text.clone(),
|
||||
},
|
||||
}),
|
||||
Self::Write(Some(params)) => Some(acp::ToolCallContent::Diff {
|
||||
}],
|
||||
Self::Write(Some(params)) => vec![acp::ToolCallContent::Diff {
|
||||
diff: acp::Diff {
|
||||
path: params.file_path.clone(),
|
||||
old_text: None,
|
||||
new_text: params.content.clone(),
|
||||
},
|
||||
}),
|
||||
}],
|
||||
Self::MultiEdit(Some(params)) => {
|
||||
// todo: show multiple edits in a multibuffer?
|
||||
params.edits.first().map(|edit| acp::ToolCallContent::Diff {
|
||||
diff: acp::Diff {
|
||||
path: params.file_path.clone(),
|
||||
old_text: Some(edit.old_string.clone()),
|
||||
new_text: edit.new_string.clone(),
|
||||
},
|
||||
})
|
||||
params
|
||||
.edits
|
||||
.first()
|
||||
.map(|edit| {
|
||||
vec![acp::ToolCallContent::Diff {
|
||||
diff: acp::Diff {
|
||||
path: params.file_path.clone(),
|
||||
old_text: Some(edit.old_string.clone()),
|
||||
new_text: edit.new_string.clone(),
|
||||
},
|
||||
}]
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
Self::Task(None)
|
||||
| Self::NotebookRead(None)
|
||||
@@ -217,181 +207,80 @@ impl ClaudeTool {
|
||||
| Self::ExitPlanMode(None)
|
||||
| Self::Edit(None)
|
||||
| Self::Write(None)
|
||||
| Self::MultiEdit(None) => None,
|
||||
| Self::MultiEdit(None) => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn icon(&self) -> acp::Icon {
|
||||
pub fn kind(&self) -> acp::ToolKind {
|
||||
match self {
|
||||
Self::Task(_) => acp::Icon::Hammer,
|
||||
Self::NotebookRead(_) => acp::Icon::FileSearch,
|
||||
Self::NotebookEdit(_) => acp::Icon::Pencil,
|
||||
Self::Edit(_) => acp::Icon::Pencil,
|
||||
Self::MultiEdit(_) => acp::Icon::Pencil,
|
||||
Self::Write(_) => acp::Icon::Pencil,
|
||||
Self::ReadFile(_) => acp::Icon::FileSearch,
|
||||
Self::Ls(_) => acp::Icon::Folder,
|
||||
Self::Glob(_) => acp::Icon::FileSearch,
|
||||
Self::Grep(_) => acp::Icon::Regex,
|
||||
Self::Terminal(_) => acp::Icon::Terminal,
|
||||
Self::WebSearch(_) => acp::Icon::Globe,
|
||||
Self::WebFetch(_) => acp::Icon::Globe,
|
||||
Self::TodoWrite(_) => acp::Icon::LightBulb,
|
||||
Self::ExitPlanMode(_) => acp::Icon::Hammer,
|
||||
Self::Other { .. } => acp::Icon::Hammer,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn confirmation(&self, description: Option<String>) -> acp::ToolCallConfirmation {
|
||||
match &self {
|
||||
Self::Edit(_) | Self::Write(_) | Self::NotebookEdit(_) | Self::MultiEdit(_) => {
|
||||
acp::ToolCallConfirmation::Edit { description }
|
||||
}
|
||||
Self::WebFetch(params) => acp::ToolCallConfirmation::Fetch {
|
||||
urls: params
|
||||
.as_ref()
|
||||
.map(|p| vec![p.url.clone()])
|
||||
.unwrap_or_default(),
|
||||
description,
|
||||
},
|
||||
Self::Terminal(Some(BashToolParams {
|
||||
description,
|
||||
command,
|
||||
..
|
||||
})) => acp::ToolCallConfirmation::Execute {
|
||||
command: command.clone(),
|
||||
root_command: command.clone(),
|
||||
description: description.clone(),
|
||||
},
|
||||
Self::ExitPlanMode(Some(params)) => acp::ToolCallConfirmation::Other {
|
||||
description: if let Some(description) = description {
|
||||
format!("{description} {}", params.plan)
|
||||
} else {
|
||||
params.plan.clone()
|
||||
},
|
||||
},
|
||||
Self::Task(Some(params)) => acp::ToolCallConfirmation::Other {
|
||||
description: if let Some(description) = description {
|
||||
format!("{description} {}", params.description)
|
||||
} else {
|
||||
params.description.clone()
|
||||
},
|
||||
},
|
||||
Self::Ls(Some(LsToolParams { path, .. }))
|
||||
| Self::ReadFile(Some(ReadToolParams { abs_path: path, .. })) => {
|
||||
let path = path.display();
|
||||
acp::ToolCallConfirmation::Other {
|
||||
description: if let Some(description) = description {
|
||||
format!("{description} {path}")
|
||||
} else {
|
||||
path.to_string()
|
||||
},
|
||||
}
|
||||
}
|
||||
Self::NotebookRead(Some(NotebookReadToolParams { notebook_path, .. })) => {
|
||||
let path = notebook_path.display();
|
||||
acp::ToolCallConfirmation::Other {
|
||||
description: if let Some(description) = description {
|
||||
format!("{description} {path}")
|
||||
} else {
|
||||
path.to_string()
|
||||
},
|
||||
}
|
||||
}
|
||||
Self::Glob(Some(params)) => acp::ToolCallConfirmation::Other {
|
||||
description: if let Some(description) = description {
|
||||
format!("{description} {params}")
|
||||
} else {
|
||||
params.to_string()
|
||||
},
|
||||
},
|
||||
Self::Grep(Some(params)) => acp::ToolCallConfirmation::Other {
|
||||
description: if let Some(description) = description {
|
||||
format!("{description} {params}")
|
||||
} else {
|
||||
params.to_string()
|
||||
},
|
||||
},
|
||||
Self::WebSearch(Some(params)) => acp::ToolCallConfirmation::Other {
|
||||
description: if let Some(description) = description {
|
||||
format!("{description} {params}")
|
||||
} else {
|
||||
params.to_string()
|
||||
},
|
||||
},
|
||||
Self::TodoWrite(Some(params)) => {
|
||||
let params = params.todos.iter().map(|todo| &todo.content).join(", ");
|
||||
acp::ToolCallConfirmation::Other {
|
||||
description: if let Some(description) = description {
|
||||
format!("{description} {params}")
|
||||
} else {
|
||||
params
|
||||
},
|
||||
}
|
||||
}
|
||||
Self::Terminal(None)
|
||||
| Self::Task(None)
|
||||
| Self::NotebookRead(None)
|
||||
| Self::ExitPlanMode(None)
|
||||
| Self::Ls(None)
|
||||
| Self::Glob(None)
|
||||
| Self::Grep(None)
|
||||
| Self::ReadFile(None)
|
||||
| Self::WebSearch(None)
|
||||
| Self::TodoWrite(None)
|
||||
| Self::Other { .. } => acp::ToolCallConfirmation::Other {
|
||||
description: description.unwrap_or("".to_string()),
|
||||
},
|
||||
Self::Task(_) => acp::ToolKind::Think,
|
||||
Self::NotebookRead(_) => acp::ToolKind::Read,
|
||||
Self::NotebookEdit(_) => acp::ToolKind::Edit,
|
||||
Self::Edit(_) => acp::ToolKind::Edit,
|
||||
Self::MultiEdit(_) => acp::ToolKind::Edit,
|
||||
Self::Write(_) => acp::ToolKind::Edit,
|
||||
Self::ReadFile(_) => acp::ToolKind::Read,
|
||||
Self::Ls(_) => acp::ToolKind::Search,
|
||||
Self::Glob(_) => acp::ToolKind::Search,
|
||||
Self::Grep(_) => acp::ToolKind::Search,
|
||||
Self::Terminal(_) => acp::ToolKind::Execute,
|
||||
Self::WebSearch(_) => acp::ToolKind::Search,
|
||||
Self::WebFetch(_) => acp::ToolKind::Fetch,
|
||||
Self::TodoWrite(_) => acp::ToolKind::Think,
|
||||
Self::ExitPlanMode(_) => acp::ToolKind::Think,
|
||||
Self::Other { .. } => acp::ToolKind::Other,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn locations(&self) -> Vec<acp::ToolCallLocation> {
|
||||
match &self {
|
||||
Self::Edit(Some(EditToolParams { abs_path, .. })) => vec![ToolCallLocation {
|
||||
Self::Edit(Some(EditToolParams { abs_path, .. })) => vec![acp::ToolCallLocation {
|
||||
path: abs_path.clone(),
|
||||
line: None,
|
||||
}],
|
||||
Self::MultiEdit(Some(MultiEditToolParams { file_path, .. })) => {
|
||||
vec![ToolCallLocation {
|
||||
vec![acp::ToolCallLocation {
|
||||
path: file_path.clone(),
|
||||
line: None,
|
||||
}]
|
||||
}
|
||||
Self::Write(Some(WriteToolParams { file_path, .. })) => {
|
||||
vec![acp::ToolCallLocation {
|
||||
path: file_path.clone(),
|
||||
line: None,
|
||||
}]
|
||||
}
|
||||
Self::Write(Some(WriteToolParams { file_path, .. })) => vec![ToolCallLocation {
|
||||
path: file_path.clone(),
|
||||
line: None,
|
||||
}],
|
||||
Self::ReadFile(Some(ReadToolParams {
|
||||
abs_path, offset, ..
|
||||
})) => vec![ToolCallLocation {
|
||||
})) => vec![acp::ToolCallLocation {
|
||||
path: abs_path.clone(),
|
||||
line: *offset,
|
||||
}],
|
||||
Self::NotebookRead(Some(NotebookReadToolParams { notebook_path, .. })) => {
|
||||
vec![ToolCallLocation {
|
||||
vec![acp::ToolCallLocation {
|
||||
path: notebook_path.clone(),
|
||||
line: None,
|
||||
}]
|
||||
}
|
||||
Self::NotebookEdit(Some(NotebookEditToolParams { notebook_path, .. })) => {
|
||||
vec![ToolCallLocation {
|
||||
vec![acp::ToolCallLocation {
|
||||
path: notebook_path.clone(),
|
||||
line: None,
|
||||
}]
|
||||
}
|
||||
Self::Glob(Some(GlobToolParams {
|
||||
path: Some(path), ..
|
||||
})) => vec![ToolCallLocation {
|
||||
})) => vec![acp::ToolCallLocation {
|
||||
path: path.clone(),
|
||||
line: None,
|
||||
}],
|
||||
Self::Ls(Some(LsToolParams { path, .. })) => vec![ToolCallLocation {
|
||||
Self::Ls(Some(LsToolParams { path, .. })) => vec![acp::ToolCallLocation {
|
||||
path: path.clone(),
|
||||
line: None,
|
||||
}],
|
||||
Self::Grep(Some(GrepToolParams {
|
||||
path: Some(path), ..
|
||||
})) => vec![ToolCallLocation {
|
||||
})) => vec![acp::ToolCallLocation {
|
||||
path: PathBuf::from(path),
|
||||
line: None,
|
||||
}],
|
||||
@@ -414,12 +303,15 @@ impl ClaudeTool {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_acp(&self) -> PushToolCallParams {
|
||||
PushToolCallParams {
|
||||
pub fn as_acp(&self, id: acp::ToolCallId) -> acp::ToolCall {
|
||||
acp::ToolCall {
|
||||
id,
|
||||
kind: self.kind(),
|
||||
status: acp::ToolCallStatus::InProgress,
|
||||
label: self.label(),
|
||||
content: self.content(),
|
||||
icon: self.icon(),
|
||||
locations: self.locations(),
|
||||
raw_input: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -434,10 +326,6 @@ pub struct EditToolParams {
|
||||
pub new_text: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EditToolResponse;
|
||||
|
||||
#[derive(Deserialize, JsonSchema, Debug)]
|
||||
pub struct ReadToolParams {
|
||||
/// The absolute path to the file to read.
|
||||
@@ -450,12 +338,6 @@ pub struct ReadToolParams {
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ReadToolResponse {
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, JsonSchema, Debug)]
|
||||
pub struct WriteToolParams {
|
||||
/// Absolute path for new file
|
||||
|
||||
319
crates/agent_servers/src/codex.rs
Normal file
@@ -0,0 +1,319 @@
|
||||
use agent_client_protocol as acp;
|
||||
use anyhow::anyhow;
|
||||
use collections::HashMap;
|
||||
use context_server::listener::McpServerTool;
|
||||
use context_server::types::requests;
|
||||
use context_server::{ContextServer, ContextServerCommand, ContextServerId};
|
||||
use futures::channel::{mpsc, oneshot};
|
||||
use project::Project;
|
||||
use settings::SettingsStore;
|
||||
use smol::stream::StreamExt as _;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::{path::Path, sync::Arc};
|
||||
use util::ResultExt;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use gpui::{App, AppContext as _, AsyncApp, Entity, Task, WeakEntity};
|
||||
|
||||
use crate::mcp_server::ZedMcpServer;
|
||||
use crate::{AgentServer, AgentServerCommand, AllAgentServersSettings, mcp_server};
|
||||
use acp_thread::{AcpThread, AgentConnection};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Codex;
|
||||
|
||||
impl AgentServer for Codex {
|
||||
fn name(&self) -> &'static str {
|
||||
"Codex"
|
||||
}
|
||||
|
||||
fn empty_state_headline(&self) -> &'static str {
|
||||
"Welcome to Codex"
|
||||
}
|
||||
|
||||
fn empty_state_message(&self) -> &'static str {
|
||||
"What can I help with?"
|
||||
}
|
||||
|
||||
fn logo(&self) -> ui::IconName {
|
||||
ui::IconName::AiOpenAi
|
||||
}
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
_root_dir: &Path,
|
||||
project: &Entity<Project>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Rc<dyn AgentConnection>>> {
|
||||
let project = project.clone();
|
||||
let working_directory = project.read(cx).active_project_directory(cx);
|
||||
cx.spawn(async move |cx| {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).codex.clone()
|
||||
})?;
|
||||
|
||||
let Some(command) =
|
||||
AgentServerCommand::resolve("codex", &["mcp"], settings, &project, cx).await
|
||||
else {
|
||||
anyhow::bail!("Failed to find codex binary");
|
||||
};
|
||||
|
||||
let client: Arc<ContextServer> = ContextServer::stdio(
|
||||
ContextServerId("codex-mcp-server".into()),
|
||||
ContextServerCommand {
|
||||
path: command.path,
|
||||
args: command.args,
|
||||
env: command.env,
|
||||
},
|
||||
working_directory,
|
||||
)
|
||||
.into();
|
||||
ContextServer::start(client.clone(), cx).await?;
|
||||
|
||||
let (notification_tx, mut notification_rx) = mpsc::unbounded();
|
||||
client
|
||||
.client()
|
||||
.context("Failed to subscribe")?
|
||||
.on_notification(acp::SESSION_UPDATE_METHOD_NAME, {
|
||||
move |notification, _cx| {
|
||||
let notification_tx = notification_tx.clone();
|
||||
log::trace!(
|
||||
"ACP Notification: {}",
|
||||
serde_json::to_string_pretty(¬ification).unwrap()
|
||||
);
|
||||
|
||||
if let Some(notification) =
|
||||
serde_json::from_value::<acp::SessionNotification>(notification)
|
||||
.log_err()
|
||||
{
|
||||
notification_tx.unbounded_send(notification).ok();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let sessions = Rc::new(RefCell::new(HashMap::default()));
|
||||
|
||||
let notification_handler_task = cx.spawn({
|
||||
let sessions = sessions.clone();
|
||||
async move |cx| {
|
||||
while let Some(notification) = notification_rx.next().await {
|
||||
CodexConnection::handle_session_notification(
|
||||
notification,
|
||||
sessions.clone(),
|
||||
cx,
|
||||
)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let connection = CodexConnection {
|
||||
client,
|
||||
sessions,
|
||||
_notification_handler_task: notification_handler_task,
|
||||
};
|
||||
Ok(Rc::new(connection) as _)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct CodexConnection {
|
||||
client: Arc<context_server::ContextServer>,
|
||||
sessions: Rc<RefCell<HashMap<acp::SessionId, CodexSession>>>,
|
||||
_notification_handler_task: Task<()>,
|
||||
}
|
||||
|
||||
struct CodexSession {
|
||||
thread: WeakEntity<AcpThread>,
|
||||
cancel_tx: Option<oneshot::Sender<()>>,
|
||||
_mcp_server: ZedMcpServer,
|
||||
}
|
||||
|
||||
impl AgentConnection for CodexConnection {
|
||||
fn name(&self) -> &'static str {
|
||||
"Codex"
|
||||
}
|
||||
|
||||
fn new_thread(
|
||||
self: Rc<Self>,
|
||||
project: Entity<Project>,
|
||||
cwd: &Path,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Task<Result<Entity<AcpThread>>> {
|
||||
let client = self.client.client();
|
||||
let sessions = self.sessions.clone();
|
||||
let cwd = cwd.to_path_buf();
|
||||
cx.spawn(async move |cx| {
|
||||
let client = client.context("MCP server is not initialized yet")?;
|
||||
let (mut thread_tx, thread_rx) = watch::channel(WeakEntity::new_invalid());
|
||||
|
||||
let mcp_server = ZedMcpServer::new(thread_rx, cx).await?;
|
||||
|
||||
let response = client
|
||||
.request::<requests::CallTool>(context_server::types::CallToolParams {
|
||||
name: acp::NEW_SESSION_TOOL_NAME.into(),
|
||||
arguments: Some(serde_json::to_value(acp::NewSessionArguments {
|
||||
mcp_servers: [(
|
||||
mcp_server::SERVER_NAME.to_string(),
|
||||
mcp_server.server_config()?,
|
||||
)]
|
||||
.into(),
|
||||
client_tools: acp::ClientTools {
|
||||
request_permission: Some(acp::McpToolId {
|
||||
mcp_server: mcp_server::SERVER_NAME.into(),
|
||||
tool_name: mcp_server::RequestPermissionTool::NAME.into(),
|
||||
}),
|
||||
read_text_file: Some(acp::McpToolId {
|
||||
mcp_server: mcp_server::SERVER_NAME.into(),
|
||||
tool_name: mcp_server::ReadTextFileTool::NAME.into(),
|
||||
}),
|
||||
write_text_file: Some(acp::McpToolId {
|
||||
mcp_server: mcp_server::SERVER_NAME.into(),
|
||||
tool_name: mcp_server::WriteTextFileTool::NAME.into(),
|
||||
}),
|
||||
},
|
||||
cwd,
|
||||
})?),
|
||||
meta: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
if response.is_error.unwrap_or_default() {
|
||||
return Err(anyhow!(response.text_contents()));
|
||||
}
|
||||
|
||||
let result = serde_json::from_value::<acp::NewSessionOutput>(
|
||||
response.structured_content.context("Empty response")?,
|
||||
)?;
|
||||
|
||||
let thread =
|
||||
cx.new(|cx| AcpThread::new(self.clone(), project, result.session_id.clone(), cx))?;
|
||||
|
||||
thread_tx.send(thread.downgrade())?;
|
||||
|
||||
let session = CodexSession {
|
||||
thread: thread.downgrade(),
|
||||
cancel_tx: None,
|
||||
_mcp_server: mcp_server,
|
||||
};
|
||||
sessions.borrow_mut().insert(result.session_id, session);
|
||||
|
||||
Ok(thread)
|
||||
})
|
||||
}
|
||||
|
||||
fn authenticate(&self, _cx: &mut App) -> Task<Result<()>> {
|
||||
Task::ready(Err(anyhow!("Authentication not supported")))
|
||||
}
|
||||
|
||||
fn prompt(
|
||||
&self,
|
||||
params: agent_client_protocol::PromptArguments,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<()>> {
|
||||
let client = self.client.client();
|
||||
let sessions = self.sessions.clone();
|
||||
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let client = client.context("MCP server is not initialized yet")?;
|
||||
|
||||
let (new_cancel_tx, cancel_rx) = oneshot::channel();
|
||||
{
|
||||
let mut sessions = sessions.borrow_mut();
|
||||
let session = sessions
|
||||
.get_mut(¶ms.session_id)
|
||||
.context("Session not found")?;
|
||||
session.cancel_tx.replace(new_cancel_tx);
|
||||
}
|
||||
|
||||
let result = client
|
||||
.request_with::<requests::CallTool>(
|
||||
context_server::types::CallToolParams {
|
||||
name: acp::PROMPT_TOOL_NAME.into(),
|
||||
arguments: Some(serde_json::to_value(params)?),
|
||||
meta: None,
|
||||
},
|
||||
Some(cancel_rx),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Err(err) = &result
|
||||
&& err.is::<context_server::client::RequestCanceled>()
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let response = result?;
|
||||
|
||||
if response.is_error.unwrap_or_default() {
|
||||
return Err(anyhow!(response.text_contents()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn cancel(&self, session_id: &agent_client_protocol::SessionId, _cx: &mut App) {
|
||||
let mut sessions = self.sessions.borrow_mut();
|
||||
|
||||
if let Some(cancel_tx) = sessions
|
||||
.get_mut(session_id)
|
||||
.and_then(|session| session.cancel_tx.take())
|
||||
{
|
||||
cancel_tx.send(()).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CodexConnection {
|
||||
pub fn handle_session_notification(
|
||||
notification: acp::SessionNotification,
|
||||
threads: Rc<RefCell<HashMap<acp::SessionId, CodexSession>>>,
|
||||
cx: &mut AsyncApp,
|
||||
) {
|
||||
let threads = threads.borrow();
|
||||
let Some(thread) = threads
|
||||
.get(¬ification.session_id)
|
||||
.and_then(|session| session.thread.upgrade())
|
||||
else {
|
||||
log::error!(
|
||||
"Thread not found for session ID: {}",
|
||||
notification.session_id
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.handle_session_update(notification.update, cx)
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for CodexConnection {
|
||||
fn drop(&mut self) {
|
||||
self.client.stop().log_err();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
use crate::AgentServerCommand;
|
||||
use std::path::Path;
|
||||
|
||||
crate::common_e2e_tests!(Codex, allow_option_id = "approve");
|
||||
|
||||
pub fn local_command() -> AgentServerCommand {
|
||||
let cli_path = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../../codex/codex-rs/target/debug/codex");
|
||||
|
||||
AgentServerCommand {
|
||||
path: cli_path,
|
||||
args: vec![],
|
||||
env: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,17 @@
|
||||
use std::{path::Path, sync::Arc, time::Duration};
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crate::{AgentServer, AgentServerSettings, AllAgentServersSettings};
|
||||
use acp_thread::{
|
||||
AcpThread, AgentThreadEntry, ToolCall, ToolCallConfirmation, ToolCallContent, ToolCallStatus,
|
||||
};
|
||||
use agentic_coding_protocol as acp;
|
||||
use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus};
|
||||
use agent_client_protocol as acp;
|
||||
|
||||
use futures::{FutureExt, StreamExt, channel::mpsc, select};
|
||||
use gpui::{Entity, TestAppContext};
|
||||
use indoc::indoc;
|
||||
use project::{FakeFs, Project};
|
||||
use serde_json::json;
|
||||
use settings::{Settings, SettingsStore};
|
||||
use util::path;
|
||||
|
||||
@@ -24,7 +26,11 @@ pub async fn test_basic(server: impl AgentServer + 'static, cx: &mut TestAppCont
|
||||
.unwrap();
|
||||
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert_eq!(thread.entries().len(), 2);
|
||||
assert!(
|
||||
thread.entries().len() >= 2,
|
||||
"Expected at least 2 entries. Got: {:?}",
|
||||
thread.entries()
|
||||
);
|
||||
assert!(matches!(
|
||||
thread.entries()[0],
|
||||
AgentThreadEntry::UserMessage(_)
|
||||
@@ -54,19 +60,25 @@ pub async fn test_path_mentions(server: impl AgentServer + 'static, cx: &mut Tes
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(
|
||||
acp::SendUserMessageParams {
|
||||
chunks: vec![
|
||||
acp::UserMessageChunk::Text {
|
||||
text: "Read the file ".into(),
|
||||
},
|
||||
acp::UserMessageChunk::Path {
|
||||
path: Path::new("foo.rs").into(),
|
||||
},
|
||||
acp::UserMessageChunk::Text {
|
||||
text: " and tell me what the content of the println! is".into(),
|
||||
},
|
||||
],
|
||||
},
|
||||
vec![
|
||||
acp::ContentBlock::Text(acp::TextContent {
|
||||
text: "Read the file ".into(),
|
||||
annotations: None,
|
||||
}),
|
||||
acp::ContentBlock::ResourceLink(acp::ResourceLink {
|
||||
uri: "foo.rs".into(),
|
||||
name: "foo.rs".into(),
|
||||
annotations: None,
|
||||
description: None,
|
||||
mime_type: None,
|
||||
size: None,
|
||||
title: None,
|
||||
}),
|
||||
acp::ContentBlock::Text(acp::TextContent {
|
||||
text: " and tell me what the content of the println! is".into(),
|
||||
annotations: None,
|
||||
}),
|
||||
],
|
||||
cx,
|
||||
)
|
||||
})
|
||||
@@ -74,37 +86,44 @@ pub async fn test_path_mentions(server: impl AgentServer + 'static, cx: &mut Tes
|
||||
.unwrap();
|
||||
|
||||
thread.read_with(cx, |thread, cx| {
|
||||
assert_eq!(thread.entries().len(), 3);
|
||||
assert!(matches!(
|
||||
thread.entries()[0],
|
||||
AgentThreadEntry::UserMessage(_)
|
||||
));
|
||||
assert!(matches!(thread.entries()[1], AgentThreadEntry::ToolCall(_)));
|
||||
let AgentThreadEntry::AssistantMessage(assistant_message) = &thread.entries()[2] else {
|
||||
panic!("Expected AssistantMessage")
|
||||
};
|
||||
let assistant_message = &thread
|
||||
.entries()
|
||||
.iter()
|
||||
.rev()
|
||||
.find_map(|entry| match entry {
|
||||
AgentThreadEntry::AssistantMessage(msg) => Some(msg),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
assistant_message.to_markdown(cx).contains("Hello, world!"),
|
||||
"unexpected assistant message: {:?}",
|
||||
assistant_message.to_markdown(cx)
|
||||
);
|
||||
});
|
||||
|
||||
drop(tempdir);
|
||||
}
|
||||
|
||||
pub async fn test_tool_call(server: impl AgentServer + 'static, cx: &mut TestAppContext) {
|
||||
let fs = init_test(cx).await;
|
||||
fs.insert_tree(
|
||||
path!("/private/tmp"),
|
||||
json!({"foo": "Lorem ipsum dolor", "bar": "bar", "baz": "baz"}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await;
|
||||
let _fs = init_test(cx).await;
|
||||
|
||||
let tempdir = tempfile::tempdir().unwrap();
|
||||
let foo_path = tempdir.path().join("foo");
|
||||
std::fs::write(&foo_path, "Lorem ipsum dolor").expect("failed to write file");
|
||||
|
||||
let project = Project::example([tempdir.path()], &mut cx.to_async()).await;
|
||||
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
|
||||
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send_raw(
|
||||
"Read the '/private/tmp/foo' file and tell me what you see.",
|
||||
&format!("Read {} and tell me what you see.", foo_path.display()),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
@@ -127,10 +146,13 @@ pub async fn test_tool_call(server: impl AgentServer + 'static, cx: &mut TestApp
|
||||
.any(|entry| { matches!(entry, AgentThreadEntry::AssistantMessage(_)) })
|
||||
);
|
||||
});
|
||||
|
||||
drop(tempdir);
|
||||
}
|
||||
|
||||
pub async fn test_tool_call_with_confirmation(
|
||||
server: impl AgentServer + 'static,
|
||||
allow_option_id: acp::PermissionOptionId,
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
let fs = init_test(cx).await;
|
||||
@@ -138,7 +160,7 @@ pub async fn test_tool_call_with_confirmation(
|
||||
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
|
||||
let full_turn = thread.update(cx, |thread, cx| {
|
||||
thread.send_raw(
|
||||
r#"Run `touch hello.txt && echo "Hello, world!" | tee hello.txt`"#,
|
||||
r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -158,14 +180,11 @@ pub async fn test_tool_call_with_confirmation(
|
||||
)
|
||||
.await;
|
||||
|
||||
let tool_call_id = thread.read_with(cx, |thread, _cx| {
|
||||
let tool_call_id = thread.read_with(cx, |thread, cx| {
|
||||
let AgentThreadEntry::ToolCall(ToolCall {
|
||||
id,
|
||||
status:
|
||||
ToolCallStatus::WaitingForConfirmation {
|
||||
confirmation: ToolCallConfirmation::Execute { root_command, .. },
|
||||
..
|
||||
},
|
||||
label,
|
||||
status: ToolCallStatus::WaitingForConfirmation { .. },
|
||||
..
|
||||
}) = &thread
|
||||
.entries()
|
||||
@@ -176,13 +195,19 @@ pub async fn test_tool_call_with_confirmation(
|
||||
panic!();
|
||||
};
|
||||
|
||||
assert!(root_command.contains("touch"));
|
||||
let label = label.read(cx).source();
|
||||
assert!(label.contains("touch"), "Got: {}", label);
|
||||
|
||||
*id
|
||||
id.clone()
|
||||
});
|
||||
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.authorize_tool_call(tool_call_id, acp::ToolCallConfirmationOutcome::Allow, cx);
|
||||
thread.authorize_tool_call(
|
||||
tool_call_id,
|
||||
allow_option_id,
|
||||
acp::PermissionOptionKind::AllowOnce,
|
||||
cx,
|
||||
);
|
||||
|
||||
assert!(thread.entries().iter().any(|entry| matches!(
|
||||
entry,
|
||||
@@ -197,7 +222,7 @@ pub async fn test_tool_call_with_confirmation(
|
||||
|
||||
thread.read_with(cx, |thread, cx| {
|
||||
let AgentThreadEntry::ToolCall(ToolCall {
|
||||
content: Some(ToolCallContent::Markdown { markdown }),
|
||||
content,
|
||||
status: ToolCallStatus::Allowed { .. },
|
||||
..
|
||||
}) = thread
|
||||
@@ -209,13 +234,10 @@ pub async fn test_tool_call_with_confirmation(
|
||||
panic!();
|
||||
};
|
||||
|
||||
markdown.read_with(cx, |md, _cx| {
|
||||
assert!(
|
||||
md.source().contains("Hello"),
|
||||
r#"Expected '{}' to contain "Hello""#,
|
||||
md.source()
|
||||
);
|
||||
});
|
||||
assert!(
|
||||
content.iter().any(|c| c.to_markdown(cx).contains("Hello")),
|
||||
"Expected content to contain 'Hello'"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -226,7 +248,7 @@ pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppCon
|
||||
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
|
||||
let full_turn = thread.update(cx, |thread, cx| {
|
||||
thread.send_raw(
|
||||
r#"Run `touch hello.txt && echo "Hello, world!" >> hello.txt`"#,
|
||||
r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -246,29 +268,24 @@ pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppCon
|
||||
)
|
||||
.await;
|
||||
|
||||
thread.read_with(cx, |thread, _cx| {
|
||||
thread.read_with(cx, |thread, cx| {
|
||||
let AgentThreadEntry::ToolCall(ToolCall {
|
||||
id,
|
||||
status:
|
||||
ToolCallStatus::WaitingForConfirmation {
|
||||
confirmation: ToolCallConfirmation::Execute { root_command, .. },
|
||||
..
|
||||
},
|
||||
label,
|
||||
status: ToolCallStatus::WaitingForConfirmation { .. },
|
||||
..
|
||||
}) = &thread.entries()[first_tool_call_ix]
|
||||
else {
|
||||
panic!("{:?}", thread.entries()[1]);
|
||||
};
|
||||
|
||||
assert!(root_command.contains("touch"));
|
||||
let label = label.read(cx).source();
|
||||
assert!(label.contains("touch"), "Got: {}", label);
|
||||
|
||||
*id
|
||||
id.clone()
|
||||
});
|
||||
|
||||
thread
|
||||
.update(cx, |thread, cx| thread.cancel(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let _ = thread.update(cx, |thread, cx| thread.cancel(cx));
|
||||
full_turn.await.unwrap();
|
||||
thread.read_with(cx, |thread, _| {
|
||||
let AgentThreadEntry::ToolCall(ToolCall {
|
||||
@@ -296,7 +313,7 @@ pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppCon
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! common_e2e_tests {
|
||||
($server:expr) => {
|
||||
($server:expr, allow_option_id = $allow_option_id:expr) => {
|
||||
mod common_e2e {
|
||||
use super::*;
|
||||
|
||||
@@ -321,7 +338,12 @@ macro_rules! common_e2e_tests {
|
||||
#[::gpui::test]
|
||||
#[cfg_attr(not(feature = "e2e"), ignore)]
|
||||
async fn tool_call_with_confirmation(cx: &mut ::gpui::TestAppContext) {
|
||||
$crate::e2e_tests::test_tool_call_with_confirmation($server, cx).await;
|
||||
$crate::e2e_tests::test_tool_call_with_confirmation(
|
||||
$server,
|
||||
::agent_client_protocol::PermissionOptionId($allow_option_id.into()),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[::gpui::test]
|
||||
@@ -353,6 +375,9 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
|
||||
gemini: Some(AgentServerSettings {
|
||||
command: crate::gemini::tests::local_command(),
|
||||
}),
|
||||
codex: Some(AgentServerSettings {
|
||||
command: crate::codex::tests::local_command(),
|
||||
}),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
@@ -369,15 +394,16 @@ pub async fn new_test_thread(
|
||||
current_dir: impl AsRef<Path>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> Entity<AcpThread> {
|
||||
let thread = cx
|
||||
.update(|cx| server.new_thread(current_dir.as_ref(), &project, cx))
|
||||
let connection = cx
|
||||
.update(|cx| server.connect(current_dir.as_ref(), &project, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
thread
|
||||
.update(cx, |thread, _| thread.initialize())
|
||||
let thread = connection
|
||||
.new_thread(project.clone(), current_dir.as_ref(), &mut cx.to_async())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
thread
|
||||
}
|
||||
|
||||
@@ -410,3 +436,24 @@ pub async fn run_until_first_tool_call(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_zed_path() -> PathBuf {
|
||||
let mut zed_path = std::env::current_exe().unwrap();
|
||||
|
||||
while zed_path
|
||||
.file_name()
|
||||
.map_or(true, |name| name.to_string_lossy() != "debug")
|
||||
{
|
||||
if !zed_path.pop() {
|
||||
panic!("Could not find target directory");
|
||||
}
|
||||
}
|
||||
|
||||
zed_path.push("zed");
|
||||
|
||||
if !zed_path.exists() {
|
||||
panic!("\n🚨 Run `cargo build` at least once before running e2e tests\n\n");
|
||||
}
|
||||
|
||||
zed_path
|
||||
}
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
use crate::stdio_agent_server::StdioAgentServer;
|
||||
use crate::{AgentServerCommand, AgentServerVersion};
|
||||
use anyhow::anyhow;
|
||||
use std::cell::RefCell;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::{AgentServer, AgentServerCommand, AgentServerVersion};
|
||||
use acp_thread::{AgentConnection, LoadError, OldAcpAgentConnection, OldAcpClientDelegate};
|
||||
use agentic_coding_protocol as acp_old;
|
||||
use anyhow::{Context as _, Result};
|
||||
use gpui::{AsyncApp, Entity};
|
||||
use gpui::{AppContext as _, AsyncApp, Entity, Task, WeakEntity};
|
||||
use project::Project;
|
||||
use settings::SettingsStore;
|
||||
use ui::App;
|
||||
|
||||
use crate::AllAgentServersSettings;
|
||||
|
||||
@@ -12,7 +20,7 @@ pub struct Gemini;
|
||||
|
||||
const ACP_ARG: &str = "--experimental-acp";
|
||||
|
||||
impl StdioAgentServer for Gemini {
|
||||
impl AgentServer for Gemini {
|
||||
fn name(&self) -> &'static str {
|
||||
"Gemini"
|
||||
}
|
||||
@@ -25,14 +33,89 @@ impl StdioAgentServer for Gemini {
|
||||
"Ask questions, edit files, run commands.\nBe specific for the best results."
|
||||
}
|
||||
|
||||
fn supports_always_allow(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn logo(&self) -> ui::IconName {
|
||||
ui::IconName::AiGemini
|
||||
}
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
root_dir: &Path,
|
||||
project: &Entity<Project>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Rc<dyn AgentConnection>>> {
|
||||
let root_dir = root_dir.to_path_buf();
|
||||
let project = project.clone();
|
||||
let this = self.clone();
|
||||
let name = self.name();
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let command = this.command(&project, cx).await?;
|
||||
|
||||
let mut child = util::command::new_smol_command(&command.path)
|
||||
.args(command.args.iter())
|
||||
.current_dir(root_dir)
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::inherit())
|
||||
.kill_on_drop(true)
|
||||
.spawn()?;
|
||||
|
||||
let stdin = child.stdin.take().unwrap();
|
||||
let stdout = child.stdout.take().unwrap();
|
||||
|
||||
let foreground_executor = cx.foreground_executor().clone();
|
||||
|
||||
let thread_rc = Rc::new(RefCell::new(WeakEntity::new_invalid()));
|
||||
|
||||
let (connection, io_fut) = acp_old::AgentConnection::connect_to_agent(
|
||||
OldAcpClientDelegate::new(thread_rc.clone(), cx.clone()),
|
||||
stdin,
|
||||
stdout,
|
||||
move |fut| foreground_executor.spawn(fut).detach(),
|
||||
);
|
||||
|
||||
let io_task = cx.background_spawn(async move {
|
||||
io_fut.await.log_err();
|
||||
});
|
||||
|
||||
let child_status = cx.background_spawn(async move {
|
||||
let result = match child.status().await {
|
||||
Err(e) => Err(anyhow!(e)),
|
||||
Ok(result) if result.success() => Ok(()),
|
||||
Ok(result) => {
|
||||
if let Some(AgentServerVersion::Unsupported {
|
||||
error_message,
|
||||
upgrade_message,
|
||||
upgrade_command,
|
||||
}) = this.version(&command).await.log_err()
|
||||
{
|
||||
Err(anyhow!(LoadError::Unsupported {
|
||||
error_message,
|
||||
upgrade_message,
|
||||
upgrade_command
|
||||
}))
|
||||
} else {
|
||||
Err(anyhow!(LoadError::Exited(result.code().unwrap_or(-127))))
|
||||
}
|
||||
}
|
||||
};
|
||||
drop(io_task);
|
||||
result
|
||||
});
|
||||
|
||||
let connection: Rc<dyn AgentConnection> = Rc::new(OldAcpAgentConnection {
|
||||
name,
|
||||
connection,
|
||||
child_status,
|
||||
current_thread: thread_rc,
|
||||
});
|
||||
|
||||
Ok(connection)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Gemini {
|
||||
async fn command(
|
||||
&self,
|
||||
project: &Entity<Project>,
|
||||
@@ -106,7 +189,7 @@ pub(crate) mod tests {
|
||||
use crate::AgentServerCommand;
|
||||
use std::path::Path;
|
||||
|
||||
crate::common_e2e_tests!(Gemini);
|
||||
crate::common_e2e_tests!(Gemini, allow_option_id = "0");
|
||||
|
||||
pub fn local_command() -> AgentServerCommand {
|
||||
let cli_path = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
|
||||
207
crates/agent_servers/src/mcp_server.rs
Normal file
@@ -0,0 +1,207 @@
|
||||
use acp_thread::AcpThread;
|
||||
use agent_client_protocol as acp;
|
||||
use anyhow::Result;
|
||||
use context_server::listener::{McpServerTool, ToolResponse};
|
||||
use context_server::types::{
|
||||
Implementation, InitializeParams, InitializeResponse, ProtocolVersion, ServerCapabilities,
|
||||
ToolsCapabilities, requests,
|
||||
};
|
||||
use futures::channel::oneshot;
|
||||
use gpui::{App, AsyncApp, Task, WeakEntity};
|
||||
use indoc::indoc;
|
||||
|
||||
pub struct ZedMcpServer {
|
||||
server: context_server::listener::McpServer,
|
||||
}
|
||||
|
||||
pub const SERVER_NAME: &str = "zed";
|
||||
|
||||
impl ZedMcpServer {
|
||||
pub async fn new(
|
||||
thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
|
||||
cx: &AsyncApp,
|
||||
) -> Result<Self> {
|
||||
let mut mcp_server = context_server::listener::McpServer::new(cx).await?;
|
||||
mcp_server.handle_request::<requests::Initialize>(Self::handle_initialize);
|
||||
|
||||
mcp_server.add_tool(RequestPermissionTool {
|
||||
thread_rx: thread_rx.clone(),
|
||||
});
|
||||
mcp_server.add_tool(ReadTextFileTool {
|
||||
thread_rx: thread_rx.clone(),
|
||||
});
|
||||
mcp_server.add_tool(WriteTextFileTool {
|
||||
thread_rx: thread_rx.clone(),
|
||||
});
|
||||
|
||||
Ok(Self { server: mcp_server })
|
||||
}
|
||||
|
||||
pub fn server_config(&self) -> Result<acp::McpServerConfig> {
|
||||
#[cfg(not(test))]
|
||||
let zed_path = anyhow::Context::context(
|
||||
std::env::current_exe(),
|
||||
"finding current executable path for use in mcp_server",
|
||||
)?;
|
||||
|
||||
#[cfg(test)]
|
||||
let zed_path = crate::e2e_tests::get_zed_path();
|
||||
|
||||
Ok(acp::McpServerConfig {
|
||||
command: zed_path,
|
||||
args: vec![
|
||||
"--nc".into(),
|
||||
self.server.socket_path().display().to_string(),
|
||||
],
|
||||
env: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_initialize(_: InitializeParams, cx: &App) -> Task<Result<InitializeResponse>> {
|
||||
cx.foreground_executor().spawn(async move {
|
||||
Ok(InitializeResponse {
|
||||
protocol_version: ProtocolVersion("2025-06-18".into()),
|
||||
capabilities: ServerCapabilities {
|
||||
experimental: None,
|
||||
logging: None,
|
||||
completions: None,
|
||||
prompts: None,
|
||||
resources: None,
|
||||
tools: Some(ToolsCapabilities {
|
||||
list_changed: Some(false),
|
||||
}),
|
||||
},
|
||||
server_info: Implementation {
|
||||
name: SERVER_NAME.into(),
|
||||
version: "0.1.0".into(),
|
||||
},
|
||||
meta: None,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Tools
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RequestPermissionTool {
|
||||
thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
|
||||
}
|
||||
|
||||
impl McpServerTool for RequestPermissionTool {
|
||||
type Input = acp::RequestPermissionArguments;
|
||||
type Output = acp::RequestPermissionOutput;
|
||||
|
||||
const NAME: &'static str = "Confirmation";
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
indoc! {"
|
||||
Request permission for tool calls.
|
||||
|
||||
This tool is meant to be called programmatically by the agent loop, not the LLM.
|
||||
"}
|
||||
}
|
||||
|
||||
async fn run(
|
||||
&self,
|
||||
input: Self::Input,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<ToolResponse<Self::Output>> {
|
||||
let mut thread_rx = self.thread_rx.clone();
|
||||
let Some(thread) = thread_rx.recv().await?.upgrade() else {
|
||||
anyhow::bail!("Thread closed");
|
||||
};
|
||||
|
||||
let result = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.request_tool_call_permission(input.tool_call, input.options, cx)
|
||||
})?
|
||||
.await;
|
||||
|
||||
let outcome = match result {
|
||||
Ok(option_id) => acp::RequestPermissionOutcome::Selected { option_id },
|
||||
Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Canceled,
|
||||
};
|
||||
|
||||
Ok(ToolResponse {
|
||||
content: vec![],
|
||||
structured_content: acp::RequestPermissionOutput { outcome },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ReadTextFileTool {
|
||||
thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
|
||||
}
|
||||
|
||||
impl McpServerTool for ReadTextFileTool {
|
||||
type Input = acp::ReadTextFileArguments;
|
||||
type Output = acp::ReadTextFileOutput;
|
||||
|
||||
const NAME: &'static str = "Read";
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Reads the content of the given file in the project including unsaved changes."
|
||||
}
|
||||
|
||||
async fn run(
|
||||
&self,
|
||||
input: Self::Input,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<ToolResponse<Self::Output>> {
|
||||
let mut thread_rx = self.thread_rx.clone();
|
||||
let Some(thread) = thread_rx.recv().await?.upgrade() else {
|
||||
anyhow::bail!("Thread closed");
|
||||
};
|
||||
|
||||
let content = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.read_text_file(input.path, input.line, input.limit, false, cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
Ok(ToolResponse {
|
||||
content: vec![],
|
||||
structured_content: acp::ReadTextFileOutput { content },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct WriteTextFileTool {
|
||||
thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
|
||||
}
|
||||
|
||||
impl McpServerTool for WriteTextFileTool {
|
||||
type Input = acp::WriteTextFileArguments;
|
||||
type Output = ();
|
||||
|
||||
const NAME: &'static str = "Write";
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Write to a file replacing its contents"
|
||||
}
|
||||
|
||||
async fn run(
|
||||
&self,
|
||||
input: Self::Input,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<ToolResponse<Self::Output>> {
|
||||
let mut thread_rx = self.thread_rx.clone();
|
||||
let Some(thread) = thread_rx.recv().await?.upgrade() else {
|
||||
anyhow::bail!("Thread closed");
|
||||
};
|
||||
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.write_text_file(input.path, input.content, cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
Ok(ToolResponse {
|
||||
content: vec![],
|
||||
structured_content: (),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ pub fn init(cx: &mut App) {
|
||||
pub struct AllAgentServersSettings {
|
||||
pub gemini: Option<AgentServerSettings>,
|
||||
pub claude: Option<AgentServerSettings>,
|
||||
pub codex: Option<AgentServerSettings>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug)]
|
||||
@@ -29,13 +30,21 @@ impl settings::Settings for AllAgentServersSettings {
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
|
||||
let mut settings = AllAgentServersSettings::default();
|
||||
|
||||
for AllAgentServersSettings { gemini, claude } in sources.defaults_and_customizations() {
|
||||
for AllAgentServersSettings {
|
||||
gemini,
|
||||
claude,
|
||||
codex,
|
||||
} in sources.defaults_and_customizations()
|
||||
{
|
||||
if gemini.is_some() {
|
||||
settings.gemini = gemini.clone();
|
||||
}
|
||||
if claude.is_some() {
|
||||
settings.claude = claude.clone();
|
||||
}
|
||||
if codex.is_some() {
|
||||
settings.codex = codex.clone();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(settings)
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
use crate::{AgentServer, AgentServerCommand, AgentServerVersion};
|
||||
use acp_thread::{AcpClientDelegate, AcpThread, LoadError};
|
||||
use agentic_coding_protocol as acp;
|
||||
use anyhow::{Result, anyhow};
|
||||
use gpui::{App, AsyncApp, Entity, Task, prelude::*};
|
||||
use project::Project;
|
||||
use std::path::Path;
|
||||
use util::ResultExt;
|
||||
|
||||
pub trait StdioAgentServer: Send + Clone {
|
||||
fn logo(&self) -> ui::IconName;
|
||||
fn name(&self) -> &'static str;
|
||||
fn empty_state_headline(&self) -> &'static str;
|
||||
fn empty_state_message(&self) -> &'static str;
|
||||
fn supports_always_allow(&self) -> bool;
|
||||
|
||||
fn command(
|
||||
&self,
|
||||
project: &Entity<Project>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> impl Future<Output = Result<AgentServerCommand>>;
|
||||
|
||||
fn version(
|
||||
&self,
|
||||
command: &AgentServerCommand,
|
||||
) -> impl Future<Output = Result<AgentServerVersion>> + Send;
|
||||
}
|
||||
|
||||
impl<T: StdioAgentServer + 'static> AgentServer for T {
|
||||
fn name(&self) -> &'static str {
|
||||
self.name()
|
||||
}
|
||||
|
||||
fn empty_state_headline(&self) -> &'static str {
|
||||
self.empty_state_headline()
|
||||
}
|
||||
|
||||
fn empty_state_message(&self) -> &'static str {
|
||||
self.empty_state_message()
|
||||
}
|
||||
|
||||
fn logo(&self) -> ui::IconName {
|
||||
self.logo()
|
||||
}
|
||||
|
||||
fn supports_always_allow(&self) -> bool {
|
||||
self.supports_always_allow()
|
||||
}
|
||||
|
||||
fn new_thread(
|
||||
&self,
|
||||
root_dir: &Path,
|
||||
project: &Entity<Project>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Entity<AcpThread>>> {
|
||||
let root_dir = root_dir.to_path_buf();
|
||||
let project = project.clone();
|
||||
let this = self.clone();
|
||||
let title = self.name().into();
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let command = this.command(&project, cx).await?;
|
||||
|
||||
let mut child = util::command::new_smol_command(&command.path)
|
||||
.args(command.args.iter())
|
||||
.current_dir(root_dir)
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::inherit())
|
||||
.kill_on_drop(true)
|
||||
.spawn()?;
|
||||
|
||||
let stdin = child.stdin.take().unwrap();
|
||||
let stdout = child.stdout.take().unwrap();
|
||||
|
||||
cx.new(|cx| {
|
||||
let foreground_executor = cx.foreground_executor().clone();
|
||||
|
||||
let (connection, io_fut) = acp::AgentConnection::connect_to_agent(
|
||||
AcpClientDelegate::new(cx.entity().downgrade(), cx.to_async()),
|
||||
stdin,
|
||||
stdout,
|
||||
move |fut| foreground_executor.spawn(fut).detach(),
|
||||
);
|
||||
|
||||
let io_task = cx.background_spawn(async move {
|
||||
io_fut.await.log_err();
|
||||
});
|
||||
|
||||
let child_status = cx.background_spawn(async move {
|
||||
let result = match child.status().await {
|
||||
Err(e) => Err(anyhow!(e)),
|
||||
Ok(result) if result.success() => Ok(()),
|
||||
Ok(result) => {
|
||||
if let Some(AgentServerVersion::Unsupported {
|
||||
error_message,
|
||||
upgrade_message,
|
||||
upgrade_command,
|
||||
}) = this.version(&command).await.log_err()
|
||||
{
|
||||
Err(anyhow!(LoadError::Unsupported {
|
||||
error_message,
|
||||
upgrade_message,
|
||||
upgrade_command
|
||||
}))
|
||||
} else {
|
||||
Err(anyhow!(LoadError::Exited(result.code().unwrap_or(-127))))
|
||||
}
|
||||
}
|
||||
};
|
||||
drop(io_task);
|
||||
result
|
||||
});
|
||||
|
||||
AcpThread::new(connection, title, Some(child_status), project.clone(), cx)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ path = "src/agent_settings.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
gpui.workspace = true
|
||||
language_model.workspace = true
|
||||
@@ -20,7 +21,6 @@ schemars.workspace = true
|
||||
serde.workspace = true
|
||||
settings.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
fs.workspace = true
|
||||
|
||||
@@ -321,11 +321,11 @@ pub enum CompletionMode {
|
||||
Burn,
|
||||
}
|
||||
|
||||
impl From<CompletionMode> for zed_llm_client::CompletionMode {
|
||||
impl From<CompletionMode> for cloud_llm_client::CompletionMode {
|
||||
fn from(value: CompletionMode) -> Self {
|
||||
match value {
|
||||
CompletionMode::Normal => zed_llm_client::CompletionMode::Normal,
|
||||
CompletionMode::Burn => zed_llm_client::CompletionMode::Max,
|
||||
CompletionMode::Normal => cloud_llm_client::CompletionMode::Normal,
|
||||
CompletionMode::Burn => cloud_llm_client::CompletionMode::Max,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,10 +17,10 @@ test-support = ["gpui/test-support", "language/test-support"]
|
||||
|
||||
[dependencies]
|
||||
acp_thread.workspace = true
|
||||
agent-client-protocol.workspace = true
|
||||
agent.workspace = true
|
||||
agentic-coding-protocol.workspace = true
|
||||
agent_settings.workspace = true
|
||||
agent_servers.workspace = true
|
||||
agent_settings.workspace = true
|
||||
ai_onboarding.workspace = true
|
||||
anyhow.workspace = true
|
||||
assistant_context.workspace = true
|
||||
@@ -31,7 +31,9 @@ audio.workspace = true
|
||||
buffer_diff.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
component.workspace = true
|
||||
context_server.workspace = true
|
||||
db.workspace = true
|
||||
@@ -45,14 +47,15 @@ futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
html_to_markdown.workspace = true
|
||||
indoc.workspace = true
|
||||
http_client.workspace = true
|
||||
indexed_docs.workspace = true
|
||||
indoc.workspace = true
|
||||
inventory.workspace = true
|
||||
itertools.workspace = true
|
||||
jsonschema.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
language_models.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
markdown.workspace = true
|
||||
@@ -87,6 +90,7 @@ theme.workspace = true
|
||||
time.workspace = true
|
||||
time_format.workspace = true
|
||||
ui.workspace = true
|
||||
ui_input.workspace = true
|
||||
urlencoding.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
@@ -94,7 +98,6 @@ watch.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
assistant_tools.workspace = true
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use acp_thread::Plan;
|
||||
use acp_thread::{AgentConnection, Plan};
|
||||
use agent_servers::AgentServer;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::BTreeMap;
|
||||
@@ -7,7 +7,7 @@ use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use agentic_coding_protocol::{self as acp};
|
||||
use agent_client_protocol as acp;
|
||||
use assistant_tool::ActionLog;
|
||||
use buffer_diff::BufferDiff;
|
||||
use collections::{HashMap, HashSet};
|
||||
@@ -16,7 +16,6 @@ use editor::{
|
||||
EditorStyle, MinimapVisibility, MultiBuffer, PathKey,
|
||||
};
|
||||
use file_icons::FileIcons;
|
||||
use futures::channel::oneshot;
|
||||
use gpui::{
|
||||
Action, Animation, AnimationExt, App, BorderStyle, EdgesRefinement, Empty, Entity, EntityId,
|
||||
FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, SharedString, StyleRefinement,
|
||||
@@ -39,8 +38,7 @@ use zed_actions::agent::{Chat, NextHistoryMessage, PreviousHistoryMessage};
|
||||
|
||||
use ::acp_thread::{
|
||||
AcpThread, AcpThreadEvent, AgentThreadEntry, AssistantMessage, AssistantMessageChunk, Diff,
|
||||
LoadError, MentionPath, ThreadStatus, ToolCall, ToolCallConfirmation, ToolCallContent,
|
||||
ToolCallId, ToolCallStatus,
|
||||
LoadError, MentionPath, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus,
|
||||
};
|
||||
|
||||
use crate::acp::completion_provider::{ContextPickerCompletionProvider, MentionSet};
|
||||
@@ -64,12 +62,13 @@ pub struct AcpThreadView {
|
||||
last_error: Option<Entity<Markdown>>,
|
||||
list_state: ListState,
|
||||
auth_task: Option<Task<()>>,
|
||||
expanded_tool_calls: HashSet<ToolCallId>,
|
||||
expanded_tool_calls: HashSet<acp::ToolCallId>,
|
||||
expanded_thinking_blocks: HashSet<(usize, usize)>,
|
||||
edits_expanded: bool,
|
||||
plan_expanded: bool,
|
||||
editor_expanded: bool,
|
||||
message_history: Rc<RefCell<MessageHistory<acp::SendUserMessageParams>>>,
|
||||
message_history: Rc<RefCell<MessageHistory<Vec<acp::ContentBlock>>>>,
|
||||
_cancel_task: Option<Task<()>>,
|
||||
}
|
||||
|
||||
enum ThreadState {
|
||||
@@ -82,22 +81,16 @@ enum ThreadState {
|
||||
},
|
||||
LoadError(LoadError),
|
||||
Unauthenticated {
|
||||
thread: Entity<AcpThread>,
|
||||
connection: Rc<dyn AgentConnection>,
|
||||
},
|
||||
}
|
||||
|
||||
struct AlwaysAllowOption {
|
||||
id: &'static str,
|
||||
label: SharedString,
|
||||
outcome: acp::ToolCallConfirmationOutcome,
|
||||
}
|
||||
|
||||
impl AcpThreadView {
|
||||
pub fn new(
|
||||
agent: Rc<dyn AgentServer>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
message_history: Rc<RefCell<MessageHistory<acp::SendUserMessageParams>>>,
|
||||
message_history: Rc<RefCell<MessageHistory<Vec<acp::ContentBlock>>>>,
|
||||
min_lines: usize,
|
||||
max_lines: Option<usize>,
|
||||
window: &mut Window,
|
||||
@@ -191,6 +184,7 @@ impl AcpThreadView {
|
||||
plan_expanded: false,
|
||||
editor_expanded: false,
|
||||
message_history,
|
||||
_cancel_task: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,9 +202,9 @@ impl AcpThreadView {
|
||||
.map(|worktree| worktree.read(cx).abs_path())
|
||||
.unwrap_or_else(|| paths::home_dir().as_path().into());
|
||||
|
||||
let task = agent.new_thread(&root_dir, &project, cx);
|
||||
let connect_task = agent.connect(&root_dir, &project, cx);
|
||||
let load_task = cx.spawn_in(window, async move |this, cx| {
|
||||
let thread = match task.await {
|
||||
let connection = match connect_task.await {
|
||||
Ok(thread) => thread,
|
||||
Err(err) => {
|
||||
this.update(cx, |this, cx| {
|
||||
@@ -222,48 +216,30 @@ impl AcpThreadView {
|
||||
}
|
||||
};
|
||||
|
||||
let init_response = async {
|
||||
let resp = thread
|
||||
.read_with(cx, |thread, _cx| thread.initialize())?
|
||||
.await?;
|
||||
anyhow::Ok(resp)
|
||||
};
|
||||
|
||||
let result = match init_response.await {
|
||||
let result = match connection
|
||||
.clone()
|
||||
.new_thread(project.clone(), &root_dir, cx)
|
||||
.await
|
||||
{
|
||||
Err(e) => {
|
||||
let mut cx = cx.clone();
|
||||
if e.downcast_ref::<oneshot::Canceled>().is_some() {
|
||||
let child_status = thread
|
||||
.update(&mut cx, |thread, _| thread.child_status())
|
||||
.ok()
|
||||
.flatten();
|
||||
if let Some(child_status) = child_status {
|
||||
match child_status.await {
|
||||
Ok(_) => Err(e),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
if e.downcast_ref::<acp_thread::Unauthenticated>().is_some() {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.thread_state = ThreadState::Unauthenticated { connection };
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
return;
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
Ok(response) => {
|
||||
if !response.is_authenticated {
|
||||
this.update(cx, |this, _| {
|
||||
this.thread_state = ThreadState::Unauthenticated { thread };
|
||||
})
|
||||
.ok();
|
||||
return;
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
Ok(session_id) => Ok(session_id),
|
||||
};
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
match result {
|
||||
Ok(()) => {
|
||||
Ok(thread) => {
|
||||
let thread_subscription =
|
||||
cx.subscribe_in(&thread, window, Self::handle_thread_event);
|
||||
|
||||
@@ -305,10 +281,10 @@ impl AcpThreadView {
|
||||
|
||||
pub fn thread(&self) -> Option<&Entity<AcpThread>> {
|
||||
match &self.thread_state {
|
||||
ThreadState::Ready { thread, .. } | ThreadState::Unauthenticated { thread } => {
|
||||
Some(thread)
|
||||
}
|
||||
ThreadState::Loading { .. } | ThreadState::LoadError(..) => None,
|
||||
ThreadState::Ready { thread, .. } => Some(thread),
|
||||
ThreadState::Unauthenticated { .. }
|
||||
| ThreadState::Loading { .. }
|
||||
| ThreadState::LoadError(..) => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,7 +301,7 @@ impl AcpThreadView {
|
||||
self.last_error.take();
|
||||
|
||||
if let Some(thread) = self.thread() {
|
||||
thread.update(cx, |thread, cx| thread.cancel(cx)).detach();
|
||||
self._cancel_task = Some(thread.update(cx, |thread, cx| thread.cancel(cx)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,7 +338,7 @@ impl AcpThreadView {
|
||||
self.last_error.take();
|
||||
|
||||
let mut ix = 0;
|
||||
let mut chunks: Vec<acp::UserMessageChunk> = Vec::new();
|
||||
let mut chunks: Vec<acp::ContentBlock> = Vec::new();
|
||||
let project = self.project.clone();
|
||||
self.message_editor.update(cx, |editor, cx| {
|
||||
let text = editor.text(cx);
|
||||
@@ -374,12 +350,19 @@ impl AcpThreadView {
|
||||
{
|
||||
let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot);
|
||||
if crease_range.start > ix {
|
||||
chunks.push(acp::UserMessageChunk::Text {
|
||||
text: text[ix..crease_range.start].to_string(),
|
||||
});
|
||||
chunks.push(text[ix..crease_range.start].into());
|
||||
}
|
||||
if let Some(abs_path) = project.read(cx).absolute_path(&project_path, cx) {
|
||||
chunks.push(acp::UserMessageChunk::Path { path: abs_path });
|
||||
let path_str = abs_path.display().to_string();
|
||||
chunks.push(acp::ContentBlock::ResourceLink(acp::ResourceLink {
|
||||
uri: path_str.clone(),
|
||||
name: path_str,
|
||||
annotations: None,
|
||||
description: None,
|
||||
mime_type: None,
|
||||
size: None,
|
||||
title: None,
|
||||
}));
|
||||
}
|
||||
ix = crease_range.end;
|
||||
}
|
||||
@@ -388,9 +371,7 @@ impl AcpThreadView {
|
||||
if ix < text.len() {
|
||||
let last_chunk = text[ix..].trim();
|
||||
if !last_chunk.is_empty() {
|
||||
chunks.push(acp::UserMessageChunk::Text {
|
||||
text: last_chunk.into(),
|
||||
});
|
||||
chunks.push(last_chunk.into());
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -401,8 +382,7 @@ impl AcpThreadView {
|
||||
}
|
||||
|
||||
let Some(thread) = self.thread() else { return };
|
||||
let message = acp::SendUserMessageParams { chunks };
|
||||
let task = thread.update(cx, |thread, cx| thread.send(message.clone(), cx));
|
||||
let task = thread.update(cx, |thread, cx| thread.send(chunks.clone(), cx));
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let result = task.await;
|
||||
@@ -424,7 +404,7 @@ impl AcpThreadView {
|
||||
editor.remove_creases(mention_set.lock().drain(), cx)
|
||||
});
|
||||
|
||||
self.message_history.borrow_mut().push(message);
|
||||
self.message_history.borrow_mut().push(chunks);
|
||||
}
|
||||
|
||||
fn previous_history_message(
|
||||
@@ -490,7 +470,7 @@ impl AcpThreadView {
|
||||
message_editor: Entity<Editor>,
|
||||
mention_set: Arc<Mutex<MentionSet>>,
|
||||
project: Entity<Project>,
|
||||
message: Option<&acp::SendUserMessageParams>,
|
||||
message: Option<&Vec<acp::ContentBlock>>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
@@ -503,18 +483,19 @@ impl AcpThreadView {
|
||||
let mut text = String::new();
|
||||
let mut mentions = Vec::new();
|
||||
|
||||
for chunk in &message.chunks {
|
||||
for chunk in message {
|
||||
match chunk {
|
||||
acp::UserMessageChunk::Text { text: chunk } => {
|
||||
text.push_str(&chunk);
|
||||
acp::ContentBlock::Text(text_content) => {
|
||||
text.push_str(&text_content.text);
|
||||
}
|
||||
acp::UserMessageChunk::Path { path } => {
|
||||
acp::ContentBlock::ResourceLink(resource_link) => {
|
||||
let path = Path::new(&resource_link.uri);
|
||||
let start = text.len();
|
||||
let content = MentionPath::new(path).to_string();
|
||||
let content = MentionPath::new(&path).to_string();
|
||||
text.push_str(&content);
|
||||
let end = text.len();
|
||||
if let Some(project_path) =
|
||||
project.read(cx).project_path_for_absolute_path(path, cx)
|
||||
project.read(cx).project_path_for_absolute_path(&path, cx)
|
||||
{
|
||||
let filename: SharedString = path
|
||||
.file_name()
|
||||
@@ -525,6 +506,9 @@ impl AcpThreadView {
|
||||
mentions.push((start..end, project_path, filename));
|
||||
}
|
||||
}
|
||||
acp::ContentBlock::Image(_)
|
||||
| acp::ContentBlock::Audio(_)
|
||||
| acp::ContentBlock::Resource(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -590,71 +574,79 @@ impl AcpThreadView {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(multibuffer) = self.entry_diff_multibuffer(entry_ix, cx) else {
|
||||
let Some(multibuffers) = self.entry_diff_multibuffers(entry_ix, cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if self.diff_editors.contains_key(&multibuffer.entity_id()) {
|
||||
return;
|
||||
}
|
||||
let multibuffers = multibuffers.collect::<Vec<_>>();
|
||||
|
||||
let editor = cx.new(|cx| {
|
||||
let mut editor = Editor::new(
|
||||
EditorMode::Full {
|
||||
scale_ui_elements_with_buffer_font_size: false,
|
||||
show_active_line_background: false,
|
||||
sized_by_content: true,
|
||||
},
|
||||
multibuffer.clone(),
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
editor.set_show_gutter(false, cx);
|
||||
editor.disable_inline_diagnostics();
|
||||
editor.disable_expand_excerpt_buttons(cx);
|
||||
editor.set_show_vertical_scrollbar(false, cx);
|
||||
editor.set_minimap_visibility(MinimapVisibility::Disabled, window, cx);
|
||||
editor.set_soft_wrap_mode(SoftWrap::None, cx);
|
||||
editor.scroll_manager.set_forbid_vertical_scroll(true);
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
editor.set_read_only(true);
|
||||
editor.set_show_breakpoints(false, cx);
|
||||
editor.set_show_code_actions(false, cx);
|
||||
editor.set_show_git_diff_gutter(false, cx);
|
||||
editor.set_expand_all_diff_hunks(cx);
|
||||
editor.set_text_style_refinement(TextStyleRefinement {
|
||||
font_size: Some(
|
||||
TextSize::Small
|
||||
.rems(cx)
|
||||
.to_pixels(ThemeSettings::get_global(cx).agent_font_size(cx))
|
||||
.into(),
|
||||
),
|
||||
..Default::default()
|
||||
for multibuffer in multibuffers {
|
||||
if self.diff_editors.contains_key(&multibuffer.entity_id()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let editor = cx.new(|cx| {
|
||||
let mut editor = Editor::new(
|
||||
EditorMode::Full {
|
||||
scale_ui_elements_with_buffer_font_size: false,
|
||||
show_active_line_background: false,
|
||||
sized_by_content: true,
|
||||
},
|
||||
multibuffer.clone(),
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
editor.set_show_gutter(false, cx);
|
||||
editor.disable_inline_diagnostics();
|
||||
editor.disable_expand_excerpt_buttons(cx);
|
||||
editor.set_show_vertical_scrollbar(false, cx);
|
||||
editor.set_minimap_visibility(MinimapVisibility::Disabled, window, cx);
|
||||
editor.set_soft_wrap_mode(SoftWrap::None, cx);
|
||||
editor.scroll_manager.set_forbid_vertical_scroll(true);
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
editor.set_read_only(true);
|
||||
editor.set_show_breakpoints(false, cx);
|
||||
editor.set_show_code_actions(false, cx);
|
||||
editor.set_show_git_diff_gutter(false, cx);
|
||||
editor.set_expand_all_diff_hunks(cx);
|
||||
editor.set_text_style_refinement(TextStyleRefinement {
|
||||
font_size: Some(
|
||||
TextSize::Small
|
||||
.rems(cx)
|
||||
.to_pixels(ThemeSettings::get_global(cx).agent_font_size(cx))
|
||||
.into(),
|
||||
),
|
||||
..Default::default()
|
||||
});
|
||||
editor
|
||||
});
|
||||
editor
|
||||
});
|
||||
let entity_id = multibuffer.entity_id();
|
||||
cx.observe_release(&multibuffer, move |this, _, _| {
|
||||
this.diff_editors.remove(&entity_id);
|
||||
})
|
||||
.detach();
|
||||
let entity_id = multibuffer.entity_id();
|
||||
cx.observe_release(&multibuffer, move |this, _, _| {
|
||||
this.diff_editors.remove(&entity_id);
|
||||
})
|
||||
.detach();
|
||||
|
||||
self.diff_editors.insert(entity_id, editor);
|
||||
self.diff_editors.insert(entity_id, editor);
|
||||
}
|
||||
}
|
||||
|
||||
fn entry_diff_multibuffer(&self, entry_ix: usize, cx: &App) -> Option<Entity<MultiBuffer>> {
|
||||
fn entry_diff_multibuffers(
|
||||
&self,
|
||||
entry_ix: usize,
|
||||
cx: &App,
|
||||
) -> Option<impl Iterator<Item = Entity<MultiBuffer>>> {
|
||||
let entry = self.thread()?.read(cx).entries().get(entry_ix)?;
|
||||
entry.diff().map(|diff| diff.multibuffer.clone())
|
||||
Some(entry.diffs().map(|diff| diff.multibuffer.clone()))
|
||||
}
|
||||
|
||||
fn authenticate(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(thread) = self.thread().cloned() else {
|
||||
let ThreadState::Unauthenticated { ref connection } = self.thread_state else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.last_error.take();
|
||||
let authenticate = thread.read(cx).authenticate();
|
||||
let authenticate = connection.authenticate(cx);
|
||||
self.auth_task = Some(cx.spawn_in(window, {
|
||||
let project = self.project.clone();
|
||||
let agent = self.agent.clone();
|
||||
@@ -684,15 +676,16 @@ impl AcpThreadView {
|
||||
|
||||
fn authorize_tool_call(
|
||||
&mut self,
|
||||
id: ToolCallId,
|
||||
outcome: acp::ToolCallConfirmationOutcome,
|
||||
tool_call_id: acp::ToolCallId,
|
||||
option_id: acp::PermissionOptionId,
|
||||
option_kind: acp::PermissionOptionKind,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(thread) = self.thread() else {
|
||||
return;
|
||||
};
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.authorize_tool_call(id, outcome, cx);
|
||||
thread.authorize_tool_call(tool_call_id, option_id, option_kind, cx);
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
@@ -719,10 +712,12 @@ impl AcpThreadView {
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.text_xs()
|
||||
.child(self.render_markdown(
|
||||
message.content.clone(),
|
||||
user_message_markdown_style(window, cx),
|
||||
)),
|
||||
.children(message.content.markdown().map(|md| {
|
||||
self.render_markdown(
|
||||
md.clone(),
|
||||
user_message_markdown_style(window, cx),
|
||||
)
|
||||
})),
|
||||
)
|
||||
.into_any(),
|
||||
AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => {
|
||||
@@ -730,20 +725,28 @@ impl AcpThreadView {
|
||||
let message_body = v_flex()
|
||||
.w_full()
|
||||
.gap_2p5()
|
||||
.children(chunks.iter().enumerate().map(|(chunk_ix, chunk)| {
|
||||
match chunk {
|
||||
AssistantMessageChunk::Text { chunk } => self
|
||||
.render_markdown(chunk.clone(), style.clone())
|
||||
.into_any_element(),
|
||||
AssistantMessageChunk::Thought { chunk } => self.render_thinking_block(
|
||||
index,
|
||||
chunk_ix,
|
||||
chunk.clone(),
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
}
|
||||
}))
|
||||
.children(chunks.iter().enumerate().filter_map(
|
||||
|(chunk_ix, chunk)| match chunk {
|
||||
AssistantMessageChunk::Message { block } => {
|
||||
block.markdown().map(|md| {
|
||||
self.render_markdown(md.clone(), style.clone())
|
||||
.into_any_element()
|
||||
})
|
||||
}
|
||||
AssistantMessageChunk::Thought { block } => {
|
||||
block.markdown().map(|md| {
|
||||
self.render_thinking_block(
|
||||
index,
|
||||
chunk_ix,
|
||||
md.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
}
|
||||
},
|
||||
))
|
||||
.into_any();
|
||||
|
||||
v_flex()
|
||||
@@ -869,9 +872,12 @@ impl AcpThreadView {
|
||||
let header_id = SharedString::from(format!("tool-call-header-{}", entry_ix));
|
||||
|
||||
let status_icon = match &tool_call.status {
|
||||
ToolCallStatus::WaitingForConfirmation { .. } => None,
|
||||
ToolCallStatus::Allowed {
|
||||
status: acp::ToolCallStatus::Running,
|
||||
status: acp::ToolCallStatus::Pending,
|
||||
}
|
||||
| ToolCallStatus::WaitingForConfirmation { .. } => None,
|
||||
ToolCallStatus::Allowed {
|
||||
status: acp::ToolCallStatus::InProgress,
|
||||
..
|
||||
} => Some(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
@@ -885,13 +891,13 @@ impl AcpThreadView {
|
||||
.into_any(),
|
||||
),
|
||||
ToolCallStatus::Allowed {
|
||||
status: acp::ToolCallStatus::Finished,
|
||||
status: acp::ToolCallStatus::Completed,
|
||||
..
|
||||
} => None,
|
||||
ToolCallStatus::Rejected
|
||||
| ToolCallStatus::Canceled
|
||||
| ToolCallStatus::Allowed {
|
||||
status: acp::ToolCallStatus::Error,
|
||||
status: acp::ToolCallStatus::Failed,
|
||||
..
|
||||
} => Some(
|
||||
Icon::new(IconName::X)
|
||||
@@ -909,34 +915,9 @@ impl AcpThreadView {
|
||||
.any(|content| matches!(content, ToolCallContent::Diff { .. })),
|
||||
};
|
||||
|
||||
let is_collapsible = tool_call.content.is_some() && !needs_confirmation;
|
||||
let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation;
|
||||
let is_open = !is_collapsible || self.expanded_tool_calls.contains(&tool_call.id);
|
||||
|
||||
let content = if is_open {
|
||||
match &tool_call.status {
|
||||
ToolCallStatus::WaitingForConfirmation { confirmation, .. } => {
|
||||
Some(self.render_tool_call_confirmation(
|
||||
tool_call.id,
|
||||
confirmation,
|
||||
tool_call.content.as_ref(),
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
}
|
||||
ToolCallStatus::Allowed { .. } | ToolCallStatus::Canceled => {
|
||||
tool_call.content.as_ref().map(|content| {
|
||||
div()
|
||||
.py_1p5()
|
||||
.child(self.render_tool_call_content(content, window, cx))
|
||||
.into_any_element()
|
||||
})
|
||||
}
|
||||
ToolCallStatus::Rejected => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.when(needs_confirmation, |this| {
|
||||
this.rounded_lg()
|
||||
@@ -976,9 +957,19 @@ impl AcpThreadView {
|
||||
})
|
||||
.gap_1p5()
|
||||
.child(
|
||||
Icon::new(tool_call.icon)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
Icon::new(match tool_call.kind {
|
||||
acp::ToolKind::Read => IconName::ToolRead,
|
||||
acp::ToolKind::Edit => IconName::ToolPencil,
|
||||
acp::ToolKind::Delete => IconName::ToolDeleteFile,
|
||||
acp::ToolKind::Move => IconName::ArrowRightLeft,
|
||||
acp::ToolKind::Search => IconName::ToolSearch,
|
||||
acp::ToolKind::Execute => IconName::ToolTerminal,
|
||||
acp::ToolKind::Think => IconName::ToolBulb,
|
||||
acp::ToolKind::Fetch => IconName::ToolWeb,
|
||||
acp::ToolKind::Other => IconName::ToolHammer,
|
||||
})
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(if tool_call.locations.len() == 1 {
|
||||
let name = tool_call.locations[0]
|
||||
@@ -1023,16 +1014,16 @@ impl AcpThreadView {
|
||||
.gap_0p5()
|
||||
.when(is_collapsible, |this| {
|
||||
this.child(
|
||||
Disclosure::new(("expand", tool_call.id.0), is_open)
|
||||
Disclosure::new(("expand", entry_ix), is_open)
|
||||
.opened_icon(IconName::ChevronUp)
|
||||
.closed_icon(IconName::ChevronDown)
|
||||
.on_click(cx.listener({
|
||||
let id = tool_call.id;
|
||||
let id = tool_call.id.clone();
|
||||
move |this: &mut Self, _, _, cx: &mut Context<Self>| {
|
||||
if is_open {
|
||||
this.expanded_tool_calls.remove(&id);
|
||||
} else {
|
||||
this.expanded_tool_calls.insert(id);
|
||||
this.expanded_tool_calls.insert(id.clone());
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
@@ -1042,12 +1033,12 @@ impl AcpThreadView {
|
||||
.children(status_icon),
|
||||
)
|
||||
.on_click(cx.listener({
|
||||
let id = tool_call.id;
|
||||
let id = tool_call.id.clone();
|
||||
move |this: &mut Self, _, _, cx: &mut Context<Self>| {
|
||||
if is_open {
|
||||
this.expanded_tool_calls.remove(&id);
|
||||
} else {
|
||||
this.expanded_tool_calls.insert(id);
|
||||
this.expanded_tool_calls.insert(id.clone());
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
@@ -1055,7 +1046,7 @@ impl AcpThreadView {
|
||||
)
|
||||
.when(is_open, |this| {
|
||||
this.child(
|
||||
div()
|
||||
v_flex()
|
||||
.text_xs()
|
||||
.when(is_collapsible, |this| {
|
||||
this.mt_1()
|
||||
@@ -1064,7 +1055,45 @@ impl AcpThreadView {
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.rounded_lg()
|
||||
})
|
||||
.children(content),
|
||||
.map(|this| {
|
||||
if is_open {
|
||||
match &tool_call.status {
|
||||
ToolCallStatus::WaitingForConfirmation { options, .. } => this
|
||||
.children(tool_call.content.iter().map(|content| {
|
||||
div()
|
||||
.py_1p5()
|
||||
.child(
|
||||
self.render_tool_call_content(
|
||||
content, window, cx,
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
}))
|
||||
.child(self.render_permission_buttons(
|
||||
options,
|
||||
entry_ix,
|
||||
tool_call.id.clone(),
|
||||
tool_call.content.is_empty(),
|
||||
cx,
|
||||
)),
|
||||
ToolCallStatus::Allowed { .. } | ToolCallStatus::Canceled => {
|
||||
this.children(tool_call.content.iter().map(|content| {
|
||||
div()
|
||||
.py_1p5()
|
||||
.child(
|
||||
self.render_tool_call_content(
|
||||
content, window, cx,
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
}))
|
||||
}
|
||||
ToolCallStatus::Rejected => this,
|
||||
}
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -1076,14 +1105,20 @@ impl AcpThreadView {
|
||||
cx: &Context<Self>,
|
||||
) -> AnyElement {
|
||||
match content {
|
||||
ToolCallContent::Markdown { markdown } => {
|
||||
div()
|
||||
.p_2()
|
||||
.child(self.render_markdown(
|
||||
markdown.clone(),
|
||||
default_markdown_style(false, window, cx),
|
||||
))
|
||||
.into_any_element()
|
||||
ToolCallContent::ContentBlock { content } => {
|
||||
if let Some(md) = content.markdown() {
|
||||
div()
|
||||
.p_2()
|
||||
.child(
|
||||
self.render_markdown(
|
||||
md.clone(),
|
||||
default_markdown_style(false, window, cx),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
} else {
|
||||
Empty.into_any_element()
|
||||
}
|
||||
}
|
||||
ToolCallContent::Diff {
|
||||
diff: Diff { multibuffer, .. },
|
||||
@@ -1092,223 +1127,56 @@ impl AcpThreadView {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_tool_call_confirmation(
|
||||
fn render_permission_buttons(
|
||||
&self,
|
||||
tool_call_id: ToolCallId,
|
||||
confirmation: &ToolCallConfirmation,
|
||||
content: Option<&ToolCallContent>,
|
||||
window: &Window,
|
||||
cx: &Context<Self>,
|
||||
) -> AnyElement {
|
||||
let confirmation_container = v_flex().mt_1().py_1p5();
|
||||
|
||||
match confirmation {
|
||||
ToolCallConfirmation::Edit { description } => confirmation_container
|
||||
.child(
|
||||
div()
|
||||
.px_2()
|
||||
.children(description.clone().map(|description| {
|
||||
self.render_markdown(
|
||||
description,
|
||||
default_markdown_style(false, window, cx),
|
||||
)
|
||||
})),
|
||||
)
|
||||
.children(content.map(|content| self.render_tool_call_content(content, window, cx)))
|
||||
.child(self.render_confirmation_buttons(
|
||||
&[AlwaysAllowOption {
|
||||
id: "always_allow",
|
||||
label: "Always Allow Edits".into(),
|
||||
outcome: acp::ToolCallConfirmationOutcome::AlwaysAllow,
|
||||
}],
|
||||
tool_call_id,
|
||||
cx,
|
||||
))
|
||||
.into_any(),
|
||||
ToolCallConfirmation::Execute {
|
||||
command,
|
||||
root_command,
|
||||
description,
|
||||
} => confirmation_container
|
||||
.child(v_flex().px_2().pb_1p5().child(command.clone()).children(
|
||||
description.clone().map(|description| {
|
||||
self.render_markdown(description, default_markdown_style(false, window, cx))
|
||||
.on_url_click({
|
||||
let workspace = self.workspace.clone();
|
||||
move |text, window, cx| {
|
||||
Self::open_link(text, &workspace, window, cx);
|
||||
}
|
||||
})
|
||||
}),
|
||||
))
|
||||
.children(content.map(|content| self.render_tool_call_content(content, window, cx)))
|
||||
.child(self.render_confirmation_buttons(
|
||||
&[AlwaysAllowOption {
|
||||
id: "always_allow",
|
||||
label: format!("Always Allow {root_command}").into(),
|
||||
outcome: acp::ToolCallConfirmationOutcome::AlwaysAllow,
|
||||
}],
|
||||
tool_call_id,
|
||||
cx,
|
||||
))
|
||||
.into_any(),
|
||||
ToolCallConfirmation::Mcp {
|
||||
server_name,
|
||||
tool_name: _,
|
||||
tool_display_name,
|
||||
description,
|
||||
} => confirmation_container
|
||||
.child(
|
||||
v_flex()
|
||||
.px_2()
|
||||
.pb_1p5()
|
||||
.child(format!("{server_name} - {tool_display_name}"))
|
||||
.children(description.clone().map(|description| {
|
||||
self.render_markdown(
|
||||
description,
|
||||
default_markdown_style(false, window, cx),
|
||||
)
|
||||
})),
|
||||
)
|
||||
.children(content.map(|content| self.render_tool_call_content(content, window, cx)))
|
||||
.child(self.render_confirmation_buttons(
|
||||
&[
|
||||
AlwaysAllowOption {
|
||||
id: "always_allow_server",
|
||||
label: format!("Always Allow {server_name}").into(),
|
||||
outcome: acp::ToolCallConfirmationOutcome::AlwaysAllowMcpServer,
|
||||
},
|
||||
AlwaysAllowOption {
|
||||
id: "always_allow_tool",
|
||||
label: format!("Always Allow {tool_display_name}").into(),
|
||||
outcome: acp::ToolCallConfirmationOutcome::AlwaysAllowTool,
|
||||
},
|
||||
],
|
||||
tool_call_id,
|
||||
cx,
|
||||
))
|
||||
.into_any(),
|
||||
ToolCallConfirmation::Fetch { description, urls } => confirmation_container
|
||||
.child(
|
||||
v_flex()
|
||||
.px_2()
|
||||
.pb_1p5()
|
||||
.gap_1()
|
||||
.children(urls.iter().map(|url| {
|
||||
h_flex().child(
|
||||
Button::new(url.clone(), url)
|
||||
.icon(IconName::ArrowUpRight)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.on_click({
|
||||
let url = url.clone();
|
||||
move |_, _, cx| cx.open_url(&url)
|
||||
}),
|
||||
)
|
||||
}))
|
||||
.children(description.clone().map(|description| {
|
||||
self.render_markdown(
|
||||
description,
|
||||
default_markdown_style(false, window, cx),
|
||||
)
|
||||
})),
|
||||
)
|
||||
.children(content.map(|content| self.render_tool_call_content(content, window, cx)))
|
||||
.child(self.render_confirmation_buttons(
|
||||
&[AlwaysAllowOption {
|
||||
id: "always_allow",
|
||||
label: "Always Allow".into(),
|
||||
outcome: acp::ToolCallConfirmationOutcome::AlwaysAllow,
|
||||
}],
|
||||
tool_call_id,
|
||||
cx,
|
||||
))
|
||||
.into_any(),
|
||||
ToolCallConfirmation::Other { description } => confirmation_container
|
||||
.child(v_flex().px_2().pb_1p5().child(self.render_markdown(
|
||||
description.clone(),
|
||||
default_markdown_style(false, window, cx),
|
||||
)))
|
||||
.children(content.map(|content| self.render_tool_call_content(content, window, cx)))
|
||||
.child(self.render_confirmation_buttons(
|
||||
&[AlwaysAllowOption {
|
||||
id: "always_allow",
|
||||
label: "Always Allow".into(),
|
||||
outcome: acp::ToolCallConfirmationOutcome::AlwaysAllow,
|
||||
}],
|
||||
tool_call_id,
|
||||
cx,
|
||||
))
|
||||
.into_any(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_confirmation_buttons(
|
||||
&self,
|
||||
always_allow_options: &[AlwaysAllowOption],
|
||||
tool_call_id: ToolCallId,
|
||||
options: &[acp::PermissionOption],
|
||||
entry_ix: usize,
|
||||
tool_call_id: acp::ToolCallId,
|
||||
empty_content: bool,
|
||||
cx: &Context<Self>,
|
||||
) -> Div {
|
||||
h_flex()
|
||||
.pt_1p5()
|
||||
.py_1p5()
|
||||
.px_1p5()
|
||||
.gap_1()
|
||||
.justify_end()
|
||||
.border_t_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.when(self.agent.supports_always_allow(), |this| {
|
||||
this.children(always_allow_options.into_iter().map(|always_allow_option| {
|
||||
let outcome = always_allow_option.outcome;
|
||||
Button::new(
|
||||
(always_allow_option.id, tool_call_id.0),
|
||||
always_allow_option.label.clone(),
|
||||
)
|
||||
.icon(IconName::CheckDouble)
|
||||
.when(!empty_content, |this| {
|
||||
this.border_t_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
})
|
||||
.children(options.iter().map(|option| {
|
||||
let option_id = SharedString::from(option.id.0.clone());
|
||||
Button::new((option_id, entry_ix), option.label.clone())
|
||||
.map(|this| match option.kind {
|
||||
acp::PermissionOptionKind::AllowOnce => {
|
||||
this.icon(IconName::Check).icon_color(Color::Success)
|
||||
}
|
||||
acp::PermissionOptionKind::AllowAlways => {
|
||||
this.icon(IconName::CheckDouble).icon_color(Color::Success)
|
||||
}
|
||||
acp::PermissionOptionKind::RejectOnce => {
|
||||
this.icon(IconName::X).icon_color(Color::Error)
|
||||
}
|
||||
acp::PermissionOptionKind::RejectAlways => {
|
||||
this.icon(IconName::X).icon_color(Color::Error)
|
||||
}
|
||||
})
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Success)
|
||||
.on_click(cx.listener({
|
||||
let id = tool_call_id;
|
||||
let tool_call_id = tool_call_id.clone();
|
||||
let option_id = option.id.clone();
|
||||
let option_kind = option.kind;
|
||||
move |this, _, _, cx| {
|
||||
this.authorize_tool_call(id, outcome, cx);
|
||||
this.authorize_tool_call(
|
||||
tool_call_id.clone(),
|
||||
option_id.clone(),
|
||||
option_kind,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}))
|
||||
}))
|
||||
})
|
||||
.child(
|
||||
Button::new(("allow", tool_call_id.0), "Allow")
|
||||
.icon(IconName::Check)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Success)
|
||||
.on_click(cx.listener({
|
||||
let id = tool_call_id;
|
||||
move |this, _, _, cx| {
|
||||
this.authorize_tool_call(
|
||||
id,
|
||||
acp::ToolCallConfirmationOutcome::Allow,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new(("reject", tool_call_id.0), "Reject")
|
||||
.icon(IconName::X)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Error)
|
||||
.on_click(cx.listener({
|
||||
let id = tool_call_id;
|
||||
move |this, _, _, cx| {
|
||||
this.authorize_tool_call(
|
||||
id,
|
||||
acp::ToolCallConfirmationOutcome::Reject,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})),
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
fn render_diff_editor(&self, multibuffer: &Entity<MultiBuffer>) -> AnyElement {
|
||||
@@ -2245,12 +2113,11 @@ impl AcpThreadView {
|
||||
.languages
|
||||
.language_for_name("Markdown");
|
||||
|
||||
let (thread_summary, markdown) = match &self.thread_state {
|
||||
ThreadState::Ready { thread, .. } | ThreadState::Unauthenticated { thread } => {
|
||||
let thread = thread.read(cx);
|
||||
(thread.title().to_string(), thread.to_markdown(cx))
|
||||
}
|
||||
ThreadState::Loading { .. } | ThreadState::LoadError(..) => return Task::ready(Ok(())),
|
||||
let (thread_summary, markdown) = if let Some(thread) = self.thread() {
|
||||
let thread = thread.read(cx);
|
||||
(thread.title().to_string(), thread.to_markdown(cx))
|
||||
} else {
|
||||
return Task::ready(Ok(()));
|
||||
};
|
||||
|
||||
window.spawn(cx, async move |cx| {
|
||||
|
||||
@@ -14,6 +14,7 @@ use agent_settings::{AgentSettings, NotifyWhenAgentWaiting};
|
||||
use anyhow::Context as _;
|
||||
use assistant_tool::ToolUseStatus;
|
||||
use audio::{Audio, Sound};
|
||||
use cloud_llm_client::CompletionIntent;
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::actions::{MoveUp, Paste};
|
||||
use editor::scroll::Autoscroll;
|
||||
@@ -52,7 +53,6 @@ use util::ResultExt as _;
|
||||
use util::markdown::MarkdownCodeBlock;
|
||||
use workspace::{CollaboratorId, Workspace};
|
||||
use zed_actions::assistant::OpenRulesLibrary;
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
const CODEBLOCK_CONTAINER_GROUP: &str = "codeblock_container";
|
||||
const EDIT_PREVIOUS_MESSAGE_MIN_LINES: usize = 1;
|
||||
@@ -3895,7 +3895,7 @@ mod tests {
|
||||
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
|
||||
registry.set_default_model(
|
||||
Some(ConfiguredModel {
|
||||
provider: Arc::new(FakeLanguageModelProvider),
|
||||
provider: Arc::new(FakeLanguageModelProvider::default()),
|
||||
model,
|
||||
}),
|
||||
cx,
|
||||
@@ -3979,7 +3979,7 @@ mod tests {
|
||||
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
|
||||
registry.set_default_model(
|
||||
Some(ConfiguredModel {
|
||||
provider: Arc::new(FakeLanguageModelProvider),
|
||||
provider: Arc::new(FakeLanguageModelProvider::default()),
|
||||
model: model.clone(),
|
||||
}),
|
||||
cx,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
mod add_llm_provider_modal;
|
||||
mod configure_context_server_modal;
|
||||
mod manage_profiles_modal;
|
||||
mod tool_picker;
|
||||
@@ -28,7 +29,7 @@ use proto::Plan;
|
||||
use settings::{Settings, update_settings_file};
|
||||
use ui::{
|
||||
Chip, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu,
|
||||
Scrollbar, ScrollbarState, Switch, SwitchColor, Tooltip, prelude::*,
|
||||
Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip, prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use workspace::Workspace;
|
||||
@@ -37,7 +38,10 @@ use zed_actions::ExtensionCategoryFilter;
|
||||
pub(crate) use configure_context_server_modal::ConfigureContextServerModal;
|
||||
pub(crate) use manage_profiles_modal::ManageProfilesModal;
|
||||
|
||||
use crate::AddContextServer;
|
||||
use crate::{
|
||||
AddContextServer,
|
||||
agent_configuration::add_llm_provider_modal::{AddLlmProviderModal, LlmCompatibleProvider},
|
||||
};
|
||||
|
||||
pub struct AgentConfiguration {
|
||||
fs: Arc<dyn Fs>,
|
||||
@@ -181,7 +185,15 @@ impl AgentConfiguration {
|
||||
None
|
||||
};
|
||||
|
||||
let is_signed_in = self
|
||||
.workspace
|
||||
.read_with(cx, |workspace, _| {
|
||||
workspace.client().status().borrow().is_connected()
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
v_flex()
|
||||
.w_full()
|
||||
.when(is_expanded, |this| this.mb_2())
|
||||
.child(
|
||||
div()
|
||||
@@ -212,6 +224,7 @@ impl AgentConfiguration {
|
||||
.hover(|hover| hover.bg(cx.theme().colors().element_hover))
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.child(
|
||||
Icon::new(provider.icon())
|
||||
@@ -220,14 +233,15 @@ impl AgentConfiguration {
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new(provider_name.clone())
|
||||
.size(LabelSize::Large),
|
||||
)
|
||||
.map(|this| {
|
||||
if is_zed_provider {
|
||||
this.gap_2().child(
|
||||
if is_zed_provider && is_signed_in {
|
||||
this.child(
|
||||
self.render_zed_plan_info(current_plan, cx),
|
||||
)
|
||||
} else {
|
||||
@@ -303,21 +317,78 @@ impl AgentConfiguration {
|
||||
let providers = LanguageModelRegistry::read_global(cx).providers();
|
||||
|
||||
v_flex()
|
||||
.w_full()
|
||||
.child(
|
||||
v_flex()
|
||||
h_flex()
|
||||
.p(DynamicSpacing::Base16.rems(cx))
|
||||
.pr(DynamicSpacing::Base20.rems(cx))
|
||||
.pb_0()
|
||||
.mb_2p5()
|
||||
.gap_0p5()
|
||||
.child(Headline::new("LLM Providers"))
|
||||
.items_start()
|
||||
.justify_between()
|
||||
.child(
|
||||
Label::new("Add at least one provider to use AI-powered features.")
|
||||
.color(Color::Muted),
|
||||
v_flex()
|
||||
.w_full()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(Headline::new("LLM Providers"))
|
||||
.child(
|
||||
PopoverMenu::new("add-provider-popover")
|
||||
.trigger(
|
||||
Button::new("add-provider", "Add Provider")
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon(IconName::Plus)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.label_size(LabelSize::Small),
|
||||
)
|
||||
.anchor(gpui::Corner::TopRight)
|
||||
.menu({
|
||||
let workspace = self.workspace.clone();
|
||||
move |window, cx| {
|
||||
Some(ContextMenu::build(
|
||||
window,
|
||||
cx,
|
||||
|menu, _window, _cx| {
|
||||
menu.header("Compatible APIs").entry(
|
||||
"OpenAI",
|
||||
None,
|
||||
{
|
||||
let workspace =
|
||||
workspace.clone();
|
||||
move |window, cx| {
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
AddLlmProviderModal::toggle(
|
||||
LlmCompatibleProvider::OpenAi,
|
||||
workspace,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
))
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Label::new("Add at least one provider to use AI-powered features.")
|
||||
.color(Color::Muted),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.pl(DynamicSpacing::Base08.rems(cx))
|
||||
.pr(DynamicSpacing::Base20.rems(cx))
|
||||
.children(
|
||||
@@ -330,119 +401,74 @@ impl AgentConfiguration {
|
||||
|
||||
fn render_command_permission(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let always_allow_tool_actions = AgentSettings::get_global(cx).always_allow_tool_actions;
|
||||
let fs = self.fs.clone();
|
||||
|
||||
h_flex()
|
||||
.gap_4()
|
||||
.justify_between()
|
||||
.flex_wrap()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.max_w_5_6()
|
||||
.child(Label::new("Allow running editing tools without asking for confirmation"))
|
||||
.child(
|
||||
Label::new(
|
||||
"The agent can perform potentially destructive actions without asking for your confirmation.",
|
||||
)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Switch::new(
|
||||
"always-allow-tool-actions-switch",
|
||||
always_allow_tool_actions.into(),
|
||||
)
|
||||
.color(SwitchColor::Accent)
|
||||
.on_click({
|
||||
let fs = self.fs.clone();
|
||||
move |state, _window, cx| {
|
||||
let allow = state == &ToggleState::Selected;
|
||||
update_settings_file::<AgentSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _| {
|
||||
settings.set_always_allow_tool_actions(allow);
|
||||
},
|
||||
);
|
||||
}
|
||||
}),
|
||||
)
|
||||
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.",
|
||||
always_allow_tool_actions,
|
||||
move |state, _window, cx| {
|
||||
let allow = state == &ToggleState::Selected;
|
||||
update_settings_file::<AgentSettings>(fs.clone(), cx, move |settings, _| {
|
||||
settings.set_always_allow_tool_actions(allow);
|
||||
});
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn render_single_file_review(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let single_file_review = AgentSettings::get_global(cx).single_file_review;
|
||||
let fs = self.fs.clone();
|
||||
|
||||
h_flex()
|
||||
.gap_4()
|
||||
.justify_between()
|
||||
.flex_wrap()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.max_w_5_6()
|
||||
.child(Label::new("Enable single-file agent reviews"))
|
||||
.child(
|
||||
Label::new(
|
||||
"Agent edits are also displayed in single-file editors for review.",
|
||||
)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Switch::new("single-file-review-switch", single_file_review.into())
|
||||
.color(SwitchColor::Accent)
|
||||
.on_click({
|
||||
let fs = self.fs.clone();
|
||||
move |state, _window, cx| {
|
||||
let allow = state == &ToggleState::Selected;
|
||||
update_settings_file::<AgentSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _| {
|
||||
settings.set_single_file_review(allow);
|
||||
},
|
||||
);
|
||||
}
|
||||
}),
|
||||
)
|
||||
SwitchField::new(
|
||||
"single-file-review",
|
||||
"Enable single-file agent reviews",
|
||||
"Agent edits are also displayed in single-file editors for review.",
|
||||
single_file_review,
|
||||
move |state, _window, cx| {
|
||||
let allow = state == &ToggleState::Selected;
|
||||
update_settings_file::<AgentSettings>(fs.clone(), cx, move |settings, _| {
|
||||
settings.set_single_file_review(allow);
|
||||
});
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn render_sound_notification(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let play_sound_when_agent_done = AgentSettings::get_global(cx).play_sound_when_agent_done;
|
||||
let fs = self.fs.clone();
|
||||
|
||||
h_flex()
|
||||
.gap_4()
|
||||
.justify_between()
|
||||
.flex_wrap()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.max_w_5_6()
|
||||
.child(Label::new("Play sound when finished generating"))
|
||||
.child(
|
||||
Label::new(
|
||||
"Hear a notification sound when the agent is done generating changes or needs your input.",
|
||||
)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Switch::new("play-sound-notification-switch", play_sound_when_agent_done.into())
|
||||
.color(SwitchColor::Accent)
|
||||
.on_click({
|
||||
let fs = self.fs.clone();
|
||||
move |state, _window, cx| {
|
||||
let allow = state == &ToggleState::Selected;
|
||||
update_settings_file::<AgentSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _| {
|
||||
settings.set_play_sound_when_agent_done(allow);
|
||||
},
|
||||
);
|
||||
}
|
||||
}),
|
||||
)
|
||||
SwitchField::new(
|
||||
"sound-notification",
|
||||
"Play sound when finished generating",
|
||||
"Hear a notification sound when the agent is done generating changes or needs your input.",
|
||||
play_sound_when_agent_done,
|
||||
move |state, _window, cx| {
|
||||
let allow = state == &ToggleState::Selected;
|
||||
update_settings_file::<AgentSettings>(fs.clone(), cx, move |settings, _| {
|
||||
settings.set_play_sound_when_agent_done(allow);
|
||||
});
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn render_modifier_to_send(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let use_modifier_to_send = AgentSettings::get_global(cx).use_modifier_to_send;
|
||||
let fs = self.fs.clone();
|
||||
|
||||
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.",
|
||||
use_modifier_to_send,
|
||||
move |state, _window, cx| {
|
||||
let allow = state == &ToggleState::Selected;
|
||||
update_settings_file::<AgentSettings>(fs.clone(), cx, move |settings, _| {
|
||||
settings.set_use_modifier_to_send(allow);
|
||||
});
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn render_general_settings_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
@@ -456,6 +482,7 @@ impl AgentConfiguration {
|
||||
.child(self.render_command_permission(cx))
|
||||
.child(self.render_single_file_review(cx))
|
||||
.child(self.render_sound_notification(cx))
|
||||
.child(self.render_modifier_to_send(cx))
|
||||
}
|
||||
|
||||
fn render_zed_plan_info(&self, plan: Option<Plan>, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
|
||||
@@ -0,0 +1,639 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use collections::HashSet;
|
||||
use fs::Fs;
|
||||
use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, Task};
|
||||
use language_model::LanguageModelRegistry;
|
||||
use language_models::{
|
||||
AllLanguageModelSettings, OpenAiCompatibleSettingsContent,
|
||||
provider::open_ai_compatible::AvailableModel,
|
||||
};
|
||||
use settings::update_settings_file;
|
||||
use ui::{Banner, KeyBinding, Modal, ModalFooter, ModalHeader, Section, prelude::*};
|
||||
use ui_input::SingleLineInput;
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum LlmCompatibleProvider {
|
||||
OpenAi,
|
||||
}
|
||||
|
||||
impl LlmCompatibleProvider {
|
||||
fn name(&self) -> &'static str {
|
||||
match self {
|
||||
LlmCompatibleProvider::OpenAi => "OpenAI",
|
||||
}
|
||||
}
|
||||
|
||||
fn api_url(&self) -> &'static str {
|
||||
match self {
|
||||
LlmCompatibleProvider::OpenAi => "https://api.openai.com/v1",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AddLlmProviderInput {
|
||||
provider_name: Entity<SingleLineInput>,
|
||||
api_url: Entity<SingleLineInput>,
|
||||
api_key: Entity<SingleLineInput>,
|
||||
models: Vec<ModelInput>,
|
||||
}
|
||||
|
||||
impl AddLlmProviderInput {
|
||||
fn new(provider: LlmCompatibleProvider, window: &mut Window, cx: &mut App) -> Self {
|
||||
let provider_name = single_line_input("Provider Name", provider.name(), None, window, cx);
|
||||
let api_url = single_line_input("API URL", provider.api_url(), None, window, cx);
|
||||
let api_key = single_line_input(
|
||||
"API Key",
|
||||
"000000000000000000000000000000000000000000000000",
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
Self {
|
||||
provider_name,
|
||||
api_url,
|
||||
api_key,
|
||||
models: vec![ModelInput::new(window, cx)],
|
||||
}
|
||||
}
|
||||
|
||||
fn add_model(&mut self, window: &mut Window, cx: &mut App) {
|
||||
self.models.push(ModelInput::new(window, cx));
|
||||
}
|
||||
|
||||
fn remove_model(&mut self, index: usize) {
|
||||
self.models.remove(index);
|
||||
}
|
||||
}
|
||||
|
||||
struct ModelInput {
|
||||
name: Entity<SingleLineInput>,
|
||||
max_completion_tokens: Entity<SingleLineInput>,
|
||||
max_output_tokens: Entity<SingleLineInput>,
|
||||
max_tokens: Entity<SingleLineInput>,
|
||||
}
|
||||
|
||||
impl ModelInput {
|
||||
fn new(window: &mut Window, cx: &mut App) -> Self {
|
||||
let model_name = single_line_input(
|
||||
"Model Name",
|
||||
"e.g. gpt-4o, claude-opus-4, gemini-2.5-pro",
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let max_completion_tokens = single_line_input(
|
||||
"Max Completion Tokens",
|
||||
"200000",
|
||||
Some("200000"),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let max_output_tokens = single_line_input(
|
||||
"Max Output Tokens",
|
||||
"Max Output Tokens",
|
||||
Some("32000"),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let max_tokens = single_line_input("Max Tokens", "Max Tokens", Some("200000"), window, cx);
|
||||
Self {
|
||||
name: model_name,
|
||||
max_completion_tokens,
|
||||
max_output_tokens,
|
||||
max_tokens,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(&self, cx: &App) -> Result<AvailableModel, SharedString> {
|
||||
let name = self.name.read(cx).text(cx);
|
||||
if name.is_empty() {
|
||||
return Err(SharedString::from("Model Name cannot be empty"));
|
||||
}
|
||||
Ok(AvailableModel {
|
||||
name,
|
||||
display_name: None,
|
||||
max_completion_tokens: Some(
|
||||
self.max_completion_tokens
|
||||
.read(cx)
|
||||
.text(cx)
|
||||
.parse::<u64>()
|
||||
.map_err(|_| SharedString::from("Max Completion Tokens must be a number"))?,
|
||||
),
|
||||
max_output_tokens: Some(
|
||||
self.max_output_tokens
|
||||
.read(cx)
|
||||
.text(cx)
|
||||
.parse::<u64>()
|
||||
.map_err(|_| SharedString::from("Max Output Tokens must be a number"))?,
|
||||
),
|
||||
max_tokens: self
|
||||
.max_tokens
|
||||
.read(cx)
|
||||
.text(cx)
|
||||
.parse::<u64>()
|
||||
.map_err(|_| SharedString::from("Max Tokens must be a number"))?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn single_line_input(
|
||||
label: impl Into<SharedString>,
|
||||
placeholder: impl Into<SharedString>,
|
||||
text: Option<&str>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<SingleLineInput> {
|
||||
cx.new(|cx| {
|
||||
let input = SingleLineInput::new(window, cx, placeholder).label(label);
|
||||
if let Some(text) = text {
|
||||
input
|
||||
.editor()
|
||||
.update(cx, |editor, cx| editor.set_text(text, window, cx));
|
||||
}
|
||||
input
|
||||
})
|
||||
}
|
||||
|
||||
fn save_provider_to_settings(
|
||||
input: &AddLlmProviderInput,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<(), SharedString>> {
|
||||
let provider_name: Arc<str> = input.provider_name.read(cx).text(cx).into();
|
||||
if provider_name.is_empty() {
|
||||
return Task::ready(Err("Provider Name cannot be empty".into()));
|
||||
}
|
||||
|
||||
if LanguageModelRegistry::read_global(cx)
|
||||
.providers()
|
||||
.iter()
|
||||
.any(|provider| {
|
||||
provider.id().0.as_ref() == provider_name.as_ref()
|
||||
|| provider.name().0.as_ref() == provider_name.as_ref()
|
||||
})
|
||||
{
|
||||
return Task::ready(Err(
|
||||
"Provider Name is already taken by another provider".into()
|
||||
));
|
||||
}
|
||||
|
||||
let api_url = input.api_url.read(cx).text(cx);
|
||||
if api_url.is_empty() {
|
||||
return Task::ready(Err("API URL cannot be empty".into()));
|
||||
}
|
||||
|
||||
let api_key = input.api_key.read(cx).text(cx);
|
||||
if api_key.is_empty() {
|
||||
return Task::ready(Err("API Key cannot be empty".into()));
|
||||
}
|
||||
|
||||
let mut models = Vec::new();
|
||||
let mut model_names: HashSet<String> = HashSet::default();
|
||||
for model in &input.models {
|
||||
match model.parse(cx) {
|
||||
Ok(model) => {
|
||||
if !model_names.insert(model.name.clone()) {
|
||||
return Task::ready(Err("Model Names must be unique".into()));
|
||||
}
|
||||
models.push(model)
|
||||
}
|
||||
Err(err) => return Task::ready(Err(err)),
|
||||
}
|
||||
}
|
||||
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
let task = cx.write_credentials(&api_url, "Bearer", api_key.as_bytes());
|
||||
cx.spawn(async move |cx| {
|
||||
task.await
|
||||
.map_err(|_| "Failed to write API key to keychain")?;
|
||||
cx.update(|cx| {
|
||||
update_settings_file::<AllLanguageModelSettings>(fs, cx, |settings, _cx| {
|
||||
settings.openai_compatible.get_or_insert_default().insert(
|
||||
provider_name,
|
||||
OpenAiCompatibleSettingsContent {
|
||||
api_url,
|
||||
available_models: models,
|
||||
},
|
||||
);
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub struct AddLlmProviderModal {
|
||||
provider: LlmCompatibleProvider,
|
||||
input: AddLlmProviderInput,
|
||||
focus_handle: FocusHandle,
|
||||
last_error: Option<SharedString>,
|
||||
}
|
||||
|
||||
impl AddLlmProviderModal {
|
||||
pub fn toggle(
|
||||
provider: LlmCompatibleProvider,
|
||||
workspace: &mut Workspace,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
workspace.toggle_modal(window, cx, |window, cx| Self::new(provider, window, cx));
|
||||
}
|
||||
|
||||
fn new(provider: LlmCompatibleProvider, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
Self {
|
||||
input: AddLlmProviderInput::new(provider, window, cx),
|
||||
provider,
|
||||
last_error: None,
|
||||
focus_handle: cx.focus_handle(),
|
||||
}
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &menu::Confirm, _: &mut Window, cx: &mut Context<Self>) {
|
||||
let task = save_provider_to_settings(&self.input, cx);
|
||||
cx.spawn(async move |this, cx| {
|
||||
let result = task.await;
|
||||
this.update(cx, |this, cx| match result {
|
||||
Ok(_) => {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
Err(error) => {
|
||||
this.last_error = Some(error);
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
fn render_section(&self) -> Section {
|
||||
Section::new()
|
||||
.child(self.input.provider_name.clone())
|
||||
.child(self.input.api_url.clone())
|
||||
.child(self.input.api_key.clone())
|
||||
}
|
||||
|
||||
fn render_model_section(&self, cx: &mut Context<Self>) -> Section {
|
||||
Section::new().child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(Label::new("Models").size(LabelSize::Small))
|
||||
.child(
|
||||
Button::new("add-model", "Add Model")
|
||||
.icon(IconName::Plus)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.input.add_model(window, cx);
|
||||
cx.notify();
|
||||
})),
|
||||
),
|
||||
)
|
||||
.children(
|
||||
self.input
|
||||
.models
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, _)| self.render_model(ix, cx)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_model(&self, ix: usize, cx: &mut Context<Self>) -> impl IntoElement + use<> {
|
||||
let has_more_than_one_model = self.input.models.len() > 1;
|
||||
let model = &self.input.models[ix];
|
||||
|
||||
v_flex()
|
||||
.p_2()
|
||||
.gap_2()
|
||||
.rounded_sm()
|
||||
.border_1()
|
||||
.border_dashed()
|
||||
.border_color(cx.theme().colors().border.opacity(0.6))
|
||||
.bg(cx.theme().colors().element_active.opacity(0.15))
|
||||
.child(model.name.clone())
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(model.max_completion_tokens.clone())
|
||||
.child(model.max_output_tokens.clone()),
|
||||
)
|
||||
.child(model.max_tokens.clone())
|
||||
.when(has_more_than_one_model, |this| {
|
||||
this.child(
|
||||
Button::new(("remove-model", ix), "Remove Model")
|
||||
.icon(IconName::Trash)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.label_size(LabelSize::Small)
|
||||
.style(ButtonStyle::Outlined)
|
||||
.full_width()
|
||||
.on_click(cx.listener(move |this, _, _window, cx| {
|
||||
this.input.remove_model(ix);
|
||||
cx.notify();
|
||||
})),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for AddLlmProviderModal {}
|
||||
|
||||
impl Focusable for AddLlmProviderModal {
|
||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl ModalView for AddLlmProviderModal {}
|
||||
|
||||
impl Render for AddLlmProviderModal {
|
||||
fn render(&mut self, window: &mut ui::Window, cx: &mut ui::Context<Self>) -> impl IntoElement {
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
|
||||
div()
|
||||
.id("add-llm-provider-modal")
|
||||
.key_context("AddLlmProviderModal")
|
||||
.w(rems(34.))
|
||||
.elevation_3(cx)
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.capture_any_mouse_down(cx.listener(|this, _, window, cx| {
|
||||
this.focus_handle(cx).focus(window);
|
||||
}))
|
||||
.child(
|
||||
Modal::new("configure-context-server", None)
|
||||
.header(ModalHeader::new().headline("Add LLM Provider").description(
|
||||
match self.provider {
|
||||
LlmCompatibleProvider::OpenAi => {
|
||||
"This provider will use an OpenAI compatible API."
|
||||
}
|
||||
},
|
||||
))
|
||||
.when_some(self.last_error.clone(), |this, error| {
|
||||
this.section(
|
||||
Section::new().child(
|
||||
Banner::new()
|
||||
.severity(ui::Severity::Warning)
|
||||
.child(div().text_xs().child(error)),
|
||||
),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
v_flex()
|
||||
.id("modal_content")
|
||||
.max_h_128()
|
||||
.overflow_y_scroll()
|
||||
.gap_2()
|
||||
.child(self.render_section())
|
||||
.child(self.render_model_section(cx)),
|
||||
)
|
||||
.footer(
|
||||
ModalFooter::new().end_slot(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Button::new("cancel", "Cancel")
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(
|
||||
&menu::Cancel,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(12.))),
|
||||
)
|
||||
.on_click(cx.listener(|this, _event, window, cx| {
|
||||
this.cancel(&menu::Cancel, window, cx)
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("save-server", "Save Provider")
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(
|
||||
&menu::Confirm,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(12.))),
|
||||
)
|
||||
.on_click(cx.listener(|this, _event, window, cx| {
|
||||
this.confirm(&menu::Confirm, window, cx)
|
||||
})),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use editor::EditorSettings;
|
||||
use fs::FakeFs;
|
||||
use gpui::{TestAppContext, VisualTestContext};
|
||||
use language::language_settings;
|
||||
use language_model::{
|
||||
LanguageModelProviderId, LanguageModelProviderName,
|
||||
fake_provider::FakeLanguageModelProvider,
|
||||
};
|
||||
use project::Project;
|
||||
use settings::{Settings as _, SettingsStore};
|
||||
use util::path;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_save_provider_invalid_inputs(cx: &mut TestAppContext) {
|
||||
let cx = setup_test(cx).await;
|
||||
|
||||
assert_eq!(
|
||||
save_provider_validation_errors("", "someurl", "somekey", vec![], cx,).await,
|
||||
Some("Provider Name cannot be empty".into())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
save_provider_validation_errors("someprovider", "", "somekey", vec![], cx,).await,
|
||||
Some("API URL cannot be empty".into())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
save_provider_validation_errors("someprovider", "someurl", "", vec![], cx,).await,
|
||||
Some("API Key cannot be empty".into())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
save_provider_validation_errors(
|
||||
"someprovider",
|
||||
"someurl",
|
||||
"somekey",
|
||||
vec![("", "200000", "200000", "32000")],
|
||||
cx,
|
||||
)
|
||||
.await,
|
||||
Some("Model Name cannot be empty".into())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
save_provider_validation_errors(
|
||||
"someprovider",
|
||||
"someurl",
|
||||
"somekey",
|
||||
vec![("somemodel", "abc", "200000", "32000")],
|
||||
cx,
|
||||
)
|
||||
.await,
|
||||
Some("Max Tokens must be a number".into())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
save_provider_validation_errors(
|
||||
"someprovider",
|
||||
"someurl",
|
||||
"somekey",
|
||||
vec![("somemodel", "200000", "abc", "32000")],
|
||||
cx,
|
||||
)
|
||||
.await,
|
||||
Some("Max Completion Tokens must be a number".into())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
save_provider_validation_errors(
|
||||
"someprovider",
|
||||
"someurl",
|
||||
"somekey",
|
||||
vec![("somemodel", "200000", "200000", "abc")],
|
||||
cx,
|
||||
)
|
||||
.await,
|
||||
Some("Max Output Tokens must be a number".into())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
save_provider_validation_errors(
|
||||
"someprovider",
|
||||
"someurl",
|
||||
"somekey",
|
||||
vec![
|
||||
("somemodel", "200000", "200000", "32000"),
|
||||
("somemodel", "200000", "200000", "32000"),
|
||||
],
|
||||
cx,
|
||||
)
|
||||
.await,
|
||||
Some("Model Names must be unique".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_save_provider_name_conflict(cx: &mut TestAppContext) {
|
||||
let cx = setup_test(cx).await;
|
||||
|
||||
cx.update(|_window, cx| {
|
||||
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
|
||||
registry.register_provider(
|
||||
FakeLanguageModelProvider::new(
|
||||
LanguageModelProviderId::new("someprovider"),
|
||||
LanguageModelProviderName::new("Some Provider"),
|
||||
),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
save_provider_validation_errors(
|
||||
"someprovider",
|
||||
"someurl",
|
||||
"someapikey",
|
||||
vec![("somemodel", "200000", "200000", "32000")],
|
||||
cx,
|
||||
)
|
||||
.await,
|
||||
Some("Provider Name is already taken by another provider".into())
|
||||
);
|
||||
}
|
||||
|
||||
async fn setup_test(cx: &mut TestAppContext) -> &mut VisualTestContext {
|
||||
cx.update(|cx| {
|
||||
let store = SettingsStore::test(cx);
|
||||
cx.set_global(store);
|
||||
workspace::init_settings(cx);
|
||||
Project::init_settings(cx);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
language_settings::init(cx);
|
||||
EditorSettings::register(cx);
|
||||
language_model::init_settings(cx);
|
||||
language_models::init_settings(cx);
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
cx.update(|cx| <dyn Fs>::set_global(fs.clone(), cx));
|
||||
let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
|
||||
let (_, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
|
||||
cx
|
||||
}
|
||||
|
||||
async fn save_provider_validation_errors(
|
||||
provider_name: &str,
|
||||
api_url: &str,
|
||||
api_key: &str,
|
||||
models: Vec<(&str, &str, &str, &str)>,
|
||||
cx: &mut VisualTestContext,
|
||||
) -> Option<SharedString> {
|
||||
fn set_text(
|
||||
input: &Entity<SingleLineInput>,
|
||||
text: &str,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
input.update(cx, |input, cx| {
|
||||
input.editor().update(cx, |editor, cx| {
|
||||
editor.set_text(text, window, cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let task = cx.update(|window, cx| {
|
||||
let mut input = AddLlmProviderInput::new(LlmCompatibleProvider::OpenAi, window, cx);
|
||||
set_text(&input.provider_name, provider_name, window, cx);
|
||||
set_text(&input.api_url, api_url, window, cx);
|
||||
set_text(&input.api_key, api_key, window, cx);
|
||||
|
||||
for (i, (name, max_tokens, max_completion_tokens, max_output_tokens)) in
|
||||
models.iter().enumerate()
|
||||
{
|
||||
if i >= input.models.len() {
|
||||
input.models.push(ModelInput::new(window, cx));
|
||||
}
|
||||
let model = &mut input.models[i];
|
||||
set_text(&model.name, name, window, cx);
|
||||
set_text(&model.max_tokens, max_tokens, window, cx);
|
||||
set_text(
|
||||
&model.max_completion_tokens,
|
||||
max_completion_tokens,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
set_text(&model.max_output_tokens, max_output_tokens, window, cx);
|
||||
}
|
||||
save_provider_to_settings(&input, cx)
|
||||
});
|
||||
|
||||
task.await.err()
|
||||
}
|
||||
}
|
||||
@@ -1506,8 +1506,7 @@ impl AgentDiff {
|
||||
.read(cx)
|
||||
.entries()
|
||||
.last()
|
||||
.and_then(|entry| entry.diff())
|
||||
.is_some()
|
||||
.map_or(false, |entry| entry.diffs().next().is_some())
|
||||
{
|
||||
self.update_reviewing_editors(workspace, window, cx);
|
||||
}
|
||||
@@ -1517,8 +1516,7 @@ impl AgentDiff {
|
||||
.read(cx)
|
||||
.entries()
|
||||
.get(*ix)
|
||||
.and_then(|entry| entry.diff())
|
||||
.is_some()
|
||||
.map_or(false, |entry| entry.diffs().next().is_some())
|
||||
{
|
||||
self.update_reviewing_editors(workspace, window, cx);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::cell::RefCell;
|
||||
use std::ops::Range;
|
||||
use std::ops::{Not, Range};
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
@@ -43,7 +43,8 @@ use anyhow::{Result, anyhow};
|
||||
use assistant_context::{AssistantContext, ContextEvent, ContextSummary};
|
||||
use assistant_slash_command::SlashCommandWorkingSet;
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use client::{UserStore, zed_urls};
|
||||
use client::{DisableAiSettings, UserStore, zed_urls};
|
||||
use cloud_llm_client::{CompletionIntent, UsageLimit};
|
||||
use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
|
||||
use feature_flags::{self, FeatureFlagAppExt};
|
||||
use fs::Fs;
|
||||
@@ -80,7 +81,6 @@ use zed_actions::{
|
||||
agent::{OpenConfiguration, OpenOnboardingModal, ResetOnboarding, ToggleModelSelector},
|
||||
assistant::{OpenRulesLibrary, ToggleFocus},
|
||||
};
|
||||
use zed_llm_client::{CompletionIntent, UsageLimit};
|
||||
|
||||
const AGENT_PANEL_KEY: &str = "agent_panel";
|
||||
|
||||
@@ -440,7 +440,7 @@ pub struct AgentPanel {
|
||||
local_timezone: UtcOffset,
|
||||
active_view: ActiveView,
|
||||
acp_message_history:
|
||||
Rc<RefCell<crate::acp::MessageHistory<agentic_coding_protocol::SendUserMessageParams>>>,
|
||||
Rc<RefCell<crate::acp::MessageHistory<Vec<agent_client_protocol::ContentBlock>>>>,
|
||||
previous_view: Option<ActiveView>,
|
||||
history_store: Entity<HistoryStore>,
|
||||
history: Entity<ThreadHistory>,
|
||||
@@ -564,6 +564,17 @@ impl AgentPanel {
|
||||
let inline_assist_context_store =
|
||||
cx.new(|_cx| ContextStore::new(project.downgrade(), Some(thread_store.downgrade())));
|
||||
|
||||
let thread_id = thread.read(cx).id().clone();
|
||||
|
||||
let history_store = cx.new(|cx| {
|
||||
HistoryStore::new(
|
||||
thread_store.clone(),
|
||||
context_store.clone(),
|
||||
[HistoryEntryId::Thread(thread_id)],
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let message_editor = cx.new(|cx| {
|
||||
MessageEditor::new(
|
||||
fs.clone(),
|
||||
@@ -573,22 +584,13 @@ impl AgentPanel {
|
||||
prompt_store.clone(),
|
||||
thread_store.downgrade(),
|
||||
context_store.downgrade(),
|
||||
Some(history_store.downgrade()),
|
||||
thread.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let thread_id = thread.read(cx).id().clone();
|
||||
let history_store = cx.new(|cx| {
|
||||
HistoryStore::new(
|
||||
thread_store.clone(),
|
||||
context_store.clone(),
|
||||
[HistoryEntryId::Thread(thread_id)],
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
cx.observe(&history_store, |_, _, cx| cx.notify()).detach();
|
||||
|
||||
let active_thread = cx.new(|cx| {
|
||||
@@ -744,6 +746,7 @@ impl AgentPanel {
|
||||
if workspace
|
||||
.panel::<Self>(cx)
|
||||
.is_some_and(|panel| panel.read(cx).enabled(cx))
|
||||
&& !DisableAiSettings::get_global(cx).disable_ai
|
||||
{
|
||||
workspace.toggle_panel_focus::<Self>(window, cx);
|
||||
}
|
||||
@@ -850,6 +853,7 @@ impl AgentPanel {
|
||||
self.prompt_store.clone(),
|
||||
self.thread_store.downgrade(),
|
||||
self.context_store.downgrade(),
|
||||
Some(self.history_store.downgrade()),
|
||||
thread.clone(),
|
||||
window,
|
||||
cx,
|
||||
@@ -1123,6 +1127,7 @@ impl AgentPanel {
|
||||
self.prompt_store.clone(),
|
||||
self.thread_store.downgrade(),
|
||||
self.context_store.downgrade(),
|
||||
Some(self.history_store.downgrade()),
|
||||
thread.clone(),
|
||||
window,
|
||||
cx,
|
||||
@@ -1681,7 +1686,7 @@ impl Panel for AgentPanel {
|
||||
}
|
||||
|
||||
fn enabled(&self, cx: &App) -> bool {
|
||||
AgentSettings::get_global(cx).enabled
|
||||
DisableAiSettings::get_global(cx).disable_ai.not() && AgentSettings::get_global(cx).enabled
|
||||
}
|
||||
|
||||
fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
|
||||
@@ -1900,85 +1905,110 @@ impl AgentPanel {
|
||||
)
|
||||
.anchor(Corner::TopRight)
|
||||
.with_handle(self.new_thread_menu_handle.clone())
|
||||
.menu(move |window, cx| {
|
||||
let active_thread = active_thread.clone();
|
||||
Some(ContextMenu::build(window, cx, |mut menu, _window, cx| {
|
||||
menu = menu
|
||||
.when(cx.has_flag::<feature_flags::AcpFeatureFlag>(), |this| {
|
||||
this.header("Zed Agent")
|
||||
})
|
||||
.item(
|
||||
ContextMenuEntry::new("New Thread")
|
||||
.icon(IconName::NewThread)
|
||||
.icon_color(Color::Muted)
|
||||
.handler(move |window, cx| {
|
||||
window.dispatch_action(NewThread::default().boxed_clone(), cx);
|
||||
}),
|
||||
)
|
||||
.item(
|
||||
ContextMenuEntry::new("New Text Thread")
|
||||
.icon(IconName::NewTextThread)
|
||||
.icon_color(Color::Muted)
|
||||
.handler(move |window, cx| {
|
||||
window.dispatch_action(NewTextThread.boxed_clone(), cx);
|
||||
}),
|
||||
)
|
||||
.when_some(active_thread, |this, active_thread| {
|
||||
let thread = active_thread.read(cx);
|
||||
.menu({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
let active_thread = active_thread.clone();
|
||||
Some(ContextMenu::build(window, cx, |mut menu, _window, cx| {
|
||||
menu = menu
|
||||
.context(focus_handle.clone())
|
||||
.when(cx.has_flag::<feature_flags::AcpFeatureFlag>(), |this| {
|
||||
this.header("Zed Agent")
|
||||
})
|
||||
.item(
|
||||
ContextMenuEntry::new("New Thread")
|
||||
.icon(IconName::NewThread)
|
||||
.icon_color(Color::Muted)
|
||||
.action(NewThread::default().boxed_clone())
|
||||
.handler(move |window, cx| {
|
||||
window.dispatch_action(
|
||||
NewThread::default().boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
)
|
||||
.item(
|
||||
ContextMenuEntry::new("New Text Thread")
|
||||
.icon(IconName::NewTextThread)
|
||||
.icon_color(Color::Muted)
|
||||
.action(NewTextThread.boxed_clone())
|
||||
.handler(move |window, cx| {
|
||||
window.dispatch_action(NewTextThread.boxed_clone(), cx);
|
||||
}),
|
||||
)
|
||||
.when_some(active_thread, |this, active_thread| {
|
||||
let thread = active_thread.read(cx);
|
||||
|
||||
if !thread.is_empty() {
|
||||
let thread_id = thread.id().clone();
|
||||
this.item(
|
||||
ContextMenuEntry::new("New From Summary")
|
||||
.icon(IconName::NewFromSummary)
|
||||
.icon_color(Color::Muted)
|
||||
.handler(move |window, cx| {
|
||||
window.dispatch_action(
|
||||
Box::new(NewThread {
|
||||
from_thread_id: Some(thread_id.clone()),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
})
|
||||
.when(cx.has_flag::<feature_flags::AcpFeatureFlag>(), |this| {
|
||||
this.separator()
|
||||
.header("External Agents")
|
||||
.item(
|
||||
ContextMenuEntry::new("New Gemini Thread")
|
||||
.icon(IconName::AiGemini)
|
||||
.icon_color(Color::Muted)
|
||||
.handler(move |window, cx| {
|
||||
window.dispatch_action(
|
||||
NewExternalAgentThread {
|
||||
agent: Some(crate::ExternalAgent::Gemini),
|
||||
}
|
||||
.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
)
|
||||
.item(
|
||||
ContextMenuEntry::new("New Claude Code Thread")
|
||||
.icon(IconName::AiClaude)
|
||||
.icon_color(Color::Muted)
|
||||
.handler(move |window, cx| {
|
||||
window.dispatch_action(
|
||||
NewExternalAgentThread {
|
||||
agent: Some(crate::ExternalAgent::ClaudeCode),
|
||||
}
|
||||
.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
)
|
||||
});
|
||||
menu
|
||||
}))
|
||||
if !thread.is_empty() {
|
||||
let thread_id = thread.id().clone();
|
||||
this.item(
|
||||
ContextMenuEntry::new("New From Summary")
|
||||
.icon(IconName::NewFromSummary)
|
||||
.icon_color(Color::Muted)
|
||||
.handler(move |window, cx| {
|
||||
window.dispatch_action(
|
||||
Box::new(NewThread {
|
||||
from_thread_id: Some(thread_id.clone()),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
})
|
||||
.when(cx.has_flag::<feature_flags::AcpFeatureFlag>(), |this| {
|
||||
this.separator()
|
||||
.header("External Agents")
|
||||
.item(
|
||||
ContextMenuEntry::new("New Gemini Thread")
|
||||
.icon(IconName::AiGemini)
|
||||
.icon_color(Color::Muted)
|
||||
.handler(move |window, cx| {
|
||||
window.dispatch_action(
|
||||
NewExternalAgentThread {
|
||||
agent: Some(crate::ExternalAgent::Gemini),
|
||||
}
|
||||
.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
)
|
||||
.item(
|
||||
ContextMenuEntry::new("New Claude Code Thread")
|
||||
.icon(IconName::AiClaude)
|
||||
.icon_color(Color::Muted)
|
||||
.handler(move |window, cx| {
|
||||
window.dispatch_action(
|
||||
NewExternalAgentThread {
|
||||
agent: Some(
|
||||
crate::ExternalAgent::ClaudeCode,
|
||||
),
|
||||
}
|
||||
.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
)
|
||||
.item(
|
||||
ContextMenuEntry::new("New Codex Thread")
|
||||
.icon(IconName::AiOpenAi)
|
||||
.icon_color(Color::Muted)
|
||||
.handler(move |window, cx| {
|
||||
window.dispatch_action(
|
||||
NewExternalAgentThread {
|
||||
agent: Some(crate::ExternalAgent::Codex),
|
||||
}
|
||||
.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
)
|
||||
});
|
||||
menu
|
||||
}))
|
||||
}
|
||||
});
|
||||
|
||||
let agent_panel_menu = PopoverMenu::new("agent-options-menu")
|
||||
@@ -2000,65 +2030,69 @@ impl AgentPanel {
|
||||
)
|
||||
.anchor(Corner::TopRight)
|
||||
.with_handle(self.agent_panel_menu_handle.clone())
|
||||
.menu(move |window, cx| {
|
||||
Some(ContextMenu::build(window, cx, |mut menu, _window, _| {
|
||||
if let Some(usage) = usage {
|
||||
.menu({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Some(ContextMenu::build(window, cx, |mut menu, _window, _| {
|
||||
menu = menu.context(focus_handle.clone());
|
||||
if let Some(usage) = usage {
|
||||
menu = menu
|
||||
.header_with_link("Prompt Usage", "Manage", account_url.clone())
|
||||
.custom_entry(
|
||||
move |_window, cx| {
|
||||
let used_percentage = match usage.limit {
|
||||
UsageLimit::Limited(limit) => {
|
||||
Some((usage.amount as f32 / limit as f32) * 100.)
|
||||
}
|
||||
UsageLimit::Unlimited => None,
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.flex_1()
|
||||
.gap_1p5()
|
||||
.children(used_percentage.map(|percent| {
|
||||
ProgressBar::new("usage", percent, 100., cx)
|
||||
}))
|
||||
.child(
|
||||
Label::new(match usage.limit {
|
||||
UsageLimit::Limited(limit) => {
|
||||
format!("{} / {limit}", usage.amount)
|
||||
}
|
||||
UsageLimit::Unlimited => {
|
||||
format!("{} / ∞", usage.amount)
|
||||
}
|
||||
})
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.into_any_element()
|
||||
},
|
||||
move |_, cx| cx.open_url(&zed_urls::account_url(cx)),
|
||||
)
|
||||
.separator()
|
||||
}
|
||||
|
||||
menu = menu
|
||||
.header_with_link("Prompt Usage", "Manage", account_url.clone())
|
||||
.custom_entry(
|
||||
move |_window, cx| {
|
||||
let used_percentage = match usage.limit {
|
||||
UsageLimit::Limited(limit) => {
|
||||
Some((usage.amount as f32 / limit as f32) * 100.)
|
||||
}
|
||||
UsageLimit::Unlimited => None,
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.flex_1()
|
||||
.gap_1p5()
|
||||
.children(used_percentage.map(|percent| {
|
||||
ProgressBar::new("usage", percent, 100., cx)
|
||||
}))
|
||||
.child(
|
||||
Label::new(match usage.limit {
|
||||
UsageLimit::Limited(limit) => {
|
||||
format!("{} / {limit}", usage.amount)
|
||||
}
|
||||
UsageLimit::Unlimited => {
|
||||
format!("{} / ∞", usage.amount)
|
||||
}
|
||||
})
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.into_any_element()
|
||||
},
|
||||
move |_, cx| cx.open_url(&zed_urls::account_url(cx)),
|
||||
.header("MCP Servers")
|
||||
.action(
|
||||
"View Server Extensions",
|
||||
Box::new(zed_actions::Extensions {
|
||||
category_filter: Some(
|
||||
zed_actions::ExtensionCategoryFilter::ContextServers,
|
||||
),
|
||||
id: None,
|
||||
}),
|
||||
)
|
||||
.separator()
|
||||
}
|
||||
.action("Add Custom Server…", Box::new(AddContextServer))
|
||||
.separator();
|
||||
|
||||
menu = menu
|
||||
.header("MCP Servers")
|
||||
.action(
|
||||
"View Server Extensions",
|
||||
Box::new(zed_actions::Extensions {
|
||||
category_filter: Some(
|
||||
zed_actions::ExtensionCategoryFilter::ContextServers,
|
||||
),
|
||||
id: None,
|
||||
}),
|
||||
)
|
||||
.action("Add Custom Server…", Box::new(AddContextServer))
|
||||
.separator();
|
||||
|
||||
menu = menu
|
||||
.action("Rules…", Box::new(OpenRulesLibrary::default()))
|
||||
.action("Settings", Box::new(OpenConfiguration))
|
||||
.action(zoom_in_label, Box::new(ToggleZoom));
|
||||
menu
|
||||
}))
|
||||
menu = menu
|
||||
.action("Rules…", Box::new(OpenRulesLibrary::default()))
|
||||
.action("Settings", Box::new(OpenConfiguration))
|
||||
.action(zoom_in_label, Box::new(ToggleZoom));
|
||||
menu
|
||||
}))
|
||||
}
|
||||
});
|
||||
|
||||
h_flex()
|
||||
@@ -2271,20 +2305,21 @@ impl AgentPanel {
|
||||
}
|
||||
|
||||
match &self.active_view {
|
||||
ActiveView::Thread { thread, .. } => thread
|
||||
.read(cx)
|
||||
.thread()
|
||||
.read(cx)
|
||||
.configured_model()
|
||||
.map_or(true, |model| {
|
||||
model.provider.id() == language_model::ZED_CLOUD_PROVIDER_ID
|
||||
}),
|
||||
ActiveView::TextThread { .. } => LanguageModelRegistry::global(cx)
|
||||
.read(cx)
|
||||
.default_model()
|
||||
.map_or(true, |model| {
|
||||
model.provider.id() == language_model::ZED_CLOUD_PROVIDER_ID
|
||||
}),
|
||||
ActiveView::Thread { .. } | ActiveView::TextThread { .. } => {
|
||||
let history_is_empty = self
|
||||
.history_store
|
||||
.update(cx, |store, cx| store.recent_entries(1, cx).is_empty());
|
||||
|
||||
let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx)
|
||||
.providers()
|
||||
.iter()
|
||||
.any(|provider| {
|
||||
provider.is_authenticated(cx)
|
||||
&& provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
|
||||
});
|
||||
|
||||
history_is_empty || !has_configured_non_zed_providers
|
||||
}
|
||||
ActiveView::ExternalAgentThread { .. }
|
||||
| ActiveView::History
|
||||
| ActiveView::Configuration => false,
|
||||
@@ -2305,9 +2340,8 @@ impl AgentPanel {
|
||||
|
||||
Some(
|
||||
div()
|
||||
.size_full()
|
||||
.when(thread_view, |this| {
|
||||
this.bg(cx.theme().colors().panel_background)
|
||||
this.size_full().bg(cx.theme().colors().panel_background)
|
||||
})
|
||||
.when(text_thread_view, |this| {
|
||||
this.bg(cx.theme().colors().editor_background)
|
||||
@@ -2632,6 +2666,25 @@ impl AgentPanel {
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
.child(
|
||||
NewThreadButton::new(
|
||||
"new-codex-thread-btn",
|
||||
"New Codex Thread",
|
||||
IconName::AiOpenAi,
|
||||
)
|
||||
.on_click(
|
||||
|window, cx| {
|
||||
window.dispatch_action(
|
||||
Box::new(NewExternalAgentThread {
|
||||
agent: Some(
|
||||
crate::ExternalAgent::Codex,
|
||||
),
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}),
|
||||
|
||||
@@ -31,7 +31,8 @@ use std::sync::Arc;
|
||||
use agent::{Thread, ThreadId};
|
||||
use agent_settings::{AgentProfileId, AgentSettings, LanguageModelSelection};
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use client::Client;
|
||||
use client::{Client, DisableAiSettings};
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use feature_flags::FeatureFlagAppExt as _;
|
||||
use fs::Fs;
|
||||
use gpui::{Action, App, Entity, actions};
|
||||
@@ -43,6 +44,7 @@ use prompt_store::PromptBuilder;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings as _, SettingsStore};
|
||||
use std::any::TypeId;
|
||||
|
||||
pub use crate::active_thread::ActiveThread;
|
||||
use crate::agent_configuration::{ConfigureContextServerModal, ManageProfilesModal};
|
||||
@@ -52,6 +54,7 @@ use crate::slash_command_settings::SlashCommandSettings;
|
||||
pub use agent_diff::{AgentDiffPane, AgentDiffToolbar};
|
||||
pub use text_thread_editor::{AgentPanelDelegate, TextThreadEditor};
|
||||
pub use ui::preview::{all_agent_previews, get_agent_preview};
|
||||
use zed_actions;
|
||||
|
||||
actions!(
|
||||
agent,
|
||||
@@ -147,6 +150,7 @@ enum ExternalAgent {
|
||||
#[default]
|
||||
Gemini,
|
||||
ClaudeCode,
|
||||
Codex,
|
||||
}
|
||||
|
||||
impl ExternalAgent {
|
||||
@@ -154,6 +158,7 @@ impl ExternalAgent {
|
||||
match self {
|
||||
ExternalAgent::Gemini => Rc::new(agent_servers::Gemini),
|
||||
ExternalAgent::ClaudeCode => Rc::new(agent_servers::ClaudeCode),
|
||||
ExternalAgent::Codex => Rc::new(agent_servers::Codex),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -241,6 +246,69 @@ pub fn init(
|
||||
})
|
||||
.detach();
|
||||
cx.observe_new(ManageProfilesModal::register).detach();
|
||||
|
||||
// Update command palette filter based on AI settings
|
||||
update_command_palette_filter(cx);
|
||||
|
||||
// Watch for settings changes
|
||||
cx.observe_global::<SettingsStore>(|app_cx| {
|
||||
// When settings change, update the command palette filter
|
||||
update_command_palette_filter(app_cx);
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn update_command_palette_filter(cx: &mut App) {
|
||||
let disable_ai = DisableAiSettings::get_global(cx).disable_ai;
|
||||
CommandPaletteFilter::update_global(cx, |filter, _| {
|
||||
if disable_ai {
|
||||
filter.hide_namespace("agent");
|
||||
filter.hide_namespace("assistant");
|
||||
filter.hide_namespace("copilot");
|
||||
filter.hide_namespace("zed_predict_onboarding");
|
||||
|
||||
filter.hide_namespace("edit_prediction");
|
||||
|
||||
use editor::actions::{
|
||||
AcceptEditPrediction, AcceptPartialEditPrediction, NextEditPrediction,
|
||||
PreviousEditPrediction, ShowEditPrediction, ToggleEditPrediction,
|
||||
};
|
||||
let edit_prediction_actions = [
|
||||
TypeId::of::<AcceptEditPrediction>(),
|
||||
TypeId::of::<AcceptPartialEditPrediction>(),
|
||||
TypeId::of::<ShowEditPrediction>(),
|
||||
TypeId::of::<NextEditPrediction>(),
|
||||
TypeId::of::<PreviousEditPrediction>(),
|
||||
TypeId::of::<ToggleEditPrediction>(),
|
||||
];
|
||||
filter.hide_action_types(&edit_prediction_actions);
|
||||
filter.hide_action_types(&[TypeId::of::<zed_actions::OpenZedPredictOnboarding>()]);
|
||||
} else {
|
||||
filter.show_namespace("agent");
|
||||
filter.show_namespace("assistant");
|
||||
filter.show_namespace("copilot");
|
||||
filter.show_namespace("zed_predict_onboarding");
|
||||
|
||||
filter.show_namespace("edit_prediction");
|
||||
|
||||
use editor::actions::{
|
||||
AcceptEditPrediction, AcceptPartialEditPrediction, NextEditPrediction,
|
||||
PreviousEditPrediction, ShowEditPrediction, ToggleEditPrediction,
|
||||
};
|
||||
let edit_prediction_actions = [
|
||||
TypeId::of::<AcceptEditPrediction>(),
|
||||
TypeId::of::<AcceptPartialEditPrediction>(),
|
||||
TypeId::of::<ShowEditPrediction>(),
|
||||
TypeId::of::<NextEditPrediction>(),
|
||||
TypeId::of::<PreviousEditPrediction>(),
|
||||
TypeId::of::<ToggleEditPrediction>(),
|
||||
];
|
||||
filter.show_action_types(edit_prediction_actions.iter());
|
||||
|
||||
filter
|
||||
.show_action_types([TypeId::of::<zed_actions::OpenZedPredictOnboarding>()].iter());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn init_language_model_settings(cx: &mut App) {
|
||||
|
||||
@@ -6,6 +6,7 @@ use agent::{
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::{Context as _, Result};
|
||||
use client::telemetry::Telemetry;
|
||||
use cloud_llm_client::CompletionIntent;
|
||||
use collections::HashSet;
|
||||
use editor::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint};
|
||||
use futures::{
|
||||
@@ -35,7 +36,6 @@ use std::{
|
||||
};
|
||||
use streaming_diff::{CharOperation, LineDiff, LineOperation, StreamingDiff};
|
||||
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
pub struct BufferCodegen {
|
||||
alternatives: Vec<Entity<CodegenAlternative>>,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#![allow(unused, dead_code)]
|
||||
|
||||
use client::{ModelRequestUsage, RequestUsage};
|
||||
use cloud_llm_client::{Plan, UsageLimit};
|
||||
use gpui::Global;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use ui::prelude::*;
|
||||
use zed_llm_client::{Plan, UsageLimit};
|
||||
|
||||
/// Debug only: Used for testing various account states
|
||||
///
|
||||
|
||||
@@ -16,7 +16,7 @@ use agent::{
|
||||
};
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::{Context as _, Result};
|
||||
use client::telemetry::Telemetry;
|
||||
use client::{DisableAiSettings, telemetry::Telemetry};
|
||||
use collections::{HashMap, HashSet, VecDeque, hash_map};
|
||||
use editor::SelectionEffects;
|
||||
use editor::{
|
||||
@@ -57,6 +57,17 @@ pub fn init(
|
||||
cx: &mut App,
|
||||
) {
|
||||
cx.set_global(InlineAssistant::new(fs, prompt_builder, telemetry));
|
||||
|
||||
cx.observe_global::<SettingsStore>(|cx| {
|
||||
if DisableAiSettings::get_global(cx).disable_ai {
|
||||
// Hide any active inline assist UI when AI is disabled
|
||||
InlineAssistant::update_global(cx, |assistant, cx| {
|
||||
assistant.cancel_all_active_completions(cx);
|
||||
});
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.observe_new(|_workspace: &mut Workspace, window, cx| {
|
||||
let Some(window) = window else {
|
||||
return;
|
||||
@@ -141,6 +152,26 @@ impl InlineAssistant {
|
||||
.detach();
|
||||
}
|
||||
|
||||
/// Hides all active inline assists when AI is disabled
|
||||
pub fn cancel_all_active_completions(&mut self, cx: &mut App) {
|
||||
// Cancel all active completions in editors
|
||||
for (editor_handle, _) in self.assists_by_editor.iter() {
|
||||
if let Some(editor) = editor_handle.upgrade() {
|
||||
let windows = cx.windows();
|
||||
if !windows.is_empty() {
|
||||
let window = windows[0];
|
||||
let _ = window.update(cx, |_, window, cx| {
|
||||
editor.update(cx, |editor, cx| {
|
||||
if editor.has_active_inline_completion() {
|
||||
editor.cancel(&Default::default(), window, cx);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_workspace_event(
|
||||
&mut self,
|
||||
workspace: Entity<Workspace>,
|
||||
@@ -176,7 +207,7 @@ impl InlineAssistant {
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let is_assistant2_enabled = true;
|
||||
let is_assistant2_enabled = !DisableAiSettings::get_global(cx).disable_ai;
|
||||
|
||||
if let Some(editor) = item.act_as::<Editor>(cx) {
|
||||
editor.update(cx, |editor, cx| {
|
||||
@@ -199,6 +230,13 @@ impl InlineAssistant {
|
||||
cx,
|
||||
);
|
||||
|
||||
if DisableAiSettings::get_global(cx).disable_ai {
|
||||
// Cancel any active completions
|
||||
if editor.has_active_inline_completion() {
|
||||
editor.cancel(&Default::default(), window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the Assistant1 code action provider, as it still might be registered.
|
||||
editor.remove_code_action_provider("assistant".into(), window, cx);
|
||||
} else {
|
||||
@@ -219,7 +257,7 @@ impl InlineAssistant {
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
let settings = AgentSettings::get_global(cx);
|
||||
if !settings.enabled {
|
||||
if !settings.enabled || DisableAiSettings::get_global(cx).disable_ai {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ use crate::ui::{
|
||||
MaxModeTooltip,
|
||||
preview::{AgentPreview, UsageCallout},
|
||||
};
|
||||
use agent::history_store::HistoryStore;
|
||||
use agent::{
|
||||
context::{AgentContextKey, ContextLoadResult, load_context},
|
||||
context_store::ContextStoreEvent,
|
||||
@@ -17,6 +18,7 @@ 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};
|
||||
use editor::display_map::CreaseId;
|
||||
@@ -29,8 +31,9 @@ use fs::Fs;
|
||||
use futures::future::Shared;
|
||||
use futures::{FutureExt as _, future};
|
||||
use gpui::{
|
||||
Animation, AnimationExt, App, Entity, EventEmitter, Focusable, KeyContext, Subscription, Task,
|
||||
TextStyle, WeakEntity, linear_color_stop, linear_gradient, point, pulsating_between,
|
||||
Animation, AnimationExt, App, Entity, EventEmitter, Focusable, IntoElement, KeyContext,
|
||||
Subscription, Task, TextStyle, WeakEntity, linear_color_stop, linear_gradient, point,
|
||||
pulsating_between,
|
||||
};
|
||||
use language::{Buffer, Language, Point};
|
||||
use language_model::{
|
||||
@@ -51,7 +54,6 @@ use util::ResultExt as _;
|
||||
use workspace::{CollaboratorId, Workspace};
|
||||
use zed_actions::agent::Chat;
|
||||
use zed_actions::agent::ToggleModelSelector;
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention};
|
||||
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
|
||||
@@ -80,6 +82,7 @@ pub struct MessageEditor {
|
||||
user_store: Entity<UserStore>,
|
||||
context_store: Entity<ContextStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
history_store: Option<WeakEntity<HistoryStore>>,
|
||||
context_strip: Entity<ContextStrip>,
|
||||
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||
model_selector: Entity<AgentModelSelector>,
|
||||
@@ -161,6 +164,7 @@ impl MessageEditor {
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
thread_store: WeakEntity<ThreadStore>,
|
||||
text_thread_store: WeakEntity<TextThreadStore>,
|
||||
history_store: Option<WeakEntity<HistoryStore>>,
|
||||
thread: Entity<Thread>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
@@ -233,6 +237,7 @@ impl MessageEditor {
|
||||
workspace,
|
||||
context_store,
|
||||
prompt_store,
|
||||
history_store,
|
||||
context_strip,
|
||||
context_picker_menu_handle,
|
||||
load_context_task: None,
|
||||
@@ -625,7 +630,7 @@ impl MessageEditor {
|
||||
.unwrap_or(false);
|
||||
|
||||
IconButton::new("follow-agent", IconName::Crosshair)
|
||||
.disabled(is_model_selected)
|
||||
.disabled(!is_model_selected)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.toggle_state(following)
|
||||
@@ -910,6 +915,10 @@ impl MessageEditor {
|
||||
.on_click({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |_event, window, cx| {
|
||||
telemetry::event!(
|
||||
"Agent Message Sent",
|
||||
agent = "zed",
|
||||
);
|
||||
focus_handle.dispatch_action(
|
||||
&Chat, window, cx,
|
||||
);
|
||||
@@ -1291,11 +1300,11 @@ impl MessageEditor {
|
||||
let plan = user_store
|
||||
.current_plan()
|
||||
.map(|plan| match plan {
|
||||
Plan::Free => zed_llm_client::Plan::ZedFree,
|
||||
Plan::ZedPro => zed_llm_client::Plan::ZedPro,
|
||||
Plan::ZedProTrial => zed_llm_client::Plan::ZedProTrial,
|
||||
Plan::Free => cloud_llm_client::Plan::ZedFree,
|
||||
Plan::ZedPro => cloud_llm_client::Plan::ZedPro,
|
||||
Plan::ZedProTrial => cloud_llm_client::Plan::ZedProTrial,
|
||||
})
|
||||
.unwrap_or(zed_llm_client::Plan::ZedFree);
|
||||
.unwrap_or(cloud_llm_client::Plan::ZedFree);
|
||||
|
||||
let usage = user_store.model_request_usage()?;
|
||||
|
||||
@@ -1657,32 +1666,36 @@ impl Render for MessageEditor {
|
||||
|
||||
let line_height = TextSize::Small.rems(cx).to_pixels(window.rem_size()) * 1.5;
|
||||
|
||||
let in_pro_trial = matches!(
|
||||
self.user_store.read(cx).current_plan(),
|
||||
Some(proto::Plan::ZedProTrial)
|
||||
);
|
||||
let has_configured_providers = LanguageModelRegistry::read_global(cx)
|
||||
.providers()
|
||||
.iter()
|
||||
.filter(|provider| {
|
||||
provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
|
||||
})
|
||||
.count()
|
||||
> 0;
|
||||
|
||||
let pro_user = matches!(
|
||||
self.user_store.read(cx).current_plan(),
|
||||
Some(proto::Plan::ZedPro)
|
||||
);
|
||||
let is_signed_out = self
|
||||
.workspace
|
||||
.read_with(cx, |workspace, _| {
|
||||
workspace.client().status().borrow().is_signed_out()
|
||||
})
|
||||
.unwrap_or(true);
|
||||
|
||||
let configured_providers: Vec<(IconName, SharedString)> =
|
||||
LanguageModelRegistry::read_global(cx)
|
||||
.providers()
|
||||
.iter()
|
||||
.filter(|provider| {
|
||||
provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
|
||||
})
|
||||
.map(|provider| (provider.icon(), provider.name().0.clone()))
|
||||
.collect();
|
||||
let has_existing_providers = configured_providers.len() > 0;
|
||||
let has_history = self
|
||||
.history_store
|
||||
.as_ref()
|
||||
.and_then(|hs| hs.update(cx, |hs, cx| hs.entries(cx).len() > 0).ok())
|
||||
.unwrap_or(false)
|
||||
|| self
|
||||
.thread
|
||||
.read_with(cx, |thread, _| thread.messages().len() > 0);
|
||||
|
||||
v_flex()
|
||||
.size_full()
|
||||
.bg(cx.theme().colors().panel_background)
|
||||
.when(
|
||||
has_existing_providers && !in_pro_trial && !pro_user,
|
||||
!has_history && is_signed_out && has_configured_providers,
|
||||
|this| this.child(cx.new(ApiKeysWithProviders::new)),
|
||||
)
|
||||
.when(changed_buffers.len() > 0, |parent| {
|
||||
@@ -1774,6 +1787,7 @@ impl AgentPreview for MessageEditor {
|
||||
None,
|
||||
thread_store.downgrade(),
|
||||
text_thread_store.downgrade(),
|
||||
None,
|
||||
thread,
|
||||
window,
|
||||
cx,
|
||||
|
||||
@@ -10,6 +10,7 @@ use agent::{
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::{Context as _, Result};
|
||||
use client::telemetry::Telemetry;
|
||||
use cloud_llm_client::CompletionIntent;
|
||||
use collections::{HashMap, VecDeque};
|
||||
use editor::{MultiBuffer, actions::SelectAll};
|
||||
use fs::Fs;
|
||||
@@ -27,7 +28,6 @@ use terminal_view::TerminalView;
|
||||
use ui::prelude::*;
|
||||
use util::ResultExt;
|
||||
use workspace::{Toast, Workspace, notifications::NotificationId};
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
pub fn init(
|
||||
fs: Arc<dyn Fs>,
|
||||
|
||||
@@ -5,7 +5,6 @@ mod end_trial_upsell;
|
||||
mod new_thread_button;
|
||||
mod onboarding_modal;
|
||||
pub mod preview;
|
||||
mod upsell;
|
||||
|
||||
pub use agent_notification::*;
|
||||
pub use burn_mode_tooltip::*;
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||
use ai_onboarding::{AgentPanelOnboardingCard, BulletItem};
|
||||
use client::zed_urls;
|
||||
use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
|
||||
use ui::{Divider, List, prelude::*};
|
||||
use ui::{Divider, List, Tooltip, prelude::*};
|
||||
|
||||
#[derive(IntoElement, RegisterComponent)]
|
||||
pub struct EndTrialUpsell {
|
||||
@@ -33,14 +33,19 @@ impl RenderOnce for EndTrialUpsell {
|
||||
)
|
||||
.child(
|
||||
List::new()
|
||||
.child(BulletItem::new("500 prompts per month with Claude models"))
|
||||
.child(BulletItem::new("Unlimited edit predictions")),
|
||||
.child(BulletItem::new("500 prompts with Claude models"))
|
||||
.child(BulletItem::new(
|
||||
"Unlimited edit predictions with Zeta, our open-source model",
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
Button::new("cta-button", "Upgrade to Zed Pro")
|
||||
.full_width()
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
.on_click(|_, _, cx| cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))),
|
||||
.on_click(move |_, _window, cx| {
|
||||
telemetry::event!("Upgrade To Pro Clicked", state = "end-of-trial");
|
||||
cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))
|
||||
}),
|
||||
);
|
||||
|
||||
let free_section = v_flex()
|
||||
@@ -55,37 +60,43 @@ impl RenderOnce for EndTrialUpsell {
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
)
|
||||
.child(
|
||||
Label::new("(Current Plan)")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Custom(cx.theme().colors().text_muted.opacity(0.6)))
|
||||
.buffer_font(cx),
|
||||
)
|
||||
.child(Divider::horizontal()),
|
||||
)
|
||||
.child(
|
||||
List::new()
|
||||
.child(BulletItem::new(
|
||||
"50 prompts per month with the Claude models",
|
||||
))
|
||||
.child(BulletItem::new(
|
||||
"2000 accepted edit predictions using our open-source Zeta model",
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
Button::new("dismiss-button", "Stay on Free")
|
||||
.full_width()
|
||||
.style(ButtonStyle::Outlined)
|
||||
.on_click({
|
||||
let callback = self.dismiss_upsell.clone();
|
||||
move |_, window, cx| callback(window, cx)
|
||||
}),
|
||||
.child(BulletItem::new("50 prompts with the Claude models"))
|
||||
.child(BulletItem::new("2,000 accepted edit predictions")),
|
||||
);
|
||||
|
||||
AgentPanelOnboardingCard::new()
|
||||
.child(Headline::new("Your Zed Pro trial has expired."))
|
||||
.child(Headline::new("Your Zed Pro Trial has expired"))
|
||||
.child(
|
||||
Label::new("You've been automatically reset to the Free plan.")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.mb_1(),
|
||||
.mb_2(),
|
||||
)
|
||||
.child(pro_section)
|
||||
.child(free_section)
|
||||
.child(
|
||||
h_flex().absolute().top_4().right_4().child(
|
||||
IconButton::new("dismiss_onboarding", IconName::Close)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::text("Dismiss"))
|
||||
.on_click({
|
||||
let callback = self.dismiss_upsell.clone();
|
||||
move |_, window, cx| {
|
||||
telemetry::event!("Banner Dismissed", source = "AI Onboarding");
|
||||
callback(window, cx)
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use client::{ModelRequestUsage, RequestUsage, zed_urls};
|
||||
use cloud_llm_client::{Plan, UsageLimit};
|
||||
use component::{empty_example, example_group_with_title, single_example};
|
||||
use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
|
||||
use ui::{Callout, prelude::*};
|
||||
use zed_llm_client::{Plan, UsageLimit};
|
||||
|
||||
#[derive(IntoElement, RegisterComponent)]
|
||||
pub struct UsageCallout {
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
use component::{Component, ComponentScope, single_example};
|
||||
use gpui::{
|
||||
AnyElement, App, ClickEvent, IntoElement, ParentElement, RenderOnce, SharedString, Styled,
|
||||
Window,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{
|
||||
Button, ButtonCommon, ButtonStyle, Checkbox, Clickable, Color, Label, LabelCommon,
|
||||
RegisterComponent, ToggleState, h_flex, v_flex,
|
||||
};
|
||||
|
||||
/// A component that displays an upsell message with a call-to-action button
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// let upsell = Upsell::new(
|
||||
/// "Upgrade to Zed Pro",
|
||||
/// "Get access to advanced AI features and more",
|
||||
/// "Upgrade Now",
|
||||
/// Box::new(|_, _window, cx| {
|
||||
/// cx.open_url("https://zed.dev/pricing");
|
||||
/// }),
|
||||
/// Box::new(|_, _window, cx| {
|
||||
/// // Handle dismiss
|
||||
/// }),
|
||||
/// Box::new(|checked, window, cx| {
|
||||
/// // Handle don't show again
|
||||
/// }),
|
||||
/// );
|
||||
/// ```
|
||||
#[derive(IntoElement, RegisterComponent)]
|
||||
pub struct Upsell {
|
||||
title: SharedString,
|
||||
message: SharedString,
|
||||
cta_text: SharedString,
|
||||
on_click: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
|
||||
on_dismiss: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
|
||||
on_dont_show_again: Box<dyn Fn(bool, &mut Window, &mut App) + 'static>,
|
||||
}
|
||||
|
||||
impl Upsell {
|
||||
/// Create a new upsell component
|
||||
pub fn new(
|
||||
title: impl Into<SharedString>,
|
||||
message: impl Into<SharedString>,
|
||||
cta_text: impl Into<SharedString>,
|
||||
on_click: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
|
||||
on_dismiss: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
|
||||
on_dont_show_again: Box<dyn Fn(bool, &mut Window, &mut App) + 'static>,
|
||||
) -> Self {
|
||||
Self {
|
||||
title: title.into(),
|
||||
message: message.into(),
|
||||
cta_text: cta_text.into(),
|
||||
on_click,
|
||||
on_dismiss,
|
||||
on_dont_show_again,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Upsell {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
v_flex()
|
||||
.w_full()
|
||||
.p_4()
|
||||
.gap_3()
|
||||
.bg(cx.theme().colors().surface_background)
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new(self.title)
|
||||
.size(ui::LabelSize::Large)
|
||||
.weight(gpui::FontWeight::BOLD),
|
||||
)
|
||||
.child(Label::new(self.message).color(Color::Muted)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.items_center()
|
||||
.child(
|
||||
h_flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.child(
|
||||
Checkbox::new("dont-show-again", ToggleState::Unselected).on_click(
|
||||
move |_, window, cx| {
|
||||
(self.on_dont_show_again)(true, window, cx);
|
||||
},
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Label::new("Don't show again")
|
||||
.color(Color::Muted)
|
||||
.size(ui::LabelSize::Small),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Button::new("dismiss-button", "No Thanks")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.on_click(self.on_dismiss),
|
||||
)
|
||||
.child(
|
||||
Button::new("cta-button", self.cta_text)
|
||||
.style(ButtonStyle::Filled)
|
||||
.on_click(self.on_click),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for Upsell {
|
||||
fn scope() -> ComponentScope {
|
||||
ComponentScope::Agent
|
||||
}
|
||||
|
||||
fn name() -> &'static str {
|
||||
"Upsell"
|
||||
}
|
||||
|
||||
fn description() -> Option<&'static str> {
|
||||
Some("A promotional component that displays a message with a call-to-action.")
|
||||
}
|
||||
|
||||
fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
|
||||
let examples = vec![
|
||||
single_example(
|
||||
"Default",
|
||||
Upsell::new(
|
||||
"Upgrade to Zed Pro",
|
||||
"Get unlimited access to AI features and more with Zed Pro. Unlock advanced AI capabilities and other premium features.",
|
||||
"Upgrade Now",
|
||||
Box::new(|_, _, _| {}),
|
||||
Box::new(|_, _, _| {}),
|
||||
Box::new(|_, _, _| {}),
|
||||
).render(window, cx).into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Short Message",
|
||||
Upsell::new(
|
||||
"Try Zed Pro for free",
|
||||
"Start your 7-day trial today.",
|
||||
"Start Trial",
|
||||
Box::new(|_, _, _| {}),
|
||||
Box::new(|_, _, _| {}),
|
||||
Box::new(|_, _, _| {}),
|
||||
).render(window, cx).into_any_element(),
|
||||
),
|
||||
];
|
||||
|
||||
Some(v_flex().gap_4().children(examples).into_any_element())
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ language_model.workspace = true
|
||||
proto.workspace = true
|
||||
serde.workspace = true
|
||||
smallvec.workspace = true
|
||||
telemetry.workspace = true
|
||||
ui.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
@@ -38,10 +38,6 @@ impl ApiKeysWithProviders {
|
||||
.map(|provider| (provider.icon(), provider.name().0.clone()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn has_providers(&self) -> bool {
|
||||
!self.configured_providers.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ApiKeysWithProviders {
|
||||
@@ -53,11 +49,10 @@ impl Render for ApiKeysWithProviders {
|
||||
.map(|(icon, name)| {
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
|
||||
.child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted))
|
||||
.child(Label::new(name))
|
||||
});
|
||||
|
||||
h_flex()
|
||||
div()
|
||||
.mx_2p5()
|
||||
.p_1()
|
||||
.pb_0()
|
||||
@@ -85,8 +80,24 @@ impl Render for ApiKeysWithProviders {
|
||||
.border_x_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().panel_background)
|
||||
.child(Icon::new(IconName::Info).size(IconSize::XSmall).color(Color::Muted))
|
||||
.child(Label::new("Or start now using API keys from your environment for the following providers:").color(Color::Muted))
|
||||
.child(
|
||||
h_flex()
|
||||
.min_w_0()
|
||||
.gap_2()
|
||||
.child(
|
||||
Icon::new(IconName::Info)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.child(
|
||||
Label::new("Start now using API keys from your environment for the following providers:")
|
||||
.color(Color::Muted)
|
||||
)
|
||||
)
|
||||
)
|
||||
.children(configured_providers_list)
|
||||
)
|
||||
}
|
||||
@@ -118,7 +129,7 @@ impl RenderOnce for ApiKeysWithoutProviders {
|
||||
.child(Divider::horizontal()),
|
||||
)
|
||||
.child(List::new().child(BulletItem::new(
|
||||
"You can also use AI in Zed by bringing your own API keys",
|
||||
"Add your own keys to use AI without signing in.",
|
||||
)))
|
||||
.child(
|
||||
Button::new("configure-providers", "Configure Providers")
|
||||
|
||||
@@ -61,6 +61,11 @@ impl Render for AgentPanelOnboarding {
|
||||
Some(proto::Plan::ZedProTrial)
|
||||
);
|
||||
|
||||
let is_pro_user = matches!(
|
||||
self.user_store.read(cx).current_plan(),
|
||||
Some(proto::Plan::ZedPro)
|
||||
);
|
||||
|
||||
AgentPanelOnboardingCard::new()
|
||||
.child(
|
||||
ZedAiOnboarding::new(
|
||||
@@ -75,7 +80,7 @@ impl Render for AgentPanelOnboarding {
|
||||
}),
|
||||
)
|
||||
.map(|this| {
|
||||
if enrolled_in_trial || self.configured_providers.len() >= 1 {
|
||||
if enrolled_in_trial || is_pro_user || self.configured_providers.len() >= 1 {
|
||||
this
|
||||
} else {
|
||||
this.child(ApiKeysWithoutProviders::new())
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
mod agent_api_keys_onboarding;
|
||||
mod agent_panel_onboarding_card;
|
||||
mod agent_panel_onboarding_content;
|
||||
mod ai_upsell_card;
|
||||
mod edit_prediction_onboarding_content;
|
||||
mod young_account_banner;
|
||||
|
||||
pub use agent_api_keys_onboarding::{ApiKeysWithProviders, ApiKeysWithoutProviders};
|
||||
pub use agent_panel_onboarding_card::AgentPanelOnboardingCard;
|
||||
pub use agent_panel_onboarding_content::AgentPanelOnboarding;
|
||||
pub use ai_upsell_card::AiUpsellCard;
|
||||
pub use edit_prediction_onboarding_content::EditPredictionOnboarding;
|
||||
pub use young_account_banner::YoungAccountBanner;
|
||||
|
||||
@@ -16,6 +18,7 @@ use client::{Client, UserStore, zed_urls};
|
||||
use gpui::{AnyElement, Entity, IntoElement, ParentElement, SharedString};
|
||||
use ui::{Divider, List, ListItem, RegisterComponent, TintColor, Tooltip, prelude::*};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct BulletItem {
|
||||
label: SharedString,
|
||||
}
|
||||
@@ -28,22 +31,32 @@ impl BulletItem {
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoElement for BulletItem {
|
||||
type Element = AnyElement;
|
||||
impl RenderOnce for BulletItem {
|
||||
fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
let line_height = 0.85 * window.line_height();
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
ListItem::new("list-item")
|
||||
.selectable(false)
|
||||
.start_slot(
|
||||
Icon::new(IconName::Dash)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Hidden),
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.min_w_0()
|
||||
.gap_1()
|
||||
.items_start()
|
||||
.child(
|
||||
h_flex().h(line_height).justify_center().child(
|
||||
Icon::new(IconName::Dash)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Hidden),
|
||||
),
|
||||
)
|
||||
.child(div().w_full().min_w_0().child(Label::new(self.label))),
|
||||
)
|
||||
.child(div().w_full().child(Label::new(self.label)))
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub enum SignInStatus {
|
||||
SignedIn,
|
||||
SigningIn,
|
||||
@@ -141,22 +154,18 @@ impl ZedAiOnboarding {
|
||||
)
|
||||
.child(
|
||||
List::new()
|
||||
.child(BulletItem::new("50 prompts per month with Claude models"))
|
||||
.child(BulletItem::new(
|
||||
"50 prompts per month with the Claude models",
|
||||
))
|
||||
.child(BulletItem::new(
|
||||
"2000 accepted edit predictions using our open-source Zeta model",
|
||||
"2,000 accepted edit predictions with Zeta, our open-source model",
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
fn pro_trial_definition(&self) -> impl IntoElement {
|
||||
List::new()
|
||||
.child(BulletItem::new("150 prompts with Claude models"))
|
||||
.child(BulletItem::new(
|
||||
"150 prompts per month with the Claude models",
|
||||
))
|
||||
.child(BulletItem::new(
|
||||
"Unlimited accepted edit predictions using our open-source Zeta model",
|
||||
"Unlimited accepted edit predictions with Zeta, our open-source model",
|
||||
))
|
||||
}
|
||||
|
||||
@@ -178,15 +187,16 @@ impl ZedAiOnboarding {
|
||||
List::new()
|
||||
.child(BulletItem::new("500 prompts per month with Claude models"))
|
||||
.child(BulletItem::new(
|
||||
"Unlimited accepted edit predictions using our open-source Zeta model",
|
||||
"Unlimited accepted edit predictions with Zeta, our open-source model",
|
||||
))
|
||||
.child(BulletItem::new("USD $20 per month")),
|
||||
.child(BulletItem::new("$20 USD per month")),
|
||||
)
|
||||
.child(
|
||||
Button::new("pro", "Start with Pro")
|
||||
Button::new("pro", "Get Started")
|
||||
.full_width()
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
.on_click(move |_, _window, cx| {
|
||||
telemetry::event!("Upgrade To Pro Clicked", state = "young-account");
|
||||
cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))
|
||||
}),
|
||||
)
|
||||
@@ -206,14 +216,15 @@ impl ZedAiOnboarding {
|
||||
List::new()
|
||||
.child(self.pro_trial_definition())
|
||||
.child(BulletItem::new(
|
||||
"Try it out for 14 days with no charge and no credit card required",
|
||||
"Try it out for 14 days for free, no credit card required",
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
Button::new("pro", "Start Pro Trial")
|
||||
Button::new("pro", "Start Free Trial")
|
||||
.full_width()
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
.on_click(move |_, _window, cx| {
|
||||
telemetry::event!("Start Trial Clicked", state = "post-sign-in");
|
||||
cx.open_url(&zed_urls::start_trial_url(cx))
|
||||
}),
|
||||
)
|
||||
@@ -225,28 +236,33 @@ impl ZedAiOnboarding {
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.w_full()
|
||||
.child(Headline::new("Before starting…"))
|
||||
.child(Headline::new("Accept Terms of Service"))
|
||||
.child(
|
||||
Label::new("Make sure you have read and accepted Zed AI's terms of service.")
|
||||
Label::new("We don’t sell your data, track you across the web, or compromise your privacy.")
|
||||
.color(Color::Muted)
|
||||
.mb_2(),
|
||||
)
|
||||
.child(
|
||||
Button::new("terms_of_service", "View and Read the Terms of Service")
|
||||
Button::new("terms_of_service", "Review Terms of Service")
|
||||
.full_width()
|
||||
.style(ButtonStyle::Outlined)
|
||||
.icon(IconName::ArrowUpRight)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.on_click(move |_, _window, cx| cx.open_url(&zed_urls::terms_of_service(cx))),
|
||||
.on_click(move |_, _window, cx| {
|
||||
telemetry::event!("Review Terms of Service Clicked");
|
||||
cx.open_url(&zed_urls::terms_of_service(cx))
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("accept_terms", "I've read it and accept it")
|
||||
Button::new("accept_terms", "Accept")
|
||||
.full_width()
|
||||
.style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.on_click({
|
||||
let callback = self.accept_terms_of_service.clone();
|
||||
move |_, window, cx| (callback)(window, cx)
|
||||
move |_, window, cx| {
|
||||
telemetry::event!("Terms of Service Accepted");
|
||||
(callback)(window, cx)}
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
@@ -259,19 +275,22 @@ impl ZedAiOnboarding {
|
||||
.gap_1()
|
||||
.child(Headline::new("Welcome to Zed AI"))
|
||||
.child(
|
||||
Label::new("Sign in to start using AI in Zed with a free trial of the Pro plan, which includes:")
|
||||
Label::new("Sign in to try Zed Pro for 14 days, no credit card required.")
|
||||
.color(Color::Muted)
|
||||
.mb_2(),
|
||||
)
|
||||
.child(self.pro_trial_definition())
|
||||
.child(
|
||||
Button::new("sign_in", "Sign in to Start Trial")
|
||||
Button::new("sign_in", "Try Zed Pro for Free")
|
||||
.disabled(signing_in)
|
||||
.full_width()
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
.on_click({
|
||||
let callback = self.sign_in.clone();
|
||||
move |_, window, cx| callback(window, cx)
|
||||
move |_, window, cx| {
|
||||
telemetry::event!("Start Trial Clicked", state = "pre-sign-in");
|
||||
callback(window, cx)
|
||||
}
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
@@ -284,11 +303,6 @@ impl ZedAiOnboarding {
|
||||
.relative()
|
||||
.gap_1()
|
||||
.child(Headline::new("Welcome to Zed AI"))
|
||||
.child(
|
||||
Label::new("Choose how you want to start.")
|
||||
.color(Color::Muted)
|
||||
.mb_2(),
|
||||
)
|
||||
.map(|this| {
|
||||
if self.account_too_young {
|
||||
this.child(young_account_banner)
|
||||
@@ -303,7 +317,13 @@ impl ZedAiOnboarding {
|
||||
IconButton::new("dismiss_onboarding", IconName::Close)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::text("Dismiss"))
|
||||
.on_click(move |_, window, cx| callback(window, cx)),
|
||||
.on_click(move |_, window, cx| {
|
||||
telemetry::event!(
|
||||
"Banner Dismissed",
|
||||
source = "AI Onboarding",
|
||||
);
|
||||
callback(window, cx)
|
||||
}),
|
||||
),
|
||||
)
|
||||
},
|
||||
@@ -318,7 +338,7 @@ impl ZedAiOnboarding {
|
||||
v_flex()
|
||||
.relative()
|
||||
.gap_1()
|
||||
.child(Headline::new("Welcome to the Zed Pro free trial"))
|
||||
.child(Headline::new("Welcome to the Zed Pro Trial"))
|
||||
.child(
|
||||
Label::new("Here's what you get for the next 14 days:")
|
||||
.color(Color::Muted)
|
||||
@@ -340,7 +360,13 @@ impl ZedAiOnboarding {
|
||||
IconButton::new("dismiss_onboarding", IconName::Close)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::text("Dismiss"))
|
||||
.on_click(move |_, window, cx| callback(window, cx)),
|
||||
.on_click(move |_, window, cx| {
|
||||
telemetry::event!(
|
||||
"Banner Dismissed",
|
||||
source = "AI Onboarding",
|
||||
);
|
||||
callback(window, cx)
|
||||
}),
|
||||
),
|
||||
)
|
||||
},
|
||||
@@ -360,7 +386,9 @@ impl ZedAiOnboarding {
|
||||
.child(
|
||||
List::new()
|
||||
.child(BulletItem::new("500 prompts with Claude models"))
|
||||
.child(BulletItem::new("Unlimited edit predictions")),
|
||||
.child(BulletItem::new(
|
||||
"Unlimited edit predictions with Zeta, our open-source model",
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
Button::new("pro", "Continue with Zed Pro")
|
||||
@@ -368,7 +396,10 @@ impl ZedAiOnboarding {
|
||||
.style(ButtonStyle::Outlined)
|
||||
.on_click({
|
||||
let callback = self.continue_with_zed_ai.clone();
|
||||
move |_, window, cx| callback(window, cx)
|
||||
move |_, window, cx| {
|
||||
telemetry::event!("Banner Dismissed", source = "AI Onboarding");
|
||||
callback(window, cx)
|
||||
}
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
|
||||
201
crates/ai_onboarding/src/ai_upsell_card.rs
Normal file
@@ -0,0 +1,201 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use client::{Client, zed_urls};
|
||||
use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
|
||||
use ui::{Divider, List, Vector, VectorName, prelude::*};
|
||||
|
||||
use crate::{BulletItem, SignInStatus};
|
||||
|
||||
#[derive(IntoElement, RegisterComponent)]
|
||||
pub struct AiUpsellCard {
|
||||
pub sign_in_status: SignInStatus,
|
||||
pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
|
||||
}
|
||||
|
||||
impl AiUpsellCard {
|
||||
pub fn new(client: Arc<Client>) -> Self {
|
||||
let status = *client.status().borrow();
|
||||
|
||||
Self {
|
||||
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;
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for AiUpsellCard {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let pro_section = v_flex()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Label::new("Pro")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Accent)
|
||||
.buffer_font(cx),
|
||||
)
|
||||
.child(Divider::horizontal()),
|
||||
)
|
||||
.child(
|
||||
List::new()
|
||||
.child(BulletItem::new("500 prompts with Claude models"))
|
||||
.child(BulletItem::new(
|
||||
"Unlimited edit predictions with Zeta, our open-source model",
|
||||
)),
|
||||
);
|
||||
|
||||
let free_section = v_flex()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Label::new("Free")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
)
|
||||
.child(Divider::horizontal()),
|
||||
)
|
||||
.child(
|
||||
List::new()
|
||||
.child(BulletItem::new("50 prompts with the Claude models"))
|
||||
.child(BulletItem::new("2,000 accepted edit predictions")),
|
||||
);
|
||||
|
||||
let grid_bg = h_flex().absolute().inset_0().w_full().h(px(240.)).child(
|
||||
Vector::new(VectorName::Grid, rems_from_px(500.), rems_from_px(240.))
|
||||
.color(Color::Custom(cx.theme().colors().border.opacity(0.05))),
|
||||
);
|
||||
|
||||
let gradient_bg = div()
|
||||
.absolute()
|
||||
.inset_0()
|
||||
.size_full()
|
||||
.bg(gpui::linear_gradient(
|
||||
180.,
|
||||
gpui::linear_color_stop(
|
||||
cx.theme().colors().elevated_surface_background.opacity(0.8),
|
||||
0.,
|
||||
),
|
||||
gpui::linear_color_stop(
|
||||
cx.theme().colors().elevated_surface_background.opacity(0.),
|
||||
0.8,
|
||||
),
|
||||
));
|
||||
|
||||
const DESCRIPTION: &str = "Zed offers a complete agentic experience, with robust editing and reviewing features to collaborate with AI.";
|
||||
|
||||
let footer_buttons = match self.sign_in_status {
|
||||
SignInStatus::SignedIn => v_flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.child(
|
||||
Button::new("sign_in", "Start 14-day Free Pro Trial")
|
||||
.full_width()
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
.on_click(move |_, _window, cx| {
|
||||
telemetry::event!("Start Trial Clicked", state = "post-sign-in");
|
||||
cx.open_url(&zed_urls::start_trial_url(cx))
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Label::new("No credit card required")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.into_any_element(),
|
||||
_ => Button::new("sign_in", "Sign In")
|
||||
.full_width()
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
.on_click({
|
||||
let callback = self.sign_in.clone();
|
||||
move |_, window, cx| {
|
||||
telemetry::event!("Start Trial Clicked", state = "pre-sign-in");
|
||||
callback(window, cx)
|
||||
}
|
||||
})
|
||||
.into_any_element(),
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.relative()
|
||||
.p_6()
|
||||
.pt_4()
|
||||
.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(
|
||||
h_flex()
|
||||
.mt_1p5()
|
||||
.mb_2p5()
|
||||
.items_start()
|
||||
.gap_12()
|
||||
.child(free_section)
|
||||
.child(pro_section),
|
||||
)
|
||||
.child(footer_buttons)
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for AiUpsellCard {
|
||||
fn scope() -> ComponentScope {
|
||||
ComponentScope::Agent
|
||||
}
|
||||
|
||||
fn name() -> &'static str {
|
||||
"AI Upsell Card"
|
||||
}
|
||||
|
||||
fn sort_name() -> &'static str {
|
||||
"AI Upsell Card"
|
||||
}
|
||||
|
||||
fn description() -> Option<&'static str> {
|
||||
Some("A card presenting the Zed AI product during user's first-open onboarding flow.")
|
||||
}
|
||||
|
||||
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
|
||||
Some(
|
||||
v_flex()
|
||||
.p_4()
|
||||
.gap_4()
|
||||
.children(vec![example_group(vec![
|
||||
single_example(
|
||||
"Signed Out State",
|
||||
AiUpsellCard {
|
||||
sign_in_status: SignInStatus::SignedOut,
|
||||
sign_in: Arc::new(|_, _| {}),
|
||||
}
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Signed In State",
|
||||
AiUpsellCard {
|
||||
sign_in_status: SignInStatus::SignedIn,
|
||||
sign_in: Arc::new(|_, _| {}),
|
||||
}
|
||||
.into_any_element(),
|
||||
),
|
||||
])])
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ pub struct YoungAccountBanner;
|
||||
|
||||
impl RenderOnce for YoungAccountBanner {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
const YOUNG_ACCOUNT_DISCLAIMER: &str = "To prevent abuse of our service, we cannot offer plans to GitHub accounts created fewer than 30 days ago. To request an exception, reach out to billing@zed.dev.";
|
||||
const YOUNG_ACCOUNT_DISCLAIMER: &str = "To prevent abuse of our service, we cannot offer plans to GitHub accounts created fewer than 30 days ago. To request an exception, reach out to billing-support@zed.dev.";
|
||||
|
||||
let label = div()
|
||||
.w_full()
|
||||
|
||||
@@ -19,6 +19,7 @@ assistant_slash_commands.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
clock.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
context_server.workspace = true
|
||||
fs.workspace = true
|
||||
@@ -48,7 +49,6 @@ util.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
indoc.workspace = true
|
||||
|
||||
@@ -11,6 +11,7 @@ use assistant_slash_command::{
|
||||
use assistant_slash_commands::FileCommandMetadata;
|
||||
use client::{self, Client, proto, telemetry::Telemetry};
|
||||
use clock::ReplicaId;
|
||||
use cloud_llm_client::CompletionIntent;
|
||||
use collections::{HashMap, HashSet};
|
||||
use fs::{Fs, RenameOptions};
|
||||
use futures::{FutureExt, StreamExt, future::Shared};
|
||||
@@ -46,7 +47,6 @@ use text::{BufferSnapshot, ToPoint};
|
||||
use ui::IconName;
|
||||
use util::{ResultExt, TryFutureExt, post_inc};
|
||||
use uuid::Uuid;
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
pub use crate::context_store::*;
|
||||
|
||||
|
||||
@@ -1323,7 +1323,7 @@ fn setup_context_editor_with_fake_model(
|
||||
) -> (Entity<AssistantContext>, Arc<FakeLanguageModel>) {
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.executor().clone()));
|
||||
|
||||
let fake_provider = Arc::new(FakeLanguageModelProvider);
|
||||
let fake_provider = Arc::new(FakeLanguageModelProvider::default());
|
||||
let fake_model = Arc::new(fake_provider.test_model());
|
||||
|
||||
cx.update(|cx| {
|
||||
|
||||
@@ -767,6 +767,11 @@ impl ContextStore {
|
||||
fn reload(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
|
||||
let fs = self.fs.clone();
|
||||
cx.spawn(async move |this, cx| {
|
||||
pub static ZED_STATELESS: LazyLock<bool> =
|
||||
LazyLock::new(|| std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty()));
|
||||
if *ZED_STATELESS {
|
||||
return Ok(());
|
||||
}
|
||||
fs.create_dir(contexts_dir()).await?;
|
||||
|
||||
let mut paths = fs.read_dir(contexts_dir()).await?;
|
||||
|
||||
@@ -51,23 +51,13 @@ impl ActionLog {
|
||||
Some(self.tracked_buffers.get(buffer)?.snapshot.clone())
|
||||
}
|
||||
|
||||
pub fn has_unnotified_user_edits(&self) -> bool {
|
||||
self.tracked_buffers
|
||||
.values()
|
||||
.any(|tracked| tracked.has_unnotified_user_edits)
|
||||
}
|
||||
|
||||
/// Return a unified diff patch with user edits made since last read or notification
|
||||
pub fn unnotified_user_edits(&self, cx: &Context<Self>) -> Option<String> {
|
||||
if !self.has_unnotified_user_edits() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let unified_diff = self
|
||||
let diffs = self
|
||||
.tracked_buffers
|
||||
.values()
|
||||
.filter_map(|tracked| {
|
||||
if !tracked.has_unnotified_user_edits {
|
||||
if !tracked.may_have_unnotified_user_edits {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -95,9 +85,13 @@ impl ActionLog {
|
||||
|
||||
Some(result)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n\n");
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if diffs.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let unified_diff = diffs.join("\n\n");
|
||||
Some(unified_diff)
|
||||
}
|
||||
|
||||
@@ -106,7 +100,7 @@ impl ActionLog {
|
||||
pub fn flush_unnotified_user_edits(&mut self, cx: &Context<Self>) -> Option<String> {
|
||||
let patch = self.unnotified_user_edits(cx);
|
||||
self.tracked_buffers.values_mut().for_each(|tracked| {
|
||||
tracked.has_unnotified_user_edits = false;
|
||||
tracked.may_have_unnotified_user_edits = false;
|
||||
tracked.last_seen_base = tracked.diff_base.clone();
|
||||
});
|
||||
patch
|
||||
@@ -185,7 +179,7 @@ impl ActionLog {
|
||||
version: buffer.read(cx).version(),
|
||||
diff,
|
||||
diff_update: diff_update_tx,
|
||||
has_unnotified_user_edits: false,
|
||||
may_have_unnotified_user_edits: false,
|
||||
_open_lsp_handle: open_lsp_handle,
|
||||
_maintain_diff: cx.spawn({
|
||||
let buffer = buffer.clone();
|
||||
@@ -337,27 +331,34 @@ impl ActionLog {
|
||||
let new_snapshot = buffer_snapshot.clone();
|
||||
let unreviewed_edits = tracked_buffer.unreviewed_edits.clone();
|
||||
let edits = diff_snapshots(&old_snapshot, &new_snapshot);
|
||||
if let ChangeAuthor::User = author
|
||||
&& !edits.is_empty()
|
||||
{
|
||||
tracked_buffer.has_unnotified_user_edits = true;
|
||||
}
|
||||
let mut has_user_changes = false;
|
||||
async move {
|
||||
if let ChangeAuthor::User = author {
|
||||
apply_non_conflicting_edits(
|
||||
has_user_changes = apply_non_conflicting_edits(
|
||||
&unreviewed_edits,
|
||||
edits,
|
||||
&mut base_text,
|
||||
new_snapshot.as_rope(),
|
||||
);
|
||||
}
|
||||
(Arc::new(base_text.to_string()), base_text)
|
||||
|
||||
(Arc::new(base_text.to_string()), base_text, has_user_changes)
|
||||
}
|
||||
});
|
||||
|
||||
anyhow::Ok(rebase)
|
||||
})??;
|
||||
let (new_base_text, new_diff_base) = rebase.await;
|
||||
let (new_base_text, new_diff_base, has_user_changes) = rebase.await;
|
||||
|
||||
this.update(cx, |this, _| {
|
||||
let tracked_buffer = this
|
||||
.tracked_buffers
|
||||
.get_mut(buffer)
|
||||
.context("buffer not tracked")
|
||||
.unwrap();
|
||||
tracked_buffer.may_have_unnotified_user_edits |= has_user_changes;
|
||||
})?;
|
||||
|
||||
Self::update_diff(
|
||||
this,
|
||||
buffer,
|
||||
@@ -829,11 +830,12 @@ fn apply_non_conflicting_edits(
|
||||
edits: Vec<Edit<u32>>,
|
||||
old_text: &mut Rope,
|
||||
new_text: &Rope,
|
||||
) {
|
||||
) -> bool {
|
||||
let mut old_edits = patch.edits().iter().cloned().peekable();
|
||||
let mut new_edits = edits.into_iter().peekable();
|
||||
let mut applied_delta = 0i32;
|
||||
let mut rebased_delta = 0i32;
|
||||
let mut has_made_changes = false;
|
||||
|
||||
while let Some(mut new_edit) = new_edits.next() {
|
||||
let mut conflict = false;
|
||||
@@ -883,8 +885,10 @@ fn apply_non_conflicting_edits(
|
||||
&new_text.chunks_in_range(new_bytes).collect::<String>(),
|
||||
);
|
||||
applied_delta += new_edit.new_len() as i32 - new_edit.old_len() as i32;
|
||||
has_made_changes = true;
|
||||
}
|
||||
}
|
||||
has_made_changes
|
||||
}
|
||||
|
||||
fn diff_snapshots(
|
||||
@@ -958,7 +962,7 @@ struct TrackedBuffer {
|
||||
diff: Entity<BufferDiff>,
|
||||
snapshot: text::BufferSnapshot,
|
||||
diff_update: mpsc::UnboundedSender<(ChangeAuthor, text::BufferSnapshot)>,
|
||||
has_unnotified_user_edits: bool,
|
||||
may_have_unnotified_user_edits: bool,
|
||||
_open_lsp_handle: OpenLspBufferHandle,
|
||||
_maintain_diff: Task<()>,
|
||||
_subscription: Subscription,
|
||||
|
||||
@@ -216,7 +216,12 @@ pub trait Tool: 'static + Send + Sync {
|
||||
|
||||
/// Returns true if the tool needs the users's confirmation
|
||||
/// before having permission to run.
|
||||
fn needs_confirmation(&self, input: &serde_json::Value, cx: &App) -> bool;
|
||||
fn needs_confirmation(
|
||||
&self,
|
||||
input: &serde_json::Value,
|
||||
project: &Entity<Project>,
|
||||
cx: &App,
|
||||
) -> bool;
|
||||
|
||||
/// Returns true if the tool may perform edits.
|
||||
fn may_perform_edits(&self) -> bool;
|
||||
|
||||
@@ -375,7 +375,12 @@ mod tests {
|
||||
false
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _input: &serde_json::Value, _cx: &App) -> bool {
|
||||
fn needs_confirmation(
|
||||
&self,
|
||||
_input: &serde_json::Value,
|
||||
_project: &Entity<Project>,
|
||||
_cx: &App,
|
||||
) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
@@ -20,9 +20,12 @@ anyhow.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
buffer_diff.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
component.workspace = true
|
||||
derive_more.workspace = true
|
||||
diffy = "0.4.2"
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
futures.workspace = true
|
||||
@@ -62,8 +65,6 @@ web_search.workspace = true
|
||||
which.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
diffy = "0.4.2"
|
||||
|
||||
[dev-dependencies]
|
||||
lsp = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -20,14 +20,13 @@ mod thinking_tool;
|
||||
mod ui;
|
||||
mod web_search_tool;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_tool::ToolRegistry;
|
||||
use copy_path_tool::CopyPathTool;
|
||||
use gpui::{App, Entity};
|
||||
use http_client::HttpClientWithUrl;
|
||||
use language_model::LanguageModelRegistry;
|
||||
use move_path_tool::MovePathTool;
|
||||
use std::sync::Arc;
|
||||
use web_search_tool::WebSearchTool;
|
||||
|
||||
pub(crate) use templates::*;
|
||||
|
||||
@@ -44,7 +44,7 @@ impl Tool for CopyPathTool {
|
||||
"copy_path".into()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ impl Tool for CreateDirectoryTool {
|
||||
include_str!("./create_directory_tool/description.md").into()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ impl Tool for DeletePathTool {
|
||||
"delete_path".into()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ impl Tool for DiagnosticsTool {
|
||||
"diagnostics".into()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ mod streaming_fuzzy_matcher;
|
||||
use crate::{Template, Templates};
|
||||
use anyhow::Result;
|
||||
use assistant_tool::ActionLog;
|
||||
use cloud_llm_client::CompletionIntent;
|
||||
use create_file_parser::{CreateFileParser, CreateFileParserEvent};
|
||||
pub use edit_parser::EditFormat;
|
||||
use edit_parser::{EditParser, EditParserEvent, EditParserMetrics};
|
||||
@@ -29,7 +30,6 @@ use std::{cmp, iter, mem, ops::Range, path::PathBuf, pin::Pin, sync::Arc, task::
|
||||
use streaming_diff::{CharOperation, StreamingDiff};
|
||||
use streaming_fuzzy_matcher::StreamingFuzzyMatcher;
|
||||
use util::debug_panic;
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct CreateFilePromptTemplate {
|
||||
|
||||
@@ -25,6 +25,7 @@ use language::{
|
||||
};
|
||||
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
|
||||
use markdown::{Markdown, MarkdownElement, MarkdownStyle};
|
||||
use paths;
|
||||
use project::{
|
||||
Project, ProjectPath,
|
||||
lsp_store::{FormatTrigger, LspFormatTarget},
|
||||
@@ -126,8 +127,47 @@ impl Tool for EditFileTool {
|
||||
"edit_file".into()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
||||
false
|
||||
fn needs_confirmation(
|
||||
&self,
|
||||
input: &serde_json::Value,
|
||||
project: &Entity<Project>,
|
||||
cx: &App,
|
||||
) -> bool {
|
||||
if agent_settings::AgentSettings::get_global(cx).always_allow_tool_actions {
|
||||
return false;
|
||||
}
|
||||
|
||||
let Ok(input) = serde_json::from_value::<EditFileToolInput>(input.clone()) else {
|
||||
// If it's not valid JSON, it's going to error and confirming won't do anything.
|
||||
return false;
|
||||
};
|
||||
|
||||
// If any path component matches the local settings folder, then this could affect
|
||||
// the editor in ways beyond the project source, so prompt.
|
||||
let local_settings_folder = paths::local_settings_folder_relative_path();
|
||||
let path = Path::new(&input.path);
|
||||
if path
|
||||
.components()
|
||||
.any(|component| component.as_os_str() == local_settings_folder.as_os_str())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// It's also possible that the global config dir is configured to be inside the project,
|
||||
// so check for that edge case too.
|
||||
if let Ok(canonical_path) = std::fs::canonicalize(&input.path) {
|
||||
if canonical_path.starts_with(paths::config_dir()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if path is inside the global config directory
|
||||
// First check if it's already inside project - if not, try to canonicalize
|
||||
let project_path = project.read(cx).find_project_path(&input.path, cx);
|
||||
|
||||
// If the path is inside the project, and it's not one of the above edge cases,
|
||||
// then no confirmation is necessary. Otherwise, confirmation is necessary.
|
||||
project_path.is_none()
|
||||
}
|
||||
|
||||
fn may_perform_edits(&self) -> bool {
|
||||
@@ -148,7 +188,25 @@ impl Tool for EditFileTool {
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<EditFileToolInput>(input.clone()) {
|
||||
Ok(input) => input.display_description,
|
||||
Ok(input) => {
|
||||
let path = Path::new(&input.path);
|
||||
let mut description = input.display_description.clone();
|
||||
|
||||
// Add context about why confirmation may be needed
|
||||
let local_settings_folder = paths::local_settings_folder_relative_path();
|
||||
if path
|
||||
.components()
|
||||
.any(|c| c.as_os_str() == local_settings_folder.as_os_str())
|
||||
{
|
||||
description.push_str(" (local settings)");
|
||||
} else if let Ok(canonical_path) = std::fs::canonicalize(&input.path) {
|
||||
if canonical_path.starts_with(paths::config_dir()) {
|
||||
description.push_str(" (global settings)");
|
||||
}
|
||||
}
|
||||
|
||||
description
|
||||
}
|
||||
Err(_) => "Editing file".to_string(),
|
||||
}
|
||||
}
|
||||
@@ -278,6 +336,9 @@ impl Tool for EditFileTool {
|
||||
.unwrap_or(false);
|
||||
|
||||
if format_on_save_enabled {
|
||||
action_log.update(cx, |log, cx| {
|
||||
log.buffer_edited(buffer.clone(), cx);
|
||||
})?;
|
||||
let format_task = project.update(cx, |project, cx| {
|
||||
project.format(
|
||||
HashSet::from_iter([buffer.clone()]),
|
||||
@@ -1172,19 +1233,20 @@ async fn build_buffer_diff(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use ::fs::Fs;
|
||||
use client::TelemetrySettings;
|
||||
use fs::{FakeFs, Fs};
|
||||
use gpui::{TestAppContext, UpdateGlobal};
|
||||
use language_model::fake_provider::FakeLanguageModel;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use std::fs;
|
||||
use util::path;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_edit_nonexistent_file(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let fs = project::FakeFs::new(cx.executor());
|
||||
fs.insert_tree("/root", json!({})).await;
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
@@ -1274,7 +1336,7 @@ mod tests {
|
||||
) -> anyhow::Result<ProjectPath> {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let fs = project::FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
@@ -1381,6 +1443,21 @@ mod tests {
|
||||
cx.set_global(settings_store);
|
||||
language::init(cx);
|
||||
TelemetrySettings::register(cx);
|
||||
agent_settings::AgentSettings::register(cx);
|
||||
Project::init_settings(cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn init_test_with_config(cx: &mut TestAppContext, data_dir: &Path) {
|
||||
cx.update(|cx| {
|
||||
// Set custom data directory (config will be under data_dir/config)
|
||||
paths::set_custom_data_dir(data_dir.to_str().unwrap());
|
||||
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
language::init(cx);
|
||||
TelemetrySettings::register(cx);
|
||||
agent_settings::AgentSettings::register(cx);
|
||||
Project::init_settings(cx);
|
||||
});
|
||||
}
|
||||
@@ -1389,7 +1466,7 @@ mod tests {
|
||||
async fn test_format_on_save(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let fs = project::FakeFs::new(cx.executor());
|
||||
fs.insert_tree("/root", json!({"src": {}})).await;
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
@@ -1588,7 +1665,7 @@ mod tests {
|
||||
async fn test_remove_trailing_whitespace(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let fs = project::FakeFs::new(cx.executor());
|
||||
fs.insert_tree("/root", json!({"src": {}})).await;
|
||||
|
||||
// Create a simple file with trailing whitespace
|
||||
@@ -1720,4 +1797,641 @@ mod tests {
|
||||
"Trailing whitespace should remain when remove_trailing_whitespace_on_save is disabled"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_needs_confirmation(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let tool = Arc::new(EditFileTool);
|
||||
let fs = project::FakeFs::new(cx.executor());
|
||||
fs.insert_tree("/root", json!({})).await;
|
||||
|
||||
// Test 1: Path with .zed component should require confirmation
|
||||
let input_with_zed = json!({
|
||||
"display_description": "Edit settings",
|
||||
"path": ".zed/settings.json",
|
||||
"mode": "edit"
|
||||
});
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
cx.update(|cx| {
|
||||
assert!(
|
||||
tool.needs_confirmation(&input_with_zed, &project, cx),
|
||||
"Path with .zed component should require confirmation"
|
||||
);
|
||||
});
|
||||
|
||||
// Test 2: Absolute path should require confirmation
|
||||
let input_absolute = json!({
|
||||
"display_description": "Edit file",
|
||||
"path": "/etc/hosts",
|
||||
"mode": "edit"
|
||||
});
|
||||
cx.update(|cx| {
|
||||
assert!(
|
||||
tool.needs_confirmation(&input_absolute, &project, cx),
|
||||
"Absolute path should require confirmation"
|
||||
);
|
||||
});
|
||||
|
||||
// Test 3: Relative path without .zed should not require confirmation
|
||||
let input_relative = json!({
|
||||
"display_description": "Edit file",
|
||||
"path": "root/src/main.rs",
|
||||
"mode": "edit"
|
||||
});
|
||||
cx.update(|cx| {
|
||||
assert!(
|
||||
!tool.needs_confirmation(&input_relative, &project, cx),
|
||||
"Relative path without .zed should not require confirmation"
|
||||
);
|
||||
});
|
||||
|
||||
// Test 4: Path with .zed in the middle should require confirmation
|
||||
let input_zed_middle = json!({
|
||||
"display_description": "Edit settings",
|
||||
"path": "root/.zed/tasks.json",
|
||||
"mode": "edit"
|
||||
});
|
||||
cx.update(|cx| {
|
||||
assert!(
|
||||
tool.needs_confirmation(&input_zed_middle, &project, cx),
|
||||
"Path with .zed in any component should require confirmation"
|
||||
);
|
||||
});
|
||||
|
||||
// Test 5: When always_allow_tool_actions is enabled, no confirmation needed
|
||||
cx.update(|cx| {
|
||||
let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
|
||||
settings.always_allow_tool_actions = true;
|
||||
agent_settings::AgentSettings::override_global(settings, cx);
|
||||
|
||||
assert!(
|
||||
!tool.needs_confirmation(&input_with_zed, &project, cx),
|
||||
"When always_allow_tool_actions is true, no confirmation should be needed"
|
||||
);
|
||||
assert!(
|
||||
!tool.needs_confirmation(&input_absolute, &project, cx),
|
||||
"When always_allow_tool_actions is true, no confirmation should be needed for absolute paths"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_ui_text_shows_correct_context(cx: &mut TestAppContext) {
|
||||
// Set up a custom config directory for testing
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
init_test_with_config(cx, temp_dir.path());
|
||||
|
||||
let tool = Arc::new(EditFileTool);
|
||||
|
||||
// Test ui_text shows context for various paths
|
||||
let test_cases = vec![
|
||||
(
|
||||
json!({
|
||||
"display_description": "Update config",
|
||||
"path": ".zed/settings.json",
|
||||
"mode": "edit"
|
||||
}),
|
||||
"Update config (local settings)",
|
||||
".zed path should show local settings context",
|
||||
),
|
||||
(
|
||||
json!({
|
||||
"display_description": "Fix bug",
|
||||
"path": "src/.zed/local.json",
|
||||
"mode": "edit"
|
||||
}),
|
||||
"Fix bug (local settings)",
|
||||
"Nested .zed path should show local settings context",
|
||||
),
|
||||
(
|
||||
json!({
|
||||
"display_description": "Update readme",
|
||||
"path": "README.md",
|
||||
"mode": "edit"
|
||||
}),
|
||||
"Update readme",
|
||||
"Normal path should not show additional context",
|
||||
),
|
||||
(
|
||||
json!({
|
||||
"display_description": "Edit config",
|
||||
"path": "config.zed",
|
||||
"mode": "edit"
|
||||
}),
|
||||
"Edit config",
|
||||
".zed as extension should not show context",
|
||||
),
|
||||
];
|
||||
|
||||
for (input, expected_text, description) in test_cases {
|
||||
cx.update(|_cx| {
|
||||
let ui_text = tool.ui_text(&input);
|
||||
assert_eq!(ui_text, expected_text, "Failed for case: {}", description);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_needs_confirmation_outside_project(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let tool = Arc::new(EditFileTool);
|
||||
let fs = project::FakeFs::new(cx.executor());
|
||||
|
||||
// Create a project in /project directory
|
||||
fs.insert_tree("/project", json!({})).await;
|
||||
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
|
||||
|
||||
// Test file outside project requires confirmation
|
||||
let input_outside = json!({
|
||||
"display_description": "Edit file",
|
||||
"path": "/outside/file.txt",
|
||||
"mode": "edit"
|
||||
});
|
||||
cx.update(|cx| {
|
||||
assert!(
|
||||
tool.needs_confirmation(&input_outside, &project, cx),
|
||||
"File outside project should require confirmation"
|
||||
);
|
||||
});
|
||||
|
||||
// Test file inside project doesn't require confirmation
|
||||
let input_inside = json!({
|
||||
"display_description": "Edit file",
|
||||
"path": "project/file.txt",
|
||||
"mode": "edit"
|
||||
});
|
||||
cx.update(|cx| {
|
||||
assert!(
|
||||
!tool.needs_confirmation(&input_inside, &project, cx),
|
||||
"File inside project should not require confirmation"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_needs_confirmation_config_paths(cx: &mut TestAppContext) {
|
||||
// Set up a custom data directory for testing
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
init_test_with_config(cx, temp_dir.path());
|
||||
|
||||
let tool = Arc::new(EditFileTool);
|
||||
let fs = project::FakeFs::new(cx.executor());
|
||||
fs.insert_tree("/home/user/myproject", json!({})).await;
|
||||
let project = Project::test(fs.clone(), [path!("/home/user/myproject").as_ref()], cx).await;
|
||||
|
||||
// Get the actual local settings folder name
|
||||
let local_settings_folder = paths::local_settings_folder_relative_path();
|
||||
|
||||
// Test various config path patterns
|
||||
let test_cases = vec![
|
||||
(
|
||||
format!("{}/settings.json", local_settings_folder.display()),
|
||||
true,
|
||||
"Top-level local settings file".to_string(),
|
||||
),
|
||||
(
|
||||
format!(
|
||||
"myproject/{}/settings.json",
|
||||
local_settings_folder.display()
|
||||
),
|
||||
true,
|
||||
"Local settings in project path".to_string(),
|
||||
),
|
||||
(
|
||||
format!("src/{}/config.toml", local_settings_folder.display()),
|
||||
true,
|
||||
"Local settings in subdirectory".to_string(),
|
||||
),
|
||||
(
|
||||
".zed.backup/file.txt".to_string(),
|
||||
true,
|
||||
".zed.backup is outside project".to_string(),
|
||||
),
|
||||
(
|
||||
"my.zed/file.txt".to_string(),
|
||||
true,
|
||||
"my.zed is outside project".to_string(),
|
||||
),
|
||||
(
|
||||
"myproject/src/file.zed".to_string(),
|
||||
false,
|
||||
".zed as file extension".to_string(),
|
||||
),
|
||||
(
|
||||
"myproject/normal/path/file.rs".to_string(),
|
||||
false,
|
||||
"Normal file without config paths".to_string(),
|
||||
),
|
||||
];
|
||||
|
||||
for (path, should_confirm, description) in test_cases {
|
||||
let input = json!({
|
||||
"display_description": "Edit file",
|
||||
"path": path,
|
||||
"mode": "edit"
|
||||
});
|
||||
cx.update(|cx| {
|
||||
assert_eq!(
|
||||
tool.needs_confirmation(&input, &project, cx),
|
||||
should_confirm,
|
||||
"Failed for case: {} - path: {}",
|
||||
description,
|
||||
path
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_needs_confirmation_global_config(cx: &mut TestAppContext) {
|
||||
// Set up a custom data directory for testing
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
init_test_with_config(cx, temp_dir.path());
|
||||
|
||||
let tool = Arc::new(EditFileTool);
|
||||
let fs = project::FakeFs::new(cx.executor());
|
||||
|
||||
// Create test files in the global config directory
|
||||
let global_config_dir = paths::config_dir();
|
||||
fs::create_dir_all(&global_config_dir).unwrap();
|
||||
let global_settings_path = global_config_dir.join("settings.json");
|
||||
fs::write(&global_settings_path, "{}").unwrap();
|
||||
|
||||
fs.insert_tree("/project", json!({})).await;
|
||||
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
|
||||
|
||||
// Test global config paths
|
||||
let test_cases = vec![
|
||||
(
|
||||
global_settings_path.to_str().unwrap().to_string(),
|
||||
true,
|
||||
"Global settings file should require confirmation",
|
||||
),
|
||||
(
|
||||
global_config_dir
|
||||
.join("keymap.json")
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
true,
|
||||
"Global keymap file should require confirmation",
|
||||
),
|
||||
(
|
||||
"project/normal_file.rs".to_string(),
|
||||
false,
|
||||
"Normal project file should not require confirmation",
|
||||
),
|
||||
];
|
||||
|
||||
for (path, should_confirm, description) in test_cases {
|
||||
let input = json!({
|
||||
"display_description": "Edit file",
|
||||
"path": path,
|
||||
"mode": "edit"
|
||||
});
|
||||
cx.update(|cx| {
|
||||
assert_eq!(
|
||||
tool.needs_confirmation(&input, &project, cx),
|
||||
should_confirm,
|
||||
"Failed for case: {}",
|
||||
description
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_needs_confirmation_with_multiple_worktrees(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let tool = Arc::new(EditFileTool);
|
||||
let fs = project::FakeFs::new(cx.executor());
|
||||
|
||||
// Create multiple worktree directories
|
||||
fs.insert_tree(
|
||||
"/workspace/frontend",
|
||||
json!({
|
||||
"src": {
|
||||
"main.js": "console.log('frontend');"
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
fs.insert_tree(
|
||||
"/workspace/backend",
|
||||
json!({
|
||||
"src": {
|
||||
"main.rs": "fn main() {}"
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
fs.insert_tree(
|
||||
"/workspace/shared",
|
||||
json!({
|
||||
".zed": {
|
||||
"settings.json": "{}"
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Create project with multiple worktrees
|
||||
let project = Project::test(
|
||||
fs.clone(),
|
||||
[
|
||||
path!("/workspace/frontend").as_ref(),
|
||||
path!("/workspace/backend").as_ref(),
|
||||
path!("/workspace/shared").as_ref(),
|
||||
],
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Test files in different worktrees
|
||||
let test_cases = vec![
|
||||
("frontend/src/main.js", false, "File in first worktree"),
|
||||
("backend/src/main.rs", false, "File in second worktree"),
|
||||
(
|
||||
"shared/.zed/settings.json",
|
||||
true,
|
||||
".zed file in third worktree",
|
||||
),
|
||||
("/etc/hosts", true, "Absolute path outside all worktrees"),
|
||||
(
|
||||
"../outside/file.txt",
|
||||
true,
|
||||
"Relative path outside worktrees",
|
||||
),
|
||||
];
|
||||
|
||||
for (path, should_confirm, description) in test_cases {
|
||||
let input = json!({
|
||||
"display_description": "Edit file",
|
||||
"path": path,
|
||||
"mode": "edit"
|
||||
});
|
||||
cx.update(|cx| {
|
||||
assert_eq!(
|
||||
tool.needs_confirmation(&input, &project, cx),
|
||||
should_confirm,
|
||||
"Failed for case: {} - path: {}",
|
||||
description,
|
||||
path
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_needs_confirmation_edge_cases(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let tool = Arc::new(EditFileTool);
|
||||
let fs = project::FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/project",
|
||||
json!({
|
||||
".zed": {
|
||||
"settings.json": "{}"
|
||||
},
|
||||
"src": {
|
||||
".zed": {
|
||||
"local.json": "{}"
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
|
||||
|
||||
// Test edge cases
|
||||
let test_cases = vec![
|
||||
// Empty path - find_project_path returns Some for empty paths
|
||||
("", false, "Empty path is treated as project root"),
|
||||
// Root directory
|
||||
("/", true, "Root directory should be outside project"),
|
||||
// Parent directory references - find_project_path resolves these
|
||||
(
|
||||
"project/../other",
|
||||
false,
|
||||
"Path with .. is resolved by find_project_path",
|
||||
),
|
||||
(
|
||||
"project/./src/file.rs",
|
||||
false,
|
||||
"Path with . should work normally",
|
||||
),
|
||||
// Windows-style paths (if on Windows)
|
||||
#[cfg(target_os = "windows")]
|
||||
("C:\\Windows\\System32\\hosts", true, "Windows system path"),
|
||||
#[cfg(target_os = "windows")]
|
||||
("project\\src\\main.rs", false, "Windows-style project path"),
|
||||
];
|
||||
|
||||
for (path, should_confirm, description) in test_cases {
|
||||
let input = json!({
|
||||
"display_description": "Edit file",
|
||||
"path": path,
|
||||
"mode": "edit"
|
||||
});
|
||||
cx.update(|cx| {
|
||||
assert_eq!(
|
||||
tool.needs_confirmation(&input, &project, cx),
|
||||
should_confirm,
|
||||
"Failed for case: {} - path: {}",
|
||||
description,
|
||||
path
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_ui_text_with_all_path_types(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let tool = Arc::new(EditFileTool);
|
||||
|
||||
// Test UI text for various scenarios
|
||||
let test_cases = vec![
|
||||
(
|
||||
json!({
|
||||
"display_description": "Update config",
|
||||
"path": ".zed/settings.json",
|
||||
"mode": "edit"
|
||||
}),
|
||||
"Update config (local settings)",
|
||||
".zed path should show local settings context",
|
||||
),
|
||||
(
|
||||
json!({
|
||||
"display_description": "Fix bug",
|
||||
"path": "src/.zed/local.json",
|
||||
"mode": "edit"
|
||||
}),
|
||||
"Fix bug (local settings)",
|
||||
"Nested .zed path should show local settings context",
|
||||
),
|
||||
(
|
||||
json!({
|
||||
"display_description": "Update readme",
|
||||
"path": "README.md",
|
||||
"mode": "edit"
|
||||
}),
|
||||
"Update readme",
|
||||
"Normal path should not show additional context",
|
||||
),
|
||||
(
|
||||
json!({
|
||||
"display_description": "Edit config",
|
||||
"path": "config.zed",
|
||||
"mode": "edit"
|
||||
}),
|
||||
"Edit config",
|
||||
".zed as extension should not show context",
|
||||
),
|
||||
];
|
||||
|
||||
for (input, expected_text, description) in test_cases {
|
||||
cx.update(|_cx| {
|
||||
let ui_text = tool.ui_text(&input);
|
||||
assert_eq!(ui_text, expected_text, "Failed for case: {}", description);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_needs_confirmation_with_different_modes(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let tool = Arc::new(EditFileTool);
|
||||
let fs = project::FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/project",
|
||||
json!({
|
||||
"existing.txt": "content",
|
||||
".zed": {
|
||||
"settings.json": "{}"
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
|
||||
|
||||
// Test different EditFileMode values
|
||||
let modes = vec![
|
||||
EditFileMode::Edit,
|
||||
EditFileMode::Create,
|
||||
EditFileMode::Overwrite,
|
||||
];
|
||||
|
||||
for mode in modes {
|
||||
// Test .zed path with different modes
|
||||
let input_zed = json!({
|
||||
"display_description": "Edit settings",
|
||||
"path": "project/.zed/settings.json",
|
||||
"mode": mode
|
||||
});
|
||||
cx.update(|cx| {
|
||||
assert!(
|
||||
tool.needs_confirmation(&input_zed, &project, cx),
|
||||
".zed path should require confirmation regardless of mode: {:?}",
|
||||
mode
|
||||
);
|
||||
});
|
||||
|
||||
// Test outside path with different modes
|
||||
let input_outside = json!({
|
||||
"display_description": "Edit file",
|
||||
"path": "/outside/file.txt",
|
||||
"mode": mode
|
||||
});
|
||||
cx.update(|cx| {
|
||||
assert!(
|
||||
tool.needs_confirmation(&input_outside, &project, cx),
|
||||
"Outside path should require confirmation regardless of mode: {:?}",
|
||||
mode
|
||||
);
|
||||
});
|
||||
|
||||
// Test normal path with different modes
|
||||
let input_normal = json!({
|
||||
"display_description": "Edit file",
|
||||
"path": "project/normal.txt",
|
||||
"mode": mode
|
||||
});
|
||||
cx.update(|cx| {
|
||||
assert!(
|
||||
!tool.needs_confirmation(&input_normal, &project, cx),
|
||||
"Normal path should not require confirmation regardless of mode: {:?}",
|
||||
mode
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_always_allow_tool_actions_bypasses_all_checks(cx: &mut TestAppContext) {
|
||||
// Set up with custom directories for deterministic testing
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
init_test_with_config(cx, temp_dir.path());
|
||||
|
||||
let tool = Arc::new(EditFileTool);
|
||||
let fs = project::FakeFs::new(cx.executor());
|
||||
fs.insert_tree("/project", json!({})).await;
|
||||
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
|
||||
|
||||
// Enable always_allow_tool_actions
|
||||
cx.update(|cx| {
|
||||
let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
|
||||
settings.always_allow_tool_actions = true;
|
||||
agent_settings::AgentSettings::override_global(settings, cx);
|
||||
});
|
||||
|
||||
// Test that all paths that normally require confirmation are bypassed
|
||||
let global_settings_path = paths::config_dir().join("settings.json");
|
||||
fs::create_dir_all(paths::config_dir()).unwrap();
|
||||
fs::write(&global_settings_path, "{}").unwrap();
|
||||
|
||||
let test_cases = vec![
|
||||
".zed/settings.json",
|
||||
"project/.zed/config.toml",
|
||||
global_settings_path.to_str().unwrap(),
|
||||
"/etc/hosts",
|
||||
"/absolute/path/file.txt",
|
||||
"../outside/project.txt",
|
||||
];
|
||||
|
||||
for path in test_cases {
|
||||
let input = json!({
|
||||
"display_description": "Edit file",
|
||||
"path": path,
|
||||
"mode": "edit"
|
||||
});
|
||||
cx.update(|cx| {
|
||||
assert!(
|
||||
!tool.needs_confirmation(&input, &project, cx),
|
||||
"Path {} should not require confirmation when always_allow_tool_actions is true",
|
||||
path
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Disable always_allow_tool_actions and verify confirmation is required again
|
||||
cx.update(|cx| {
|
||||
let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
|
||||
settings.always_allow_tool_actions = false;
|
||||
agent_settings::AgentSettings::override_global(settings, cx);
|
||||
});
|
||||
|
||||
// Verify .zed path requires confirmation again
|
||||
let input = json!({
|
||||
"display_description": "Edit file",
|
||||
"path": ".zed/settings.json",
|
||||
"mode": "edit"
|
||||
});
|
||||
cx.update(|cx| {
|
||||
assert!(
|
||||
tool.needs_confirmation(&input, &project, cx),
|
||||
".zed path should require confirmation when always_allow_tool_actions is false"
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ impl Tool for FetchTool {
|
||||
"fetch".to_string()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ impl Tool for FindPathTool {
|
||||
"find_path".into()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ impl Tool for GrepTool {
|
||||
"grep".into()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ impl Tool for ListDirectoryTool {
|
||||
"list_directory".into()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ impl Tool for MovePathTool {
|
||||
"move_path".into()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ impl Tool for NowTool {
|
||||
"now".into()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ impl Tool for OpenTool {
|
||||
"open".to_string()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
|
||||
true
|
||||
}
|
||||
fn may_perform_edits(&self) -> bool {
|
||||
|
||||
@@ -19,7 +19,7 @@ impl Tool for ProjectNotificationsTool {
|
||||
"project_notifications".to_string()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
|
||||
false
|
||||
}
|
||||
fn may_perform_edits(&self) -> bool {
|
||||
@@ -200,7 +200,7 @@ mod tests {
|
||||
|
||||
// Run the tool before any changes
|
||||
let tool = Arc::new(ProjectNotificationsTool);
|
||||
let provider = Arc::new(FakeLanguageModelProvider);
|
||||
let provider = Arc::new(FakeLanguageModelProvider::default());
|
||||
let model: Arc<dyn LanguageModel> = Arc::new(provider.test_model());
|
||||
let request = Arc::new(LanguageModelRequest::default());
|
||||
let tool_input = json!({});
|
||||
|
||||
@@ -54,7 +54,7 @@ impl Tool for ReadFileTool {
|
||||
"read_file".into()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ impl Tool for TerminalTool {
|
||||
Self::NAME.to_string()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ impl Tool for ThinkingTool {
|
||||
"thinking".to_string()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::{
|
||||
ActionLog, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus,
|
||||
};
|
||||
use cloud_llm_client::{WebSearchResponse, WebSearchResult};
|
||||
use futures::{Future, FutureExt, TryFutureExt};
|
||||
use gpui::{
|
||||
AnyWindowHandle, App, AppContext, Context, Entity, IntoElement, Task, WeakEntity, Window,
|
||||
@@ -17,7 +18,6 @@ use serde::{Deserialize, Serialize};
|
||||
use ui::{IconName, Tooltip, prelude::*};
|
||||
use web_search::WebSearchRegistry;
|
||||
use workspace::Workspace;
|
||||
use zed_llm_client::{WebSearchResponse, WebSearchResult};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct WebSearchToolInput {
|
||||
@@ -32,7 +32,7 @@ impl Tool for WebSearchTool {
|
||||
"web_search".into()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,6 @@ collections.workspace = true
|
||||
derive_more.workspace = true
|
||||
gpui.workspace = true
|
||||
parking_lot.workspace = true
|
||||
rodio = { version = "0.20.0", default-features = false, features = ["wav"] }
|
||||
rodio = { version = "0.21.1", default-features = false, features = ["wav", "playback", "tracing"] }
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
@@ -3,12 +3,9 @@ use std::{io::Cursor, sync::Arc};
|
||||
use anyhow::{Context as _, Result};
|
||||
use collections::HashMap;
|
||||
use gpui::{App, AssetSource, Global};
|
||||
use rodio::{
|
||||
Decoder, Source,
|
||||
source::{Buffered, SamplesConverter},
|
||||
};
|
||||
use rodio::{Decoder, Source, source::Buffered};
|
||||
|
||||
type Sound = Buffered<SamplesConverter<Decoder<Cursor<Vec<u8>>>, f32>>;
|
||||
type Sound = Buffered<Decoder<Cursor<Vec<u8>>>>;
|
||||
|
||||
pub struct SoundRegistry {
|
||||
cache: Arc<parking_lot::Mutex<HashMap<String, Sound>>>,
|
||||
@@ -48,7 +45,7 @@ impl SoundRegistry {
|
||||
.with_context(|| format!("No asset available for path {path}"))??
|
||||
.into_owned();
|
||||
let cursor = Cursor::new(bytes);
|
||||
let source = Decoder::new(cursor)?.convert_samples::<f32>().buffered();
|
||||
let source = Decoder::new(cursor)?.buffered();
|
||||
|
||||
self.cache.lock().insert(name.to_string(), source.clone());
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use assets::SoundRegistry;
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use gpui::{App, AssetSource, BorrowAppContext, Global};
|
||||
use rodio::{OutputStream, OutputStreamHandle};
|
||||
use rodio::{OutputStream, OutputStreamBuilder};
|
||||
use util::ResultExt;
|
||||
|
||||
mod assets;
|
||||
@@ -37,8 +37,7 @@ impl Sound {
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Audio {
|
||||
_output_stream: Option<OutputStream>,
|
||||
output_handle: Option<OutputStreamHandle>,
|
||||
output_handle: Option<OutputStream>,
|
||||
}
|
||||
|
||||
#[derive(Deref, DerefMut)]
|
||||
@@ -51,11 +50,9 @@ impl Audio {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn ensure_output_exists(&mut self) -> Option<&OutputStreamHandle> {
|
||||
fn ensure_output_exists(&mut self) -> Option<&OutputStream> {
|
||||
if self.output_handle.is_none() {
|
||||
let (_output_stream, output_handle) = OutputStream::try_default().log_err().unzip();
|
||||
self.output_handle = output_handle;
|
||||
self._output_stream = _output_stream;
|
||||
self.output_handle = OutputStreamBuilder::open_default_stream().log_err();
|
||||
}
|
||||
|
||||
self.output_handle.as_ref()
|
||||
@@ -69,7 +66,7 @@ impl Audio {
|
||||
cx.update_global::<GlobalAudio, _>(|this, cx| {
|
||||
let output_handle = this.ensure_output_exists()?;
|
||||
let source = SoundRegistry::global(cx).get(sound.file()).log_err()?;
|
||||
output_handle.play_raw(source).log_err()?;
|
||||
output_handle.mixer().add(source);
|
||||
Some(())
|
||||
});
|
||||
}
|
||||
@@ -80,7 +77,6 @@ impl Audio {
|
||||
}
|
||||
|
||||
cx.update_global::<GlobalAudio, _>(|this, _| {
|
||||
this._output_stream.take();
|
||||
this.output_handle.take();
|
||||
});
|
||||
}
|
||||
|
||||