Merge branch 'main' into windows/dx11
2
.github/workflows/ci.yml
vendored
|
@ -271,7 +271,7 @@ jobs:
|
|||
|
||||
- name: Check that Cargo.lock is up to date
|
||||
run: |
|
||||
cargo update --frozen --workspace
|
||||
cargo update --locked --workspace
|
||||
|
||||
- name: cargo clippy
|
||||
run: ./script/clippy
|
||||
|
|
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
|
||||
|
|
8
Cargo.lock
generated
|
@ -138,9 +138,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "agent-client-protocol"
|
||||
version = "0.0.10"
|
||||
version = "0.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fb7f39671e02f8a1aeb625652feae40b6fc2597baaa97e028a98863477aecbd"
|
||||
checksum = "72ec54650c1fc2d63498bab47eeeaa9eddc7d239d53f615b797a0e84f7ccc87b"
|
||||
dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
|
@ -168,6 +168,7 @@ dependencies = [
|
|||
"nix 0.29.0",
|
||||
"paths",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
@ -4257,7 +4258,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",
|
||||
|
@ -11027,6 +11028,7 @@ dependencies = [
|
|||
"ui",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"zed_actions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -413,7 +413,7 @@ zlog_settings = { path = "crates/zlog_settings" }
|
|||
#
|
||||
|
||||
agentic-coding-protocol = "0.0.10"
|
||||
agent-client-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"
|
||||
|
@ -460,7 +460,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"
|
||||
|
|
|
@ -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 |
|
@ -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 |
|
@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -166,6 +166,7 @@ pub struct ToolCall {
|
|||
pub content: Vec<ToolCallContent>,
|
||||
pub status: ToolCallStatus,
|
||||
pub locations: Vec<acp::ToolCallLocation>,
|
||||
pub raw_input: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl ToolCall {
|
||||
|
@ -193,6 +194,50 @@ impl ToolCall {
|
|||
.collect(),
|
||||
locations: tool_call.locations,
|
||||
status,
|
||||
raw_input: tool_call.raw_input,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
fields: acp::ToolCallUpdateFields,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let acp::ToolCallUpdateFields {
|
||||
kind,
|
||||
status,
|
||||
label,
|
||||
content,
|
||||
locations,
|
||||
raw_input,
|
||||
} = fields;
|
||||
|
||||
if let Some(kind) = kind {
|
||||
self.kind = kind;
|
||||
}
|
||||
|
||||
if let Some(status) = status {
|
||||
self.status = ToolCallStatus::Allowed { status };
|
||||
}
|
||||
|
||||
if let Some(label) = label {
|
||||
self.label = cx.new(|cx| Markdown::new_text(label.into(), cx));
|
||||
}
|
||||
|
||||
if let Some(content) = content {
|
||||
self.content = content
|
||||
.into_iter()
|
||||
.map(|chunk| ToolCallContent::from_acp(chunk, language_registry.clone(), cx))
|
||||
.collect();
|
||||
}
|
||||
|
||||
if let Some(locations) = locations {
|
||||
self.locations = locations;
|
||||
}
|
||||
|
||||
if let Some(raw_input) = raw_input {
|
||||
self.raw_input = Some(raw_input);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -238,6 +283,7 @@ impl Display for ToolCallStatus {
|
|||
match self {
|
||||
ToolCallStatus::WaitingForConfirmation { .. } => "Waiting for confirmation",
|
||||
ToolCallStatus::Allowed { status } => match status {
|
||||
acp::ToolCallStatus::Pending => "Pending",
|
||||
acp::ToolCallStatus::InProgress => "In Progress",
|
||||
acp::ToolCallStatus::Completed => "Completed",
|
||||
acp::ToolCallStatus::Failed => "Failed",
|
||||
|
@ -345,7 +391,7 @@ impl ToolCallContent {
|
|||
cx: &mut App,
|
||||
) -> Self {
|
||||
match content {
|
||||
acp::ToolCallContent::ContentBlock { content } => Self::ContentBlock {
|
||||
acp::ToolCallContent::ContentBlock(content) => Self::ContentBlock {
|
||||
content: ContentBlock::new(content, &language_registry, cx),
|
||||
},
|
||||
acp::ToolCallContent::Diff { diff } => Self::Diff {
|
||||
|
@ -630,12 +676,50 @@ impl AcpThread {
|
|||
false
|
||||
}
|
||||
|
||||
pub fn push_entry(&mut self, entry: AgentThreadEntry, cx: &mut Context<Self>) {
|
||||
self.entries.push(entry);
|
||||
cx.emit(AcpThreadEvent::NewEntry);
|
||||
pub fn handle_session_update(
|
||||
&mut self,
|
||||
update: acp::SessionUpdate,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Result<()> {
|
||||
match update {
|
||||
acp::SessionUpdate::UserMessage(content_block) => {
|
||||
self.push_user_content_block(content_block, cx);
|
||||
}
|
||||
acp::SessionUpdate::AgentMessageChunk(content_block) => {
|
||||
self.push_assistant_content_block(content_block, false, cx);
|
||||
}
|
||||
acp::SessionUpdate::AgentThoughtChunk(content_block) => {
|
||||
self.push_assistant_content_block(content_block, true, cx);
|
||||
}
|
||||
acp::SessionUpdate::ToolCall(tool_call) => {
|
||||
self.upsert_tool_call(tool_call, cx);
|
||||
}
|
||||
acp::SessionUpdate::ToolCallUpdate(tool_call_update) => {
|
||||
self.update_tool_call(tool_call_update, cx)?;
|
||||
}
|
||||
acp::SessionUpdate::Plan(plan) => {
|
||||
self.update_plan(plan, cx);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn push_assistant_chunk(
|
||||
pub fn push_user_content_block(&mut self, chunk: acp::ContentBlock, cx: &mut Context<Self>) {
|
||||
let language_registry = self.project.read(cx).languages().clone();
|
||||
let entries_len = self.entries.len();
|
||||
|
||||
if let Some(last_entry) = self.entries.last_mut()
|
||||
&& let AgentThreadEntry::UserMessage(UserMessage { content }) = last_entry
|
||||
{
|
||||
content.append(chunk, &language_registry, cx);
|
||||
cx.emit(AcpThreadEvent::EntryUpdated(entries_len - 1));
|
||||
} else {
|
||||
let content = ContentBlock::new(chunk, &language_registry, cx);
|
||||
self.push_entry(AgentThreadEntry::UserMessage(UserMessage { content }), cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_assistant_content_block(
|
||||
&mut self,
|
||||
chunk: acp::ContentBlock,
|
||||
is_thought: bool,
|
||||
|
@ -678,23 +762,22 @@ impl AcpThread {
|
|||
}
|
||||
}
|
||||
|
||||
fn push_entry(&mut self, entry: AgentThreadEntry, cx: &mut Context<Self>) {
|
||||
self.entries.push(entry);
|
||||
cx.emit(AcpThreadEvent::NewEntry);
|
||||
}
|
||||
|
||||
pub fn update_tool_call(
|
||||
&mut self,
|
||||
id: acp::ToolCallId,
|
||||
status: acp::ToolCallStatus,
|
||||
content: Option<Vec<acp::ToolCallContent>>,
|
||||
update: acp::ToolCallUpdate,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Result<()> {
|
||||
let languages = self.project.read(cx).languages().clone();
|
||||
let (ix, current_call) = self.tool_call_mut(&id).context("Tool call not found")?;
|
||||
|
||||
if let Some(content) = content {
|
||||
current_call.content = content
|
||||
.into_iter()
|
||||
.map(|chunk| ToolCallContent::from_acp(chunk, languages.clone(), cx))
|
||||
.collect();
|
||||
}
|
||||
current_call.status = ToolCallStatus::Allowed { status };
|
||||
let (ix, current_call) = self
|
||||
.tool_call_mut(&update.id)
|
||||
.context("Tool call not found")?;
|
||||
current_call.update(update.fields, languages, cx);
|
||||
|
||||
cx.emit(AcpThreadEvent::EntryUpdated(ix));
|
||||
|
||||
|
@ -751,6 +834,37 @@ impl AcpThread {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn set_project_location(&self, location: acp::ToolCallLocation, cx: &mut Context<Self>) {
|
||||
self.project.update(cx, |project, cx| {
|
||||
let Some(path) = project.project_path_for_absolute_path(&location.path, cx) else {
|
||||
return;
|
||||
};
|
||||
let buffer = project.open_buffer(path, cx);
|
||||
cx.spawn(async move |project, cx| {
|
||||
let buffer = buffer.await?;
|
||||
|
||||
project.update(cx, |project, cx| {
|
||||
let position = if let Some(line) = location.line {
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let point = snapshot.clip_point(Point::new(line, 0), Bias::Left);
|
||||
snapshot.anchor_before(point)
|
||||
} else {
|
||||
Anchor::MIN
|
||||
};
|
||||
|
||||
project.set_agent_location(
|
||||
Some(AgentLocation {
|
||||
buffer: buffer.downgrade(),
|
||||
position,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn request_tool_call_permission(
|
||||
&mut self,
|
||||
tool_call: acp::ToolCall,
|
||||
|
@ -801,6 +915,25 @@ impl AcpThread {
|
|||
cx.emit(AcpThreadEvent::EntryUpdated(ix));
|
||||
}
|
||||
|
||||
/// Returns true if the last turn is awaiting tool authorization
|
||||
pub fn waiting_for_tool_confirmation(&self) -> bool {
|
||||
for entry in self.entries.iter().rev() {
|
||||
match &entry {
|
||||
AgentThreadEntry::ToolCall(call) => match call.status {
|
||||
ToolCallStatus::WaitingForConfirmation { .. } => return true,
|
||||
ToolCallStatus::Allowed { .. }
|
||||
| ToolCallStatus::Rejected
|
||||
| ToolCallStatus::Canceled => continue,
|
||||
},
|
||||
AgentThreadEntry::UserMessage(_) | AgentThreadEntry::AssistantMessage(_) => {
|
||||
// Reached the beginning of the turn
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn plan(&self) -> &Plan {
|
||||
&self.plan
|
||||
}
|
||||
|
@ -824,56 +957,6 @@ impl AcpThread {
|
|||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_project_location(&self, location: acp::ToolCallLocation, cx: &mut Context<Self>) {
|
||||
self.project.update(cx, |project, cx| {
|
||||
let Some(path) = project.project_path_for_absolute_path(&location.path, cx) else {
|
||||
return;
|
||||
};
|
||||
let buffer = project.open_buffer(path, cx);
|
||||
cx.spawn(async move |project, cx| {
|
||||
let buffer = buffer.await?;
|
||||
|
||||
project.update(cx, |project, cx| {
|
||||
let position = if let Some(line) = location.line {
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let point = snapshot.clip_point(Point::new(line, 0), Bias::Left);
|
||||
snapshot.anchor_before(point)
|
||||
} else {
|
||||
Anchor::MIN
|
||||
};
|
||||
|
||||
project.set_agent_location(
|
||||
Some(AgentLocation {
|
||||
buffer: buffer.downgrade(),
|
||||
position,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns true if the last turn is awaiting tool authorization
|
||||
pub fn waiting_for_tool_confirmation(&self) -> bool {
|
||||
for entry in self.entries.iter().rev() {
|
||||
match &entry {
|
||||
AgentThreadEntry::ToolCall(call) => match call.status {
|
||||
ToolCallStatus::WaitingForConfirmation { .. } => return true,
|
||||
ToolCallStatus::Allowed { .. }
|
||||
| ToolCallStatus::Rejected
|
||||
| ToolCallStatus::Canceled => continue,
|
||||
},
|
||||
AgentThreadEntry::UserMessage(_) | AgentThreadEntry::AssistantMessage(_) => {
|
||||
// Reached the beginning of the turn
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn authenticate(&self, cx: &mut App) -> impl use<> + Future<Output = Result<()>> {
|
||||
self.connection.authenticate(cx)
|
||||
}
|
||||
|
@ -919,7 +1002,7 @@ impl AcpThread {
|
|||
let result = this
|
||||
.update(cx, |this, cx| {
|
||||
this.connection.prompt(
|
||||
acp::PromptToolArguments {
|
||||
acp::PromptArguments {
|
||||
prompt: message,
|
||||
session_id: this.session_id.clone(),
|
||||
},
|
||||
|
@ -1148,7 +1231,87 @@ mod tests {
|
|||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_thinking_concatenation(cx: &mut TestAppContext) {
|
||||
async fn test_push_user_content_block(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs, [], cx).await;
|
||||
let (thread, _fake_server) = fake_acp_thread(project, cx);
|
||||
|
||||
// Test creating a new user message
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.push_user_content_block(
|
||||
acp::ContentBlock::Text(acp::TextContent {
|
||||
annotations: None,
|
||||
text: "Hello, ".to_string(),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
thread.update(cx, |thread, cx| {
|
||||
assert_eq!(thread.entries.len(), 1);
|
||||
if let AgentThreadEntry::UserMessage(user_msg) = &thread.entries[0] {
|
||||
assert_eq!(user_msg.content.to_markdown(cx), "Hello, ");
|
||||
} else {
|
||||
panic!("Expected UserMessage");
|
||||
}
|
||||
});
|
||||
|
||||
// Test appending to existing user message
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.push_user_content_block(
|
||||
acp::ContentBlock::Text(acp::TextContent {
|
||||
annotations: None,
|
||||
text: "world!".to_string(),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
thread.update(cx, |thread, cx| {
|
||||
assert_eq!(thread.entries.len(), 1);
|
||||
if let AgentThreadEntry::UserMessage(user_msg) = &thread.entries[0] {
|
||||
assert_eq!(user_msg.content.to_markdown(cx), "Hello, world!");
|
||||
} else {
|
||||
panic!("Expected UserMessage");
|
||||
}
|
||||
});
|
||||
|
||||
// Test creating new user message after assistant message
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.push_assistant_content_block(
|
||||
acp::ContentBlock::Text(acp::TextContent {
|
||||
annotations: None,
|
||||
text: "Assistant response".to_string(),
|
||||
}),
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.push_user_content_block(
|
||||
acp::ContentBlock::Text(acp::TextContent {
|
||||
annotations: None,
|
||||
text: "New user message".to_string(),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
thread.update(cx, |thread, cx| {
|
||||
assert_eq!(thread.entries.len(), 3);
|
||||
if let AgentThreadEntry::UserMessage(user_msg) = &thread.entries[2] {
|
||||
assert_eq!(user_msg.content.to_markdown(cx), "New user message");
|
||||
} else {
|
||||
panic!("Expected UserMessage at index 2");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_thinking_concatenation(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
|
|
|
@ -20,7 +20,7 @@ pub trait AgentConnection {
|
|||
|
||||
fn authenticate(&self, cx: &mut App) -> Task<Result<()>>;
|
||||
|
||||
fn prompt(&self, params: acp::PromptToolArguments, 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);
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ use project::Project;
|
|||
use std::{cell::RefCell, error::Error, fmt, path::Path, rc::Rc};
|
||||
use ui::App;
|
||||
|
||||
use crate::{AcpThread, AcpThreadEvent, AgentConnection, ToolCallContent, ToolCallStatus};
|
||||
use crate::{AcpThread, AgentConnection};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct OldAcpClientDelegate {
|
||||
|
@ -40,10 +40,10 @@ impl acp_old::Client for OldAcpClientDelegate {
|
|||
.borrow()
|
||||
.update(cx, |thread, cx| match params.chunk {
|
||||
acp_old::AssistantMessageChunk::Text { text } => {
|
||||
thread.push_assistant_chunk(text.into(), false, cx)
|
||||
thread.push_assistant_content_block(text.into(), false, cx)
|
||||
}
|
||||
acp_old::AssistantMessageChunk::Thought { thought } => {
|
||||
thread.push_assistant_chunk(thought.into(), true, cx)
|
||||
thread.push_assistant_content_block(thought.into(), true, cx)
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
|
@ -182,31 +182,23 @@ impl acp_old::Client for OldAcpClientDelegate {
|
|||
|
||||
cx.update(|cx| {
|
||||
self.thread.borrow().update(cx, |thread, cx| {
|
||||
let languages = thread.project.read(cx).languages().clone();
|
||||
|
||||
if let Some((ix, tool_call)) = thread
|
||||
.tool_call_mut(&acp::ToolCallId(request.tool_call_id.0.to_string().into()))
|
||||
{
|
||||
tool_call.status = ToolCallStatus::Allowed {
|
||||
status: into_new_tool_call_status(request.status),
|
||||
};
|
||||
tool_call.content = request
|
||||
.content
|
||||
.into_iter()
|
||||
.map(|content| {
|
||||
ToolCallContent::from_acp(
|
||||
into_new_tool_call_content(content),
|
||||
languages.clone(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
cx.emit(AcpThreadEvent::EntryUpdated(ix));
|
||||
anyhow::Ok(())
|
||||
} else {
|
||||
anyhow::bail!("Tool call not found")
|
||||
}
|
||||
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")??;
|
||||
|
@ -285,6 +277,7 @@ fn into_new_tool_call(id: acp::ToolCallId, request: acp_old::PushToolCallParams)
|
|||
.into_iter()
|
||||
.map(into_new_tool_call_location)
|
||||
.collect(),
|
||||
raw_input: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -311,12 +304,7 @@ fn into_new_tool_call_status(status: acp_old::ToolCallStatus) -> acp::ToolCallSt
|
|||
|
||||
fn into_new_tool_call_content(content: acp_old::ToolCallContent) -> acp::ToolCallContent {
|
||||
match content {
|
||||
acp_old::ToolCallContent::Markdown { markdown } => acp::ToolCallContent::ContentBlock {
|
||||
content: acp::ContentBlock::Text(acp::TextContent {
|
||||
annotations: None,
|
||||
text: markdown,
|
||||
}),
|
||||
},
|
||||
acp_old::ToolCallContent::Markdown { markdown } => markdown.into(),
|
||||
acp_old::ToolCallContent::Diff { diff } => acp::ToolCallContent::Diff {
|
||||
diff: into_new_diff(diff),
|
||||
},
|
||||
|
@ -423,7 +411,7 @@ impl AgentConnection for OldAcpAgentConnection {
|
|||
})
|
||||
}
|
||||
|
||||
fn prompt(&self, params: acp::PromptToolArguments, cx: &mut App) -> Task<Result<()>> {
|
||||
fn prompt(&self, params: acp::PromptArguments, cx: &mut App) -> Task<Result<()>> {
|
||||
let chunks = params
|
||||
.prompt
|
||||
.into_iter()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
@ -2557,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(
|
||||
|
|
|
@ -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)
|
||||
};
|
||||
|
|
|
@ -29,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
|
||||
|
@ -40,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,11 +1,14 @@
|
|||
mod claude;
|
||||
mod codex;
|
||||
mod gemini;
|
||||
mod mcp_server;
|
||||
mod settings;
|
||||
|
||||
#[cfg(test)]
|
||||
mod e2e_tests;
|
||||
|
||||
pub use claude::*;
|
||||
pub use codex::*;
|
||||
pub use gemini::*;
|
||||
pub use settings::*;
|
||||
|
||||
|
|
|
@ -9,7 +9,6 @@ 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;
|
||||
|
||||
|
@ -45,7 +44,7 @@ impl AgentServer for ClaudeCode {
|
|||
}
|
||||
|
||||
fn empty_state_message(&self) -> &'static str {
|
||||
""
|
||||
"How can I help you today?"
|
||||
}
|
||||
|
||||
fn logo(&self) -> ui::IconName {
|
||||
|
@ -66,19 +65,6 @@ impl AgentServer for ClaudeCode {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn send_interrupt(pid: libc::pid_t) -> anyhow::Result<()> {
|
||||
let pid = nix::unistd::Pid::from_raw(pid);
|
||||
|
||||
nix::sys::signal::kill(pid, nix::sys::signal::SIGINT)
|
||||
.map_err(|e| anyhow!("Failed to interrupt process: {}", e))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn send_interrupt(_pid: i32) -> anyhow::Result<()> {
|
||||
panic!("Cancel not implemented on Windows")
|
||||
}
|
||||
|
||||
struct ClaudeAgentConnection {
|
||||
sessions: Rc<RefCell<HashMap<acp::SessionId, ClaudeAgentSession>>>,
|
||||
}
|
||||
|
@ -127,7 +113,6 @@ impl AgentConnection for ClaudeAgentConnection {
|
|||
|
||||
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 = acp::SessionId(Uuid::new_v4().to_string().into());
|
||||
|
||||
|
@ -137,50 +122,28 @@ impl AgentConnection for ClaudeAgentConnection {
|
|||
let session_id = session_id.clone();
|
||||
async move {
|
||||
let mut outgoing_rx = Some(outgoing_rx);
|
||||
let mut mode = ClaudeSessionMode::Start;
|
||||
|
||||
loop {
|
||||
let mut child = spawn_claude(
|
||||
&command,
|
||||
mode,
|
||||
session_id.clone(),
|
||||
&mcp_config_path,
|
||||
&cwd,
|
||||
)
|
||||
.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 pid = child.id();
|
||||
log::trace!("Spawned (pid: {})", pid);
|
||||
|
||||
let mut io_fut = pin!(
|
||||
ClaudeAgentSession::handle_io(
|
||||
outgoing_rx.take().unwrap(),
|
||||
incoming_message_tx.clone(),
|
||||
child.stdin.take().unwrap(),
|
||||
child.stdout.take().unwrap(),
|
||||
)
|
||||
.fuse()
|
||||
);
|
||||
ClaudeAgentSession::handle_io(
|
||||
outgoing_rx.take().unwrap(),
|
||||
incoming_message_tx.clone(),
|
||||
child.stdin.take().unwrap(),
|
||||
child.stdout.take().unwrap(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
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?;
|
||||
}
|
||||
}
|
||||
|
||||
log::trace!("Stopped (pid: {})", pid);
|
||||
break;
|
||||
}
|
||||
log::trace!("Stopped (pid: {})", pid);
|
||||
|
||||
drop(mcp_config_path);
|
||||
anyhow::Ok(())
|
||||
|
@ -213,7 +176,6 @@ impl AgentConnection for ClaudeAgentConnection {
|
|||
let session = ClaudeAgentSession {
|
||||
outgoing_tx,
|
||||
end_turn_tx,
|
||||
cancel_tx,
|
||||
_handler_task: handler_task,
|
||||
_mcp_server: Some(permission_mcp_server),
|
||||
};
|
||||
|
@ -228,7 +190,7 @@ impl AgentConnection for ClaudeAgentConnection {
|
|||
Task::ready(Err(anyhow!("Authentication not supported")))
|
||||
}
|
||||
|
||||
fn prompt(&self, params: acp::PromptToolArguments, cx: &mut App) -> Task<Result<()>> {
|
||||
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!(
|
||||
|
@ -278,37 +240,24 @@ impl AgentConnection for ClaudeAgentConnection {
|
|||
})
|
||||
}
|
||||
|
||||
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
|
||||
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;
|
||||
};
|
||||
|
||||
let (done_tx, done_rx) = oneshot::channel();
|
||||
if session
|
||||
.cancel_tx
|
||||
.unbounded_send(done_tx)
|
||||
.log_err()
|
||||
.is_some()
|
||||
{
|
||||
let end_turn_tx = session.end_turn_tx.clone();
|
||||
cx.foreground_executor()
|
||||
.spawn(async move {
|
||||
done_rx.await??;
|
||||
if let Some(end_turn_tx) = end_turn_tx.take() {
|
||||
end_turn_tx.send(Ok(())).ok();
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
session
|
||||
.outgoing_tx
|
||||
.unbounded_send(SdkMessage::new_interrupt_message())
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum ClaudeSessionMode {
|
||||
Start,
|
||||
#[expect(dead_code)]
|
||||
Resume,
|
||||
}
|
||||
|
||||
|
@ -364,7 +313,6 @@ async fn spawn_claude(
|
|||
struct ClaudeAgentSession {
|
||||
outgoing_tx: UnboundedSender<SdkMessage>,
|
||||
end_turn_tx: Rc<RefCell<Option<oneshot::Sender<Result<()>>>>>,
|
||||
cancel_tx: UnboundedSender<oneshot::Sender<Result<()>>>,
|
||||
_mcp_server: Option<ClaudeZedMcpServer>,
|
||||
_handler_task: Task<()>,
|
||||
}
|
||||
|
@ -377,6 +325,8 @@ impl ClaudeAgentSession {
|
|||
cx: &mut AsyncApp,
|
||||
) {
|
||||
match message {
|
||||
// we should only be sending these out, they don't need to be in the thread
|
||||
SdkMessage::ControlRequest { .. } => {}
|
||||
SdkMessage::Assistant {
|
||||
message,
|
||||
session_id: _,
|
||||
|
@ -400,7 +350,7 @@ impl ClaudeAgentSession {
|
|||
ContentChunk::Text { text } | ContentChunk::UntaggedText(text) => {
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.push_assistant_chunk(text.into(), false, cx)
|
||||
thread.push_assistant_content_block(text.into(), false, cx)
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
|
@ -437,9 +387,15 @@ impl ClaudeAgentSession {
|
|||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.update_tool_call(
|
||||
acp::ToolCallId(tool_use_id.into()),
|
||||
acp::ToolCallStatus::Completed,
|
||||
(!content.is_empty()).then(|| vec![content.into()]),
|
||||
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()
|
||||
},
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
|
@ -452,7 +408,7 @@ impl ClaudeAgentSession {
|
|||
| ContentChunk::WebSearchToolResult => {
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.push_assistant_chunk(
|
||||
thread.push_assistant_content_block(
|
||||
format!("Unsupported content: {:?}", chunk).into(),
|
||||
false,
|
||||
cx,
|
||||
|
@ -464,17 +420,25 @@ impl ClaudeAgentSession {
|
|||
}
|
||||
}
|
||||
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 { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -643,14 +607,12 @@ enum SdkMessage {
|
|||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
session_id: Option<String>,
|
||||
},
|
||||
|
||||
// A user message
|
||||
User {
|
||||
message: Message, // from Anthropic SDK
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
session_id: Option<String>,
|
||||
},
|
||||
|
||||
// Emitted as the last message in a conversation
|
||||
Result {
|
||||
subtype: ResultErrorType,
|
||||
|
@ -675,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)]
|
||||
|
@ -695,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,
|
||||
|
@ -715,7 +715,7 @@ 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 {
|
||||
|
|
|
@ -42,9 +42,13 @@ impl ClaudeZedMcpServer {
|
|||
}
|
||||
|
||||
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")?;
|
||||
|
||||
#[cfg(test)]
|
||||
let zed_path = crate::e2e_tests::get_zed_path();
|
||||
|
||||
Ok(McpServerConfig {
|
||||
command: zed_path,
|
||||
args: vec![
|
||||
|
@ -174,6 +178,7 @@ impl McpServerTool for PermissionTool {
|
|||
updated_input: input.input,
|
||||
}
|
||||
} else {
|
||||
debug_assert_eq!(chosen_option, reject_option_id);
|
||||
PermissionToolResponse {
|
||||
behavior: PermissionToolBehavior::Deny,
|
||||
updated_input: input.input,
|
||||
|
|
|
@ -311,6 +311,7 @@ impl ClaudeTool {
|
|||
label: self.label(),
|
||||
content: self.content(),
|
||||
locations: self.locations(),
|
||||
raw_input: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
317
crates/agent_servers/src/codex.rs
Normal file
|
@ -0,0 +1,317 @@
|
|||
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();
|
||||
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,
|
||||
},
|
||||
)
|
||||
.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!["mcp".into()],
|
||||
env: None,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,8 @@
|
|||
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, ToolCallStatus};
|
||||
|
@ -79,21 +83,28 @@ 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) {
|
||||
|
@ -136,6 +147,7 @@ pub async fn test_tool_call(server: impl AgentServer + 'static, cx: &mut TestApp
|
|||
|
||||
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;
|
||||
|
@ -186,7 +198,7 @@ pub async fn test_tool_call_with_confirmation(
|
|||
thread.update(cx, |thread, cx| {
|
||||
thread.authorize_tool_call(
|
||||
tool_call_id,
|
||||
acp::PermissionOptionId("0".into()),
|
||||
allow_option_id,
|
||||
acp::PermissionOptionKind::AllowOnce,
|
||||
cx,
|
||||
);
|
||||
|
@ -294,7 +306,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::*;
|
||||
|
||||
|
@ -319,7 +331,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]
|
||||
|
@ -351,6 +368,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,
|
||||
);
|
||||
|
@ -409,3 +429,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
|
||||
}
|
||||
|
|
|
@ -188,7 +188,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)
|
||||
|
|
|
@ -872,7 +872,10 @@ 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::Pending,
|
||||
}
|
||||
| ToolCallStatus::WaitingForConfirmation { .. } => None,
|
||||
ToolCallStatus::Allowed {
|
||||
status: acp::ToolCallStatus::InProgress,
|
||||
..
|
||||
|
@ -957,6 +960,8 @@ impl AcpThreadView {
|
|||
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,
|
||||
|
@ -1068,6 +1073,7 @@ impl AcpThreadView {
|
|||
options,
|
||||
entry_ix,
|
||||
tool_call.id.clone(),
|
||||
tool_call.content.is_empty(),
|
||||
cx,
|
||||
)),
|
||||
ToolCallStatus::Allowed { .. } | ToolCallStatus::Canceled => {
|
||||
|
@ -1126,6 +1132,7 @@ impl AcpThreadView {
|
|||
options: &[acp::PermissionOption],
|
||||
entry_ix: usize,
|
||||
tool_call_id: acp::ToolCallId,
|
||||
empty_content: bool,
|
||||
cx: &Context<Self>,
|
||||
) -> Div {
|
||||
h_flex()
|
||||
|
@ -1133,8 +1140,10 @@ impl AcpThreadView {
|
|||
.px_1p5()
|
||||
.gap_1()
|
||||
.justify_end()
|
||||
.border_t_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.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())
|
||||
|
|
|
@ -1991,6 +1991,20 @@ impl AgentPanel {
|
|||
);
|
||||
}),
|
||||
)
|
||||
.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
|
||||
}))
|
||||
|
@ -2652,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,
|
||||
)
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}),
|
||||
|
|
|
@ -150,6 +150,7 @@ enum ExternalAgent {
|
|||
#[default]
|
||||
Gemini,
|
||||
ClaudeCode,
|
||||
Codex,
|
||||
}
|
||||
|
||||
impl ExternalAgent {
|
||||
|
@ -157,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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
@ -54,6 +56,7 @@ impl RenderOnce for BulletItem {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub enum SignInStatus {
|
||||
SignedIn,
|
||||
SigningIn,
|
||||
|
|
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(),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
@ -1175,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()));
|
||||
|
@ -1277,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!({
|
||||
|
@ -1384,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);
|
||||
});
|
||||
}
|
||||
|
@ -1392,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;
|
||||
|
@ -1591,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
|
||||
|
@ -1723,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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -106,7 +106,6 @@ pub fn routes(rpc_server: Arc<rpc::Server>) -> Router<(), Body> {
|
|||
.route("/users/:id/refresh_llm_tokens", post(refresh_llm_tokens))
|
||||
.route("/users/:id/update_plan", post(update_plan))
|
||||
.route("/rpc_server_snapshot", get(get_rpc_server_snapshot))
|
||||
.merge(billing::router())
|
||||
.merge(contributors::router())
|
||||
.layer(
|
||||
ServiceBuilder::new()
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
use anyhow::{Context as _, bail};
|
||||
use axum::{Extension, Json, Router, extract, routing::post};
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::{HashMap, HashSet};
|
||||
use reqwest::StatusCode;
|
||||
use sea_orm::ActiveValue;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use stripe::{CancellationDetailsReason, EventObject, EventType, ListEvents, SubscriptionStatus};
|
||||
use util::{ResultExt, maybe};
|
||||
use zed_llm_client::LanguageModelProvider;
|
||||
|
||||
use crate::AppState;
|
||||
use crate::db::billing_subscription::{
|
||||
StripeCancellationReason, StripeSubscriptionStatus, SubscriptionKind,
|
||||
};
|
||||
|
@ -19,7 +17,6 @@ use crate::stripe_client::{
|
|||
StripeCancellationDetailsReason, StripeClient, StripeCustomerId, StripeSubscription,
|
||||
StripeSubscriptionId,
|
||||
};
|
||||
use crate::{AppState, Error, Result};
|
||||
use crate::{db::UserId, llm::db::LlmDatabase};
|
||||
use crate::{
|
||||
db::{
|
||||
|
@ -30,70 +27,6 @@ use crate::{
|
|||
stripe_billing::StripeBilling,
|
||||
};
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new().route(
|
||||
"/billing/subscriptions/sync",
|
||||
post(sync_billing_subscription),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct SyncBillingSubscriptionBody {
|
||||
github_user_id: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct SyncBillingSubscriptionResponse {
|
||||
stripe_customer_id: String,
|
||||
}
|
||||
|
||||
async fn sync_billing_subscription(
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
extract::Json(body): extract::Json<SyncBillingSubscriptionBody>,
|
||||
) -> Result<Json<SyncBillingSubscriptionResponse>> {
|
||||
let Some(stripe_client) = app.stripe_client.clone() else {
|
||||
log::error!("failed to retrieve Stripe client");
|
||||
Err(Error::http(
|
||||
StatusCode::NOT_IMPLEMENTED,
|
||||
"not supported".into(),
|
||||
))?
|
||||
};
|
||||
|
||||
let user = app
|
||||
.db
|
||||
.get_user_by_github_user_id(body.github_user_id)
|
||||
.await?
|
||||
.context("user not found")?;
|
||||
|
||||
let billing_customer = app
|
||||
.db
|
||||
.get_billing_customer_by_user_id(user.id)
|
||||
.await?
|
||||
.context("billing customer not found")?;
|
||||
let stripe_customer_id = StripeCustomerId(billing_customer.stripe_customer_id.clone().into());
|
||||
|
||||
let subscriptions = stripe_client
|
||||
.list_subscriptions_for_customer(&stripe_customer_id)
|
||||
.await?;
|
||||
|
||||
for subscription in subscriptions {
|
||||
let subscription_id = subscription.id.clone();
|
||||
|
||||
sync_subscription(&app, &stripe_client, subscription)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to sync subscription {subscription_id} for user {}",
|
||||
user.id,
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(Json(SyncBillingSubscriptionResponse {
|
||||
stripe_customer_id: billing_customer.stripe_customer_id.clone(),
|
||||
}))
|
||||
}
|
||||
|
||||
/// The amount of time we wait in between each poll of Stripe events.
|
||||
///
|
||||
/// This value should strike a balance between:
|
||||
|
|
|
@ -330,23 +330,16 @@ impl Client {
|
|||
method: &str,
|
||||
params: impl Serialize,
|
||||
) -> Result<T> {
|
||||
self.request_impl(method, params, None).await
|
||||
self.request_with(method, params, None, Some(REQUEST_TIMEOUT))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn cancellable_request<T: DeserializeOwned>(
|
||||
&self,
|
||||
method: &str,
|
||||
params: impl Serialize,
|
||||
cancel_rx: oneshot::Receiver<()>,
|
||||
) -> Result<T> {
|
||||
self.request_impl(method, params, Some(cancel_rx)).await
|
||||
}
|
||||
|
||||
pub async fn request_impl<T: DeserializeOwned>(
|
||||
pub async fn request_with<T: DeserializeOwned>(
|
||||
&self,
|
||||
method: &str,
|
||||
params: impl Serialize,
|
||||
cancel_rx: Option<oneshot::Receiver<()>>,
|
||||
timeout: Option<Duration>,
|
||||
) -> Result<T> {
|
||||
let id = self.next_id.fetch_add(1, SeqCst);
|
||||
let request = serde_json::to_string(&Request {
|
||||
|
@ -382,7 +375,13 @@ impl Client {
|
|||
handle_response?;
|
||||
send?;
|
||||
|
||||
let mut timeout = executor.timer(REQUEST_TIMEOUT).fuse();
|
||||
let mut timeout_fut = pin!(
|
||||
match timeout {
|
||||
Some(timeout) => future::Either::Left(executor.timer(timeout)),
|
||||
None => future::Either::Right(future::pending()),
|
||||
}
|
||||
.fuse()
|
||||
);
|
||||
let mut cancel_fut = pin!(
|
||||
match cancel_rx {
|
||||
Some(rx) => future::Either::Left(async {
|
||||
|
@ -419,10 +418,10 @@ impl Client {
|
|||
reason: None
|
||||
})
|
||||
).log_err();
|
||||
anyhow::bail!("Request cancelled")
|
||||
anyhow::bail!(RequestCanceled)
|
||||
}
|
||||
_ = timeout => {
|
||||
log::error!("cancelled csp request task for {method:?} id {id} which took over {:?}", REQUEST_TIMEOUT);
|
||||
_ = timeout_fut => {
|
||||
log::error!("cancelled csp request task for {method:?} id {id} which took over {:?}", timeout.unwrap());
|
||||
anyhow::bail!("Context server request timeout");
|
||||
}
|
||||
}
|
||||
|
@ -452,6 +451,17 @@ impl Client {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RequestCanceled;
|
||||
|
||||
impl std::error::Error for RequestCanceled {}
|
||||
|
||||
impl std::fmt::Display for RequestCanceled {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("Context server request was canceled")
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ContextServerId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
|
|
|
@ -419,7 +419,7 @@ pub struct ToolResponse<T> {
|
|||
pub structured_content: T,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct RawRequest {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
id: Option<RequestId>,
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
//! read/write messages and the types from types.rs for serialization/deserialization
|
||||
//! of messages.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use futures::channel::oneshot;
|
||||
use gpui::AsyncApp;
|
||||
|
@ -98,13 +100,14 @@ impl InitializedContextServerProtocol {
|
|||
self.inner.request(T::METHOD, params).await
|
||||
}
|
||||
|
||||
pub async fn cancellable_request<T: Request>(
|
||||
pub async fn request_with<T: Request>(
|
||||
&self,
|
||||
params: T::Params,
|
||||
cancel_rx: oneshot::Receiver<()>,
|
||||
cancel_rx: Option<oneshot::Receiver<()>>,
|
||||
timeout: Option<Duration>,
|
||||
) -> Result<T::Response> {
|
||||
self.inner
|
||||
.cancellable_request(T::METHOD, params, cancel_rx)
|
||||
.request_with(T::METHOD, params, cancel_rx, timeout)
|
||||
.await
|
||||
}
|
||||
|
||||
|
|
|
@ -626,6 +626,7 @@ pub enum ClientNotification {
|
|||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CancelledParams {
|
||||
pub request_id: RequestId,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
|
@ -685,6 +686,18 @@ pub struct CallToolResponse {
|
|||
pub structured_content: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl CallToolResponse {
|
||||
pub fn text_contents(&self) -> String {
|
||||
let mut text = String::new();
|
||||
for chunk in &self.content {
|
||||
if let ToolResponseContent::Text { text: chunk } = chunk {
|
||||
text.push_str(&chunk)
|
||||
};
|
||||
}
|
||||
text
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ToolResponseContent {
|
||||
|
|
|
@ -918,7 +918,7 @@ async fn test_debug_panel_item_thread_status_reset_on_failure(
|
|||
.unwrap();
|
||||
|
||||
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
|
||||
const THREAD_ID_NUM: u64 = 1;
|
||||
const THREAD_ID_NUM: i64 = 1;
|
||||
|
||||
client.on_request::<dap::requests::Threads, _>(move |_, _| {
|
||||
Ok(dap::ThreadsResponse {
|
||||
|
|
|
@ -94,7 +94,7 @@ async fn test_fuzzy_score(cx: &mut TestAppContext) {
|
|||
filter_and_sort_matches("set_text", &completions, SnippetSortOrder::Top, cx).await;
|
||||
assert_eq!(matches[0].string, "set_text");
|
||||
assert_eq!(matches[1].string, "set_text_style_refinement");
|
||||
assert_eq!(matches[2].string, "set_context_menu_options");
|
||||
assert_eq!(matches[2].string, "set_placeholder_text");
|
||||
}
|
||||
|
||||
// fuzzy filter text over label, sort_text and sort_kind
|
||||
|
@ -216,6 +216,28 @@ async fn test_sort_positions(cx: &mut TestAppContext) {
|
|||
assert_eq!(matches[0].string, "rounded-full");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_fuzzy_over_sort_positions(cx: &mut TestAppContext) {
|
||||
let completions = vec![
|
||||
CompletionBuilder::variable("lsp_document_colors", None, "7fffffff"), // 0.29 fuzzy score
|
||||
CompletionBuilder::function(
|
||||
"language_servers_running_disk_based_diagnostics",
|
||||
None,
|
||||
"7fffffff",
|
||||
), // 0.168 fuzzy score
|
||||
CompletionBuilder::function("code_lens", None, "7fffffff"), // 3.2 fuzzy score
|
||||
CompletionBuilder::variable("lsp_code_lens", None, "7fffffff"), // 3.2 fuzzy score
|
||||
CompletionBuilder::function("fetch_code_lens", None, "7fffffff"), // 3.2 fuzzy score
|
||||
];
|
||||
|
||||
let matches =
|
||||
filter_and_sort_matches("lens", &completions, SnippetSortOrder::default(), cx).await;
|
||||
|
||||
assert_eq!(matches[0].string, "code_lens");
|
||||
assert_eq!(matches[1].string, "lsp_code_lens");
|
||||
assert_eq!(matches[2].string, "fetch_code_lens");
|
||||
}
|
||||
|
||||
async fn test_for_each_prefix<F>(
|
||||
target: &str,
|
||||
completions: &Vec<Completion>,
|
||||
|
|
|
@ -844,7 +844,7 @@ impl CompletionsMenu {
|
|||
.with_sizing_behavior(ListSizingBehavior::Infer)
|
||||
.w(rems(34.));
|
||||
|
||||
Popover::new().child(div().child(list)).into_any_element()
|
||||
Popover::new().child(list).into_any_element()
|
||||
}
|
||||
|
||||
fn render_aside(
|
||||
|
@ -1057,9 +1057,9 @@ impl CompletionsMenu {
|
|||
enum MatchTier<'a> {
|
||||
WordStartMatch {
|
||||
sort_exact: Reverse<i32>,
|
||||
sort_positions: Vec<usize>,
|
||||
sort_snippet: Reverse<i32>,
|
||||
sort_score: Reverse<OrderedFloat<f64>>,
|
||||
sort_positions: Vec<usize>,
|
||||
sort_text: Option<&'a str>,
|
||||
sort_kind: usize,
|
||||
sort_label: &'a str,
|
||||
|
@ -1137,9 +1137,9 @@ impl CompletionsMenu {
|
|||
|
||||
MatchTier::WordStartMatch {
|
||||
sort_exact,
|
||||
sort_positions,
|
||||
sort_snippet,
|
||||
sort_score,
|
||||
sort_positions,
|
||||
sort_text,
|
||||
sort_kind,
|
||||
sort_label,
|
||||
|
|
|
@ -1774,7 +1774,7 @@ impl Editor {
|
|||
) -> Self {
|
||||
debug_assert!(
|
||||
display_map.is_none() || mode.is_minimap(),
|
||||
"Providing a display map for a new editor is only intended for the minimap and might have unindended side effects otherwise!"
|
||||
"Providing a display map for a new editor is only intended for the minimap and might have unintended side effects otherwise!"
|
||||
);
|
||||
|
||||
let full_mode = mode.is_full();
|
||||
|
@ -8235,8 +8235,7 @@ impl Editor {
|
|||
return;
|
||||
};
|
||||
|
||||
// Try to find a closest, enclosing node using tree-sitter that has a
|
||||
// task
|
||||
// Try to find a closest, enclosing node using tree-sitter that has a task
|
||||
let Some((buffer, buffer_row, tasks)) = self
|
||||
.find_enclosing_node_task(cx)
|
||||
// Or find the task that's closest in row-distance.
|
||||
|
|
|
@ -295,11 +295,13 @@ impl CommitModal {
|
|||
IconPosition::Start,
|
||||
Some(Box::new(Amend)),
|
||||
{
|
||||
let git_panel = git_panel_entity.clone();
|
||||
move |window, cx| {
|
||||
git_panel.update(cx, |git_panel, cx| {
|
||||
git_panel.toggle_amend_pending(&Amend, window, cx);
|
||||
})
|
||||
let git_panel = git_panel_entity.downgrade();
|
||||
move |_, cx| {
|
||||
git_panel
|
||||
.update(cx, |git_panel, cx| {
|
||||
git_panel.toggle_amend_pending(cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
@ -3113,6 +3113,7 @@ impl GitPanel {
|
|||
),
|
||||
)
|
||||
.menu({
|
||||
let git_panel = cx.entity();
|
||||
let has_previous_commit = self.head_commit(cx).is_some();
|
||||
let amend = self.amend_pending();
|
||||
let signoff = self.signoff_enabled;
|
||||
|
@ -3129,7 +3130,16 @@ impl GitPanel {
|
|||
amend,
|
||||
IconPosition::Start,
|
||||
Some(Box::new(Amend)),
|
||||
move |window, cx| window.dispatch_action(Box::new(Amend), cx),
|
||||
{
|
||||
let git_panel = git_panel.downgrade();
|
||||
move |_, cx| {
|
||||
git_panel
|
||||
.update(cx, |git_panel, cx| {
|
||||
git_panel.toggle_amend_pending(cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
.toggleable_entry(
|
||||
|
@ -3500,9 +3510,11 @@ impl GitPanel {
|
|||
.truncate(),
|
||||
),
|
||||
)
|
||||
.child(panel_button("Cancel").size(ButtonSize::Default).on_click(
|
||||
cx.listener(|this, _, window, cx| this.toggle_amend_pending(&Amend, window, cx)),
|
||||
))
|
||||
.child(
|
||||
panel_button("Cancel")
|
||||
.size(ButtonSize::Default)
|
||||
.on_click(cx.listener(|this, _, _, cx| this.set_amend_pending(false, cx))),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_previous_commit(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
|
||||
|
@ -4263,17 +4275,8 @@ impl GitPanel {
|
|||
|
||||
pub fn set_amend_pending(&mut self, value: bool, cx: &mut Context<Self>) {
|
||||
self.amend_pending = value;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn toggle_amend_pending(
|
||||
&mut self,
|
||||
_: &Amend,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.set_amend_pending(!self.amend_pending, cx);
|
||||
self.serialize(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn signoff_enabled(&self) -> bool {
|
||||
|
@ -4367,6 +4370,13 @@ impl GitPanel {
|
|||
anchor: path,
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn toggle_amend_pending(&mut self, cx: &mut Context<Self>) {
|
||||
self.set_amend_pending(!self.amend_pending, cx);
|
||||
if self.amend_pending {
|
||||
self.load_last_commit_message_if_empty(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn current_language_model(cx: &Context<'_, GitPanel>) -> Option<Arc<dyn LanguageModel>> {
|
||||
|
@ -4411,7 +4421,6 @@ impl Render for GitPanel {
|
|||
.on_action(cx.listener(Self::stage_range))
|
||||
.on_action(cx.listener(GitPanel::commit))
|
||||
.on_action(cx.listener(GitPanel::amend))
|
||||
.on_action(cx.listener(GitPanel::toggle_amend_pending))
|
||||
.on_action(cx.listener(GitPanel::toggle_signoff_enabled))
|
||||
.on_action(cx.listener(Self::stage_all))
|
||||
.on_action(cx.listener(Self::unstage_all))
|
||||
|
|
|
@ -288,6 +288,10 @@ path = "examples/shadow.rs"
|
|||
name = "svg"
|
||||
path = "examples/svg/svg.rs"
|
||||
|
||||
[[example]]
|
||||
name = "tab_stop"
|
||||
path = "examples/tab_stop.rs"
|
||||
|
||||
[[example]]
|
||||
name = "text"
|
||||
path = "examples/text.rs"
|
||||
|
|
|
@ -417,17 +417,6 @@ impl Modifiers {
|
|||
self.control || self.alt || self.shift || self.platform || self.function
|
||||
}
|
||||
|
||||
/// Returns the XOR of two modifier sets
|
||||
pub fn xor(&self, other: &Modifiers) -> Modifiers {
|
||||
Modifiers {
|
||||
control: self.control ^ other.control,
|
||||
alt: self.alt ^ other.alt,
|
||||
shift: self.shift ^ other.shift,
|
||||
platform: self.platform ^ other.platform,
|
||||
function: self.function ^ other.function,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the semantically 'secondary' modifier key is pressed.
|
||||
///
|
||||
/// On macOS, this is the command key.
|
||||
|
@ -545,11 +534,62 @@ impl Modifiers {
|
|||
|
||||
/// Checks if this [`Modifiers`] is a subset of another [`Modifiers`].
|
||||
pub fn is_subset_of(&self, other: &Modifiers) -> bool {
|
||||
(other.control || !self.control)
|
||||
&& (other.alt || !self.alt)
|
||||
&& (other.shift || !self.shift)
|
||||
&& (other.platform || !self.platform)
|
||||
&& (other.function || !self.function)
|
||||
(*other & *self) == *self
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::BitOr for Modifiers {
|
||||
type Output = Self;
|
||||
|
||||
fn bitor(mut self, other: Self) -> Self::Output {
|
||||
self |= other;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::BitOrAssign for Modifiers {
|
||||
fn bitor_assign(&mut self, other: Self) {
|
||||
self.control |= other.control;
|
||||
self.alt |= other.alt;
|
||||
self.shift |= other.shift;
|
||||
self.platform |= other.platform;
|
||||
self.function |= other.function;
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::BitXor for Modifiers {
|
||||
type Output = Self;
|
||||
fn bitxor(mut self, rhs: Self) -> Self::Output {
|
||||
self ^= rhs;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::BitXorAssign for Modifiers {
|
||||
fn bitxor_assign(&mut self, other: Self) {
|
||||
self.control ^= other.control;
|
||||
self.alt ^= other.alt;
|
||||
self.shift ^= other.shift;
|
||||
self.platform ^= other.platform;
|
||||
self.function ^= other.function;
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::BitAnd for Modifiers {
|
||||
type Output = Self;
|
||||
fn bitand(mut self, rhs: Self) -> Self::Output {
|
||||
self &= rhs;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::BitAndAssign for Modifiers {
|
||||
fn bitand_assign(&mut self, other: Self) {
|
||||
self.control &= other.control;
|
||||
self.alt &= other.alt;
|
||||
self.shift &= other.shift;
|
||||
self.platform &= other.platform;
|
||||
self.function &= other.function;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -111,7 +111,7 @@ pub struct WaylandWindowState {
|
|||
resize_throttle: bool,
|
||||
in_progress_window_controls: Option<WindowControls>,
|
||||
window_controls: WindowControls,
|
||||
inset: Option<Pixels>,
|
||||
client_inset: Option<Pixels>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
@ -186,7 +186,7 @@ impl WaylandWindowState {
|
|||
hovered: false,
|
||||
in_progress_window_controls: None,
|
||||
window_controls: WindowControls::default(),
|
||||
inset: None,
|
||||
client_inset: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -211,6 +211,13 @@ impl WaylandWindowState {
|
|||
self.display = current_output;
|
||||
scale
|
||||
}
|
||||
|
||||
pub fn inset(&self) -> Pixels {
|
||||
match self.decorations {
|
||||
WindowDecorations::Server => px(0.0),
|
||||
WindowDecorations::Client => self.client_inset.unwrap_or(px(0.0)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct WaylandWindow(pub WaylandWindowStatePtr);
|
||||
|
@ -380,7 +387,7 @@ impl WaylandWindowStatePtr {
|
|||
configure.size = if got_unmaximized {
|
||||
Some(state.window_bounds.size)
|
||||
} else {
|
||||
compute_outer_size(state.inset, configure.size, state.tiling)
|
||||
compute_outer_size(state.inset(), configure.size, state.tiling)
|
||||
};
|
||||
if let Some(size) = configure.size {
|
||||
state.window_bounds = Bounds {
|
||||
|
@ -400,7 +407,7 @@ impl WaylandWindowStatePtr {
|
|||
|
||||
let window_geometry = inset_by_tiling(
|
||||
state.bounds.map_origin(|_| px(0.0)),
|
||||
state.inset.unwrap_or(px(0.0)),
|
||||
state.inset(),
|
||||
state.tiling,
|
||||
)
|
||||
.map(|v| v.0 as i32)
|
||||
|
@ -818,7 +825,7 @@ impl PlatformWindow for WaylandWindow {
|
|||
} else if state.maximized {
|
||||
WindowBounds::Maximized(state.window_bounds)
|
||||
} else {
|
||||
let inset = state.inset.unwrap_or(px(0.));
|
||||
let inset = state.inset();
|
||||
drop(state);
|
||||
WindowBounds::Windowed(self.bounds().inset(inset))
|
||||
}
|
||||
|
@ -1073,8 +1080,8 @@ impl PlatformWindow for WaylandWindow {
|
|||
|
||||
fn set_client_inset(&self, inset: Pixels) {
|
||||
let mut state = self.borrow_mut();
|
||||
if Some(inset) != state.inset {
|
||||
state.inset = Some(inset);
|
||||
if Some(inset) != state.client_inset {
|
||||
state.client_inset = Some(inset);
|
||||
update_window(state);
|
||||
}
|
||||
}
|
||||
|
@ -1094,9 +1101,7 @@ fn update_window(mut state: RefMut<WaylandWindowState>) {
|
|||
|
||||
state.renderer.update_transparency(!opaque);
|
||||
let mut opaque_area = state.window_bounds.map(|v| v.0 as i32);
|
||||
if let Some(inset) = state.inset {
|
||||
opaque_area.inset(inset.0 as i32);
|
||||
}
|
||||
opaque_area.inset(state.inset().0 as i32);
|
||||
|
||||
let region = state
|
||||
.globals
|
||||
|
@ -1169,12 +1174,10 @@ impl ResizeEdge {
|
|||
/// updating to account for the client decorations. But that's not the area we want to render
|
||||
/// to, due to our intrusize CSD. So, here we calculate the 'actual' size, by adding back in the insets
|
||||
fn compute_outer_size(
|
||||
inset: Option<Pixels>,
|
||||
inset: Pixels,
|
||||
new_size: Option<Size<Pixels>>,
|
||||
tiling: Tiling,
|
||||
) -> Option<Size<Pixels>> {
|
||||
let Some(inset) = inset else { return new_size };
|
||||
|
||||
new_size.map(|mut new_size| {
|
||||
if !tiling.top {
|
||||
new_size.height += inset;
|
||||
|
|
|
@ -5,7 +5,7 @@ use crate::{FocusHandle, FocusId};
|
|||
/// Used to manage the `Tab` event to switch between focus handles.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct TabHandles {
|
||||
handles: Vec<FocusHandle>,
|
||||
pub(crate) handles: Vec<FocusHandle>,
|
||||
}
|
||||
|
||||
impl TabHandles {
|
||||
|
|
|
@ -702,6 +702,7 @@ pub(crate) struct PaintIndex {
|
|||
input_handlers_index: usize,
|
||||
cursor_styles_index: usize,
|
||||
accessed_element_states_index: usize,
|
||||
tab_handle_index: usize,
|
||||
line_layout_index: LineLayoutIndex,
|
||||
}
|
||||
|
||||
|
@ -2208,6 +2209,7 @@ impl Window {
|
|||
input_handlers_index: self.next_frame.input_handlers.len(),
|
||||
cursor_styles_index: self.next_frame.cursor_styles.len(),
|
||||
accessed_element_states_index: self.next_frame.accessed_element_states.len(),
|
||||
tab_handle_index: self.next_frame.tab_handles.handles.len(),
|
||||
line_layout_index: self.text_system.layout_index(),
|
||||
}
|
||||
}
|
||||
|
@ -2237,6 +2239,12 @@ impl Window {
|
|||
.iter()
|
||||
.map(|(id, type_id)| (GlobalElementId(id.0.clone()), *type_id)),
|
||||
);
|
||||
self.next_frame.tab_handles.handles.extend(
|
||||
self.rendered_frame.tab_handles.handles
|
||||
[range.start.tab_handle_index..range.end.tab_handle_index]
|
||||
.iter()
|
||||
.cloned(),
|
||||
);
|
||||
|
||||
self.text_system
|
||||
.reuse_layouts(range.start.line_layout_index..range.end.line_layout_index);
|
||||
|
|
|
@ -71,6 +71,7 @@ pub enum IconName {
|
|||
CircleHelp,
|
||||
Close,
|
||||
Cloud,
|
||||
CloudDownload,
|
||||
Code,
|
||||
Cog,
|
||||
Command,
|
||||
|
|
|
@ -69,7 +69,7 @@
|
|||
(
|
||||
(
|
||||
(function_declaration name: (_) @run @_name
|
||||
(#match? @_name "^Benchmark.+"))
|
||||
(#match? @_name "^Benchmark.*"))
|
||||
) @_
|
||||
(#set! tag go-benchmark)
|
||||
)
|
||||
|
|
|
@ -5,7 +5,9 @@ use crate::{
|
|||
};
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use gpui::{AsyncApp, ScreenCaptureSource, ScreenCaptureStream, TestScreenCaptureStream};
|
||||
use gpui::{
|
||||
AsyncApp, DevicePixels, ScreenCaptureSource, ScreenCaptureStream, SourceMetadata, size,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct LocalParticipant {
|
||||
|
@ -119,3 +121,16 @@ impl RemoteParticipant {
|
|||
self.identity.clone()
|
||||
}
|
||||
}
|
||||
|
||||
struct TestScreenCaptureStream;
|
||||
|
||||
impl ScreenCaptureStream for TestScreenCaptureStream {
|
||||
fn metadata(&self) -> Result<SourceMetadata> {
|
||||
Ok(SourceMetadata {
|
||||
id: 0,
|
||||
is_main: None,
|
||||
label: None,
|
||||
resolution: size(DevicePixels(1), DevicePixels(1)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,3 +26,4 @@ theme.workspace = true
|
|||
ui.workspace = true
|
||||
workspace.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use crate::welcome::{ShowWelcome, WelcomePage};
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use feature_flags::{FeatureFlag, FeatureFlagViewExt as _};
|
||||
|
@ -20,6 +21,8 @@ use workspace::{
|
|||
open_new, with_active_or_new_workspace,
|
||||
};
|
||||
|
||||
mod welcome;
|
||||
|
||||
pub struct OnBoardingFeatureFlag {}
|
||||
|
||||
impl FeatureFlag for OnBoardingFeatureFlag {
|
||||
|
@ -63,12 +66,43 @@ pub fn init(cx: &mut App) {
|
|||
.detach();
|
||||
});
|
||||
});
|
||||
|
||||
cx.on_action(|_: &ShowWelcome, cx| {
|
||||
with_active_or_new_workspace(cx, |workspace, window, cx| {
|
||||
workspace
|
||||
.with_local_workspace(window, cx, |workspace, window, cx| {
|
||||
let existing = workspace
|
||||
.active_pane()
|
||||
.read(cx)
|
||||
.items()
|
||||
.find_map(|item| item.downcast::<WelcomePage>());
|
||||
|
||||
if let Some(existing) = existing {
|
||||
workspace.activate_item(&existing, true, true, window, cx);
|
||||
} else {
|
||||
let settings_page = WelcomePage::new(cx);
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(settings_page),
|
||||
None,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
});
|
||||
|
||||
cx.observe_new::<Workspace>(|_, window, cx| {
|
||||
let Some(window) = window else {
|
||||
return;
|
||||
};
|
||||
|
||||
let onboarding_actions = [std::any::TypeId::of::<OpenOnboarding>()];
|
||||
let onboarding_actions = [
|
||||
std::any::TypeId::of::<OpenOnboarding>(),
|
||||
std::any::TypeId::of::<ShowWelcome>(),
|
||||
];
|
||||
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.hide_action_types(&onboarding_actions);
|
||||
|
|
275
crates/onboarding/src/welcome.rs
Normal file
|
@ -0,0 +1,275 @@
|
|||
use gpui::{
|
||||
Action, App, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,
|
||||
NoAction, ParentElement, Render, Styled, Window, actions,
|
||||
};
|
||||
use ui::{ButtonLike, Divider, DividerColor, KeyBinding, Vector, VectorName, prelude::*};
|
||||
use workspace::{
|
||||
NewFile, Open, Workspace, WorkspaceId,
|
||||
item::{Item, ItemEvent},
|
||||
};
|
||||
use zed_actions::{Extensions, OpenSettings, command_palette};
|
||||
|
||||
actions!(
|
||||
zed,
|
||||
[
|
||||
/// Show the Zed welcome screen
|
||||
ShowWelcome
|
||||
]
|
||||
);
|
||||
|
||||
const CONTENT: (Section<4>, Section<3>) = (
|
||||
Section {
|
||||
title: "Get Started",
|
||||
entries: [
|
||||
SectionEntry {
|
||||
icon: IconName::Plus,
|
||||
title: "New File",
|
||||
action: &NewFile,
|
||||
},
|
||||
SectionEntry {
|
||||
icon: IconName::FolderOpen,
|
||||
title: "Open Project",
|
||||
action: &Open,
|
||||
},
|
||||
SectionEntry {
|
||||
icon: IconName::CloudDownload,
|
||||
title: "Clone a Repo",
|
||||
// TODO: use proper action
|
||||
action: &NoAction,
|
||||
},
|
||||
SectionEntry {
|
||||
icon: IconName::ListCollapse,
|
||||
title: "Open Command Palette",
|
||||
action: &command_palette::Toggle,
|
||||
},
|
||||
],
|
||||
},
|
||||
Section {
|
||||
title: "Configure",
|
||||
entries: [
|
||||
SectionEntry {
|
||||
icon: IconName::Settings,
|
||||
title: "Open Settings",
|
||||
action: &OpenSettings,
|
||||
},
|
||||
SectionEntry {
|
||||
icon: IconName::ZedAssistant,
|
||||
title: "View AI Settings",
|
||||
// TODO: use proper action
|
||||
action: &NoAction,
|
||||
},
|
||||
SectionEntry {
|
||||
icon: IconName::Blocks,
|
||||
title: "Explore Extensions",
|
||||
action: &Extensions {
|
||||
category_filter: None,
|
||||
id: None,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
struct Section<const COLS: usize> {
|
||||
title: &'static str,
|
||||
entries: [SectionEntry; COLS],
|
||||
}
|
||||
|
||||
impl<const COLS: usize> Section<COLS> {
|
||||
fn render(
|
||||
self,
|
||||
index_offset: usize,
|
||||
focus: &FocusHandle,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> impl IntoElement {
|
||||
v_flex()
|
||||
.min_w_full()
|
||||
.gap_2()
|
||||
.child(
|
||||
h_flex()
|
||||
.px_1()
|
||||
.gap_4()
|
||||
.child(
|
||||
Label::new(self.title.to_ascii_uppercase())
|
||||
.buffer_font(cx)
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::XSmall),
|
||||
)
|
||||
.child(Divider::horizontal().color(DividerColor::Border)),
|
||||
)
|
||||
.children(
|
||||
self.entries
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, entry)| entry.render(index_offset + index, &focus, window, cx)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct SectionEntry {
|
||||
icon: IconName,
|
||||
title: &'static str,
|
||||
action: &'static dyn Action,
|
||||
}
|
||||
|
||||
impl SectionEntry {
|
||||
fn render(
|
||||
&self,
|
||||
button_index: usize,
|
||||
focus: &FocusHandle,
|
||||
window: &Window,
|
||||
cx: &App,
|
||||
) -> impl IntoElement {
|
||||
ButtonLike::new(("onboarding-button-id", button_index))
|
||||
.full_width()
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Icon::new(self.icon)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
)
|
||||
.child(Label::new(self.title)),
|
||||
)
|
||||
.children(KeyBinding::for_action_in(self.action, focus, window, cx)),
|
||||
)
|
||||
.on_click(|_, window, cx| window.dispatch_action(self.action.boxed_clone(), cx))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WelcomePage {
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl Render for WelcomePage {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let (first_section, second_entries) = CONTENT;
|
||||
let first_section_entries = first_section.entries.len();
|
||||
|
||||
h_flex()
|
||||
.size_full()
|
||||
.justify_center()
|
||||
.overflow_hidden()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.key_context("Welcome")
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.child(
|
||||
h_flex()
|
||||
.px_12()
|
||||
.py_40()
|
||||
.size_full()
|
||||
.relative()
|
||||
.max_w(px(1100.))
|
||||
.child(
|
||||
div()
|
||||
.size_full()
|
||||
.max_w_128()
|
||||
.mx_auto()
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_center()
|
||||
.gap_4()
|
||||
.child(Vector::square(VectorName::ZedLogo, rems(2.)))
|
||||
.child(
|
||||
div().child(Headline::new("Welcome to Zed")).child(
|
||||
Label::new("The editor for what's next")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.italic(),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.mt_12()
|
||||
.gap_8()
|
||||
.child(first_section.render(
|
||||
Default::default(),
|
||||
&self.focus_handle,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.child(second_entries.render(
|
||||
first_section_entries,
|
||||
&self.focus_handle,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.pt_4()
|
||||
.justify_center()
|
||||
// We call this a hack
|
||||
.rounded_b_xs()
|
||||
.border_t_1()
|
||||
.border_color(DividerColor::Border.hsla(cx))
|
||||
.border_dashed()
|
||||
.child(
|
||||
div().child(
|
||||
Button::new("welcome-exit", "Return to Setup")
|
||||
.full_width()
|
||||
.label_size(LabelSize::XSmall),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl WelcomePage {
|
||||
pub fn new(cx: &mut Context<Workspace>) -> Entity<Self> {
|
||||
let this = cx.new(|cx| WelcomePage {
|
||||
focus_handle: cx.focus_handle(),
|
||||
});
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<ItemEvent> for WelcomePage {}
|
||||
|
||||
impl Focusable for WelcomePage {
|
||||
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Item for WelcomePage {
|
||||
type Event = ItemEvent;
|
||||
|
||||
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
|
||||
"Welcome".into()
|
||||
}
|
||||
|
||||
fn telemetry_event_text(&self) -> Option<&'static str> {
|
||||
Some("New Welcome Page Opened")
|
||||
}
|
||||
|
||||
fn show_toolbar(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn clone_on_split(
|
||||
&self,
|
||||
_workspace_id: Option<WorkspaceId>,
|
||||
_: &mut Window,
|
||||
_: &mut Context<Self>,
|
||||
) -> Option<Entity<Self>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
|
||||
f(*event)
|
||||
}
|
||||
}
|
|
@ -107,7 +107,7 @@ impl<T: DapCommand> DapCommand for Arc<T> {
|
|||
|
||||
#[derive(Debug, Hash, PartialEq, Eq)]
|
||||
pub struct StepCommand {
|
||||
pub thread_id: u64,
|
||||
pub thread_id: i64,
|
||||
pub granularity: Option<SteppingGranularity>,
|
||||
pub single_thread: Option<bool>,
|
||||
}
|
||||
|
@ -483,7 +483,7 @@ impl DapCommand for ContinueCommand {
|
|||
|
||||
#[derive(Debug, Hash, PartialEq, Eq)]
|
||||
pub(crate) struct PauseCommand {
|
||||
pub thread_id: u64,
|
||||
pub thread_id: i64,
|
||||
}
|
||||
|
||||
impl LocalDapCommand for PauseCommand {
|
||||
|
@ -612,7 +612,7 @@ impl DapCommand for DisconnectCommand {
|
|||
|
||||
#[derive(Debug, Hash, PartialEq, Eq)]
|
||||
pub(crate) struct TerminateThreadsCommand {
|
||||
pub thread_ids: Option<Vec<u64>>,
|
||||
pub thread_ids: Option<Vec<i64>>,
|
||||
}
|
||||
|
||||
impl LocalDapCommand for TerminateThreadsCommand {
|
||||
|
@ -1182,7 +1182,7 @@ impl DapCommand for LoadedSourcesCommand {
|
|||
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
||||
pub(crate) struct StackTraceCommand {
|
||||
pub thread_id: u64,
|
||||
pub thread_id: i64,
|
||||
pub start_frame: Option<u64>,
|
||||
pub levels: Option<u64>,
|
||||
}
|
||||
|
|
|
@ -920,12 +920,22 @@ impl dap::adapters::DapDelegate for DapAdapterDelegate {
|
|||
self.console.unbounded_send(msg).ok();
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
async fn which(&self, command: &OsStr) -> Option<PathBuf> {
|
||||
let worktree_abs_path = self.worktree.abs_path();
|
||||
let shell_path = self.shell_env().await.get("PATH").cloned();
|
||||
which::which_in(command, shell_path.as_ref(), worktree_abs_path).ok()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn which(&self, command: &OsStr) -> Option<PathBuf> {
|
||||
// On Windows, `PATH` is handled differently from Unix. Windows generally expects users to modify the `PATH` themselves,
|
||||
// and every program loads it directly from the system at startup.
|
||||
// There's also no concept of a default shell on Windows, and you can't really retrieve one, so trying to get shell environment variables
|
||||
// from a specific directory doesn’t make sense on Windows.
|
||||
which::which(command).ok()
|
||||
}
|
||||
|
||||
async fn shell_env(&self) -> HashMap<String, String> {
|
||||
let task = self.load_shell_env_task.clone();
|
||||
task.await.unwrap_or_default()
|
||||
|
|
|
@ -128,7 +128,7 @@ impl DapLocator for CargoLocator {
|
|||
.chain(Some("--message-format=json".to_owned()))
|
||||
.collect(),
|
||||
);
|
||||
let mut child = Command::new(program)
|
||||
let mut child = util::command::new_smol_command(program)
|
||||
.args(args)
|
||||
.envs(build_config.env.iter().map(|(k, v)| (k.clone(), v.clone())))
|
||||
.current_dir(cwd)
|
||||
|
|
|
@ -61,15 +61,10 @@ use worktree::Worktree;
|
|||
|
||||
#[derive(Debug, Copy, Clone, Hash, PartialEq, PartialOrd, Ord, Eq)]
|
||||
#[repr(transparent)]
|
||||
pub struct ThreadId(pub u64);
|
||||
pub struct ThreadId(pub i64);
|
||||
|
||||
impl ThreadId {
|
||||
pub const MIN: ThreadId = ThreadId(u64::MIN);
|
||||
pub const MAX: ThreadId = ThreadId(u64::MAX);
|
||||
}
|
||||
|
||||
impl From<u64> for ThreadId {
|
||||
fn from(id: u64) -> Self {
|
||||
impl From<i64> for ThreadId {
|
||||
fn from(id: i64) -> Self {
|
||||
Self(id)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -188,7 +188,7 @@ message DapSetVariableValueResponse {
|
|||
message DapPauseRequest {
|
||||
uint64 project_id = 1;
|
||||
uint64 client_id = 2;
|
||||
uint64 thread_id = 3;
|
||||
int64 thread_id = 3;
|
||||
}
|
||||
|
||||
message DapDisconnectRequest {
|
||||
|
@ -202,7 +202,7 @@ message DapDisconnectRequest {
|
|||
message DapTerminateThreadsRequest {
|
||||
uint64 project_id = 1;
|
||||
uint64 client_id = 2;
|
||||
repeated uint64 thread_ids = 3;
|
||||
repeated int64 thread_ids = 3;
|
||||
}
|
||||
|
||||
message DapThreadsRequest {
|
||||
|
@ -246,7 +246,7 @@ message IgnoreBreakpointState {
|
|||
message DapNextRequest {
|
||||
uint64 project_id = 1;
|
||||
uint64 client_id = 2;
|
||||
uint64 thread_id = 3;
|
||||
int64 thread_id = 3;
|
||||
optional bool single_thread = 4;
|
||||
optional SteppingGranularity granularity = 5;
|
||||
}
|
||||
|
@ -254,7 +254,7 @@ message DapNextRequest {
|
|||
message DapStepInRequest {
|
||||
uint64 project_id = 1;
|
||||
uint64 client_id = 2;
|
||||
uint64 thread_id = 3;
|
||||
int64 thread_id = 3;
|
||||
optional uint64 target_id = 4;
|
||||
optional bool single_thread = 5;
|
||||
optional SteppingGranularity granularity = 6;
|
||||
|
@ -263,7 +263,7 @@ message DapStepInRequest {
|
|||
message DapStepOutRequest {
|
||||
uint64 project_id = 1;
|
||||
uint64 client_id = 2;
|
||||
uint64 thread_id = 3;
|
||||
int64 thread_id = 3;
|
||||
optional bool single_thread = 4;
|
||||
optional SteppingGranularity granularity = 5;
|
||||
}
|
||||
|
@ -271,7 +271,7 @@ message DapStepOutRequest {
|
|||
message DapStepBackRequest {
|
||||
uint64 project_id = 1;
|
||||
uint64 client_id = 2;
|
||||
uint64 thread_id = 3;
|
||||
int64 thread_id = 3;
|
||||
optional bool single_thread = 4;
|
||||
optional SteppingGranularity granularity = 5;
|
||||
}
|
||||
|
@ -279,7 +279,7 @@ message DapStepBackRequest {
|
|||
message DapContinueRequest {
|
||||
uint64 project_id = 1;
|
||||
uint64 client_id = 2;
|
||||
uint64 thread_id = 3;
|
||||
int64 thread_id = 3;
|
||||
optional bool single_thread = 4;
|
||||
}
|
||||
|
||||
|
@ -311,7 +311,7 @@ message DapLoadedSourcesResponse {
|
|||
message DapStackTraceRequest {
|
||||
uint64 project_id = 1;
|
||||
uint64 client_id = 2;
|
||||
uint64 thread_id = 3;
|
||||
int64 thread_id = 3;
|
||||
optional uint64 start_frame = 4;
|
||||
optional uint64 stack_trace_levels = 5;
|
||||
}
|
||||
|
@ -358,7 +358,7 @@ message DapVariable {
|
|||
}
|
||||
|
||||
message DapThread {
|
||||
uint64 id = 1;
|
||||
int64 id = 1;
|
||||
string name = 2;
|
||||
}
|
||||
|
||||
|
|
|
@ -566,24 +566,40 @@ impl KeymapEditor {
|
|||
&& query.modifiers == keystroke.modifiers
|
||||
},
|
||||
)
|
||||
} else if keystroke_query.len() > keystrokes.len() {
|
||||
return false;
|
||||
} else {
|
||||
let key_press_query =
|
||||
KeyPressIterator::new(keystroke_query.as_slice());
|
||||
let mut last_match_idx = 0;
|
||||
for keystroke_offset in 0..keystrokes.len() {
|
||||
let mut found_count = 0;
|
||||
let mut query_cursor = 0;
|
||||
let mut keystroke_cursor = keystroke_offset;
|
||||
while query_cursor < keystroke_query.len()
|
||||
&& keystroke_cursor < keystrokes.len()
|
||||
{
|
||||
let query = &keystroke_query[query_cursor];
|
||||
let keystroke = &keystrokes[keystroke_cursor];
|
||||
let matches =
|
||||
query.modifiers.is_subset_of(&keystroke.modifiers)
|
||||
&& ((query.key.is_empty()
|
||||
|| query.key == keystroke.key)
|
||||
&& query
|
||||
.key_char
|
||||
.as_ref()
|
||||
.map_or(true, |q_kc| {
|
||||
q_kc == &keystroke.key
|
||||
}));
|
||||
if matches {
|
||||
found_count += 1;
|
||||
query_cursor += 1;
|
||||
}
|
||||
keystroke_cursor += 1;
|
||||
}
|
||||
|
||||
key_press_query.into_iter().all(|key| {
|
||||
let key_presses = KeyPressIterator::new(keystrokes);
|
||||
key_presses.into_iter().enumerate().any(
|
||||
|(index, keystroke)| {
|
||||
if last_match_idx > index || keystroke != key {
|
||||
return false;
|
||||
}
|
||||
|
||||
last_match_idx = index;
|
||||
true
|
||||
},
|
||||
)
|
||||
})
|
||||
if found_count == keystroke_query.len() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
@ -1232,11 +1248,14 @@ impl KeymapEditor {
|
|||
|
||||
match self.search_mode {
|
||||
SearchMode::KeyStroke { .. } => {
|
||||
window.focus(&self.keystroke_editor.read(cx).recording_focus_handle(cx));
|
||||
self.keystroke_editor.update(cx, |editor, cx| {
|
||||
editor.start_recording(&StartRecording, window, cx);
|
||||
});
|
||||
}
|
||||
SearchMode::Normal => {
|
||||
self.keystroke_editor.update(cx, |editor, cx| {
|
||||
editor.clear_keystrokes(&ClearKeystrokes, window, cx)
|
||||
editor.stop_recording(&StopRecording, window, cx);
|
||||
editor.clear_keystrokes(&ClearKeystrokes, window, cx);
|
||||
});
|
||||
window.focus(&self.filter_editor.focus_handle(cx));
|
||||
}
|
||||
|
@ -1671,7 +1690,7 @@ impl Render for KeymapEditor {
|
|||
move |window, cx| this.read(cx).render_no_matches_hint(window, cx)
|
||||
})
|
||||
.column_widths([
|
||||
DefiniteLength::Absolute(AbsoluteLength::Pixels(px(40.))),
|
||||
DefiniteLength::Absolute(AbsoluteLength::Pixels(px(36.))),
|
||||
DefiniteLength::Fraction(0.25),
|
||||
DefiniteLength::Fraction(0.20),
|
||||
DefiniteLength::Fraction(0.14),
|
||||
|
@ -1746,6 +1765,7 @@ impl Render for KeymapEditor {
|
|||
},
|
||||
)
|
||||
.into_any_element();
|
||||
|
||||
let keystrokes = binding.ui_key_binding().cloned().map_or(
|
||||
binding
|
||||
.keystroke_text()
|
||||
|
@ -1754,6 +1774,7 @@ impl Render for KeymapEditor {
|
|||
.into_any_element(),
|
||||
IntoElement::into_any_element,
|
||||
);
|
||||
|
||||
let action_arguments = match binding.action().arguments.clone()
|
||||
{
|
||||
Some(arguments) => arguments.into_any_element(),
|
||||
|
@ -1766,6 +1787,7 @@ impl Render for KeymapEditor {
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
let context = binding.context().cloned().map_or(
|
||||
gpui::Empty.into_any_element(),
|
||||
|context| {
|
||||
|
@ -1790,11 +1812,13 @@ impl Render for KeymapEditor {
|
|||
.into_any_element()
|
||||
},
|
||||
);
|
||||
|
||||
let source = binding
|
||||
.keybind_source()
|
||||
.map(|source| source.name())
|
||||
.unwrap_or_default()
|
||||
.into_any_element();
|
||||
|
||||
Some([
|
||||
icon.into_any_element(),
|
||||
action,
|
||||
|
@ -2962,16 +2986,6 @@ enum CloseKeystrokeResult {
|
|||
None,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||
enum KeyPress<'a> {
|
||||
Alt,
|
||||
Control,
|
||||
Function,
|
||||
Shift,
|
||||
Platform,
|
||||
Key(&'a String),
|
||||
}
|
||||
|
||||
struct KeystrokeInput {
|
||||
keystrokes: Vec<Keystroke>,
|
||||
placeholder_keystrokes: Option<Vec<Keystroke>>,
|
||||
|
@ -2983,6 +2997,7 @@ struct KeystrokeInput {
|
|||
/// Handles tripe escape to stop recording
|
||||
close_keystrokes: Option<Vec<Keystroke>>,
|
||||
close_keystrokes_start: Option<usize>,
|
||||
previous_modifiers: Modifiers,
|
||||
}
|
||||
|
||||
impl KeystrokeInput {
|
||||
|
@ -3009,6 +3024,7 @@ impl KeystrokeInput {
|
|||
search: false,
|
||||
close_keystrokes: None,
|
||||
close_keystrokes_start: None,
|
||||
previous_modifiers: Modifiers::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3031,7 +3047,7 @@ impl KeystrokeInput {
|
|||
}
|
||||
|
||||
fn key_context() -> KeyContext {
|
||||
let mut key_context = KeyContext::new_with_defaults();
|
||||
let mut key_context = KeyContext::default();
|
||||
key_context.add("KeystrokeInput");
|
||||
key_context
|
||||
}
|
||||
|
@ -3098,12 +3114,26 @@ impl KeystrokeInput {
|
|||
) {
|
||||
let keystrokes_len = self.keystrokes.len();
|
||||
|
||||
if self.previous_modifiers.modified()
|
||||
&& event.modifiers.is_subset_of(&self.previous_modifiers)
|
||||
{
|
||||
self.previous_modifiers &= event.modifiers;
|
||||
cx.stop_propagation();
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(last) = self.keystrokes.last_mut()
|
||||
&& last.key.is_empty()
|
||||
&& keystrokes_len <= Self::KEYSTROKE_COUNT_MAX
|
||||
{
|
||||
if self.search {
|
||||
last.modifiers = last.modifiers.xor(&event.modifiers);
|
||||
if self.previous_modifiers.modified() {
|
||||
last.modifiers |= event.modifiers;
|
||||
self.previous_modifiers |= event.modifiers;
|
||||
} else {
|
||||
self.keystrokes.push(Self::dummy(event.modifiers));
|
||||
self.previous_modifiers |= event.modifiers;
|
||||
}
|
||||
} else if !event.modifiers.modified() {
|
||||
self.keystrokes.pop();
|
||||
} else {
|
||||
|
@ -3113,6 +3143,9 @@ impl KeystrokeInput {
|
|||
self.keystrokes_changed(cx);
|
||||
} else if keystrokes_len < Self::KEYSTROKE_COUNT_MAX {
|
||||
self.keystrokes.push(Self::dummy(event.modifiers));
|
||||
if self.search {
|
||||
self.previous_modifiers |= event.modifiers;
|
||||
}
|
||||
self.keystrokes_changed(cx);
|
||||
}
|
||||
cx.stop_propagation();
|
||||
|
@ -3138,6 +3171,9 @@ impl KeystrokeInput {
|
|||
{
|
||||
self.close_keystrokes_start = Some(self.keystrokes.len() - 1);
|
||||
}
|
||||
if self.search {
|
||||
self.previous_modifiers = keystroke.modifiers;
|
||||
}
|
||||
self.keystrokes_changed(cx);
|
||||
cx.stop_propagation();
|
||||
return;
|
||||
|
@ -3152,7 +3188,9 @@ impl KeystrokeInput {
|
|||
self.close_keystrokes_start = Some(self.keystrokes.len());
|
||||
}
|
||||
self.keystrokes.push(keystroke.clone());
|
||||
if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX {
|
||||
if self.search {
|
||||
self.previous_modifiers = keystroke.modifiers;
|
||||
} else if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX {
|
||||
self.keystrokes.push(Self::dummy(keystroke.modifiers));
|
||||
}
|
||||
} else if close_keystroke_result != CloseKeystrokeResult::Partial {
|
||||
|
@ -3222,17 +3260,11 @@ impl KeystrokeInput {
|
|||
})
|
||||
}
|
||||
|
||||
fn recording_focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||
self.inner_focus_handle.clone()
|
||||
}
|
||||
|
||||
fn start_recording(&mut self, _: &StartRecording, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if !self.outer_focus_handle.is_focused(window) {
|
||||
return;
|
||||
}
|
||||
self.clear_keystrokes(&ClearKeystrokes, window, cx);
|
||||
window.focus(&self.inner_focus_handle);
|
||||
cx.notify();
|
||||
self.clear_keystrokes(&ClearKeystrokes, window, cx);
|
||||
self.previous_modifiers = window.modifiers();
|
||||
cx.stop_propagation();
|
||||
}
|
||||
|
||||
fn stop_recording(&mut self, _: &StopRecording, window: &mut Window, cx: &mut Context<Self>) {
|
||||
|
@ -3364,7 +3396,7 @@ impl Render for KeystrokeInput {
|
|||
})
|
||||
.key_context(Self::key_context())
|
||||
.on_action(cx.listener(Self::start_recording))
|
||||
.on_action(cx.listener(Self::stop_recording))
|
||||
.on_action(cx.listener(Self::clear_keystrokes))
|
||||
.child(
|
||||
h_flex()
|
||||
.w(horizontal_padding)
|
||||
|
@ -3633,72 +3665,3 @@ mod persistence {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator that yields KeyPress values from a slice of Keystrokes
|
||||
struct KeyPressIterator<'a> {
|
||||
keystrokes: &'a [Keystroke],
|
||||
current_keystroke_index: usize,
|
||||
current_key_press_index: usize,
|
||||
}
|
||||
|
||||
impl<'a> KeyPressIterator<'a> {
|
||||
fn new(keystrokes: &'a [Keystroke]) -> Self {
|
||||
Self {
|
||||
keystrokes,
|
||||
current_keystroke_index: 0,
|
||||
current_key_press_index: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for KeyPressIterator<'a> {
|
||||
type Item = KeyPress<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
loop {
|
||||
let keystroke = self.keystrokes.get(self.current_keystroke_index)?;
|
||||
|
||||
match self.current_key_press_index {
|
||||
0 => {
|
||||
self.current_key_press_index = 1;
|
||||
if keystroke.modifiers.platform {
|
||||
return Some(KeyPress::Platform);
|
||||
}
|
||||
}
|
||||
1 => {
|
||||
self.current_key_press_index = 2;
|
||||
if keystroke.modifiers.alt {
|
||||
return Some(KeyPress::Alt);
|
||||
}
|
||||
}
|
||||
2 => {
|
||||
self.current_key_press_index = 3;
|
||||
if keystroke.modifiers.control {
|
||||
return Some(KeyPress::Control);
|
||||
}
|
||||
}
|
||||
3 => {
|
||||
self.current_key_press_index = 4;
|
||||
if keystroke.modifiers.shift {
|
||||
return Some(KeyPress::Shift);
|
||||
}
|
||||
}
|
||||
4 => {
|
||||
self.current_key_press_index = 5;
|
||||
if keystroke.modifiers.function {
|
||||
return Some(KeyPress::Function);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
self.current_keystroke_index += 1;
|
||||
self.current_key_press_index = 0;
|
||||
|
||||
if keystroke.key.is_empty() {
|
||||
continue;
|
||||
}
|
||||
return Some(KeyPress::Key(&keystroke.key));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ use ui::{
|
|||
StyledTypography, Window, div, example_group_with_title, h_flex, px, single_example, v_flex,
|
||||
};
|
||||
|
||||
const RESIZE_COLUMN_WIDTH: f32 = 5.0;
|
||||
const RESIZE_COLUMN_WIDTH: f32 = 8.0;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct DraggedColumn(usize);
|
||||
|
@ -214,6 +214,7 @@ impl TableInteractionState {
|
|||
let mut column_ix = 0;
|
||||
let resizable_columns_slice = *resizable_columns;
|
||||
let mut resizable_columns = resizable_columns.into_iter();
|
||||
|
||||
let dividers = intersperse_with(spacers, || {
|
||||
window.with_id(column_ix, |window| {
|
||||
let mut resize_divider = div()
|
||||
|
@ -221,9 +222,9 @@ impl TableInteractionState {
|
|||
.id(column_ix)
|
||||
.relative()
|
||||
.top_0()
|
||||
.w_0p5()
|
||||
.w_px()
|
||||
.h_full()
|
||||
.bg(cx.theme().colors().border.opacity(0.5));
|
||||
.bg(cx.theme().colors().border.opacity(0.8));
|
||||
|
||||
let mut resize_handle = div()
|
||||
.id("column-resize-handle")
|
||||
|
@ -237,9 +238,11 @@ impl TableInteractionState {
|
|||
.is_some_and(ResizeBehavior::is_resizable)
|
||||
{
|
||||
let hovered = window.use_state(cx, |_window, _cx| false);
|
||||
|
||||
resize_divider = resize_divider.when(*hovered.read(cx), |div| {
|
||||
div.bg(cx.theme().colors().border_focused)
|
||||
});
|
||||
|
||||
resize_handle = resize_handle
|
||||
.on_hover(move |&was_hovered, _, cx| hovered.write(cx, was_hovered))
|
||||
.cursor_col_resize()
|
||||
|
@ -269,12 +272,11 @@ impl TableInteractionState {
|
|||
})
|
||||
});
|
||||
|
||||
div()
|
||||
h_flex()
|
||||
.id("resize-handles")
|
||||
.h_flex()
|
||||
.absolute()
|
||||
.w_full()
|
||||
.inset_0()
|
||||
.w_full()
|
||||
.children(dividers)
|
||||
.into_any_element()
|
||||
}
|
||||
|
@ -896,7 +898,6 @@ fn base_cell_style(width: Option<Length>) -> Div {
|
|||
.px_1p5()
|
||||
.when_some(width, |this, width| this.w(width))
|
||||
.when(width.is_none(), |this| this.flex_1())
|
||||
.justify_start()
|
||||
.whitespace_nowrap()
|
||||
.text_ellipsis()
|
||||
.overflow_hidden()
|
||||
|
@ -941,7 +942,7 @@ pub fn render_row<const COLS: usize>(
|
|||
.map(IntoElement::into_any_element)
|
||||
.into_iter()
|
||||
.zip(column_widths)
|
||||
.map(|(cell, width)| base_cell_style_text(width, cx).px_1p5().py_1().child(cell)),
|
||||
.map(|(cell, width)| base_cell_style_text(width, cx).px_1().py_0p5().child(cell)),
|
||||
);
|
||||
|
||||
let row = if let Some(map_row) = table_context.map_row {
|
||||
|
@ -950,7 +951,7 @@ pub fn render_row<const COLS: usize>(
|
|||
row.into_any_element()
|
||||
};
|
||||
|
||||
div().h_full().w_full().child(row).into_any_element()
|
||||
div().size_full().child(row).into_any_element()
|
||||
}
|
||||
|
||||
pub fn render_header<const COLS: usize>(
|
||||
|
|
|
@ -11,8 +11,8 @@ use gpui::{App, Task, Window, actions};
|
|||
use rpc::proto::{self};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{
|
||||
Avatar, AvatarAudioStatusIndicator, ContextMenu, ContextMenuItem, Divider, Facepile,
|
||||
PopoverMenu, SplitButton, SplitButtonStyle, TintColor, Tooltip, prelude::*,
|
||||
Avatar, AvatarAudioStatusIndicator, ContextMenu, ContextMenuItem, Divider, DividerColor,
|
||||
Facepile, PopoverMenu, SplitButton, SplitButtonStyle, TintColor, Tooltip, prelude::*,
|
||||
};
|
||||
use util::maybe;
|
||||
use workspace::notifications::DetachAndPromptErr;
|
||||
|
@ -343,6 +343,24 @@ impl TitleBar {
|
|||
|
||||
let mut children = Vec::new();
|
||||
|
||||
children.push(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
IconButton::new("leave-call", IconName::Exit)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(Tooltip::text("Leave Call"))
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(move |_, _window, cx| {
|
||||
ActiveCall::global(cx)
|
||||
.update(cx, |call, cx| call.hang_up(cx))
|
||||
.detach_and_log_err(cx);
|
||||
}),
|
||||
)
|
||||
.child(Divider::vertical().color(DividerColor::Border))
|
||||
.into_any_element(),
|
||||
);
|
||||
|
||||
if is_local && can_share_projects && !is_connecting_to_project {
|
||||
children.push(
|
||||
Button::new(
|
||||
|
@ -369,32 +387,14 @@ impl TitleBar {
|
|||
);
|
||||
}
|
||||
|
||||
children.push(
|
||||
div()
|
||||
.pr_2()
|
||||
.child(
|
||||
IconButton::new("leave-call", ui::IconName::Exit)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(Tooltip::text("Leave call"))
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(move |_, _window, cx| {
|
||||
ActiveCall::global(cx)
|
||||
.update(cx, |call, cx| call.hang_up(cx))
|
||||
.detach_and_log_err(cx);
|
||||
}),
|
||||
)
|
||||
.child(Divider::vertical())
|
||||
.into_any_element(),
|
||||
);
|
||||
|
||||
if can_use_microphone {
|
||||
children.push(
|
||||
IconButton::new(
|
||||
"mute-microphone",
|
||||
if is_muted {
|
||||
ui::IconName::MicMute
|
||||
IconName::MicMute
|
||||
} else {
|
||||
ui::IconName::Mic
|
||||
IconName::Mic
|
||||
},
|
||||
)
|
||||
.tooltip(move |window, cx| {
|
||||
|
@ -429,9 +429,9 @@ impl TitleBar {
|
|||
IconButton::new(
|
||||
"mute-sound",
|
||||
if is_deafened {
|
||||
ui::IconName::AudioOff
|
||||
IconName::AudioOff
|
||||
} else {
|
||||
ui::IconName::AudioOn
|
||||
IconName::AudioOn
|
||||
},
|
||||
)
|
||||
.style(ButtonStyle::Subtle)
|
||||
|
@ -462,7 +462,7 @@ impl TitleBar {
|
|||
);
|
||||
|
||||
if can_use_microphone && screen_sharing_supported {
|
||||
let trigger = IconButton::new("screen-share", ui::IconName::Screen)
|
||||
let trigger = IconButton::new("screen-share", IconName::Screen)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.icon_size(IconSize::Small)
|
||||
.toggle_state(is_screen_sharing)
|
||||
|
@ -498,7 +498,7 @@ impl TitleBar {
|
|||
trigger.render(window, cx),
|
||||
self.render_screen_list().into_any_element(),
|
||||
)
|
||||
.style(SplitButtonStyle::Outlined)
|
||||
.style(SplitButtonStyle::Transparent)
|
||||
.into_any_element(),
|
||||
);
|
||||
}
|
||||
|
@ -513,11 +513,11 @@ impl TitleBar {
|
|||
.with_handle(self.screen_share_popover_handle.clone())
|
||||
.trigger(
|
||||
ui::ButtonLike::new_rounded_right("screen-share-screen-list-trigger")
|
||||
.layer(ui::ElevationIndex::ModalSurface)
|
||||
.size(ui::ButtonSize::None)
|
||||
.child(
|
||||
div()
|
||||
.px_1()
|
||||
h_flex()
|
||||
.mx_neg_0p5()
|
||||
.h_full()
|
||||
.justify_center()
|
||||
.child(Icon::new(IconName::ChevronDownSmall).size(IconSize::XSmall)),
|
||||
)
|
||||
.toggle_state(self.screen_share_popover_handle.is_deployed()),
|
||||
|
|
|
@ -12,6 +12,7 @@ use super::ButtonLike;
|
|||
pub enum SplitButtonStyle {
|
||||
Filled,
|
||||
Outlined,
|
||||
Transparent,
|
||||
}
|
||||
|
||||
/// /// A button with two parts: a primary action on the left and a secondary action on the right.
|
||||
|
@ -44,10 +45,17 @@ impl SplitButton {
|
|||
|
||||
impl RenderOnce for SplitButton {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let is_filled_or_outlined = matches!(
|
||||
self.style,
|
||||
SplitButtonStyle::Filled | SplitButtonStyle::Outlined
|
||||
);
|
||||
|
||||
h_flex()
|
||||
.rounded_sm()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border.opacity(0.5))
|
||||
.when(is_filled_or_outlined, |this| {
|
||||
this.border_1()
|
||||
.border_color(cx.theme().colors().border.opacity(0.8))
|
||||
})
|
||||
.child(div().flex_grow().child(self.left))
|
||||
.child(
|
||||
div()
|
||||
|
|
|
@ -44,7 +44,7 @@ impl KeyBinding {
|
|||
pub fn for_action_in(
|
||||
action: &dyn Action,
|
||||
focus: &FocusHandle,
|
||||
window: &mut Window,
|
||||
window: &Window,
|
||||
cx: &App,
|
||||
) -> Option<Self> {
|
||||
let key_binding = window.highest_precedence_binding_for_action_in(action, focus)?;
|
||||
|
|
|
@ -50,7 +50,7 @@ impl RenderOnce for Popover {
|
|||
v_flex()
|
||||
.elevation_2(cx)
|
||||
.py(POPOVER_Y_PADDING / 2.)
|
||||
.children(self.children),
|
||||
.child(div().children(self.children)),
|
||||
)
|
||||
.when_some(self.aside, |this, aside| {
|
||||
this.child(
|
||||
|
|
|
@ -30,6 +30,7 @@ pub fn capture(directory: &std::path::Path) -> Result<collections::HashMap<Strin
|
|||
command.stdout(Stdio::piped());
|
||||
command.stderr(Stdio::piped());
|
||||
|
||||
let mut command_prefix = String::new();
|
||||
match shell_name {
|
||||
Some("tcsh" | "csh") => {
|
||||
// For csh/tcsh, login shell requires passing `-` as 0th argument (instead of `-l`)
|
||||
|
@ -40,13 +41,20 @@ pub fn capture(directory: &std::path::Path) -> Result<collections::HashMap<Strin
|
|||
command_string.push_str("emit fish_prompt;");
|
||||
command.arg("-l");
|
||||
}
|
||||
Some("nu") => {
|
||||
// nu needs special handling for -- options.
|
||||
command_prefix = String::from("^");
|
||||
}
|
||||
_ => {
|
||||
command.arg("-l");
|
||||
}
|
||||
}
|
||||
// cd into the directory, triggering directory specific side-effects (asdf, direnv, etc)
|
||||
command_string.push_str(&format!("cd '{}';", directory.display()));
|
||||
command_string.push_str(&format!("{} --printenv {}", zed_path, redir));
|
||||
command_string.push_str(&format!(
|
||||
"{}{} --printenv {}",
|
||||
command_prefix, zed_path, redir
|
||||
));
|
||||
command.args(["-i", "-c", &command_string]);
|
||||
|
||||
super::set_pre_exec_to_start_new_session(&mut command);
|
||||
|
|
|
@ -73,7 +73,7 @@ impl Workspace {
|
|||
|
||||
if let Some(terminal_provider) = self.terminal_provider.as_ref() {
|
||||
let task_status = terminal_provider.spawn(spawn_in_terminal, window, cx);
|
||||
cx.background_spawn(async move {
|
||||
let task = cx.background_spawn(async move {
|
||||
match task_status.await {
|
||||
Some(Ok(status)) => {
|
||||
if status.success() {
|
||||
|
@ -82,11 +82,11 @@ impl Workspace {
|
|||
log::debug!("Task spawn failed, code: {:?}", status.code());
|
||||
}
|
||||
}
|
||||
Some(Err(e)) => log::error!("Task spawn failed: {e}"),
|
||||
Some(Err(e)) => log::error!("Task spawn failed: {e:#}"),
|
||||
None => log::debug!("Task spawn got cancelled"),
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
self.scheduled_tasks.push(task);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1104,6 +1104,7 @@ pub struct Workspace {
|
|||
serialized_ssh_project: Option<SerializedSshProject>,
|
||||
_items_serializer: Task<Result<()>>,
|
||||
session_id: Option<String>,
|
||||
scheduled_tasks: Vec<Task<()>>,
|
||||
}
|
||||
|
||||
impl EventEmitter<Event> for Workspace {}
|
||||
|
@ -1435,6 +1436,7 @@ impl Workspace {
|
|||
_items_serializer,
|
||||
session_id: Some(session_id),
|
||||
serialized_ssh_project: None,
|
||||
scheduled_tasks: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Before Width: | Height: | Size: 225 KiB After Width: | Height: | Size: 237 KiB |
Before Width: | Height: | Size: 821 KiB After Width: | Height: | Size: 902 KiB |
|
@ -126,17 +126,28 @@ pub fn init(cx: &mut App) {
|
|||
cx.on_action(quit);
|
||||
|
||||
cx.on_action(|_: &RestoreBanner, cx| title_bar::restore_banner(cx));
|
||||
if ReleaseChannel::global(cx) == ReleaseChannel::Dev || cx.has_flag::<PanicFeatureFlag>() {
|
||||
cx.on_action(|_: &TestPanic, _| panic!("Ran the TestPanic action"));
|
||||
cx.on_action(|_: &TestCrash, _| {
|
||||
unsafe extern "C" {
|
||||
fn puts(s: *const i8);
|
||||
}
|
||||
unsafe {
|
||||
puts(0xabad1d3a as *const i8);
|
||||
}
|
||||
});
|
||||
}
|
||||
let flag = cx.wait_for_flag::<PanicFeatureFlag>();
|
||||
cx.spawn(async |cx| {
|
||||
if cx
|
||||
.update(|cx| ReleaseChannel::global(cx) == ReleaseChannel::Dev)
|
||||
.unwrap_or_default()
|
||||
|| flag.await
|
||||
{
|
||||
cx.update(|cx| {
|
||||
cx.on_action(|_: &TestPanic, _| panic!("Ran the TestPanic action"));
|
||||
cx.on_action(|_: &TestCrash, _| {
|
||||
unsafe extern "C" {
|
||||
fn puts(s: *const i8);
|
||||
}
|
||||
unsafe {
|
||||
puts(0xabad1d3a as *const i8);
|
||||
}
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
};
|
||||
})
|
||||
.detach();
|
||||
cx.on_action(|_: &OpenLog, cx| {
|
||||
with_active_or_new_workspace(cx, |workspace, window, cx| {
|
||||
open_log_file(workspace, window, cx);
|
||||
|
|
|
@ -6,13 +6,12 @@ Learn about all the settings you can customize in Zed's Agent Panel.
|
|||
|
||||
### Default Model {#default-model}
|
||||
|
||||
If you're using Zed's hosted LLM service, it sets `claude-sonnet-4` as the default model.
|
||||
But if you're not subscribed to the hosted service or simply just want to change it, you can do it so either via the model dropdown in the Agent Panel's bottom-right corner or by manually editing the `default_model` object in your settings:
|
||||
If you're using [Zed's hosted LLM service](./plans-and-usage.md), it sets `claude-sonnet-4` as the default model.
|
||||
But if you're not subscribed to it or simply just want to change it, you can do it so either via the model dropdown in the Agent Panel's bottom-right corner or by manually editing the `default_model` object in your settings:
|
||||
|
||||
```json
|
||||
{
|
||||
"agent": {
|
||||
"version": "2",
|
||||
"default_model": {
|
||||
"provider": "zed.dev",
|
||||
"model": "gpt-4o"
|
||||
|
@ -32,7 +31,6 @@ Assign distinct and specific models for the following AI-powered features in Zed
|
|||
```json
|
||||
{
|
||||
"agent": {
|
||||
"version": "2",
|
||||
"default_model": {
|
||||
"provider": "zed.dev",
|
||||
"model": "claude-sonnet-4"
|
||||
|
@ -53,7 +51,7 @@ Assign distinct and specific models for the following AI-powered features in Zed
|
|||
}
|
||||
```
|
||||
|
||||
> If a model isn't set for one of these features, they automatically fall back to using the default model.
|
||||
> If a custom model isn't set for one of these features, they automatically fall back to using the default model.
|
||||
|
||||
### Alternative Models for Inline Assists {#alternative-assists}
|
||||
|
||||
|
@ -128,6 +126,7 @@ You can choose between `thread` (the default) and `text_thread`:
|
|||
### Auto-run Commands
|
||||
|
||||
Control whether you want to allow the agent to run commands without asking you for permission.
|
||||
The default value is `false`.
|
||||
|
||||
```json
|
||||
{
|
||||
|
@ -142,6 +141,7 @@ Control whether you want to allow the agent to run commands without asking you f
|
|||
### Single-file Review
|
||||
|
||||
Control whether you want to see review actions (accept & reject) in single buffers after the agent is done performing edits.
|
||||
The default value is `false`.
|
||||
|
||||
```json
|
||||
{
|
||||
|
@ -158,6 +158,7 @@ When set to false, these controls are only available in the multibuffer review t
|
|||
### Sound Notification
|
||||
|
||||
Control whether you want to hear a notification sound when the agent is done generating changes or needs your input.
|
||||
The default value is `false`.
|
||||
|
||||
```json
|
||||
{
|
||||
|
@ -173,6 +174,7 @@ Control whether you want to hear a notification sound when the agent is done gen
|
|||
|
||||
Make a modifier (`cmd` on macOS, `ctrl` on Linux) required to send messages.
|
||||
This is encouraged for more thoughtful prompt crafting.
|
||||
The default value is `false`.
|
||||
|
||||
```json
|
||||
{
|
||||
|
@ -213,6 +215,7 @@ It is set to `true` by default, but if set to false, the card will be fully coll
|
|||
### Feedback Controls
|
||||
|
||||
Control whether you want to see the thumbs up/down buttons to give Zed feedback about the agent's performance.
|
||||
The default value is `true`.
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# Billing
|
||||
|
||||
We use Stripe as our billing and payments provider. All Pro plans require payment via credit card.
|
||||
For invoice-based billing, a Business plan is required. Contact sales@zed.dev for more information.
|
||||
For invoice-based billing, a Business plan is required. Contact [sales@zed.dev](mailto:sales@zed.dev) for more information.
|
||||
|
||||
## Settings {#settings}
|
||||
|
||||
|
@ -12,7 +12,8 @@ Clicking the button under Account Settings will navigate you to Stripe’s secur
|
|||
|
||||
Zed is billed on a monthly basis based on the date you initially subscribe.
|
||||
|
||||
We’ll also bill in-month for additional prompts used beyond your plan’s prompt limit, if usage exceeds $20 before month end. See [usage-based pricing](./plans-and-usage.md#ubp) for more.
|
||||
We’ll also bill in-month for additional prompts used beyond your plan’s prompt limit, if usage exceeds $20 before month end.
|
||||
See [usage-based pricing](./plans-and-usage.md#ubp) for more.
|
||||
|
||||
## Invoice History {#invoice-history}
|
||||
|
||||
|
@ -33,4 +34,4 @@ Zed partners with [Sphere](https://www.getsphere.com/) to calculate indirect tax
|
|||
If you have a VAT/GST ID, you can add it at [zed.dev/account](https://zed.dev/account) by clicking "Manage" on your subscription. Check the box that denotes you as a business.
|
||||
|
||||
Please note that changes to VAT/GST IDs and address will **only** affect future invoices — **we cannot modify historical invoices**.
|
||||
Questions or issues can be directed to billing-support@zed.dev.
|
||||
Questions or issues can be directed to [billing-support@zed.dev](mailto:billing-support@zed.dev).
|
||||
|
|
|
@ -6,7 +6,7 @@ When using AI in Zed, you can customize several aspects:
|
|||
2. [Model parameters and usage](./agent-settings.md#model-settings)
|
||||
3. [Interactions with the Agent Panel](./agent-settings.md#agent-panel-settings)
|
||||
|
||||
## Turning AI off entirely
|
||||
## Turning AI Off Entirely
|
||||
|
||||
We want to respect users who want to use Zed without interacting with AI whatsoever.
|
||||
To do that, add the following key to your `settings.json`:
|
||||
|
|
|
@ -2,13 +2,13 @@
|
|||
|
||||
To use AI in Zed, you need to have at least one large language model provider set up.
|
||||
|
||||
You can do that by either subscribing to [one of Zed's plans](./subscription.md), or by using API keys you already have for the supported providers.
|
||||
You can do that by either subscribing to [one of Zed's plans](./plans-and-usage.md), or by using API keys you already have for the supported providers.
|
||||
|
||||
## Use Your Own Keys {#use-your-own-keys}
|
||||
|
||||
If you already have an API key for an existing LLM provider—say Anthropic or OpenAI, for example—you can insert them in Zed and use the Agent Panel **_for free_**.
|
||||
|
||||
You can add your API key to a given provider either via the Agent Panel's settings UI or the `settings.json` directly, through the `language_models` key.
|
||||
You can add your API key to a given provider either via the Agent Panel's settings UI or directly via the `settings.json` through the `language_models` key.
|
||||
|
||||
## Supported Providers
|
||||
|
||||
|
@ -25,7 +25,7 @@ Here's all the supported LLM providers for which you can use your own API keys:
|
|||
| [Mistral](#mistral) | ✅ |
|
||||
| [Ollama](#ollama) | ✅ |
|
||||
| [OpenAI](#openai) | ✅ |
|
||||
| [OpenAI API Compatible](#openai-api-compatible) | 🚫 |
|
||||
| [OpenAI API Compatible](#openai-api-compatible) | ✅ |
|
||||
| [OpenRouter](#openrouter) | ✅ |
|
||||
| [Vercel](#vercel-v0) | ✅ |
|
||||
| [xAI](#xai) | ✅ |
|
||||
|
|