Compare commits
374 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88a37b04aa | ||
|
|
23dddba1e6 | ||
|
|
faa45c53d7 | ||
|
|
b31f893408 | ||
|
|
f888f3fc0b | ||
|
|
b01d1872cc | ||
|
|
e5c6a596a9 | ||
|
|
106aa0d9cc | ||
|
|
f7f90593ac | ||
|
|
8be3f48f37 | ||
|
|
76a8293cc6 | ||
|
|
2315962e18 | ||
|
|
f8673dacf5 | ||
|
|
72d354de6c | ||
|
|
09b93caa9b | ||
|
|
7c169fc9b5 | ||
|
|
2b36d4ec94 | ||
|
|
4a82b6c5ee | ||
|
|
5feb759c20 | ||
|
|
410348deb0 | ||
|
|
8e7f1899e1 | ||
|
|
aea1d48184 | ||
|
|
c946b98ea1 | ||
|
|
c6947ee4f0 | ||
|
|
b59f992928 | ||
|
|
0a21b845fa | ||
|
|
6a8be1714e | ||
|
|
a2aea00253 | ||
|
|
98c66eddb8 | ||
|
|
558bbfffae | ||
|
|
89ed0b9601 | ||
|
|
4b9334b910 | ||
|
|
47af878ebb | ||
|
|
5488398986 | ||
|
|
b1a7993544 | ||
|
|
b90fd4287f | ||
|
|
e1e2775b80 | ||
|
|
ed104ec5e0 | ||
|
|
67a491df50 | ||
|
|
f003036aec | ||
|
|
fbc784d323 | ||
|
|
296bb66b65 | ||
|
|
bb1a7ccbba | ||
|
|
289f420504 | ||
|
|
15ad986329 | ||
|
|
0d9715325c | ||
|
|
5ef5f3c5ca | ||
|
|
2d4afd2119 | ||
|
|
afcb8f2a3f | ||
|
|
cdce3b3620 | ||
|
|
bc6bb42745 | ||
|
|
7695c4b82e | ||
|
|
794ade8b6d | ||
|
|
f4bd524d7f | ||
|
|
9d82e148de | ||
|
|
f8d1062484 | ||
|
|
45af1fcc2f | ||
|
|
0aea5acc68 | ||
|
|
4d66d967f2 | ||
|
|
93e6b01486 | ||
|
|
00725273e4 | ||
|
|
c22fa9adee | ||
|
|
49b75e9e93 | ||
|
|
7be1f2418d | ||
|
|
17a0179f0a | ||
|
|
b8f3a9101c | ||
|
|
3824751e61 | ||
|
|
57766199cf | ||
|
|
0be83f1c67 | ||
|
|
f0927faf61 | ||
|
|
d2d116cb02 | ||
|
|
9f69b53869 | ||
|
|
48e085a523 | ||
|
|
3378f02b7e | ||
|
|
c110f78015 | ||
|
|
85b712c04e | ||
|
|
5fa212183a | ||
|
|
1501ae0013 | ||
|
|
3973142324 | ||
|
|
7878eacc73 | ||
|
|
72f8fa6d1e | ||
|
|
902c17ac1a | ||
|
|
efa3cc13ef | ||
|
|
65250fe08d | ||
|
|
77dc65d826 | ||
|
|
f9224b1d74 | ||
|
|
aa3437e98f | ||
|
|
397b5f9301 | ||
|
|
d43f464174 | ||
|
|
511fdaed43 | ||
|
|
a8bdf30259 | ||
|
|
2fced602b8 | ||
|
|
3fc84f8a62 | ||
|
|
5a218d8323 | ||
|
|
9353ba7887 | ||
|
|
8f952f1b58 | ||
|
|
6c5791532e | ||
|
|
691b3ca238 | ||
|
|
cfd5b8ff10 | ||
|
|
e5269212ad | ||
|
|
d2ef287791 | ||
|
|
109eddafd0 | ||
|
|
798aa50df8 | ||
|
|
11c7b498b3 | ||
|
|
ca34ead6d9 | ||
|
|
158f65fd1e | ||
|
|
fa6b1a0114 | ||
|
|
f3dc842ce6 | ||
|
|
7ccf8c2f8c | ||
|
|
8207621a4a | ||
|
|
ab90ed41da | ||
|
|
994d400ab8 | ||
|
|
cf13a76618 | ||
|
|
b64977f6f4 | ||
|
|
c3920b806b | ||
|
|
055a9f21a0 | ||
|
|
a55bd49c8d | ||
|
|
a57e4dc8a8 | ||
|
|
eef15abbe4 | ||
|
|
5aa8425449 | ||
|
|
05d3473df8 | ||
|
|
3dc36dc7c3 | ||
|
|
fd68265efd | ||
|
|
5e2da042ef | ||
|
|
c2fc70eef7 | ||
|
|
b02ae771cd | ||
|
|
3a1b1847c1 | ||
|
|
4aae7aed93 | ||
|
|
cef7d53607 | ||
|
|
3ad0546b0d | ||
|
|
e38f5759c4 | ||
|
|
2566acc2e7 | ||
|
|
45b3af713e | ||
|
|
ee9b60e60c | ||
|
|
2a0aad0aaa | ||
|
|
e7b5d93b7c | ||
|
|
c995d45bd9 | ||
|
|
a5b7cfd128 | ||
|
|
89e88c245e | ||
|
|
2a0170dc3c | ||
|
|
6a9a539b10 | ||
|
|
d7b403e981 | ||
|
|
290f84a9e1 | ||
|
|
08402e25e0 | ||
|
|
e911364664 | ||
|
|
4854f83e8c | ||
|
|
6a79c4e50a | ||
|
|
aea309160b | ||
|
|
43d0aae617 | ||
|
|
07252c3309 | ||
|
|
4d00d07df1 | ||
|
|
bf8e4272bc | ||
|
|
f787f7d291 | ||
|
|
0e7eea0d10 | ||
|
|
ff67f18e0d | ||
|
|
4abe14f94a | ||
|
|
cd50958727 | ||
|
|
2f812c339c | ||
|
|
993d5753d5 | ||
|
|
abb3ed1ed1 | ||
|
|
985350f9e8 | ||
|
|
0e9d955e9b | ||
|
|
5de544eb4b | ||
|
|
f21ba9e2c6 | ||
|
|
a2408f353b | ||
|
|
9071341a1d | ||
|
|
57b463fd0d | ||
|
|
631f9a1b31 | ||
|
|
af0c909924 | ||
|
|
15c9da4ea4 | ||
|
|
b446d66be7 | ||
|
|
a0f2019b6f | ||
|
|
6e4f747041 | ||
|
|
4ee52433ae | ||
|
|
f78a112387 | ||
|
|
66acc2698a | ||
|
|
707df51664 | ||
|
|
13df1dd5ff | ||
|
|
1f7ff956bc | ||
|
|
2d0f10c48a | ||
|
|
45ddf32a1d | ||
|
|
2658b2801e | ||
|
|
2a9355a3d2 | ||
|
|
fa788a39a4 | ||
|
|
7cdd808db2 | ||
|
|
29332c1962 | ||
|
|
fab450e39d | ||
|
|
4fb540d6d2 | ||
|
|
1e2b0fcab6 | ||
|
|
0af690080b | ||
|
|
dd52fb58fe | ||
|
|
913b9296d7 | ||
|
|
5c9363b1c4 | ||
|
|
cd9bcc7f09 | ||
|
|
65759d4316 | ||
|
|
ddd50aabba | ||
|
|
34bf6ebba6 | ||
|
|
a6956eebcb | ||
|
|
8b0ec287a5 | ||
|
|
c08851a85e | ||
|
|
b93e1c736b | ||
|
|
67027bb241 | ||
|
|
31afda3c0c | ||
|
|
3d4266bb8f | ||
|
|
4a87397d37 | ||
|
|
3da23cc65b | ||
|
|
b63d820be2 | ||
|
|
7e9d6cc25c | ||
|
|
8bf7dcb613 | ||
|
|
edceb7284f | ||
|
|
50985b7d23 | ||
|
|
be0d9eecb7 | ||
|
|
9863c8a44e | ||
|
|
a48247a313 | ||
|
|
5f0edd38f8 | ||
|
|
56b64b1d3f | ||
|
|
fdcd86617a | ||
|
|
3b428e2ecc | ||
|
|
986b446749 | ||
|
|
8713c556d6 | ||
|
|
326fe05b33 | ||
|
|
1f4c9b9427 | ||
|
|
14171e0721 | ||
|
|
326ab5fa3f | ||
|
|
2bc6e18ac9 | ||
|
|
c2c2264a60 | ||
|
|
6cd3726a5a | ||
|
|
7db110f48d | ||
|
|
6122f46095 | ||
|
|
500ceaabcd | ||
|
|
3e27fa1d92 | ||
|
|
11ac83f3d4 | ||
|
|
e90cf0b941 | ||
|
|
056003860a | ||
|
|
5d985fa1d8 | ||
|
|
7f70325a93 | ||
|
|
f3c332d839 | ||
|
|
446d333515 | ||
|
|
c0f75e1a17 | ||
|
|
708c2645d1 | ||
|
|
4272c1508e | ||
|
|
99466f4aeb | ||
|
|
9e280d0905 | ||
|
|
d81a8178e9 | ||
|
|
14cea06f0f | ||
|
|
caa520c499 | ||
|
|
64d0fec699 | ||
|
|
fa3e1ccc37 | ||
|
|
56b99f49fd | ||
|
|
2b888e1d30 | ||
|
|
96f9942791 | ||
|
|
939f9fffa3 | ||
|
|
230061a6cb | ||
|
|
1a76a6b0bf | ||
|
|
2eeab5b0bf | ||
|
|
30177b87d6 | ||
|
|
31aab89ab0 | ||
|
|
a3850b3d38 | ||
|
|
c7158f0bd7 | ||
|
|
3a651c546b | ||
|
|
87014cec71 | ||
|
|
2b671a46f2 | ||
|
|
eaccd542fd | ||
|
|
5a530ecd39 | ||
|
|
233e66d35f | ||
|
|
15353630e4 | ||
|
|
5289b815fe | ||
|
|
8515487bbc | ||
|
|
19ab1eb792 | ||
|
|
722a05bc21 | ||
|
|
5b3e371812 | ||
|
|
8eca7f32e2 | ||
|
|
1a1715766f | ||
|
|
241acbe4be | ||
|
|
6ea09beea8 | ||
|
|
3e50d997dd | ||
|
|
da8bf9ad79 | ||
|
|
589af59dfe | ||
|
|
254c7a330a | ||
|
|
b6cf398eab | ||
|
|
bc5c5cf5d6 | ||
|
|
bf8aba566c | ||
|
|
e14c9479e4 | ||
|
|
97af7e1bd9 | ||
|
|
c251f2a2d4 | ||
|
|
cc56196152 | ||
|
|
405244d422 | ||
|
|
35b4a918c9 | ||
|
|
56fd950d94 | ||
|
|
88af35fe47 | ||
|
|
57ab09c2da | ||
|
|
caa4b529e4 | ||
|
|
137081f050 | ||
|
|
7c1040bc93 | ||
|
|
ff79b29f38 | ||
|
|
2e41e312ad | ||
|
|
5f92ac25a7 | ||
|
|
fb88de9223 | ||
|
|
29111304dd | ||
|
|
0ffd93774c | ||
|
|
2da2ae65a0 | ||
|
|
4bdac8026c | ||
|
|
70bde54a2c | ||
|
|
43486c416c | ||
|
|
d197c96cdc | ||
|
|
7b6b75b63f | ||
|
|
fd64ee1bb6 | ||
|
|
e1d28ff957 | ||
|
|
64ce696aae | ||
|
|
87555d3f0b | ||
|
|
5b18ce79ab | ||
|
|
1dd470ca48 | ||
|
|
8bc8d61fa6 | ||
|
|
750ceeb760 | ||
|
|
4476860664 | ||
|
|
9a20843ba2 | ||
|
|
e421fc7a2d | ||
|
|
f461290ac3 | ||
|
|
6a24b2479c | ||
|
|
2ac99e7a11 | ||
|
|
d604b3b291 | ||
|
|
4002801034 | ||
|
|
fd8480a9dc | ||
|
|
1070de47ec | ||
|
|
cfe1adc792 | ||
|
|
fd05f17fa7 | ||
|
|
7e3fd7bb02 | ||
|
|
00097df0d5 | ||
|
|
c13322397e | ||
|
|
c1307cead4 | ||
|
|
8a7bd5f47b | ||
|
|
c287397a18 | ||
|
|
a7284adafa | ||
|
|
4314b35288 | ||
|
|
ed4deaa738 | ||
|
|
f0a91502a9 | ||
|
|
1ab659c71f | ||
|
|
d470411725 | ||
|
|
6c741292df | ||
|
|
29030a243c | ||
|
|
237ce5c8e8 | ||
|
|
0c88189aab | ||
|
|
1e60ebb2c6 | ||
|
|
9efe9df800 | ||
|
|
dab0b3509d | ||
|
|
0f72d7ed52 | ||
|
|
1ceda2babd | ||
|
|
ae0d4f6a0d | ||
|
|
8980526a85 | ||
|
|
b4dc7f8a8a | ||
|
|
b94649ce1e | ||
|
|
948c1f22bb | ||
|
|
b0eac4267d | ||
|
|
8e4555455c | ||
|
|
5b97cd1900 | ||
|
|
ceab139f54 | ||
|
|
4df7f52bf3 | ||
|
|
1e67e30034 | ||
|
|
758c5fb955 | ||
|
|
acb3ecef0c | ||
|
|
ad2bfa3edd | ||
|
|
1d72fa8e9e | ||
|
|
1ce384bbda | ||
|
|
9f302df6d6 | ||
|
|
ebad5ca50e | ||
|
|
b9ff538747 | ||
|
|
c0261a1ea9 | ||
|
|
f43bcc1492 | ||
|
|
e23a4564cc | ||
|
|
f82ef1f76f | ||
|
|
b4c2ae5196 | ||
|
|
0023773c68 | ||
|
|
0bde929d54 | ||
|
|
6f60939d30 |
@@ -24,7 +24,7 @@ workspace-members = [
|
||||
third-party = [
|
||||
{ name = "reqwest", version = "0.11.27" },
|
||||
# build of remote_server should not include scap / its x11 dependency
|
||||
{ name = "scap", git = "https://github.com/zed-industries/scap", rev = "270538dc780f5240723233ff901e1054641ed318" },
|
||||
{ name = "scap", git = "https://github.com/zed-industries/scap", rev = "808aa5c45b41e8f44729d02e38fd00a2fe2722e7" },
|
||||
]
|
||||
|
||||
[final-excludes]
|
||||
|
||||
30
.github/actionlint.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
# Configuration related to self-hosted runner.
|
||||
self-hosted-runner:
|
||||
# Labels of self-hosted runner in array of strings.
|
||||
labels:
|
||||
# GitHub-hosted Runners
|
||||
- github-8vcpu-ubuntu-2404
|
||||
- github-16vcpu-ubuntu-2404
|
||||
- windows-2025-16
|
||||
- windows-2025-32
|
||||
- windows-2025-64
|
||||
# Buildjet Ubuntu 20.04 - AMD x86_64
|
||||
- buildjet-2vcpu-ubuntu-2004
|
||||
- buildjet-4vcpu-ubuntu-2004
|
||||
- buildjet-8vcpu-ubuntu-2004
|
||||
- buildjet-16vcpu-ubuntu-2004
|
||||
- buildjet-32vcpu-ubuntu-2004
|
||||
# Buildjet Ubuntu 22.04 - AMD x86_64
|
||||
- buildjet-2vcpu-ubuntu-2204
|
||||
- buildjet-4vcpu-ubuntu-2204
|
||||
- buildjet-8vcpu-ubuntu-2204
|
||||
- buildjet-16vcpu-ubuntu-2204
|
||||
- buildjet-32vcpu-ubuntu-2204
|
||||
# Buildjet Ubuntu 22.04 - Graviton aarch64
|
||||
- buildjet-8vcpu-ubuntu-2204-arm
|
||||
- buildjet-16vcpu-ubuntu-2204-arm
|
||||
- buildjet-32vcpu-ubuntu-2204-arm
|
||||
- buildjet-64vcpu-ubuntu-2204-arm
|
||||
# Self Hosted Runners
|
||||
- self-mini-macos
|
||||
- self-32vcpu-windows-2022
|
||||
8
.github/actions/build_docs/action.yml
vendored
@@ -19,7 +19,7 @@ runs:
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: ./script/linux
|
||||
|
||||
- name: Check for broken links
|
||||
- name: Check for broken links (in MD)
|
||||
uses: lycheeverse/lychee-action@82202e5e9c2f4ef1a55a3d02563e1cb6041e5332 # v2.4.1
|
||||
with:
|
||||
args: --no-progress --exclude '^http' './docs/src/**/*'
|
||||
@@ -30,3 +30,9 @@ runs:
|
||||
run: |
|
||||
mkdir -p target/deploy
|
||||
mdbook build ./docs --dest-dir=../target/deploy/docs/
|
||||
|
||||
- name: Check for broken links (in HTML)
|
||||
uses: lycheeverse/lychee-action@82202e5e9c2f4ef1a55a3d02563e1cb6041e5332 # v2.4.1
|
||||
with:
|
||||
args: --no-progress --exclude '^http' 'target/deploy/docs/'
|
||||
fail: true
|
||||
|
||||
8
.github/workflows/bump_patch_version.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
run: |
|
||||
set -eux
|
||||
|
||||
channel=$(cat crates/zed/RELEASE_CHANNEL)
|
||||
channel="$(cat crates/zed/RELEASE_CHANNEL)"
|
||||
|
||||
tag_suffix=""
|
||||
case $channel in
|
||||
@@ -43,9 +43,9 @@ jobs:
|
||||
;;
|
||||
esac
|
||||
which cargo-set-version > /dev/null || cargo install cargo-edit
|
||||
output=$(cargo set-version -p zed --bump patch 2>&1 | sed 's/.* //')
|
||||
output="$(cargo set-version -p zed --bump patch 2>&1 | sed 's/.* //')"
|
||||
export GIT_COMMITTER_NAME="Zed Bot"
|
||||
export GIT_COMMITTER_EMAIL="hi@zed.dev"
|
||||
git commit -am "Bump to $output for @$GITHUB_ACTOR" --author "Zed Bot <hi@zed.dev>"
|
||||
git tag v${output}${tag_suffix}
|
||||
git push origin HEAD v${output}${tag_suffix}
|
||||
git tag "v${output}${tag_suffix}"
|
||||
git push origin HEAD "v${output}${tag_suffix}"
|
||||
|
||||
109
.github/workflows/ci.yml
vendored
@@ -34,6 +34,7 @@ jobs:
|
||||
run_license: ${{ steps.filter.outputs.run_license }}
|
||||
run_docs: ${{ steps.filter.outputs.run_docs }}
|
||||
run_nix: ${{ steps.filter.outputs.run_nix }}
|
||||
run_actionlint: ${{ steps.filter.outputs.run_actionlint }}
|
||||
runs-on:
|
||||
- ubuntu-latest
|
||||
steps:
|
||||
@@ -47,39 +48,40 @@ jobs:
|
||||
run: |
|
||||
if [ -z "$GITHUB_BASE_REF" ]; then
|
||||
echo "Not in a PR context (i.e., push to main/stable/preview)"
|
||||
COMPARE_REV=$(git rev-parse HEAD~1)
|
||||
COMPARE_REV="$(git rev-parse HEAD~1)"
|
||||
else
|
||||
echo "In a PR context comparing to pull_request.base.ref"
|
||||
git fetch origin "$GITHUB_BASE_REF" --depth=350
|
||||
COMPARE_REV=$(git merge-base "origin/${GITHUB_BASE_REF}" HEAD)
|
||||
COMPARE_REV="$(git merge-base "origin/${GITHUB_BASE_REF}" HEAD)"
|
||||
fi
|
||||
# Specify anything which should skip full CI in this regex:
|
||||
CHANGED_FILES="$(git diff --name-only "$COMPARE_REV" ${{ github.sha }})"
|
||||
|
||||
# Specify anything which should potentially skip full test suite in this regex:
|
||||
# - docs/
|
||||
# - script/update_top_ranking_issues/
|
||||
# - .github/ISSUE_TEMPLATE/
|
||||
# - .github/workflows/ (except .github/workflows/ci.yml)
|
||||
SKIP_REGEX='^(docs/|script/update_top_ranking_issues/|\.github/(ISSUE_TEMPLATE|workflows/(?!ci)))'
|
||||
if [[ $(git diff --name-only $COMPARE_REV ${{ github.sha }} | grep -vP "$SKIP_REGEX") ]]; then
|
||||
echo "run_tests=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "run_tests=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
if [[ $(git diff --name-only $COMPARE_REV ${{ github.sha }} | grep '^docs/') ]]; then
|
||||
echo "run_docs=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "run_docs=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
if [[ $(git diff --name-only $COMPARE_REV ${{ github.sha }} | grep -P '^(Cargo.lock|script/.*licenses)') ]]; then
|
||||
echo "run_license=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "run_license=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
NIX_REGEX='^(nix/|flake\.|Cargo\.|rust-toolchain.toml|\.cargo/config.toml)'
|
||||
if [[ $(git diff --name-only $COMPARE_REV ${{ github.sha }} | grep -P "$NIX_REGEX") ]]; then
|
||||
echo "run_nix=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "run_nix=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
echo "$CHANGED_FILES" | grep -qvP "$SKIP_REGEX" && \
|
||||
echo "run_tests=true" >> "$GITHUB_OUTPUT" || \
|
||||
echo "run_tests=false" >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "$CHANGED_FILES" | grep -qP '^docs/' && \
|
||||
echo "run_docs=true" >> "$GITHUB_OUTPUT" || \
|
||||
echo "run_docs=false" >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "$CHANGED_FILES" | grep -qP '^\.github/(workflows/|actions/|actionlint.yml)' && \
|
||||
echo "run_actionlint=true" >> "$GITHUB_OUTPUT" || \
|
||||
echo "run_actionlint=false" >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "$CHANGED_FILES" | grep -qP '^(Cargo.lock|script/.*licenses)' && \
|
||||
echo "run_license=true" >> "$GITHUB_OUTPUT" || \
|
||||
echo "run_license=false" >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "$CHANGED_FILES" | grep -qP '^(nix/|flake\.|Cargo\.|rust-toolchain.toml|\.cargo/config.toml)' && \
|
||||
echo "run_nix=true" >> "$GITHUB_OUTPUT" || \
|
||||
echo "run_nix=false" >> "$GITHUB_OUTPUT"
|
||||
|
||||
migration_checks:
|
||||
name: Check Postgres and Protobuf migrations, mergability
|
||||
@@ -89,8 +91,7 @@ jobs:
|
||||
needs.job_spec.outputs.run_tests == 'true'
|
||||
timeout-minutes: 60
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- macOS
|
||||
- self-mini-macos
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
@@ -112,11 +113,11 @@ jobs:
|
||||
run: |
|
||||
if [ -z "$GITHUB_BASE_REF" ];
|
||||
then
|
||||
echo "BUF_BASE_BRANCH=$(git merge-base origin/main HEAD)" >> $GITHUB_ENV
|
||||
echo "BUF_BASE_BRANCH=$(git merge-base origin/main HEAD)" >> "$GITHUB_ENV"
|
||||
else
|
||||
git checkout -B temp
|
||||
git merge -q origin/$GITHUB_BASE_REF -m "merge main into temp"
|
||||
echo "BUF_BASE_BRANCH=$GITHUB_BASE_REF" >> $GITHUB_ENV
|
||||
git merge -q "origin/$GITHUB_BASE_REF" -m "merge main into temp"
|
||||
echo "BUF_BASE_BRANCH=$GITHUB_BASE_REF" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
- uses: bufbuild/buf-setup-action@v1
|
||||
@@ -140,7 +141,7 @@ jobs:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- name: Add Rust to the PATH
|
||||
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||
- name: Install cargo-hakari
|
||||
uses: clechasseur/rs-cargo@8435b10f6e71c2e3d4d3b7573003a8ce4bfc6386 # v2
|
||||
with:
|
||||
@@ -178,7 +179,7 @@ jobs:
|
||||
- name: Prettier Check on /docs
|
||||
working-directory: ./docs
|
||||
run: |
|
||||
pnpm dlx prettier@${PRETTIER_VERSION} . --check || {
|
||||
pnpm dlx "prettier@${PRETTIER_VERSION}" . --check || {
|
||||
echo "To fix, run from the root of the Zed repo:"
|
||||
echo " cd docs && pnpm dlx prettier@${PRETTIER_VERSION} . --write && cd .."
|
||||
false
|
||||
@@ -188,7 +189,7 @@ jobs:
|
||||
|
||||
- name: Prettier Check on default.json
|
||||
run: |
|
||||
pnpm dlx prettier@${PRETTIER_VERSION} assets/settings/default.json --check || {
|
||||
pnpm dlx "prettier@${PRETTIER_VERSION}" assets/settings/default.json --check || {
|
||||
echo "To fix, run from the root of the Zed repo:"
|
||||
echo " pnpm dlx prettier@${PRETTIER_VERSION} assets/settings/default.json --write"
|
||||
false
|
||||
@@ -234,6 +235,20 @@ jobs:
|
||||
- name: Build docs
|
||||
uses: ./.github/actions/build_docs
|
||||
|
||||
actionlint:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository_owner == 'zed-industries' && needs.job_spec.outputs.run_actionlint == 'true'
|
||||
needs: [job_spec]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Download actionlint
|
||||
id: get_actionlint
|
||||
run: bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
|
||||
shell: bash
|
||||
- name: Check workflow files
|
||||
run: ${{ steps.get_actionlint.outputs.executable }} -color
|
||||
shell: bash
|
||||
|
||||
macos_tests:
|
||||
timeout-minutes: 60
|
||||
name: (macOS) Run Clippy and tests
|
||||
@@ -242,8 +257,7 @@ jobs:
|
||||
github.repository_owner == 'zed-industries' &&
|
||||
needs.job_spec.outputs.run_tests == 'true'
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- macOS
|
||||
- self-mini-macos
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
@@ -255,6 +269,10 @@ jobs:
|
||||
mkdir -p ./../.cargo
|
||||
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
|
||||
|
||||
- name: Check that Cargo.lock is up to date
|
||||
run: |
|
||||
cargo update --locked --workspace
|
||||
|
||||
- name: cargo clippy
|
||||
run: ./script/clippy
|
||||
|
||||
@@ -312,7 +330,7 @@ jobs:
|
||||
- buildjet-16vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: Add Rust to the PATH
|
||||
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
@@ -364,7 +382,7 @@ jobs:
|
||||
- buildjet-8vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: Add Rust to the PATH
|
||||
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
@@ -444,6 +462,7 @@ jobs:
|
||||
- job_spec
|
||||
- style
|
||||
- check_docs
|
||||
- actionlint
|
||||
- migration_checks
|
||||
# run_tests: If adding required tests, add them here and to script below.
|
||||
- workspace_hack
|
||||
@@ -465,6 +484,11 @@ jobs:
|
||||
if [[ "${{ needs.job_spec.outputs.run_docs }}" == "true" ]]; then
|
||||
[[ "${{ needs.check_docs.result }}" != 'success' ]] && { RET_CODE=1; echo "docs checks failed"; }
|
||||
fi
|
||||
|
||||
if [[ "${{ needs.job_spec.outputs.run_actionlint }}" == "true" ]]; then
|
||||
[[ "${{ needs.actionlint.result }}" != 'success' ]] && { RET_CODE=1; echo "actionlint checks failed"; }
|
||||
fi
|
||||
|
||||
# Only check test jobs if they were supposed to run
|
||||
if [[ "${{ needs.job_spec.outputs.run_tests }}" == "true" ]]; then
|
||||
[[ "${{ needs.workspace_hack.result }}" != 'success' ]] && { RET_CODE=1; echo "Workspace Hack failed"; }
|
||||
@@ -484,8 +508,7 @@ jobs:
|
||||
timeout-minutes: 120
|
||||
name: Create a macOS bundle
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- bundle
|
||||
- self-mini-macos
|
||||
if: |
|
||||
startsWith(github.ref, 'refs/tags/v')
|
||||
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
|
||||
@@ -748,7 +771,8 @@ jobs:
|
||||
timeout-minutes: 120
|
||||
name: Create a Windows installer
|
||||
runs-on: [self-hosted, Windows, X64]
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
if: contains(github.event.pull_request.labels.*.name, 'run-bundling')
|
||||
# if: (startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling'))
|
||||
needs: [windows_tests]
|
||||
env:
|
||||
AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }}
|
||||
@@ -787,7 +811,7 @@ jobs:
|
||||
- name: Upload Artifacts to release
|
||||
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
|
||||
# Re-enable when we are ready to publish windows preview releases
|
||||
if: false && ${{ !(contains(github.event.pull_request.labels.*.name, 'run-bundling')) && env.RELEASE_CHANNEL == 'preview' }} # upload only preview
|
||||
if: ${{ !(contains(github.event.pull_request.labels.*.name, 'run-bundling')) && env.RELEASE_CHANNEL == 'preview' }} # upload only preview
|
||||
with:
|
||||
draft: true
|
||||
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
||||
@@ -802,10 +826,9 @@ jobs:
|
||||
&& endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre')
|
||||
needs: [bundle-mac, bundle-linux-x86_x64, bundle-linux-aarch64, bundle-windows-x64]
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- bundle
|
||||
- self-mini-macos
|
||||
steps:
|
||||
- name: gh release
|
||||
run: gh release edit $GITHUB_REF_NAME --draft=false
|
||||
run: gh release edit "$GITHUB_REF_NAME" --draft=false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
URL="https://zed.dev/releases/stable/latest"
|
||||
fi
|
||||
|
||||
echo "URL=$URL" >> $GITHUB_OUTPUT
|
||||
echo "URL=$URL" >> "$GITHUB_OUTPUT"
|
||||
- name: Get content
|
||||
uses: 2428392/gh-truncate-string-action@b3ff790d21cf42af3ca7579146eedb93c8fb0757 # v1.4.1
|
||||
id: get-content
|
||||
@@ -50,9 +50,9 @@ jobs:
|
||||
PREVIEW_TAG="${VERSION}-pre"
|
||||
|
||||
if git rev-parse "$PREVIEW_TAG" > /dev/null 2>&1; then
|
||||
echo "was_promoted_from_preview=true" >> $GITHUB_OUTPUT
|
||||
echo "was_promoted_from_preview=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "was_promoted_from_preview=false" >> $GITHUB_OUTPUT
|
||||
echo "was_promoted_from_preview=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Send release notes email
|
||||
|
||||
9
.github/workflows/deploy_collab.yml
vendored
@@ -79,12 +79,12 @@ jobs:
|
||||
- name: Build docker image
|
||||
run: |
|
||||
docker build -f Dockerfile-collab \
|
||||
--build-arg GITHUB_SHA=$GITHUB_SHA \
|
||||
--tag registry.digitalocean.com/zed/collab:$GITHUB_SHA \
|
||||
--build-arg "GITHUB_SHA=$GITHUB_SHA" \
|
||||
--tag "registry.digitalocean.com/zed/collab:$GITHUB_SHA" \
|
||||
.
|
||||
|
||||
- name: Publish docker image
|
||||
run: docker push registry.digitalocean.com/zed/collab:${GITHUB_SHA}
|
||||
run: docker push "registry.digitalocean.com/zed/collab:${GITHUB_SHA}"
|
||||
|
||||
- name: Prune Docker system
|
||||
run: docker system prune --filter 'until=72h' -f
|
||||
@@ -131,7 +131,8 @@ jobs:
|
||||
source script/lib/deploy-helpers.sh
|
||||
export_vars_for_environment $ZED_KUBE_NAMESPACE
|
||||
|
||||
export ZED_DO_CERTIFICATE_ID=$(doctl compute certificate list --format ID --no-header)
|
||||
ZED_DO_CERTIFICATE_ID="$(doctl compute certificate list --format ID --no-header)"
|
||||
export ZED_DO_CERTIFICATE_ID
|
||||
export ZED_IMAGE_ID="registry.digitalocean.com/zed/collab:${GITHUB_SHA}"
|
||||
|
||||
export ZED_SERVICE_NAME=collab
|
||||
|
||||
2
.github/workflows/eval.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
- buildjet-16vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: Add Rust to the PATH
|
||||
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
10
.github/workflows/nix.yml
vendored
@@ -43,8 +43,8 @@ jobs:
|
||||
- name: Set path
|
||||
if: ${{ ! matrix.system.install_nix }}
|
||||
run: |
|
||||
echo "/nix/var/nix/profiles/default/bin" >> $GITHUB_PATH
|
||||
echo "/Users/administrator/.nix-profile/bin" >> $GITHUB_PATH
|
||||
echo "/nix/var/nix/profiles/default/bin" >> "$GITHUB_PATH"
|
||||
echo "/Users/administrator/.nix-profile/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31
|
||||
if: ${{ matrix.system.install_nix }}
|
||||
@@ -56,11 +56,13 @@ jobs:
|
||||
name: zed
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
pushFilter: "${{ inputs.cachix-filter }}"
|
||||
cachixArgs: '-v'
|
||||
cachixArgs: "-v"
|
||||
|
||||
- run: nix build .#${{ inputs.flake-output }} -L --accept-flake-config
|
||||
|
||||
- name: Limit /nix/store to 50GB on macs
|
||||
if: ${{ ! matrix.system.install_nix }}
|
||||
run: |
|
||||
[ $(du -sm /nix/store | cut -f1) -gt 50000 ] && nix-collect-garbage -d || :
|
||||
if [ "$(du -sm /nix/store | cut -f1)" -gt 50000 ]; then
|
||||
nix-collect-garbage -d || true
|
||||
fi
|
||||
|
||||
25
.github/workflows/release_nightly.yml
vendored
@@ -85,8 +85,7 @@ jobs:
|
||||
name: Create a macOS bundle
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- bundle
|
||||
- self-mini-macos
|
||||
needs: tests
|
||||
env:
|
||||
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
|
||||
@@ -112,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
|
||||
|
||||
@@ -132,11 +136,16 @@ jobs:
|
||||
clean: false
|
||||
|
||||
- name: Add Rust to the PATH
|
||||
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- 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
|
||||
|
||||
@@ -169,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
|
||||
|
||||
@@ -263,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
|
||||
|
||||
2
.github/workflows/unit_evals.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
- buildjet-16vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: Add Rust to the PATH
|
||||
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
645
Cargo.lock
generated
65
Cargo.toml
@@ -1,12 +1,13 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"crates/acp_thread",
|
||||
"crates/activity_indicator",
|
||||
"crates/acp",
|
||||
"crates/agent_ui",
|
||||
"crates/agent",
|
||||
"crates/agent_settings",
|
||||
"crates/agent_servers",
|
||||
"crates/agent_settings",
|
||||
"crates/agent_ui",
|
||||
"crates/ai_onboarding",
|
||||
"crates/anthropic",
|
||||
"crates/askpass",
|
||||
"crates/assets",
|
||||
@@ -28,6 +29,9 @@ members = [
|
||||
"crates/cli",
|
||||
"crates/client",
|
||||
"crates/clock",
|
||||
"crates/cloud_api_client",
|
||||
"crates/cloud_api_types",
|
||||
"crates/cloud_llm_client",
|
||||
"crates/collab",
|
||||
"crates/collab_ui",
|
||||
"crates/collections",
|
||||
@@ -47,8 +51,8 @@ members = [
|
||||
"crates/diagnostics",
|
||||
"crates/docs_preprocessor",
|
||||
"crates/editor",
|
||||
"crates/explorer_command_injector",
|
||||
"crates/eval",
|
||||
"crates/explorer_command_injector",
|
||||
"crates/extension",
|
||||
"crates/extension_api",
|
||||
"crates/extension_cli",
|
||||
@@ -69,7 +73,6 @@ members = [
|
||||
"crates/gpui",
|
||||
"crates/gpui_macros",
|
||||
"crates/gpui_tokio",
|
||||
|
||||
"crates/html_to_markdown",
|
||||
"crates/http_client",
|
||||
"crates/http_client_tls",
|
||||
@@ -98,14 +101,15 @@ members = [
|
||||
"crates/markdown_preview",
|
||||
"crates/media",
|
||||
"crates/menu",
|
||||
"crates/svg_preview",
|
||||
"crates/migrator",
|
||||
"crates/mistral",
|
||||
"crates/multi_buffer",
|
||||
"crates/nc",
|
||||
"crates/net",
|
||||
"crates/node_runtime",
|
||||
"crates/notifications",
|
||||
"crates/ollama",
|
||||
"crates/onboarding",
|
||||
"crates/open_ai",
|
||||
"crates/open_router",
|
||||
"crates/outline",
|
||||
@@ -137,6 +141,7 @@ members = [
|
||||
"crates/semantic_version",
|
||||
"crates/session",
|
||||
"crates/settings",
|
||||
"crates/settings_profile_selector",
|
||||
"crates/settings_ui",
|
||||
"crates/snippet",
|
||||
"crates/snippet_provider",
|
||||
@@ -149,6 +154,7 @@ members = [
|
||||
"crates/sum_tree",
|
||||
"crates/supermaven",
|
||||
"crates/supermaven_api",
|
||||
"crates/svg_preview",
|
||||
"crates/tab_switcher",
|
||||
"crates/task",
|
||||
"crates/tasks_ui",
|
||||
@@ -219,13 +225,14 @@ edition = "2024"
|
||||
# Workspace member crates
|
||||
#
|
||||
|
||||
acp = { path = "crates/acp" }
|
||||
acp_thread = { path = "crates/acp_thread" }
|
||||
agent = { path = "crates/agent" }
|
||||
activity_indicator = { path = "crates/activity_indicator" }
|
||||
agent_ui = { path = "crates/agent_ui" }
|
||||
agent_settings = { path = "crates/agent_settings" }
|
||||
agent_servers = { path = "crates/agent_servers" }
|
||||
ai = { path = "crates/ai" }
|
||||
ai_onboarding = { path = "crates/ai_onboarding" }
|
||||
anthropic = { path = "crates/anthropic" }
|
||||
askpass = { path = "crates/askpass" }
|
||||
assets = { path = "crates/assets" }
|
||||
@@ -247,6 +254,9 @@ channel = { path = "crates/channel" }
|
||||
cli = { path = "crates/cli" }
|
||||
client = { path = "crates/client" }
|
||||
clock = { path = "crates/clock" }
|
||||
cloud_api_client = { path = "crates/cloud_api_client" }
|
||||
cloud_api_types = { path = "crates/cloud_api_types" }
|
||||
cloud_llm_client = { path = "crates/cloud_llm_client" }
|
||||
collab = { path = "crates/collab" }
|
||||
collab_ui = { path = "crates/collab_ui" }
|
||||
collections = { path = "crates/collections" }
|
||||
@@ -317,10 +327,12 @@ menu = { path = "crates/menu" }
|
||||
migrator = { path = "crates/migrator" }
|
||||
mistral = { path = "crates/mistral" }
|
||||
multi_buffer = { path = "crates/multi_buffer" }
|
||||
nc = { path = "crates/nc" }
|
||||
net = { path = "crates/net" }
|
||||
node_runtime = { path = "crates/node_runtime" }
|
||||
notifications = { path = "crates/notifications" }
|
||||
ollama = { path = "crates/ollama" }
|
||||
onboarding = { path = "crates/onboarding" }
|
||||
open_ai = { path = "crates/open_ai" }
|
||||
open_router = { path = "crates/open_router", features = ["schemars"] }
|
||||
outline = { path = "crates/outline" }
|
||||
@@ -331,6 +343,7 @@ picker = { path = "crates/picker" }
|
||||
plugin = { path = "crates/plugin" }
|
||||
plugin_macros = { path = "crates/plugin_macros" }
|
||||
prettier = { path = "crates/prettier" }
|
||||
settings_profile_selector = { path = "crates/settings_profile_selector" }
|
||||
project = { path = "crates/project" }
|
||||
project_panel = { path = "crates/project_panel" }
|
||||
project_symbols = { path = "crates/project_symbols" }
|
||||
@@ -406,7 +419,8 @@ zlog_settings = { path = "crates/zlog_settings" }
|
||||
# External crates
|
||||
#
|
||||
|
||||
agentic-coding-protocol = { version = "0.0.9" }
|
||||
agentic-coding-protocol = "0.0.10"
|
||||
agent-client-protocol = "0.0.11"
|
||||
aho-corasick = "1.1"
|
||||
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
|
||||
any_vec = "0.14"
|
||||
@@ -434,9 +448,9 @@ aws-smithy-runtime-api = { version = "1.7.4", features = ["http-1x", "client"] }
|
||||
aws-smithy-types = { version = "1.3.0", features = ["http-body-1-x"] }
|
||||
base64 = "0.22"
|
||||
bitflags = "2.6.0"
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "416375211bb0b5826b3584dccdb6a43369e499ad" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "416375211bb0b5826b3584dccdb6a43369e499ad" }
|
||||
blade-util = { git = "https://github.com/kvark/blade", rev = "416375211bb0b5826b3584dccdb6a43369e499ad" }
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
|
||||
blade-util = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
|
||||
blake3 = "1.5.3"
|
||||
bytes = "1.0"
|
||||
cargo_metadata = "0.19"
|
||||
@@ -453,7 +467,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"
|
||||
@@ -476,6 +490,7 @@ heed = { version = "0.21.0", features = ["read-txn-no-tls"] }
|
||||
hex = "0.4.3"
|
||||
html5ever = "0.27.0"
|
||||
http = "1.1"
|
||||
http-body = "1.0"
|
||||
hyper = "0.14"
|
||||
ignore = "0.4.22"
|
||||
image = "0.25.1"
|
||||
@@ -489,18 +504,18 @@ json_dotpath = "1.1"
|
||||
jsonschema = "0.30.0"
|
||||
jsonwebtoken = "9.3"
|
||||
jupyter-protocol = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
|
||||
jupyter-websocket-client = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
|
||||
jupyter-websocket-client = { git = "https://github.com/ConradIrwin/runtimed" ,rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
|
||||
libc = "0.2"
|
||||
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
|
||||
linkify = "0.10.0"
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
|
||||
lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "6add7052b598ea1f40f7e8913622c3958b009b60" }
|
||||
lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "39f629bdd03d59abd786ed9fc27e8bca02c0c0ec" }
|
||||
markup5ever_rcdom = "0.3.0"
|
||||
metal = "0.29"
|
||||
moka = { version = "0.12.10", features = ["sync"] }
|
||||
naga = { version = "25.0", features = ["wgsl-in"] }
|
||||
nanoid = "0.4"
|
||||
nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
|
||||
nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
|
||||
nix = "0.29"
|
||||
num-format = "0.4.4"
|
||||
objc = "0.2"
|
||||
@@ -541,7 +556,7 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "951c77
|
||||
"stream",
|
||||
] }
|
||||
rsa = "0.9.6"
|
||||
runtimelib = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734", default-features = false, features = [
|
||||
runtimelib = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734", default-features = false, features = [
|
||||
"async-dispatcher-runtime",
|
||||
] }
|
||||
rust-embed = { version = "8.4", features = ["include-exclude"] }
|
||||
@@ -549,8 +564,7 @@ rustc-demangle = "0.1.23"
|
||||
rustc-hash = "2.1.0"
|
||||
rustls = { version = "0.23.26" }
|
||||
rustls-platform-verifier = "0.5.0"
|
||||
# When updating scap rev, also update it in .config/hakari.toml
|
||||
scap = { git = "https://github.com/zed-industries/scap", rev = "270538dc780f5240723233ff901e1054641ed318", default-features = false }
|
||||
scap = { git = "https://github.com/zed-industries/scap", rev = "808aa5c45b41e8f44729d02e38fd00a2fe2722e7", default-features = false }
|
||||
schemars = { version = "1.0", features = ["indexmap2"] }
|
||||
semver = "1.0"
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
@@ -638,7 +652,6 @@ which = "6.0.0"
|
||||
windows-core = "0.61"
|
||||
wit-component = "0.221"
|
||||
workspace-hack = "0.1.0"
|
||||
zed_llm_client = "= 0.8.6"
|
||||
zstd = "0.11"
|
||||
|
||||
[workspace.dependencies.async-stripe]
|
||||
@@ -665,14 +678,16 @@ features = [
|
||||
"UI_ViewManagement",
|
||||
"Wdk_System_SystemServices",
|
||||
"Win32_Globalization",
|
||||
"Win32_Graphics_Direct2D",
|
||||
"Win32_Graphics_Direct2D_Common",
|
||||
"Win32_Graphics_Direct3D",
|
||||
"Win32_Graphics_Direct3D11",
|
||||
"Win32_Graphics_Direct3D_Fxc",
|
||||
"Win32_Graphics_DirectComposition",
|
||||
"Win32_Graphics_DirectWrite",
|
||||
"Win32_Graphics_Dwm",
|
||||
"Win32_Graphics_Dxgi",
|
||||
"Win32_Graphics_Dxgi_Common",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_Graphics_Imaging",
|
||||
"Win32_Graphics_Imaging_D2D",
|
||||
"Win32_Networking_WinSock",
|
||||
"Win32_Security",
|
||||
"Win32_Security_Credentials",
|
||||
@@ -704,6 +719,7 @@ features = [
|
||||
[patch.crates-io]
|
||||
notify = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" }
|
||||
notify-types = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" }
|
||||
windows-capture = { git = "https://github.com/zed-industries/windows-capture.git", rev = "f0d6c1b6691db75461b732f6d5ff56eed002eeb9" }
|
||||
|
||||
# Makes the workspace hack crate refer to the local one, but only when you're building locally
|
||||
workspace-hack = { path = "tooling/workspace-hack" }
|
||||
@@ -712,6 +728,11 @@ workspace-hack = { path = "tooling/workspace-hack" }
|
||||
split-debuginfo = "unpacked"
|
||||
codegen-units = 16
|
||||
|
||||
# mirror configuration for crates compiled for the build platform
|
||||
# (without this cargo will compile ~400 crates twice)
|
||||
[profile.dev.build-override]
|
||||
codegen-units = 16
|
||||
|
||||
[profile.dev.package]
|
||||
taffy = { opt-level = 3 }
|
||||
cranelift-codegen = { opt-level = 3 }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Zed
|
||||
|
||||
[](https://zed.dev)
|
||||
[](https://github.com/zed-industries/zed/actions/workflows/ci.yml)
|
||||
|
||||
Welcome to Zed, a high-performance, multiplayer code editor from the creators of [Atom](https://github.com/atom/atom) and [Tree-sitter](https://github.com/tree-sitter/tree-sitter).
|
||||
|
||||
8
assets/badge/v0.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"label": "",
|
||||
"message": "Zed",
|
||||
"logoSvg": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 96 96\"><rect width=\"96\" height=\"96\" fill=\"#000\"/><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M9 6C7.34315 6 6 7.34315 6 9V75H0V9C0 4.02944 4.02944 0 9 0H89.3787C93.3878 0 95.3955 4.84715 92.5607 7.68198L43.0551 57.1875H57V51H63V58.6875C63 61.1728 60.9853 63.1875 58.5 63.1875H37.0551L26.7426 73.5H73.5V36H79.5V73.5C79.5 76.8137 76.8137 79.5 73.5 79.5H20.7426L10.2426 90H87C88.6569 90 90 88.6569 90 87V21H96V87C96 91.9706 91.9706 96 87 96H6.62132C2.61224 96 0.604504 91.1529 3.43934 88.318L52.7574 39H39V45H33V37.5C33 35.0147 35.0147 33 37.5 33H58.7574L69.2574 22.5H22.5V60H16.5V22.5C16.5 19.1863 19.1863 16.5 22.5 16.5H75.2574L85.7574 6H9Z\" fill=\"#fff\"/></svg>",
|
||||
"logoWidth": 16,
|
||||
"labelColor": "black",
|
||||
"color": "white"
|
||||
}
|
||||
3
assets/icons/ai_claude.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.35443 9.97775L6.71495 8.65418L6.75443 8.53883L6.71495 8.47508H6.59948L6.20456 8.45081L4.8557 8.41436L3.68608 8.36579L2.55291 8.30507L2.26734 8.24438L2 7.89224L2.02734 7.71617L2.26734 7.55528L2.61063 7.58564L3.37013 7.63724L4.50937 7.71617L5.3357 7.76474L6.56 7.89224H6.75443L6.78176 7.81331L6.71495 7.76474L6.66329 7.71617L5.48456 6.91778L4.20861 6.07388L3.54025 5.58815L3.17873 5.34227L2.99646 5.11157L2.91747 4.60764L3.24557 4.24639L3.68608 4.27675L3.79848 4.30711L4.24506 4.65014L5.19899 5.38781L6.44456 6.30458L6.62684 6.45635L6.69974 6.40475L6.70886 6.36833L6.62684 6.23171L5.94938 5.00834L5.22632 3.76372L4.9043 3.24766L4.81924 2.93802C4.78886 2.81053 4.7676 2.70427 4.7676 2.57374L5.14127 2.06678L5.34785 2L5.84609 2.06678L6.0557 2.24893L6.36557 2.95624L6.86684 4.07033L7.64456 5.58512L7.87241 6.0344L7.99391 6.45029L8.03948 6.57779H8.11847V6.50492L8.18228 5.6519L8.30075 4.6046L8.41619 3.25677L8.4557 2.87731L8.64404 2.42196L9.01772 2.17607L9.30938 2.31571L9.54938 2.65874L9.51596 2.88034L9.37316 3.80622L9.09368 5.25728L8.9114 6.22868H9.01772L9.13925 6.10727L9.6314 5.45459L10.4577 4.42246L10.8223 4.01265L11.2476 3.56033L11.521 3.3448H12.0375L12.4172 3.90944L12.2471 4.49228L11.7154 5.1662L11.275 5.73692L10.643 6.58691L10.2481 7.26689L10.2846 7.32152L10.3787 7.31243L11.8066 7.00886L12.5782 6.86921L13.4987 6.71135L13.915 6.90563L13.9605 7.10297L13.7965 7.50671L12.8122 7.74956L11.6577 7.98026L9.93824 8.38706L9.91697 8.40224L9.94127 8.43257L10.716 8.50544L11.0471 8.52365H11.8582L13.3681 8.63597L13.763 8.89703L14 9.21578L13.9605 9.45863L13.3529 9.76829L12.5327 9.57398L10.6187 9.11864L9.96254 8.95472H9.8714V9.00935L10.4182 9.54365L11.4208 10.4483L12.6754 11.614L12.7393 11.9023L12.5782 12.13L12.4081 12.1057L11.3053 11.277L10.88 10.9036L9.91697 10.0931H9.85316V10.1781L10.075 10.5029L11.2476 12.2636L11.3083 12.804L11.2233 12.98L10.9195 13.0863L10.5853 13.0255L9.89873 12.0632L9.19088 10.9795L8.61974 10.0081L8.54987 10.0476L8.21267 13.6752L8.05469 13.8604L7.69013 14L7.38632 13.7693L7.22531 13.3959L7.38632 12.6582L7.58075 11.6959L7.73873 10.9309L7.88153 9.98078L7.96658 9.66506L7.96052 9.64382L7.89062 9.65291L7.17368 10.6365L6.08303 12.1088L5.22026 13.0316L5.01368 13.1136L4.65519 12.9284L4.68861 12.5975L4.88911 12.303L6.08303 10.7852L6.80303 9.84416L7.26785 9.30077L7.26482 9.22187H7.23746L4.06582 11.2801L3.50076 11.3529L3.25772 11.1252L3.2881 10.7518L3.40354 10.6304L4.35747 9.97469L4.35443 9.97775Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -1 +1,3 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Google Gemini</title><path d="M11.04 19.32Q12 21.51 12 24q0-2.49.93-4.68.96-2.19 2.58-3.81t3.81-2.55Q21.51 12 24 12q-2.49 0-4.68-.93a12.3 12.3 0 0 1-3.81-2.58 12.3 12.3 0 0 1-2.58-3.81Q12 2.49 12 0q0 2.49-.96 4.68-.93 2.19-2.55 3.81a12.3 12.3 0 0 1-3.81 2.58Q2.49 12 0 12q2.49 0 4.68.96 2.19.93 3.81 2.55t2.55 3.81"/></svg>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.44 12.27C7.81333 13.1217 8 14.0317 8 15C8 14.0317 8.18083 13.1217 8.5425 12.27C8.91583 11.4183 9.4175 10.6775 10.0475 10.0475C10.6775 9.4175 11.4183 8.92167 12.27 8.56C13.1217 8.18667 14.0317 8 15 8C14.0317 8 13.1217 7.81917 12.27 7.4575C11.4411 7.1001 10.6871 6.5895 10.0475 5.9525C9.4105 5.31293 8.8999 4.55891 8.5425 3.73C8.18083 2.87833 8 1.96833 8 1C8 1.96833 7.81333 2.87833 7.44 3.73C7.07833 4.58167 6.5825 5.3225 5.9525 5.9525C5.31293 6.5895 4.55891 7.1001 3.73 7.4575C2.87833 7.81917 1.96833 8 1 8C1.96833 8 2.87833 8.18667 3.73 8.56C4.58167 8.92167 5.3225 9.4175 5.9525 10.0475C6.5825 10.6775 7.07833 11.4183 7.44 12.27Z" fill="black"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 402 B After Width: | Height: | Size: 762 B |
4
assets/icons/ai_open_ai_compat.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.25669 0.999943C8.27509 0.993825 9.24655 1.42125 9.9227 2.17279C11.4427 1.85079 12.9991 2.53518 13.7733 3.86518C14.159 4.5149 14.3171 5.26409 14.2372 5.99994H13.2967C13.3789 5.42185 13.265 4.8321 12.9686 4.32514C12.2353 3.06961 10.6088 2.63919 9.33676 3.36322L6.48032 4.98822C6.46926 4.99697 6.46284 5.01135 6.46372 5.02533V6.38568L9.91294 4.42084C10.0565 4.33818 10.2336 4.33823 10.3768 4.42084L13.1502 5.99994H11.2948L9.88364 5.19623C9.87034 5.19054 9.85459 5.19128 9.84262 5.19916L8.64926 5.87983L8.8602 5.99994H7.99985C6.89539 6.00004 5.99988 6.89547 5.99985 7.99994V9.34955L3.90219 8.15522C3.75815 8.07431 3.66897 7.92228 3.66977 7.75873V4.53803C3.66977 4.50828 3.67172 4.4654 3.67172 4.44135C3.08836 4.65262 2.59832 5.0599 2.28794 5.59174C1.55635 6.84647 1.99122 8.44936 3.26059 9.17475L5.99985 10.7363V11.6162C5.87564 11.6568 5.73827 11.6456 5.6229 11.579L2.7977 9.96869C2.77156 9.95382 2.73449 9.9311 2.71372 9.91889C2.60687 10.5231 2.7194 11.1466 3.0311 11.6777C3.6435 12.7209 4.87159 13.1902 5.99985 12.9023V13.8398C4.50443 14.1233 2.98758 13.4424 2.22641 12.1347C1.71174 11.2677 1.60096 10.2237 1.9227 9.27045C0.880739 8.13295 0.703328 6.46023 1.48325 5.13373C1.98739 4.26024 2.84863 3.64401 3.84653 3.44233C4.3245 1.9837 5.70306 0.996447 7.25669 0.999943ZM7.25766 1.91498C5.78932 1.9143 4.59839 3.08914 4.59751 4.53803V7.79193C4.59926 7.80578 4.60735 7.81796 4.61997 7.82416L5.8143 8.50483L5.81626 4.57611C5.81537 4.41216 5.90431 4.2606 6.04868 4.17963L8.87387 2.56928C8.89868 2.55441 8.93612 2.53379 8.95786 2.5224C8.48035 2.13046 7.8788 1.91498 7.25766 1.91498Z" fill="black"/>
|
||||
<path d="M13.5 6C14.6046 6 15.5 6.89543 15.5 8V13.5C15.5 14.6046 14.6046 15.5 13.5 15.5H8C6.89543 15.5 6 14.6046 6 13.5V8C6 6.89543 6.89543 6 8 6H13.5ZM10.8916 8.02539C10.0563 8.02539 9.33453 8.27982 8.81934 8.76562C8.30213 9.25335 8.02547 9.94371 8.02539 10.748C8.02539 11.557 8.29852 12.2492 8.81543 12.7373C9.33013 13.2232 10.0521 13.4746 10.8916 13.4746C11.9865 13.4745 12.8545 13.1022 13.3076 12.3525C13.3894 12.2176 13.4521 12.0693 13.4521 11.8857C13.4521 11.4795 13.0933 11.2773 12.7842 11.2773C12.6604 11.2774 12.5292 11.3025 12.4072 11.3779C12.2862 11.4529 12.2058 11.5586 12.1494 11.666L12.1475 11.6689C11.9677 12.0213 11.5535 12.246 10.8955 12.2461C10.4219 12.2461 10.0667 12.0932 9.83008 11.8506C9.59255 11.607 9.44141 11.2389 9.44141 10.748C9.44148 10.264 9.59319 9.89628 9.83203 9.65137C10.0702 9.40725 10.4255 9.25391 10.8916 9.25391C11.4912 9.25399 11.9415 9.50614 12.1289 9.8916V9.89062C12.1888 10.0157 12.276 10.1311 12.4023 10.2129C12.5303 10.2956 12.6724 10.3271 12.8115 10.3271C12.9661 10.3271 13.1303 10.2857 13.2627 10.1758C13.4018 10.0603 13.4746 9.89383 13.4746 9.71582C13.4746 9.61857 13.4542 9.52036 13.4199 9.42773L13.3818 9.33691C12.9749 8.49175 11.9927 8.02548 10.8916 8.02539ZM10.3203 8.97852L10.1494 9.03516C10.2095 9.01178 10.2716 8.99089 10.3359 8.97363C10.3307 8.97505 10.3256 8.97706 10.3203 8.97852ZM10.4814 8.94141C10.4969 8.9385 10.5126 8.93616 10.5283 8.93359C10.5126 8.93617 10.4969 8.9385 10.4814 8.94141ZM10.6709 8.91504C10.6819 8.91399 10.693 8.913 10.7041 8.91211C10.693 8.913 10.6819 8.91399 10.6709 8.91504Z" fill="black" fill-opacity="0.95"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
@@ -1,5 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor" stroke="currentColor">
|
||||
<g clip-path="url(#clip0_205_3)">
|
||||
<g>
|
||||
<path d="M0.094 7.78c0.469 0 2.281 -0.405 3.219 -0.936s0.938 -0.531 2.875 -1.906c2.453 -1.741 4.188 -1.158 7.031 -1.158" stroke-width="2.8125" />
|
||||
<path d="m15.969 3.797 -4.805 2.774V1.023z" />
|
||||
<path d="M0 7.781c0.469 0 2.281 0.405 3.219 0.936s0.938 0.531 2.875 1.906C8.547 12.364 10.281 11.781 13.125 11.781" stroke-width="2.8125" />
|
||||
|
||||
|
Before Width: | Height: | Size: 575 B After Width: | Height: | Size: 545 B |
@@ -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 |
3
assets/icons/editor_atom.svg
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
9
assets/icons/editor_cursor.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path opacity="0.6" d="M3.5 11V5.5L8.5 8L3.5 11Z" fill="black"/>
|
||||
<path opacity="0.4" d="M8.5 14L3.5 11L8.5 8V14Z" fill="black"/>
|
||||
<path opacity="0.6" d="M8.5 5.5H3.5L8.5 2.5L8.5 5.5Z" fill="black"/>
|
||||
<path opacity="0.8" d="M8.5 5.5V2.5L13.5 5.5H8.5Z" fill="black"/>
|
||||
<path opacity="0.2" d="M13.5 11L8.5 14L11 9.5L13.5 11Z" fill="black"/>
|
||||
<path opacity="0.5" d="M13.5 11L11 9.5L13.5 5V11Z" fill="black"/>
|
||||
<path d="M3.5 11V5L8.5 2.11325L13.5 5V11L8.5 13.8868L3.5 11Z" stroke="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 583 B |
10
assets/icons/editor_emacs.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2716_663)">
|
||||
<path d="M8.47552 2.45453C11.5167 2.45457 13.9814 4.94501 13.9814 8.01623C13.9814 11.0875 11.5167 13.578 8.47552 13.5781C5.43427 13.5781 2.96948 11.0875 2.96948 8.01623C2.9695 4.94498 5.43429 2.45453 8.47552 2.45453ZM10.8795 4.70348C10.7605 4.16887 10.1328 3.85468 9.53627 3.96342C8.97622 4.06552 7.62871 4.45681 7.62057 4.45916C9.29414 4.44469 9.57429 4.4726 9.69939 4.64751C9.77324 4.7508 9.66576 4.89248 9.21944 4.96538C8.73515 5.04447 7.73014 5.13958 7.72343 5.14022C6.75441 5.19776 6.07177 5.20168 5.86705 5.63512C5.73334 5.91827 6.00968 6.16857 6.13082 6.32527C6.64271 6.89455 7.38215 7.20158 7.85809 7.42767C8.03716 7.51274 8.56257 7.67345 8.56257 7.67345C7.01855 7.58853 5.90474 8.06267 5.2514 8.60855C4.51246 9.29204 4.83937 10.1067 6.35327 10.6084C7.24742 10.9047 7.69094 11.0439 9.02473 10.9238C9.81031 10.8815 9.9342 10.9068 9.94203 10.9712C9.95275 11.062 9.06932 11.2874 8.82812 11.357C8.21455 11.534 6.60645 11.8913 6.59758 11.8932C6.60115 11.8935 7.06249 11.9257 7.65531 11.8735C7.89632 11.8522 8.81142 11.7624 9.49557 11.6123C9.49557 11.6123 10.3297 11.4338 10.7759 11.2693C11.2429 11.0973 11.497 10.9512 11.6113 10.7443C11.6063 10.7019 11.6465 10.5516 11.4313 10.4613C10.8807 10.2304 10.2423 10.2721 8.9789 10.2453C7.57789 10.1972 7.11184 9.9626 6.86356 9.77373C6.62548 9.58212 6.74518 9.05204 7.76528 8.5851C8.27917 8.33646 10.2935 7.87759 10.2935 7.87759C9.61511 7.54227 8.35014 6.95284 8.09005 6.82552C7.86199 6.71388 7.49701 6.54572 7.4179 6.34233C7.32824 6.14709 7.6297 5.97888 7.79813 5.9307C8.34057 5.77424 9.10635 5.67701 9.8033 5.66609C10.1536 5.66061 10.2105 5.63806 10.2105 5.63806C10.6939 5.55787 11.0121 5.22722 10.8795 4.70348Z" fill="black"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2716_663">
|
||||
<rect width="12" height="12" fill="white" transform="translate(2.5 2)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
3
assets/icons/editor_jet_brains.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.6725 13.9985C3.36161 13.9982 3.06354 13.8746 2.84371 13.6548C2.62388 13.435 2.50026 13.1369 2.5 12.826V7.494C2.5 6.8325 2.7675 6.185 3.2365 5.7165L6.219 2.736C6.45192 2.50247 6.72867 2.31724 7.03335 2.19094C7.33804 2.06464 7.66467 1.99975 7.9945 2H13.3275C13.6384 2.00027 13.9365 2.12388 14.1563 2.34371C14.3761 2.56354 14.4997 2.86162 14.5 3.1725V8.5045C14.4983 9.17074 14.2336 9.80936 13.7635 10.2815L10.781 13.264C10.5477 13.4976 10.2706 13.6829 9.96561 13.8092C9.66059 13.9355 9.33364 14.0003 9.0035 14V13.9985H3.6725ZM8.157 10.5715H5.243V11.257H8.157V10.5715ZM4.4815 5.257H11.243V12.0165L13.3715 9.888C13.7373 9.52036 13.9433 9.02316 13.9445 8.5045V3.1725C13.9445 2.8335 13.6685 2.5555 13.3275 2.5555H7.9945C7.73753 2.55499 7.483 2.6053 7.24556 2.70356C7.00813 2.80181 6.79246 2.94606 6.611 3.128L4.4815 5.257ZM4.3855 5.353L3.628 6.11C3.26258 6.47809 3.0569 6.97533 3.0555 7.494V12.826C3.0555 13.165 3.3315 13.443 3.6725 13.443H9.0055C9.26249 13.4434 9.51701 13.3929 9.75445 13.2946C9.99188 13.1963 10.2075 13.052 10.389 12.87L11.145 12.1145H4.3855V5.353Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
5
assets/icons/editor_sublime.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.0945 8.01611C13.0945 7.87619 12.9911 7.79551 12.8642 7.8356L4.13456 10.6038C4.00742 10.6441 3.90427 10.7904 3.90427 10.9301V13.7593C3.90427 13.8992 4.00742 13.9801 4.13456 13.9398L12.8642 11.1719C12.9911 11.1315 13.0945 10.9852 13.0945 10.8453V8.01611Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.90427 7.92597C3.90427 8.06588 4.00742 8.21218 4.13456 8.25252L12.8655 11.0209C12.9926 11.0613 13.0958 10.9803 13.0958 10.8407V8.01124C13.0958 7.87158 12.9926 7.72529 12.8655 7.68494L4.13456 4.91652C4.00742 4.87618 3.90427 4.95686 3.90427 5.09677V7.92597Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.0945 2.20248C13.0945 2.06256 12.9911 1.98163 12.8642 2.02197L4.13456 4.78988C4.00742 4.83022 3.90427 4.97652 3.90427 5.11644V7.94563C3.90427 8.08554 4.00742 8.16622 4.13456 8.12614L12.8642 5.35797C12.9911 5.31763 13.0945 5.17133 13.0945 5.03167V2.20248Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
3
assets/icons/editor_vs_code.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.0094 13.9181C11.1984 13.9917 11.4139 13.987 11.6047 13.8952L14.0753 12.7064C14.3349 12.5814 14.5 12.3187 14.5 12.0305V3.9696C14.5 3.68136 14.3349 3.41862 14.0753 3.2937L11.6047 2.10485C11.3543 1.98438 11.0614 2.01389 10.8416 2.17363C10.8102 2.19645 10.7803 2.22193 10.7523 2.25001L6.02261 6.56498L3.96246 5.00115C3.77068 4.85558 3.50244 4.86751 3.32432 5.02953L2.66356 5.63059C2.44569 5.82877 2.44544 6.17152 2.66302 6.37004L4.44965 8.00001L2.66302 9.62998C2.44544 9.82849 2.44569 10.1713 2.66356 10.3694L3.32432 10.9705C3.50244 11.1325 3.77068 11.1444 3.96246 10.9989L6.02261 9.43504L10.7523 13.75C10.8271 13.8249 10.915 13.8812 11.0094 13.9181ZM11.5018 5.27587L7.91309 8.00001L11.5018 10.7241V5.27587Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 876 B |
@@ -1,8 +1,5 @@
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M3 1C2.44771 1 2 1.44772 2 2V13C2 13.5523 2.44772 14 3 14H10.5C10.7761 14 11 13.7761 11 13.5C11 13.2239 10.7761 13 10.5 13H3V2L10.5 2C10.7761 2 11 1.77614 11 1.5C11 1.22386 10.7761 1 10.5 1H3ZM12.6036 4.89645C12.4083 4.70118 12.0917 4.70118 11.8964 4.89645C11.7012 5.09171 11.7012 5.40829 11.8964 5.60355L13.2929 7H6.5C6.22386 7 6 7.22386 6 7.5C6 7.77614 6.22386 8 6.5 8H13.2929L11.8964 9.39645C11.7012 9.59171 11.7012 9.90829 11.8964 10.1036C12.0917 10.2988 12.4083 10.2988 12.6036 10.1036L14.8536 7.85355C15.0488 7.65829 15.0488 7.34171 14.8536 7.14645L12.6036 4.89645Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.437 11.0461L13.4831 8L10.437 4.95392" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13 8L8 8" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6.6553 13.4659H4.21843C3.89528 13.4659 3.58537 13.3375 3.35687 13.109C3.12837 12.8805 3 12.5706 3 12.2475V3.71843C3 3.39528 3.12837 3.08537 3.35687 2.85687C3.58537 2.62837 3.89528 2.5 4.21843 2.5H6.6553" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 768 B After Width: | Height: | Size: 637 B |
3
assets/icons/file_icons/surrealql.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.628 11.0743V10.4575H8.45562L8.65084 10.2445C8.75911 10.1264 8.96952 9.79454 9.11862 9.50789C9.52153 8.73047 9.51798 7.25107 9.11862 6.43992C8.58614 5.35722 7.49453 4.56381 6.24942 4.35703C4.59252 4.08192 2.86196 5.00312 2.14045 6.54287C1.77038 7.33182 1.77038 8.64437 2.14045 9.43333C2.45905 10.1122 3.11309 10.8204 3.73609 11.1595C4.51439 11.5828 5.18264 11.676 7.51312 11.6848L9.62627 11.6928L9.628 11.0743ZM5.30605 10.169C4.24109 10.0111 3.45215 9.07124 3.45659 7.96813C3.45659 7.33004 3.70064 6.80022 4.18697 6.36182C4.67685 5.91986 5.1312 5.77344 5.86602 5.82048C7.00287 5.89236 7.82382 6.79845 7.82382 7.98056C7.82382 8.61332 7.71996 8.91682 7.33036 9.42534C6.90172 9.98444 6.08345 10.2853 5.30692 10.1699M15.1374 10.9802V10.2684H11.8138V4.47509H10.1986V11.6928H15.1374V10.9802Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 916 B |
@@ -1,3 +1,5 @@
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.72742 8.83338C3.63539 8.57302 3.34973 8.43656 3.08937 8.52858C2.82901 8.6206 2.69255 8.90626 2.78458 9.16662C2.86101 9.38288 2.95188 9.59228 3.056 9.79364C3.81427 11.2601 5.27842 12.3044 7.00014 12.4753L7.00014 14L5.50014 14C5.22399 14 5.00014 14.2239 5.00014 14.5C5.00014 14.7761 5.22399 15 5.50014 15L7.50014 15L9.50014 15C9.77628 15 10.0001 14.7761 10.0001 14.5C10.0001 14.2239 9.77628 14 9.50014 14L8.00014 14L8.00014 12.4753C9.72168 12.3043 11.1857 11.26 11.9439 9.79364C12.048 9.59228 12.1389 9.38288 12.2153 9.16662C12.3073 8.90626 12.1709 8.6206 11.9105 8.52858C11.6501 8.43656 11.3645 8.57302 11.2725 8.83338C11.2114 9.00607 11.1388 9.17337 11.0556 9.33433C10.3899 10.6218 9.04706 11.5 7.49994 11.5C5.95282 11.5 4.60997 10.6218 3.94428 9.33433C3.86104 9.17337 3.78845 9.00607 3.72742 8.83338ZM5.5 3.5L5.5 7.5C5.5 8.60457 6.39543 9.5 7.5 9.5C8.60457 9.5 9.5 8.60457 9.5 7.5L9.5 3.5C9.5 2.39543 8.60457 1.5 7.5 1.5C6.39543 1.5 5.5 2.39543 5.5 3.5ZM4.5 7.5C4.5 9.15685 5.84315 10.5 7.5 10.5C9.15685 10.5 10.5 9.15685 10.5 7.5L10.5 3.5C10.5 1.84315 9.15685 0.5 7.5 0.5C5.84315 0.5 4.5 1.84315 4.5 3.5L4.5 7.5Z" fill="black"/>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 12.2028V14.3042" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12.2027 6.94928V8.11672C12.2027 9.20041 11.7599 10.2397 10.9717 11.006C10.1836 11.7723 9.11457 12.2028 7.99992 12.2028C6.88527 12.2028 5.81627 11.7723 5.02809 11.006C4.23991 10.2397 3.79712 9.20041 3.79712 8.11672V6.94928" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10.1015 3.63555C10.1015 2.56426 9.16065 1.6958 8.00008 1.6958C6.83951 1.6958 5.89868 2.56426 5.89868 3.63555V8.16165C5.89868 9.23294 6.83951 10.1014 8.00008 10.1014C9.16065 10.1014 10.1015 9.23294 10.1015 8.16165V3.63555Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 847 B |
@@ -1,3 +1,8 @@
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.87 1.83637C13.0557 1.63204 13.0407 1.31581 12.8363 1.13006C12.632 0.944307 12.3158 0.959365 12.13 1.16369L10.4589 3.00199C10.2216 1.58215 8.98719 0.5 7.5 0.5C5.84315 0.5 4.5 1.84315 4.5 3.5L4.5 7.5C4.5 8.0754 4.66199 8.61297 4.94286 9.06958L4.24966 9.8321C4.1363 9.6744 4.03412 9.5081 3.94428 9.33433C3.86104 9.17337 3.78845 9.00607 3.72742 8.83338C3.63539 8.57302 3.34973 8.43656 3.08937 8.52858C2.82901 8.6206 2.69255 8.90626 2.78458 9.16662C2.86101 9.38288 2.95188 9.59228 3.056 9.79364C3.20094 10.074 3.37167 10.3388 3.56506 10.5852L2.13003 12.1637C1.94428 12.368 1.95933 12.6842 2.16366 12.87C2.36799 13.0558 2.68422 13.0407 2.86997 12.8364L4.25951 11.3079C5.01297 11.9497 5.95951 12.372 7.00014 12.4753L7.00014 14L5.50014 14C5.22399 14 5.00014 14.2239 5.00014 14.5C5.00014 14.7761 5.22399 15 5.50014 15L7.50014 15L9.50014 15C9.77628 15 10.0001 14.7761 10.0001 14.5C10.0001 14.2239 9.77628 14 9.50014 14L8.00014 14L8.00014 12.4753C9.72168 12.3043 11.1857 11.26 11.9439 9.79364C12.048 9.59228 12.1389 9.38288 12.2153 9.16662C12.3073 8.90626 12.1709 8.6206 11.9105 8.52858C11.6501 8.43656 11.3645 8.57302 11.2725 8.83338C11.2114 9.00607 11.1388 9.17337 11.0556 9.33433C10.3899 10.6218 9.04706 11.5 7.49994 11.5C6.523 11.5 5.62751 11.1498 4.93254 10.5675L5.60604 9.82669C6.12251 10.2476 6.78178 10.5 7.5 10.5C9.15685 10.5 10.5 9.15685 10.5 7.5L10.5 4.44333L12.87 1.83637ZM9.5 4.05673L9.5 3.5C9.5 2.39543 8.60457 1.5 7.5 1.5C6.39543 1.5 5.5 2.39543 5.5 3.5L5.5 7.5C5.5 7.77755 5.55653 8.04189 5.65872 8.28214L9.5 4.05673ZM6.28022 9.08509L9.5 5.54333L9.5 7.5C9.5 8.60457 8.60457 9.5 7.5 9.5C7.04083 9.5 6.6178 9.34527 6.28022 9.08509Z" fill="black"/>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 3L13 13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12 9C12 8.74858 12 8.49375 12 8.23839V7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4.00043 7V8.09869C3.98856 8.86731 4.22157 9.62164 4.66938 10.2643C5.11718 10.907 5.75924 11.4085 6.51267 11.7042C7.2661 11.9999 8.09632 12.0761 8.89619 11.923C9.47851 11.8115 10.0253 11.5823 10.5 11.2539" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10 6V3.62904C9.99714 3.26103 9.8347 2.90448 9.53885 2.6168C9.24299 2.32913 8.83093 2.12707 8.36903 2.04316C7.90713 1.95926 7.42226 1.9984 6.99252 2.15427C6.56278 2.31015 6.21317 2.57369 6 2.90245" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6 6V8.00088C6.00031 8.39636 6.10356 8.78287 6.29674 9.11159C6.48991 9.44031 6.76433 9.69649 7.08534 9.84779C7.40634 9.99909 7.75954 10.0387 8.10032 9.96165C8.4411 9.88459 8.75417 9.69431 9 9.41483" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8 12V14" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.3 KiB |
7
assets/icons/new_from_summary.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 4H2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4.66667 8H2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4.66667 12H2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8.00016 12C8.41993 12.5597 9.00515 12.9731 9.67294 13.1817C10.3407 13.3903 11.0572 13.3835 11.7209 13.1623C12.3846 12.941 12.9619 12.5166 13.371 11.949C13.78 11.3815 14.0002 10.6996 14.0002 10C14.0002 9.20435 13.6841 8.44129 13.1215 7.87868C12.5589 7.31607 11.7958 7 11.0002 7C10.1135 7 9.30683 7.36 8.72683 7.94L7.3335 9.33333" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7.3335 6.66669V9.33335H10.0002" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 964 B |
7
assets/icons/new_text_thread.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.33333 8H2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10.6667 5H2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9 11H2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12 7V11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M14 9H10" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 620 B |
3
assets/icons/new_thread.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.31254 12.549C7.3841 13.0987 8.61676 13.2476 9.78839 12.9688C10.96 12.6901 11.9936 12.0021 12.7028 11.0287C13.412 10.0554 13.7503 8.8607 13.6566 7.66002C13.5629 6.45934 13.0435 5.33159 12.1919 4.48C11.3403 3.62841 10.2126 3.10898 9.01188 3.01531C7.8112 2.92164 6.61655 3.2599 5.64319 3.96912C4.66984 4.67834 3.9818 5.71188 3.70306 6.88351C3.42432 8.05514 3.5732 9.2878 4.12289 10.3594L3 13.6719L6.31254 12.549Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 609 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="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 |
4
assets/icons/shield_check.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.0001 8.62505C13.0001 11.75 10.8126 13.3125 8.21266 14.2187C8.07651 14.2648 7.92862 14.2626 7.79392 14.2125C5.18771 13.3125 3.00024 11.75 3.00024 8.62505V4.25012C3.00024 4.08436 3.06609 3.92539 3.1833 3.80818C3.30051 3.69098 3.45948 3.62513 3.62523 3.62513C4.87521 3.62513 6.43769 2.87514 7.52517 1.92516C7.65758 1.81203 7.82601 1.74988 8.00016 1.74988C8.17431 1.74988 8.34275 1.81203 8.47515 1.92516C9.56889 2.88139 11.1251 3.62513 12.3751 3.62513C12.5408 3.62513 12.6998 3.69098 12.817 3.80818C12.9342 3.92539 13.0001 4.08436 13.0001 4.25012V8.62505Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6 8.00002L7.33333 9.33335L10 6.66669" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 883 B |
4
assets/icons/todo_complete.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 13C10.7614 13 13 10.7614 13 8C13 5.23858 10.7614 3 8 3C5.23858 3 3 5.23858 3 8C3 10.7614 5.23858 13 8 13Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6 8L7.33333 9L10 7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 418 B |
10
assets/icons/todo_pending.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 3C7.66045 3 8.33955 3 9 3" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M11 4C11.3949 4.26602 11.7345 4.60558 12 5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13 7C13 7.66045 13 8.33955 13 9" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12 11C11.734 11.3949 11.3944 11.7345 11 12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9 13C8.33954 13 7.66046 13 7 13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5 12C4.6051 11.734 4.26554 11.3944 4 11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3 9C3 8.33955 3 7.66045 3 7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4 5C4.26602 4.6051 4.60558 4.26554 5 4" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
11
assets/icons/todo_progress.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 3C7.66045 3 8.33955 3 9 3" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M11 4C11.3949 4.26602 11.7345 4.60558 12 5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13 7C13 7.66045 13 8.33955 13 9" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12 11C11.734 11.3949 11.3944 11.7345 11 12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9 13C8.33954 13 7.66046 13 7 13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5 12C4.6051 11.734 4.26554 11.3944 4 11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3 9C3 8.33955 3 7.66045 3 7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4 5C4.26602 4.6051 4.60558 4.26554 5 4" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8.00016 8.66665C8.36835 8.66665 8.66683 8.36817 8.66683 7.99998C8.66683 7.63179 8.36835 7.33331 8.00016 7.33331C7.63197 7.33331 7.3335 7.63179 7.3335 7.99998C7.3335 8.36817 7.63197 8.66665 8.00016 8.66665Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -232,7 +232,7 @@
|
||||
"ctrl-n": "agent::NewThread",
|
||||
"ctrl-alt-n": "agent::NewTextThread",
|
||||
"ctrl-shift-h": "agent::OpenHistory",
|
||||
"ctrl-alt-c": "agent::OpenConfiguration",
|
||||
"ctrl-alt-c": "agent::OpenSettings",
|
||||
"ctrl-alt-p": "agent::OpenRulesLibrary",
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"ctrl-alt-/": "agent::ToggleModelSelector",
|
||||
@@ -269,15 +269,15 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel && acp_thread",
|
||||
"context": "AgentPanel && external_agent_thread",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-n": "agent::NewAcpThread",
|
||||
"ctrl-n": "agent::NewExternalAgentThread",
|
||||
"ctrl-alt-t": "agent::NewThread"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "MessageEditor > Editor",
|
||||
"context": "MessageEditor && !Picker > Editor && !use_modifier_to_send",
|
||||
"bindings": {
|
||||
"enter": "agent::Chat",
|
||||
"ctrl-enter": "agent::ChatWithFollow",
|
||||
@@ -287,6 +287,17 @@
|
||||
"ctrl-shift-n": "agent::RejectAll"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "MessageEditor && !Picker > Editor && use_modifier_to_send",
|
||||
"bindings": {
|
||||
"ctrl-enter": "agent::Chat",
|
||||
"enter": "editor::Newline",
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "EditMessageEditor > Editor",
|
||||
"bindings": {
|
||||
@@ -419,7 +430,7 @@
|
||||
"ctrl-shift-pagedown": "pane::SwapItemRight",
|
||||
"ctrl-f4": ["pane::CloseActiveItem", { "close_pinned": false }],
|
||||
"ctrl-w": ["pane::CloseActiveItem", { "close_pinned": false }],
|
||||
"alt-ctrl-t": ["pane::CloseInactiveItems", { "close_pinned": false }],
|
||||
"alt-ctrl-t": ["pane::CloseOtherItems", { "close_pinned": false }],
|
||||
"alt-ctrl-shift-w": "workspace::CloseInactiveTabsAndPanes",
|
||||
"ctrl-k e": ["pane::CloseItemsToTheLeft", { "close_pinned": false }],
|
||||
"ctrl-k t": ["pane::CloseItemsToTheRight", { "close_pinned": false }],
|
||||
@@ -472,9 +483,8 @@
|
||||
"ctrl-k ctrl-d": ["editor::SelectNext", { "replace_newest": true }], // editor.action.moveSelectionToNextFindMatch / find_under_expand_skip
|
||||
"ctrl-k ctrl-shift-d": ["editor::SelectPrevious", { "replace_newest": true }], // editor.action.moveSelectionToPreviousFindMatch
|
||||
"ctrl-k ctrl-i": "editor::Hover",
|
||||
"ctrl-k ctrl-b": "editor::BlameHover",
|
||||
"ctrl-/": ["editor::ToggleComments", { "advance_downwards": false }],
|
||||
"ctrl-u": "editor::UndoSelection",
|
||||
"ctrl-shift-u": "editor::RedoSelection",
|
||||
"f8": ["editor::GoToDiagnostic", { "severity": { "min": "hint", "max": "error" } }],
|
||||
"shift-f8": ["editor::GoToPreviousDiagnostic", { "severity": { "min": "hint", "max": "error" } }],
|
||||
"f2": "editor::Rename",
|
||||
@@ -485,7 +495,7 @@
|
||||
"shift-f12": "editor::GoToImplementation",
|
||||
"alt-ctrl-f12": "editor::GoToTypeDefinitionSplit",
|
||||
"alt-shift-f12": "editor::FindAllReferences",
|
||||
"ctrl-m": "editor::MoveToEnclosingBracket",
|
||||
"ctrl-m": "editor::MoveToEnclosingBracket", // from jetbrains
|
||||
"ctrl-|": "editor::MoveToEnclosingBracket",
|
||||
"ctrl-{": "editor::Fold",
|
||||
"ctrl-}": "editor::UnfoldLines",
|
||||
@@ -588,6 +598,7 @@
|
||||
"ctrl-shift-t": "pane::ReopenClosedItem",
|
||||
"ctrl-k ctrl-s": "zed::OpenKeymapEditor",
|
||||
"ctrl-k ctrl-t": "theme_selector::Toggle",
|
||||
"ctrl-alt-super-p": "settings_profile_selector::Toggle",
|
||||
"ctrl-t": "project_symbols::Toggle",
|
||||
"ctrl-p": "file_finder::Toggle",
|
||||
"ctrl-tab": "tab_switcher::Toggle",
|
||||
@@ -652,6 +663,8 @@
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"ctrl-u": "editor::UndoSelection",
|
||||
"ctrl-shift-u": "editor::RedoSelection",
|
||||
"ctrl-shift-j": "editor::JoinLines",
|
||||
"ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",
|
||||
"ctrl-alt-h": "editor::DeleteToPreviousSubwordStart",
|
||||
@@ -860,8 +873,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 }],
|
||||
@@ -898,7 +909,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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -917,7 +930,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitPanel > Editor",
|
||||
"context": "CommitEditor > Editor",
|
||||
"bindings": {
|
||||
"escape": "git_panel::FocusChanges",
|
||||
"tab": "git_panel::FocusChanges",
|
||||
@@ -963,9 +976,14 @@
|
||||
"context": "CollabPanel && not_editing",
|
||||
"bindings": {
|
||||
"ctrl-backspace": "collab_panel::Remove",
|
||||
"space": "menu::Confirm",
|
||||
"ctrl-up": "collab_panel::MoveChannelUp",
|
||||
"ctrl-down": "collab_panel::MoveChannelDown"
|
||||
"space": "menu::Confirm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "CollabPanel",
|
||||
"bindings": {
|
||||
"alt-up": "collab_panel::MoveChannelUp",
|
||||
"alt-down": "collab_panel::MoveChannelDown"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1118,7 +1136,12 @@
|
||||
"ctrl-f": "search::FocusSearch",
|
||||
"alt-find": "keymap_editor::ToggleKeystrokeSearch",
|
||||
"alt-ctrl-f": "keymap_editor::ToggleKeystrokeSearch",
|
||||
"alt-c": "keymap_editor::ToggleConflictFilter"
|
||||
"alt-c": "keymap_editor::ToggleConflictFilter",
|
||||
"enter": "keymap_editor::EditBinding",
|
||||
"alt-enter": "keymap_editor::CreateBinding",
|
||||
"ctrl-c": "keymap_editor::CopyAction",
|
||||
"ctrl-shift-c": "keymap_editor::CopyContext",
|
||||
"ctrl-t": "keymap_editor::ShowMatchingKeybinds"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -272,7 +272,7 @@
|
||||
"cmd-n": "agent::NewThread",
|
||||
"cmd-alt-n": "agent::NewTextThread",
|
||||
"cmd-shift-h": "agent::OpenHistory",
|
||||
"cmd-alt-c": "agent::OpenConfiguration",
|
||||
"cmd-alt-c": "agent::OpenSettings",
|
||||
"cmd-alt-p": "agent::OpenRulesLibrary",
|
||||
"cmd-i": "agent::ToggleProfileSelector",
|
||||
"cmd-alt-/": "agent::ToggleModelSelector",
|
||||
@@ -310,15 +310,15 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel && acp_thread",
|
||||
"context": "AgentPanel && external_agent_thread",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-n": "agent::NewAcpThread",
|
||||
"cmd-n": "agent::NewExternalAgentThread",
|
||||
"cmd-alt-t": "agent::NewThread"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "MessageEditor > Editor",
|
||||
"context": "MessageEditor && !Picker > Editor && !use_modifier_to_send",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "agent::Chat",
|
||||
@@ -329,6 +329,18 @@
|
||||
"cmd-shift-n": "agent::RejectAll"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "MessageEditor && !Picker > Editor && use_modifier_to_send",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-enter": "agent::Chat",
|
||||
"enter": "editor::Newline",
|
||||
"cmd-i": "agent::ToggleProfileSelector",
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"cmd-shift-y": "agent::KeepAll",
|
||||
"cmd-shift-n": "agent::RejectAll"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "EditMessageEditor > Editor",
|
||||
"use_key_equivalents": true,
|
||||
@@ -477,7 +489,7 @@
|
||||
"ctrl-shift-pageup": "pane::SwapItemLeft",
|
||||
"ctrl-shift-pagedown": "pane::SwapItemRight",
|
||||
"cmd-w": ["pane::CloseActiveItem", { "close_pinned": false }],
|
||||
"alt-cmd-t": ["pane::CloseInactiveItems", { "close_pinned": false }],
|
||||
"alt-cmd-t": ["pane::CloseOtherItems", { "close_pinned": false }],
|
||||
"ctrl-alt-cmd-w": "workspace::CloseInactiveTabsAndPanes",
|
||||
"cmd-k e": ["pane::CloseItemsToTheLeft", { "close_pinned": false }],
|
||||
"cmd-k t": ["pane::CloseItemsToTheRight", { "close_pinned": false }],
|
||||
@@ -525,9 +537,8 @@
|
||||
"ctrl-cmd-d": ["editor::SelectPrevious", { "replace_newest": false }], // editor.action.addSelectionToPreviousFindMatch
|
||||
"cmd-k ctrl-cmd-d": ["editor::SelectPrevious", { "replace_newest": true }], // editor.action.moveSelectionToPreviousFindMatch
|
||||
"cmd-k cmd-i": "editor::Hover",
|
||||
"cmd-k cmd-b": "editor::BlameHover",
|
||||
"cmd-/": ["editor::ToggleComments", { "advance_downwards": false }],
|
||||
"cmd-u": "editor::UndoSelection",
|
||||
"cmd-shift-u": "editor::RedoSelection",
|
||||
"f8": ["editor::GoToDiagnostic", { "severity": { "min": "hint", "max": "error" } }],
|
||||
"shift-f8": ["editor::GoToPreviousDiagnostic", { "severity": { "min": "hint", "max": "error" } }],
|
||||
"f2": "editor::Rename",
|
||||
@@ -538,7 +549,7 @@
|
||||
"alt-cmd-f12": "editor::GoToTypeDefinitionSplit",
|
||||
"alt-shift-f12": "editor::FindAllReferences",
|
||||
"cmd-|": "editor::MoveToEnclosingBracket",
|
||||
"ctrl-m": "editor::MoveToEnclosingBracket",
|
||||
"ctrl-m": "editor::MoveToEnclosingBracket", // From Jetbrains
|
||||
"alt-cmd-[": "editor::Fold",
|
||||
"alt-cmd-]": "editor::UnfoldLines",
|
||||
"cmd-k cmd-l": "editor::ToggleFold",
|
||||
@@ -654,6 +665,7 @@
|
||||
"cmd-shift-t": "pane::ReopenClosedItem",
|
||||
"cmd-k cmd-s": "zed::OpenKeymapEditor",
|
||||
"cmd-k cmd-t": "theme_selector::Toggle",
|
||||
"ctrl-alt-cmd-p": "settings_profile_selector::Toggle",
|
||||
"cmd-t": "project_symbols::Toggle",
|
||||
"cmd-p": "file_finder::Toggle",
|
||||
"ctrl-tab": "tab_switcher::Toggle",
|
||||
@@ -714,6 +726,8 @@
|
||||
"context": "Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-u": "editor::UndoSelection",
|
||||
"cmd-shift-u": "editor::RedoSelection",
|
||||
"ctrl-j": "editor::JoinLines",
|
||||
"ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",
|
||||
"ctrl-alt-h": "editor::DeleteToPreviousSubwordStart",
|
||||
@@ -937,8 +951,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 }],
|
||||
@@ -963,7 +975,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitPanel > Editor",
|
||||
"context": "CommitEditor > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "editor::Newline",
|
||||
@@ -988,7 +1000,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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1024,9 +1038,15 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-backspace": "collab_panel::Remove",
|
||||
"space": "menu::Confirm",
|
||||
"cmd-up": "collab_panel::MoveChannelUp",
|
||||
"cmd-down": "collab_panel::MoveChannelDown"
|
||||
"space": "menu::Confirm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "CollabPanel",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"alt-up": "collab_panel::MoveChannelUp",
|
||||
"alt-down": "collab_panel::MoveChannelDown"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1216,8 +1236,14 @@
|
||||
"context": "KeymapEditor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-f": "search::FocusSearch",
|
||||
"cmd-alt-f": "keymap_editor::ToggleKeystrokeSearch",
|
||||
"cmd-alt-c": "keymap_editor::ToggleConflictFilter"
|
||||
"cmd-alt-c": "keymap_editor::ToggleConflictFilter",
|
||||
"enter": "keymap_editor::EditBinding",
|
||||
"alt-enter": "keymap_editor::CreateBinding",
|
||||
"cmd-c": "keymap_editor::CopyAction",
|
||||
"cmd-shift-c": "keymap_editor::CopyContext",
|
||||
"cmd-t": "keymap_editor::ShowMatchingKeybinds"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == insert && !menu",
|
||||
"context": "Editor && vim_mode == insert",
|
||||
"bindings": {
|
||||
// "j k": "vim::SwitchToNormalMode"
|
||||
// "j k": "vim::NormalBefore"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"ctrl-shift-i": "agent::ToggleFocus",
|
||||
"ctrl-l": "agent::ToggleFocus",
|
||||
"ctrl-shift-l": "agent::ToggleFocus",
|
||||
"ctrl-shift-j": "agent::OpenConfiguration"
|
||||
"ctrl-shift-j": "agent::OpenSettings"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
"ctrl-x o": "workspace::ActivateNextPane", // other-window
|
||||
"ctrl-x k": "pane::CloseActiveItem", // kill-buffer
|
||||
"ctrl-x 0": "pane::CloseActiveItem", // delete-window
|
||||
"ctrl-x 1": "pane::CloseInactiveItems", // delete-other-windows
|
||||
"ctrl-x 1": "pane::CloseOtherItems", // delete-other-windows
|
||||
"ctrl-x 2": "pane::SplitDown", // split-window-below
|
||||
"ctrl-x 3": "pane::SplitRight", // split-window-right
|
||||
"ctrl-x ctrl-f": "file_finder::Toggle", // find-file
|
||||
|
||||
@@ -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",
|
||||
@@ -66,22 +67,66 @@
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
"ctrl-f12": "outline::Toggle",
|
||||
"alt-7": "outline::Toggle",
|
||||
"ctrl-r": ["buffer_search::Deploy", { "replace_enabled": true }],
|
||||
"ctrl-shift-n": "file_finder::Toggle",
|
||||
"ctrl-g": "go_to_line::Toggle",
|
||||
"alt-enter": "editor::ToggleCodeActions"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar",
|
||||
"bindings": {
|
||||
"shift-enter": "search::SelectPreviousMatch"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar || ProjectSearchBar",
|
||||
"bindings": {
|
||||
"alt-c": "search::ToggleCaseSensitive",
|
||||
"alt-e": "search::ToggleSelection",
|
||||
"alt-x": "search::ToggleRegex",
|
||||
"alt-w": "search::ToggleWholeWord"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
"ctrl-shift-f12": "workspace::CloseAllDocks",
|
||||
"ctrl-shift-r": ["pane::DeploySearch", { "replace_enabled": true }],
|
||||
"alt-shift-f10": "task::Spawn",
|
||||
"ctrl-e": "file_finder::Toggle",
|
||||
// "ctrl-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor
|
||||
"ctrl-shift-n": "file_finder::Toggle",
|
||||
"ctrl-shift-a": "command_palette::Toggle",
|
||||
"shift shift": "command_palette::Toggle",
|
||||
"ctrl-alt-shift-n": "project_symbols::Toggle",
|
||||
"alt-1": "workspace::ToggleLeftDock",
|
||||
"ctrl-e": "tab_switcher::Toggle",
|
||||
"alt-6": "diagnostics::Deploy"
|
||||
"alt-0": "git_panel::ToggleFocus",
|
||||
"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": {
|
||||
"alt-f12": "terminal_panel::ToggleFocus",
|
||||
"ctrl-shift-k": "git::Push"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -95,10 +140,36 @@
|
||||
"context": "ProjectPanel",
|
||||
"bindings": {
|
||||
"enter": "project_panel::Open",
|
||||
"ctrl-shift-f": "project_panel::NewSearchInDirectory",
|
||||
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
|
||||
"delete": ["project_panel::Trash", { "skip_prompt": false }],
|
||||
"shift-delete": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"shift-f6": "project_panel::Rename"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Terminal",
|
||||
"bindings": {
|
||||
"ctrl-shift-t": "workspace::NewTerminal",
|
||||
"alt-f12": "workspace::CloseActiveDock",
|
||||
"alt-left": "pane::ActivatePreviousItem",
|
||||
"alt-right": "pane::ActivateNextItem",
|
||||
"ctrl-up": "terminal::ScrollLineUp",
|
||||
"ctrl-down": "terminal::ScrollLineDown",
|
||||
"shift-pageup": "terminal::ScrollPageUp",
|
||||
"shift-pagedown": "terminal::ScrollPageDown"
|
||||
}
|
||||
},
|
||||
{ "context": "GitPanel", "bindings": { "alt-0": "workspace::CloseActiveDock" } },
|
||||
{ "context": "ProjectPanel", "bindings": { "alt-1": "workspace::CloseActiveDock" } },
|
||||
{ "context": "DebugPanel", "bindings": { "alt-5": "workspace::CloseActiveDock" } },
|
||||
{ "context": "Diagnostics > Editor", "bindings": { "alt-6": "pane::CloseActiveItem" } },
|
||||
{ "context": "OutlinePanel", "bindings": { "alt-7": "workspace::CloseActiveDock" } },
|
||||
{
|
||||
"context": "Dock || Workspace || Terminal || OutlinePanel || ProjectPanel || CollabPanel || (Editor && mode == auto_height)",
|
||||
"bindings": {
|
||||
"escape": "editor::ToggleFocus",
|
||||
"shift-escape": "workspace::CloseActiveDock"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"cmd-shift-i": "agent::ToggleFocus",
|
||||
"cmd-l": "agent::ToggleFocus",
|
||||
"cmd-shift-l": "agent::ToggleFocus",
|
||||
"cmd-shift-j": "agent::OpenConfiguration"
|
||||
"cmd-shift-j": "agent::OpenSettings"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
"ctrl-x o": "workspace::ActivateNextPane", // other-window
|
||||
"ctrl-x k": "pane::CloseActiveItem", // kill-buffer
|
||||
"ctrl-x 0": "pane::CloseActiveItem", // delete-window
|
||||
"ctrl-x 1": "pane::CloseInactiveItems", // delete-other-windows
|
||||
"ctrl-x 1": "pane::CloseOtherItems", // delete-other-windows
|
||||
"ctrl-x 2": "pane::SplitDown", // split-window-below
|
||||
"ctrl-x 3": "pane::SplitRight", // split-window-right
|
||||
"ctrl-x ctrl-f": "file_finder::Toggle", // find-file
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
"bindings": {
|
||||
"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",
|
||||
@@ -63,28 +65,70 @@
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
"cmd-f12": "outline::Toggle",
|
||||
"cmd-7": "outline::Toggle",
|
||||
"cmd-r": ["buffer_search::Deploy", { "replace_enabled": true }],
|
||||
"cmd-shift-o": "file_finder::Toggle",
|
||||
"cmd-l": "go_to_line::Toggle",
|
||||
"alt-enter": "editor::ToggleCodeActions"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar > Editor",
|
||||
"context": "BufferSearchBar",
|
||||
"bindings": {
|
||||
"shift-enter": "search::SelectPreviousMatch"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar || ProjectSearchBar",
|
||||
"bindings": {
|
||||
"alt-c": "search::ToggleCaseSensitive",
|
||||
"alt-e": "search::ToggleSelection",
|
||||
"alt-x": "search::ToggleRegex",
|
||||
"alt-w": "search::ToggleWholeWord",
|
||||
"ctrl-alt-c": "search::ToggleCaseSensitive",
|
||||
"ctrl-alt-e": "search::ToggleSelection",
|
||||
"ctrl-alt-w": "search::ToggleWholeWord",
|
||||
"ctrl-alt-x": "search::ToggleRegex"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
"cmd-shift-f12": "workspace::CloseAllDocks",
|
||||
"cmd-shift-r": ["pane::DeploySearch", { "replace_enabled": true }],
|
||||
"ctrl-alt-r": "task::Spawn",
|
||||
"cmd-e": "file_finder::Toggle",
|
||||
// "cmd-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor
|
||||
"cmd-shift-o": "file_finder::Toggle",
|
||||
"cmd-shift-a": "command_palette::Toggle",
|
||||
"shift shift": "command_palette::Toggle",
|
||||
"cmd-alt-o": "project_symbols::Toggle", // JetBrains: Go to Symbol
|
||||
"cmd-o": "project_symbols::Toggle", // JetBrains: Go to Class
|
||||
"cmd-1": "workspace::ToggleLeftDock",
|
||||
"cmd-6": "diagnostics::Deploy"
|
||||
"cmd-1": "project_panel::ToggleFocus",
|
||||
"cmd-5": "debug_panel::ToggleFocus",
|
||||
"cmd-6": "diagnostics::Deploy",
|
||||
"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": {
|
||||
"alt-f12": "terminal_panel::ToggleFocus",
|
||||
"cmd-shift-k": "git::Push"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -98,11 +142,35 @@
|
||||
"context": "ProjectPanel",
|
||||
"bindings": {
|
||||
"enter": "project_panel::Open",
|
||||
"cmd-shift-f": "project_panel::NewSearchInDirectory",
|
||||
"cmd-backspace": ["project_panel::Trash", { "skip_prompt": false }],
|
||||
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
|
||||
"delete": ["project_panel::Trash", { "skip_prompt": false }],
|
||||
"shift-delete": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"shift-f6": "project_panel::Rename"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Terminal",
|
||||
"bindings": {
|
||||
"cmd-t": "workspace::NewTerminal",
|
||||
"alt-f12": "workspace::CloseActiveDock",
|
||||
"cmd-up": "terminal::ScrollLineUp",
|
||||
"cmd-down": "terminal::ScrollLineDown",
|
||||
"shift-pageup": "terminal::ScrollPageUp",
|
||||
"shift-pagedown": "terminal::ScrollPageDown"
|
||||
}
|
||||
},
|
||||
{ "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",
|
||||
"shift-escape": "workspace::CloseActiveDock"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
"cmd-l": "go_to_line::Toggle",
|
||||
"ctrl-shift-d": "editor::DuplicateLineDown",
|
||||
@@ -15,7 +15,12 @@
|
||||
"cmd-enter": "editor::NewlineBelow",
|
||||
"cmd-alt-enter": "editor::NewlineAbove",
|
||||
"cmd-shift-l": "editor::SelectLine",
|
||||
"cmd-shift-t": "outline::Toggle",
|
||||
"cmd-shift-t": "outline::Toggle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"alt-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"alt-shift-backspace": "editor::DeleteToNextWordEnd",
|
||||
"alt-delete": "editor::DeleteToNextWordEnd",
|
||||
@@ -39,10 +44,6 @@
|
||||
"ctrl-_": "editor::ConvertToSnakeCase"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {}
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar",
|
||||
"bindings": {
|
||||
|
||||
@@ -124,6 +124,7 @@
|
||||
"g r a": "editor::ToggleCodeActions",
|
||||
"g g": "vim::StartOfDocument",
|
||||
"g h": "editor::Hover",
|
||||
"g B": "editor::BlameHover",
|
||||
"g t": "pane::ActivateNextItem",
|
||||
"g shift-t": "pane::ActivatePreviousItem",
|
||||
"g d": "editor::GoToDefinition",
|
||||
@@ -219,6 +220,8 @@
|
||||
{
|
||||
"context": "vim_mode == normal",
|
||||
"bindings": {
|
||||
"i": "vim::InsertBefore",
|
||||
"a": "vim::InsertAfter",
|
||||
"ctrl-[": "editor::Cancel",
|
||||
":": "command_palette::Toggle",
|
||||
"c": "vim::PushChange",
|
||||
@@ -352,9 +355,7 @@
|
||||
"shift-d": "vim::DeleteToEndOfLine",
|
||||
"shift-j": "vim::JoinLines",
|
||||
"shift-y": "vim::YankLine",
|
||||
"i": "vim::InsertBefore",
|
||||
"shift-i": "vim::InsertFirstNonWhitespace",
|
||||
"a": "vim::InsertAfter",
|
||||
"shift-a": "vim::InsertEndOfLine",
|
||||
"o": "vim::InsertLineBelow",
|
||||
"shift-o": "vim::InsertLineAbove",
|
||||
@@ -376,7 +377,10 @@
|
||||
{
|
||||
"context": "vim_mode == helix_normal && !menu",
|
||||
"bindings": {
|
||||
"i": "vim::HelixInsert",
|
||||
"a": "vim::HelixAppend",
|
||||
"ctrl-[": "editor::Cancel",
|
||||
";": "vim::HelixCollapseSelection",
|
||||
":": "command_palette::Toggle",
|
||||
"left": "vim::WrappingLeft",
|
||||
"right": "vim::WrappingRight",
|
||||
@@ -723,7 +727,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel || GitPanel || ProjectPanel || CollabPanel || OutlinePanel || ChatPanel || VimControl || EmptyPane || SharedScreen || MarkdownPreview || KeyContextView || DebugPanel",
|
||||
"context": "VimControl || !Editor && !Terminal",
|
||||
"bindings": {
|
||||
// window related commands (ctrl-w X)
|
||||
"ctrl-w": null,
|
||||
@@ -781,7 +785,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ChangesList || EmptyPane || SharedScreen || MarkdownPreview || KeyContextView || Welcome",
|
||||
"context": "!Editor && !Terminal",
|
||||
"bindings": {
|
||||
":": "command_palette::Toggle",
|
||||
"g /": "pane::DeploySearch"
|
||||
@@ -857,6 +861,14 @@
|
||||
"shift-n": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Picker > Editor",
|
||||
"bindings": {
|
||||
"ctrl-h": "editor::Backspace",
|
||||
"ctrl-u": "editor::DeleteToBeginningOfLine",
|
||||
"ctrl-w": "editor::DeleteToPreviousWordStart"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitCommit > Editor && VimControl && vim_mode == normal",
|
||||
"bindings": {
|
||||
|
||||
@@ -197,6 +197,8 @@
|
||||
// "inline"
|
||||
// 3. Place snippets at the bottom of the completion list:
|
||||
// "bottom"
|
||||
// 4. Do not show snippets in the completion list:
|
||||
// "none"
|
||||
"snippet_sort_order": "inline",
|
||||
// How to highlight the current line in the editor.
|
||||
//
|
||||
@@ -689,7 +691,10 @@
|
||||
// 5. Never show the scrollbar:
|
||||
// "never"
|
||||
"show": null
|
||||
}
|
||||
},
|
||||
// Default depth to expand outline items in the current file.
|
||||
// Set to 0 to collapse all items that have children, 1 or higher to collapse items at that depth or deeper.
|
||||
"expand_outlines_with_depth": 100
|
||||
},
|
||||
"collaboration_panel": {
|
||||
// Whether to show the collaboration panel button in the status bar.
|
||||
@@ -817,7 +822,7 @@
|
||||
"edit_file": true,
|
||||
"fetch": true,
|
||||
"list_directory": true,
|
||||
"project_notifications": true,
|
||||
"project_notifications": false,
|
||||
"move_path": true,
|
||||
"now": true,
|
||||
"find_path": true,
|
||||
@@ -837,7 +842,7 @@
|
||||
"diagnostics": true,
|
||||
"fetch": true,
|
||||
"list_directory": true,
|
||||
"project_notifications": true,
|
||||
"project_notifications": false,
|
||||
"now": true,
|
||||
"find_path": true,
|
||||
"read_file": true,
|
||||
@@ -1074,6 +1079,10 @@
|
||||
// Send anonymized usage data like what languages you're using Zed with.
|
||||
"metrics": true
|
||||
},
|
||||
// Whether to disable all AI features in Zed.
|
||||
//
|
||||
// Default: false
|
||||
"disable_ai": false,
|
||||
// Automatically update Zed. This setting may be ignored on Linux if
|
||||
// installed through a package manager.
|
||||
"auto_update": true,
|
||||
@@ -1710,6 +1719,7 @@
|
||||
"openai": {
|
||||
"api_url": "https://api.openai.com/v1"
|
||||
},
|
||||
"openai_compatible": {},
|
||||
"open_router": {
|
||||
"api_url": "https://openrouter.ai/api/v1"
|
||||
},
|
||||
@@ -1867,5 +1877,25 @@
|
||||
"save_breakpoints": true,
|
||||
"dock": "bottom",
|
||||
"button": true
|
||||
}
|
||||
},
|
||||
// Configures any number of settings profiles that are temporarily applied on
|
||||
// top of your existing user settings when selected from
|
||||
// `settings profile selector: toggle`.
|
||||
// Examples:
|
||||
// "profiles": {
|
||||
// "Presenting": {
|
||||
// "agent_font_size": 20.0,
|
||||
// "buffer_font_size": 20.0,
|
||||
// "theme": "One Light",
|
||||
// "ui_font_size": 20.0
|
||||
// },
|
||||
// "Python (ty)": {
|
||||
// "languages": {
|
||||
// "Python": {
|
||||
// "language_servers": ["ty"]
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
"profiles": []
|
||||
}
|
||||
|
||||
@@ -15,13 +15,15 @@
|
||||
"adapter": "JavaScript",
|
||||
"program": "$ZED_FILE",
|
||||
"request": "launch",
|
||||
"cwd": "$ZED_WORKTREE_ROOT"
|
||||
"cwd": "$ZED_WORKTREE_ROOT",
|
||||
"type": "pwa-node"
|
||||
},
|
||||
{
|
||||
"label": "JavaScript debug terminal",
|
||||
"adapter": "JavaScript",
|
||||
"request": "launch",
|
||||
"cwd": "$ZED_WORKTREE_ROOT",
|
||||
"console": "integratedTerminal"
|
||||
"console": "integratedTerminal",
|
||||
"type": "pwa-node"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
// command palette (cmd-shift-p / ctrl-shift-p)
|
||||
{
|
||||
"ui_font_size": 16,
|
||||
"buffer_font_size": 16,
|
||||
"buffer_font_size": 15,
|
||||
"theme": {
|
||||
"mode": "system",
|
||||
"light": "One Light",
|
||||
|
||||
@@ -59,5 +59,11 @@ services:
|
||||
depends_on:
|
||||
- postgres
|
||||
|
||||
stripe-mock:
|
||||
image: stripe/stripe-mock:v0.178.0
|
||||
ports:
|
||||
- 12111:12111
|
||||
- 12112:12112
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "acp"
|
||||
name = "acp_thread"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
@@ -9,15 +9,14 @@ license = "GPL-3.0-or-later"
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/acp.rs"
|
||||
path = "src/acp_thread.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = ["gpui/test-support", "project/test-support"]
|
||||
gemini = []
|
||||
|
||||
[dependencies]
|
||||
agent_servers.workspace = true
|
||||
agent-client-protocol.workspace = true
|
||||
agentic-coding-protocol.workspace = true
|
||||
anyhow.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
@@ -29,6 +28,8 @@ itertools.workspace = true
|
||||
language.workspace = true
|
||||
markdown.workspace = true
|
||||
project.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
ui.workspace = true
|
||||
@@ -41,7 +42,6 @@ env_logger.workspace = true
|
||||
gpui = { workspace = true, "features" = ["test-support"] }
|
||||
indoc.workspace = true
|
||||
project = { workspace = true, "features" = ["test-support"] }
|
||||
serde_json.workspace = true
|
||||
tempfile.workspace = true
|
||||
util.workspace = true
|
||||
settings.workspace = true
|
||||
1757
crates/acp_thread/src/acp_thread.rs
Normal file
26
crates/acp_thread/src/connection.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use std::{path::Path, rc::Rc};
|
||||
|
||||
use agent_client_protocol as acp;
|
||||
use anyhow::Result;
|
||||
use gpui::{AsyncApp, Entity, Task};
|
||||
use project::Project;
|
||||
use ui::App;
|
||||
|
||||
use crate::AcpThread;
|
||||
|
||||
pub trait AgentConnection {
|
||||
fn name(&self) -> &'static str;
|
||||
|
||||
fn new_thread(
|
||||
self: Rc<Self>,
|
||||
project: Entity<Project>,
|
||||
cwd: &Path,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Task<Result<Entity<AcpThread>>>;
|
||||
|
||||
fn authenticate(&self, cx: &mut App) -> Task<Result<()>>;
|
||||
|
||||
fn prompt(&self, params: acp::PromptArguments, cx: &mut App) -> Task<Result<()>>;
|
||||
|
||||
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App);
|
||||
}
|
||||
453
crates/acp_thread/src/old_acp_support.rs
Normal file
@@ -0,0 +1,453 @@
|
||||
// Translates old acp agents into the new schema
|
||||
use agent_client_protocol as acp;
|
||||
use agentic_coding_protocol::{self as acp_old, AgentRequest as _};
|
||||
use anyhow::{Context as _, Result};
|
||||
use futures::channel::oneshot;
|
||||
use gpui::{AppContext as _, AsyncApp, Entity, Task, WeakEntity};
|
||||
use project::Project;
|
||||
use std::{cell::RefCell, error::Error, fmt, path::Path, rc::Rc};
|
||||
use ui::App;
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::{AcpThread, AgentConnection};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct OldAcpClientDelegate {
|
||||
thread: Rc<RefCell<WeakEntity<AcpThread>>>,
|
||||
cx: AsyncApp,
|
||||
next_tool_call_id: Rc<RefCell<u64>>,
|
||||
// sent_buffer_versions: HashMap<Entity<Buffer>, HashMap<u64, BufferSnapshot>>,
|
||||
}
|
||||
|
||||
impl OldAcpClientDelegate {
|
||||
pub fn new(thread: Rc<RefCell<WeakEntity<AcpThread>>>, cx: AsyncApp) -> Self {
|
||||
Self {
|
||||
thread,
|
||||
cx,
|
||||
next_tool_call_id: Rc::new(RefCell::new(0)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl acp_old::Client for OldAcpClientDelegate {
|
||||
async fn stream_assistant_message_chunk(
|
||||
&self,
|
||||
params: acp_old::StreamAssistantMessageChunkParams,
|
||||
) -> Result<(), acp_old::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
|
||||
cx.update(|cx| {
|
||||
self.thread
|
||||
.borrow()
|
||||
.update(cx, |thread, cx| match params.chunk {
|
||||
acp_old::AssistantMessageChunk::Text { text } => {
|
||||
thread.push_assistant_content_block(text.into(), false, cx)
|
||||
}
|
||||
acp_old::AssistantMessageChunk::Thought { thought } => {
|
||||
thread.push_assistant_content_block(thought.into(), true, cx)
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn request_tool_call_confirmation(
|
||||
&self,
|
||||
request: acp_old::RequestToolCallConfirmationParams,
|
||||
) -> Result<acp_old::RequestToolCallConfirmationResponse, acp_old::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
|
||||
let old_acp_id = *self.next_tool_call_id.borrow() + 1;
|
||||
self.next_tool_call_id.replace(old_acp_id);
|
||||
|
||||
let tool_call = into_new_tool_call(
|
||||
acp::ToolCallId(old_acp_id.to_string().into()),
|
||||
request.tool_call,
|
||||
);
|
||||
|
||||
let mut options = match request.confirmation {
|
||||
acp_old::ToolCallConfirmation::Edit { .. } => vec![(
|
||||
acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
|
||||
acp::PermissionOptionKind::AllowAlways,
|
||||
"Always Allow Edits".to_string(),
|
||||
)],
|
||||
acp_old::ToolCallConfirmation::Execute { root_command, .. } => vec![(
|
||||
acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
|
||||
acp::PermissionOptionKind::AllowAlways,
|
||||
format!("Always Allow {}", root_command),
|
||||
)],
|
||||
acp_old::ToolCallConfirmation::Mcp {
|
||||
server_name,
|
||||
tool_name,
|
||||
..
|
||||
} => vec![
|
||||
(
|
||||
acp_old::ToolCallConfirmationOutcome::AlwaysAllowMcpServer,
|
||||
acp::PermissionOptionKind::AllowAlways,
|
||||
format!("Always Allow {}", server_name),
|
||||
),
|
||||
(
|
||||
acp_old::ToolCallConfirmationOutcome::AlwaysAllowTool,
|
||||
acp::PermissionOptionKind::AllowAlways,
|
||||
format!("Always Allow {}", tool_name),
|
||||
),
|
||||
],
|
||||
acp_old::ToolCallConfirmation::Fetch { .. } => vec![(
|
||||
acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
|
||||
acp::PermissionOptionKind::AllowAlways,
|
||||
"Always Allow".to_string(),
|
||||
)],
|
||||
acp_old::ToolCallConfirmation::Other { .. } => vec![(
|
||||
acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
|
||||
acp::PermissionOptionKind::AllowAlways,
|
||||
"Always Allow".to_string(),
|
||||
)],
|
||||
};
|
||||
|
||||
options.extend([
|
||||
(
|
||||
acp_old::ToolCallConfirmationOutcome::Allow,
|
||||
acp::PermissionOptionKind::AllowOnce,
|
||||
"Allow".to_string(),
|
||||
),
|
||||
(
|
||||
acp_old::ToolCallConfirmationOutcome::Reject,
|
||||
acp::PermissionOptionKind::RejectOnce,
|
||||
"Reject".to_string(),
|
||||
),
|
||||
]);
|
||||
|
||||
let mut outcomes = Vec::with_capacity(options.len());
|
||||
let mut acp_options = Vec::with_capacity(options.len());
|
||||
|
||||
for (index, (outcome, kind, label)) in options.into_iter().enumerate() {
|
||||
outcomes.push(outcome);
|
||||
acp_options.push(acp::PermissionOption {
|
||||
id: acp::PermissionOptionId(index.to_string().into()),
|
||||
label,
|
||||
kind,
|
||||
})
|
||||
}
|
||||
|
||||
let response = cx
|
||||
.update(|cx| {
|
||||
self.thread.borrow().update(cx, |thread, cx| {
|
||||
thread.request_tool_call_permission(tool_call, acp_options, cx)
|
||||
})
|
||||
})?
|
||||
.context("Failed to update thread")?
|
||||
.await;
|
||||
|
||||
let outcome = match response {
|
||||
Ok(option_id) => outcomes[option_id.0.parse::<usize>().unwrap_or(0)],
|
||||
Err(oneshot::Canceled) => acp_old::ToolCallConfirmationOutcome::Cancel,
|
||||
};
|
||||
|
||||
Ok(acp_old::RequestToolCallConfirmationResponse {
|
||||
id: acp_old::ToolCallId(old_acp_id),
|
||||
outcome: outcome,
|
||||
})
|
||||
}
|
||||
|
||||
async fn push_tool_call(
|
||||
&self,
|
||||
request: acp_old::PushToolCallParams,
|
||||
) -> Result<acp_old::PushToolCallResponse, acp_old::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
|
||||
let old_acp_id = *self.next_tool_call_id.borrow() + 1;
|
||||
self.next_tool_call_id.replace(old_acp_id);
|
||||
|
||||
cx.update(|cx| {
|
||||
self.thread.borrow().update(cx, |thread, cx| {
|
||||
thread.upsert_tool_call(
|
||||
into_new_tool_call(acp::ToolCallId(old_acp_id.to_string().into()), request),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})?
|
||||
.context("Failed to update thread")?;
|
||||
|
||||
Ok(acp_old::PushToolCallResponse {
|
||||
id: acp_old::ToolCallId(old_acp_id),
|
||||
})
|
||||
}
|
||||
|
||||
async fn update_tool_call(
|
||||
&self,
|
||||
request: acp_old::UpdateToolCallParams,
|
||||
) -> Result<(), acp_old::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
|
||||
cx.update(|cx| {
|
||||
self.thread.borrow().update(cx, |thread, cx| {
|
||||
thread.update_tool_call(
|
||||
acp::ToolCallUpdate {
|
||||
id: acp::ToolCallId(request.tool_call_id.0.to_string().into()),
|
||||
fields: acp::ToolCallUpdateFields {
|
||||
status: Some(into_new_tool_call_status(request.status)),
|
||||
content: Some(
|
||||
request
|
||||
.content
|
||||
.into_iter()
|
||||
.map(into_new_tool_call_content)
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})?
|
||||
.context("Failed to update thread")??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_plan(&self, request: acp_old::UpdatePlanParams) -> Result<(), acp_old::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
|
||||
cx.update(|cx| {
|
||||
self.thread.borrow().update(cx, |thread, cx| {
|
||||
thread.update_plan(
|
||||
acp::Plan {
|
||||
entries: request
|
||||
.entries
|
||||
.into_iter()
|
||||
.map(into_new_plan_entry)
|
||||
.collect(),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})?
|
||||
.context("Failed to update thread")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn read_text_file(
|
||||
&self,
|
||||
acp_old::ReadTextFileParams { path, line, limit }: acp_old::ReadTextFileParams,
|
||||
) -> Result<acp_old::ReadTextFileResponse, acp_old::Error> {
|
||||
let content = self
|
||||
.cx
|
||||
.update(|cx| {
|
||||
self.thread.borrow().update(cx, |thread, cx| {
|
||||
thread.read_text_file(path, line, limit, false, cx)
|
||||
})
|
||||
})?
|
||||
.context("Failed to update thread")?
|
||||
.await?;
|
||||
Ok(acp_old::ReadTextFileResponse { content })
|
||||
}
|
||||
|
||||
async fn write_text_file(
|
||||
&self,
|
||||
acp_old::WriteTextFileParams { path, content }: acp_old::WriteTextFileParams,
|
||||
) -> Result<(), acp_old::Error> {
|
||||
self.cx
|
||||
.update(|cx| {
|
||||
self.thread
|
||||
.borrow()
|
||||
.update(cx, |thread, cx| thread.write_text_file(path, content, cx))
|
||||
})?
|
||||
.context("Failed to update thread")?
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn into_new_tool_call(id: acp::ToolCallId, request: acp_old::PushToolCallParams) -> acp::ToolCall {
|
||||
acp::ToolCall {
|
||||
id: id,
|
||||
label: request.label,
|
||||
kind: acp_kind_from_old_icon(request.icon),
|
||||
status: acp::ToolCallStatus::InProgress,
|
||||
content: request
|
||||
.content
|
||||
.into_iter()
|
||||
.map(into_new_tool_call_content)
|
||||
.collect(),
|
||||
locations: request
|
||||
.locations
|
||||
.into_iter()
|
||||
.map(into_new_tool_call_location)
|
||||
.collect(),
|
||||
raw_input: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn acp_kind_from_old_icon(icon: acp_old::Icon) -> acp::ToolKind {
|
||||
match icon {
|
||||
acp_old::Icon::FileSearch => acp::ToolKind::Search,
|
||||
acp_old::Icon::Folder => acp::ToolKind::Search,
|
||||
acp_old::Icon::Globe => acp::ToolKind::Search,
|
||||
acp_old::Icon::Hammer => acp::ToolKind::Other,
|
||||
acp_old::Icon::LightBulb => acp::ToolKind::Think,
|
||||
acp_old::Icon::Pencil => acp::ToolKind::Edit,
|
||||
acp_old::Icon::Regex => acp::ToolKind::Search,
|
||||
acp_old::Icon::Terminal => acp::ToolKind::Execute,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_new_tool_call_status(status: acp_old::ToolCallStatus) -> acp::ToolCallStatus {
|
||||
match status {
|
||||
acp_old::ToolCallStatus::Running => acp::ToolCallStatus::InProgress,
|
||||
acp_old::ToolCallStatus::Finished => acp::ToolCallStatus::Completed,
|
||||
acp_old::ToolCallStatus::Error => acp::ToolCallStatus::Failed,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_new_tool_call_content(content: acp_old::ToolCallContent) -> acp::ToolCallContent {
|
||||
match content {
|
||||
acp_old::ToolCallContent::Markdown { markdown } => markdown.into(),
|
||||
acp_old::ToolCallContent::Diff { diff } => acp::ToolCallContent::Diff {
|
||||
diff: into_new_diff(diff),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn into_new_diff(diff: acp_old::Diff) -> acp::Diff {
|
||||
acp::Diff {
|
||||
path: diff.path,
|
||||
old_text: diff.old_text,
|
||||
new_text: diff.new_text,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_new_tool_call_location(location: acp_old::ToolCallLocation) -> acp::ToolCallLocation {
|
||||
acp::ToolCallLocation {
|
||||
path: location.path,
|
||||
line: location.line,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_new_plan_entry(entry: acp_old::PlanEntry) -> acp::PlanEntry {
|
||||
acp::PlanEntry {
|
||||
content: entry.content,
|
||||
priority: into_new_plan_priority(entry.priority),
|
||||
status: into_new_plan_status(entry.status),
|
||||
}
|
||||
}
|
||||
|
||||
fn into_new_plan_priority(priority: acp_old::PlanEntryPriority) -> acp::PlanEntryPriority {
|
||||
match priority {
|
||||
acp_old::PlanEntryPriority::Low => acp::PlanEntryPriority::Low,
|
||||
acp_old::PlanEntryPriority::Medium => acp::PlanEntryPriority::Medium,
|
||||
acp_old::PlanEntryPriority::High => acp::PlanEntryPriority::High,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_new_plan_status(status: acp_old::PlanEntryStatus) -> acp::PlanEntryStatus {
|
||||
match status {
|
||||
acp_old::PlanEntryStatus::Pending => acp::PlanEntryStatus::Pending,
|
||||
acp_old::PlanEntryStatus::InProgress => acp::PlanEntryStatus::InProgress,
|
||||
acp_old::PlanEntryStatus::Completed => acp::PlanEntryStatus::Completed,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Unauthenticated;
|
||||
|
||||
impl Error for Unauthenticated {}
|
||||
impl fmt::Display for Unauthenticated {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "Unauthenticated")
|
||||
}
|
||||
}
|
||||
|
||||
pub struct OldAcpAgentConnection {
|
||||
pub name: &'static str,
|
||||
pub connection: acp_old::AgentConnection,
|
||||
pub child_status: Task<Result<()>>,
|
||||
pub current_thread: Rc<RefCell<WeakEntity<AcpThread>>>,
|
||||
}
|
||||
|
||||
impl AgentConnection for OldAcpAgentConnection {
|
||||
fn name(&self) -> &'static str {
|
||||
self.name
|
||||
}
|
||||
|
||||
fn new_thread(
|
||||
self: Rc<Self>,
|
||||
project: Entity<Project>,
|
||||
_cwd: &Path,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Task<Result<Entity<AcpThread>>> {
|
||||
let task = self.connection.request_any(
|
||||
acp_old::InitializeParams {
|
||||
protocol_version: acp_old::ProtocolVersion::latest(),
|
||||
}
|
||||
.into_any(),
|
||||
);
|
||||
let current_thread = self.current_thread.clone();
|
||||
cx.spawn(async move |cx| {
|
||||
let result = task.await?;
|
||||
let result = acp_old::InitializeParams::response_from_any(result)?;
|
||||
|
||||
if !result.is_authenticated {
|
||||
anyhow::bail!(Unauthenticated)
|
||||
}
|
||||
|
||||
cx.update(|cx| {
|
||||
let thread = cx.new(|cx| {
|
||||
let session_id = acp::SessionId("acp-old-no-id".into());
|
||||
AcpThread::new(self.clone(), project, session_id, cx)
|
||||
});
|
||||
current_thread.replace(thread.downgrade());
|
||||
thread
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn authenticate(&self, cx: &mut App) -> Task<Result<()>> {
|
||||
let task = self
|
||||
.connection
|
||||
.request_any(acp_old::AuthenticateParams.into_any());
|
||||
cx.foreground_executor().spawn(async move {
|
||||
task.await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn prompt(&self, params: acp::PromptArguments, cx: &mut App) -> Task<Result<()>> {
|
||||
let chunks = params
|
||||
.prompt
|
||||
.into_iter()
|
||||
.filter_map(|block| match block {
|
||||
acp::ContentBlock::Text(text) => {
|
||||
Some(acp_old::UserMessageChunk::Text { text: text.text })
|
||||
}
|
||||
acp::ContentBlock::ResourceLink(link) => Some(acp_old::UserMessageChunk::Path {
|
||||
path: link.uri.into(),
|
||||
}),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let task = self
|
||||
.connection
|
||||
.request_any(acp_old::SendUserMessageParams { chunks }.into_any());
|
||||
cx.foreground_executor().spawn(async move {
|
||||
task.await?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn cancel(&self, _session_id: &acp::SessionId, cx: &mut App) {
|
||||
let task = self
|
||||
.connection
|
||||
.request_any(acp_old::CancelSendMessageParams.into_any());
|
||||
cx.foreground_executor()
|
||||
.spawn(async move {
|
||||
task.await?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx)
|
||||
}
|
||||
}
|
||||
@@ -231,7 +231,6 @@ impl ActivityIndicator {
|
||||
status,
|
||||
} => {
|
||||
let create_buffer = project.update(cx, |project, cx| project.create_buffer(cx));
|
||||
let project = project.clone();
|
||||
let status = status.clone();
|
||||
let server_name = server_name.clone();
|
||||
cx.spawn_in(window, async move |workspace, cx| {
|
||||
@@ -247,8 +246,7 @@ impl ActivityIndicator {
|
||||
workspace.update_in(cx, |workspace, window, cx| {
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(cx.new(|cx| {
|
||||
let mut editor =
|
||||
Editor::for_buffer(buffer, Some(project.clone()), window, cx);
|
||||
let mut editor = Editor::for_buffer(buffer, None, window, cx);
|
||||
editor.set_read_only(true);
|
||||
editor
|
||||
})),
|
||||
|
||||
@@ -25,6 +25,7 @@ assistant_context.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
component.workspace = true
|
||||
context_server.workspace = true
|
||||
@@ -35,9 +36,9 @@ futures.workspace = true
|
||||
git.workspace = true
|
||||
gpui.workspace = true
|
||||
heed.workspace = true
|
||||
http_client.workspace = true
|
||||
icons.workspace = true
|
||||
indoc.workspace = true
|
||||
http_client.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
@@ -46,7 +47,6 @@ paths.workspace = true
|
||||
postage.workspace = true
|
||||
project.workspace = true
|
||||
prompt_store.workspace = true
|
||||
proto.workspace = true
|
||||
ref-cast.workspace = true
|
||||
rope.workspace = true
|
||||
schemars.workspace = true
|
||||
@@ -63,7 +63,6 @@ time.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
zstd.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -308,7 +308,12 @@ mod tests {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _input: &serde_json::Value, _cx: &App) -> bool {
|
||||
fn needs_confirmation(
|
||||
&self,
|
||||
_input: &serde_json::Value,
|
||||
_project: &Entity<Project>,
|
||||
_cx: &App,
|
||||
) -> bool {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ impl Tool for ContextServerTool {
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Cog
|
||||
IconName::ToolHammer
|
||||
}
|
||||
|
||||
fn source(&self) -> ToolSource {
|
||||
@@ -47,7 +47,7 @@ impl Tool for ContextServerTool {
|
||||
}
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet};
|
||||
use chrono::{DateTime, Utc};
|
||||
use client::{ModelRequestUsage, RequestUsage};
|
||||
use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, Plan, UsageLimit};
|
||||
use collections::HashMap;
|
||||
use feature_flags::{self, FeatureFlagAppExt};
|
||||
use futures::{FutureExt, StreamExt as _, future::Shared};
|
||||
@@ -36,7 +37,6 @@ use project::{
|
||||
git_store::{GitStore, GitStoreCheckpoint, RepositoryState},
|
||||
};
|
||||
use prompt_store::{ModelContext, PromptBuilder};
|
||||
use proto::Plan;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
@@ -47,11 +47,10 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use thiserror::Error;
|
||||
use util::{ResultExt as _, debug_panic, post_inc};
|
||||
use util::{ResultExt as _, post_inc};
|
||||
use uuid::Uuid;
|
||||
use zed_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit};
|
||||
|
||||
const MAX_RETRY_ATTEMPTS: u8 = 3;
|
||||
const MAX_RETRY_ATTEMPTS: u8 = 4;
|
||||
const BASE_RETRY_DELAY: Duration = Duration::from_secs(5);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -396,6 +395,7 @@ pub struct Thread {
|
||||
remaining_turns: u32,
|
||||
configured_model: Option<ConfiguredModel>,
|
||||
profile: AgentProfile,
|
||||
last_error_context: Option<(Arc<dyn LanguageModel>, CompletionIntent)>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -489,10 +489,11 @@ impl Thread {
|
||||
retry_state: None,
|
||||
message_feedback: HashMap::default(),
|
||||
last_auto_capture_at: None,
|
||||
last_error_context: None,
|
||||
last_received_chunk_at: None,
|
||||
request_callback: None,
|
||||
remaining_turns: u32::MAX,
|
||||
configured_model,
|
||||
configured_model: configured_model.clone(),
|
||||
profile: AgentProfile::new(profile_id, tools),
|
||||
}
|
||||
}
|
||||
@@ -613,6 +614,7 @@ impl Thread {
|
||||
feedback: None,
|
||||
message_feedback: HashMap::default(),
|
||||
last_auto_capture_at: None,
|
||||
last_error_context: None,
|
||||
last_received_chunk_at: None,
|
||||
request_callback: None,
|
||||
remaining_turns: u32::MAX,
|
||||
@@ -939,7 +941,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(
|
||||
@@ -1264,9 +1266,58 @@ impl Thread {
|
||||
|
||||
self.flush_notifications(model.clone(), intent, cx);
|
||||
|
||||
let request = self.to_completion_request(model.clone(), intent, cx);
|
||||
let _checkpoint = self.finalize_pending_checkpoint(cx);
|
||||
self.stream_completion(
|
||||
self.to_completion_request(model.clone(), intent, cx),
|
||||
model,
|
||||
intent,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
self.stream_completion(request, model, intent, window, cx);
|
||||
pub fn retry_last_completion(
|
||||
&mut self,
|
||||
window: Option<AnyWindowHandle>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
// Clear any existing error state
|
||||
self.retry_state = None;
|
||||
|
||||
// Use the last error context if available, otherwise fall back to configured model
|
||||
let (model, intent) = if let Some((model, intent)) = self.last_error_context.take() {
|
||||
(model, intent)
|
||||
} else if let Some(configured_model) = self.configured_model.as_ref() {
|
||||
let model = configured_model.model.clone();
|
||||
let intent = if self.has_pending_tool_uses() {
|
||||
CompletionIntent::ToolResults
|
||||
} else {
|
||||
CompletionIntent::UserPrompt
|
||||
};
|
||||
(model, intent)
|
||||
} else if let Some(configured_model) = self.get_or_init_configured_model(cx) {
|
||||
let model = configured_model.model.clone();
|
||||
let intent = if self.has_pending_tool_uses() {
|
||||
CompletionIntent::ToolResults
|
||||
} else {
|
||||
CompletionIntent::UserPrompt
|
||||
};
|
||||
(model, intent)
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.send_to_model(model, intent, window, cx);
|
||||
}
|
||||
|
||||
pub fn enable_burn_mode_and_retry(
|
||||
&mut self,
|
||||
window: Option<AnyWindowHandle>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.completion_mode = CompletionMode::Burn;
|
||||
cx.emit(ThreadEvent::ProfileChanged);
|
||||
self.retry_last_completion(window, cx);
|
||||
}
|
||||
|
||||
pub fn used_tools_since_last_user_message(&self) -> bool {
|
||||
@@ -1530,20 +1581,18 @@ impl Thread {
|
||||
model: Arc<dyn LanguageModel>,
|
||||
cx: &mut App,
|
||||
) -> Option<PendingToolUse> {
|
||||
let action_log = self.action_log.read(cx);
|
||||
// Represent notification as a simulated `project_notifications` tool call
|
||||
let tool_name = Arc::from("project_notifications");
|
||||
let tool = self.tools.read(cx).tool(&tool_name, cx)?;
|
||||
|
||||
if !action_log.has_unnotified_user_edits() {
|
||||
if !self.profile.is_tool_enabled(tool.source(), tool.name(), cx) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Represent notification as a simulated `project_notifications` tool call
|
||||
let tool_name = Arc::from("project_notifications");
|
||||
let Some(tool) = self.tools.read(cx).tool(&tool_name, cx) else {
|
||||
debug_panic!("`project_notifications` tool not found");
|
||||
return None;
|
||||
};
|
||||
|
||||
if !self.profile.is_tool_enabled(tool.source(), tool.name(), cx) {
|
||||
if self
|
||||
.action_log
|
||||
.update(cx, |log, cx| log.unnotified_user_edits(cx).is_none())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -1631,7 +1680,7 @@ impl Thread {
|
||||
|
||||
let completion_mode = request
|
||||
.mode
|
||||
.unwrap_or(zed_llm_client::CompletionMode::Normal);
|
||||
.unwrap_or(cloud_llm_client::CompletionMode::Normal);
|
||||
|
||||
self.last_received_chunk_at = Some(Instant::now());
|
||||
|
||||
@@ -1987,6 +2036,12 @@ impl Thread {
|
||||
if let Some(retry_strategy) =
|
||||
Thread::get_retry_strategy(completion_error)
|
||||
{
|
||||
log::info!(
|
||||
"Retrying with {:?} for language model completion error {:?}",
|
||||
retry_strategy,
|
||||
completion_error
|
||||
);
|
||||
|
||||
retry_scheduled = thread
|
||||
.handle_retryable_error_with_delay(
|
||||
&completion_error,
|
||||
@@ -2130,8 +2185,8 @@ impl Thread {
|
||||
|
||||
// General strategy here:
|
||||
// - If retrying won't help (e.g. invalid API key or payload too large), return None so we don't retry at all.
|
||||
// - If it's a time-based issue (e.g. server overloaded, rate limit exceeded), try multiple times with exponential backoff.
|
||||
// - If it's an issue that *might* be fixed by retrying (e.g. internal server error), just retry once.
|
||||
// - If it's a time-based issue (e.g. server overloaded, rate limit exceeded), retry up to 4 times with exponential backoff.
|
||||
// - If it's an issue that *might* be fixed by retrying (e.g. internal server error), retry up to 3 times.
|
||||
match error {
|
||||
HttpResponseError {
|
||||
status_code: StatusCode::TOO_MANY_REQUESTS,
|
||||
@@ -2146,16 +2201,48 @@ impl Thread {
|
||||
max_attempts: MAX_RETRY_ATTEMPTS,
|
||||
})
|
||||
}
|
||||
UpstreamProviderError {
|
||||
status,
|
||||
retry_after,
|
||||
..
|
||||
} => match *status {
|
||||
StatusCode::TOO_MANY_REQUESTS | StatusCode::SERVICE_UNAVAILABLE => {
|
||||
Some(RetryStrategy::Fixed {
|
||||
delay: retry_after.unwrap_or(BASE_RETRY_DELAY),
|
||||
max_attempts: MAX_RETRY_ATTEMPTS,
|
||||
})
|
||||
}
|
||||
StatusCode::INTERNAL_SERVER_ERROR => Some(RetryStrategy::Fixed {
|
||||
delay: retry_after.unwrap_or(BASE_RETRY_DELAY),
|
||||
// Internal Server Error could be anything, retry up to 3 times.
|
||||
max_attempts: 3,
|
||||
}),
|
||||
status => {
|
||||
// There is no StatusCode variant for the unofficial HTTP 529 ("The service is overloaded"),
|
||||
// but we frequently get them in practice. See https://http.dev/529
|
||||
if status.as_u16() == 529 {
|
||||
Some(RetryStrategy::Fixed {
|
||||
delay: retry_after.unwrap_or(BASE_RETRY_DELAY),
|
||||
max_attempts: MAX_RETRY_ATTEMPTS,
|
||||
})
|
||||
} else {
|
||||
Some(RetryStrategy::Fixed {
|
||||
delay: retry_after.unwrap_or(BASE_RETRY_DELAY),
|
||||
max_attempts: 2,
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
ApiInternalServerError { .. } => Some(RetryStrategy::Fixed {
|
||||
delay: BASE_RETRY_DELAY,
|
||||
max_attempts: 1,
|
||||
max_attempts: 3,
|
||||
}),
|
||||
ApiReadResponseError { .. }
|
||||
| HttpSend { .. }
|
||||
| DeserializeResponse { .. }
|
||||
| BadRequestFormat { .. } => Some(RetryStrategy::Fixed {
|
||||
delay: BASE_RETRY_DELAY,
|
||||
max_attempts: 1,
|
||||
max_attempts: 3,
|
||||
}),
|
||||
// Retrying these errors definitely shouldn't help.
|
||||
HttpResponseError {
|
||||
@@ -2163,24 +2250,30 @@ impl Thread {
|
||||
StatusCode::PAYLOAD_TOO_LARGE | StatusCode::FORBIDDEN | StatusCode::UNAUTHORIZED,
|
||||
..
|
||||
}
|
||||
| SerializeRequest { .. }
|
||||
| BuildRequestBody { .. }
|
||||
| PromptTooLarge { .. }
|
||||
| AuthenticationError { .. }
|
||||
| PermissionError { .. }
|
||||
| NoApiKey { .. }
|
||||
| ApiEndpointNotFound { .. }
|
||||
| NoApiKey { .. } => None,
|
||||
| PromptTooLarge { .. } => None,
|
||||
// These errors might be transient, so retry them
|
||||
SerializeRequest { .. } | BuildRequestBody { .. } => Some(RetryStrategy::Fixed {
|
||||
delay: BASE_RETRY_DELAY,
|
||||
max_attempts: 1,
|
||||
}),
|
||||
// Retry all other 4xx and 5xx errors once.
|
||||
HttpResponseError { status_code, .. }
|
||||
if status_code.is_client_error() || status_code.is_server_error() =>
|
||||
{
|
||||
Some(RetryStrategy::Fixed {
|
||||
delay: BASE_RETRY_DELAY,
|
||||
max_attempts: 1,
|
||||
max_attempts: 3,
|
||||
})
|
||||
}
|
||||
// Conservatively assume that any other errors are non-retryable
|
||||
HttpResponseError { .. } | Other(..) => None,
|
||||
HttpResponseError { .. } | Other(..) => Some(RetryStrategy::Fixed {
|
||||
delay: BASE_RETRY_DELAY,
|
||||
max_attempts: 2,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2193,6 +2286,23 @@ impl Thread {
|
||||
window: Option<AnyWindowHandle>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
// Store context for the Retry button
|
||||
self.last_error_context = Some((model.clone(), intent));
|
||||
|
||||
// Only auto-retry if Burn Mode is enabled
|
||||
if self.completion_mode != CompletionMode::Burn {
|
||||
// Show error with retry options
|
||||
cx.emit(ThreadEvent::ShowError(ThreadError::RetryableError {
|
||||
message: format!(
|
||||
"{}\n\nTo automatically retry when similar errors happen, enable Burn Mode.",
|
||||
error
|
||||
)
|
||||
.into(),
|
||||
can_enable_burn_mode: true,
|
||||
}));
|
||||
return false;
|
||||
}
|
||||
|
||||
let Some(strategy) = strategy.or_else(|| Self::get_retry_strategy(error)) else {
|
||||
return false;
|
||||
};
|
||||
@@ -2273,6 +2383,13 @@ impl Thread {
|
||||
// Stop generating since we're giving up on retrying.
|
||||
self.pending_completions.clear();
|
||||
|
||||
// Show error alongside a Retry button, but no
|
||||
// Enable Burn Mode button (since it's already enabled)
|
||||
cx.emit(ThreadEvent::ShowError(ThreadError::RetryableError {
|
||||
message: format!("Failed after retrying: {}", error).into(),
|
||||
can_enable_burn_mode: false,
|
||||
}));
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -2439,7 +2556,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(
|
||||
@@ -3137,8 +3254,10 @@ impl Thread {
|
||||
}
|
||||
|
||||
fn update_model_request_usage(&self, amount: u32, limit: UsageLimit, cx: &mut Context<Self>) {
|
||||
self.project.update(cx, |project, cx| {
|
||||
project.user_store().update(cx, |user_store, cx| {
|
||||
self.project
|
||||
.read(cx)
|
||||
.user_store()
|
||||
.update(cx, |user_store, cx| {
|
||||
user_store.update_model_request_usage(
|
||||
ModelRequestUsage(RequestUsage {
|
||||
amount: amount as i32,
|
||||
@@ -3146,8 +3265,7 @@ impl Thread {
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn deny_tool_use(
|
||||
@@ -3183,6 +3301,11 @@ pub enum ThreadError {
|
||||
header: SharedString,
|
||||
message: SharedString,
|
||||
},
|
||||
#[error("Retryable error: {message}")]
|
||||
RetryableError {
|
||||
message: SharedString,
|
||||
can_enable_burn_mode: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -3583,6 +3706,7 @@ fn main() {{
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
#[ignore] // turn this test on when project_notifications tool is re-enabled
|
||||
async fn test_stale_buffer_notification(cx: &mut TestAppContext) {
|
||||
init_test_settings(cx);
|
||||
|
||||
@@ -4137,6 +4261,11 @@ fn main() {{
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Enable Burn Mode to allow retries
|
||||
thread.update(cx, |thread, _| {
|
||||
thread.set_completion_mode(CompletionMode::Burn);
|
||||
});
|
||||
|
||||
// Create model that returns overloaded error
|
||||
let model = Arc::new(ErrorInjector::new(TestError::Overloaded));
|
||||
|
||||
@@ -4210,6 +4339,11 @@ fn main() {{
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Enable Burn Mode to allow retries
|
||||
thread.update(cx, |thread, _| {
|
||||
thread.set_completion_mode(CompletionMode::Burn);
|
||||
});
|
||||
|
||||
// Create model that returns internal server error
|
||||
let model = Arc::new(ErrorInjector::new(TestError::InternalServerError));
|
||||
|
||||
@@ -4231,7 +4365,7 @@ fn main() {{
|
||||
let retry_state = thread.retry_state.as_ref().unwrap();
|
||||
assert_eq!(retry_state.attempt, 1, "Should be first retry attempt");
|
||||
assert_eq!(
|
||||
retry_state.max_attempts, 1,
|
||||
retry_state.max_attempts, 3,
|
||||
"Should have correct max attempts"
|
||||
);
|
||||
});
|
||||
@@ -4247,8 +4381,9 @@ fn main() {{
|
||||
if let MessageSegment::Text(text) = seg {
|
||||
text.contains("internal")
|
||||
&& text.contains("Fake")
|
||||
&& text.contains("Retrying in")
|
||||
&& !text.contains("attempt")
|
||||
&& text.contains("Retrying")
|
||||
&& text.contains("attempt 1 of 3")
|
||||
&& text.contains("seconds")
|
||||
} else {
|
||||
false
|
||||
}
|
||||
@@ -4286,6 +4421,11 @@ fn main() {{
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Enable Burn Mode to allow retries
|
||||
thread.update(cx, |thread, _| {
|
||||
thread.set_completion_mode(CompletionMode::Burn);
|
||||
});
|
||||
|
||||
// Create model that returns internal server error
|
||||
let model = Arc::new(ErrorInjector::new(TestError::InternalServerError));
|
||||
|
||||
@@ -4338,8 +4478,8 @@ fn main() {{
|
||||
let retry_state = thread.retry_state.as_ref().unwrap();
|
||||
assert_eq!(retry_state.attempt, 1, "Should be first retry attempt");
|
||||
assert_eq!(
|
||||
retry_state.max_attempts, 1,
|
||||
"Internal server errors should only retry once"
|
||||
retry_state.max_attempts, 3,
|
||||
"Internal server errors should retry up to 3 times"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -4347,7 +4487,15 @@ fn main() {{
|
||||
cx.executor().advance_clock(BASE_RETRY_DELAY);
|
||||
cx.run_until_parked();
|
||||
|
||||
// Should have scheduled second retry - count retry messages
|
||||
// Advance clock for second retry
|
||||
cx.executor().advance_clock(BASE_RETRY_DELAY);
|
||||
cx.run_until_parked();
|
||||
|
||||
// Advance clock for third retry
|
||||
cx.executor().advance_clock(BASE_RETRY_DELAY);
|
||||
cx.run_until_parked();
|
||||
|
||||
// Should have completed all retries - count retry messages
|
||||
let retry_count = thread.update(cx, |thread, _| {
|
||||
thread
|
||||
.messages
|
||||
@@ -4365,24 +4513,24 @@ fn main() {{
|
||||
.count()
|
||||
});
|
||||
assert_eq!(
|
||||
retry_count, 1,
|
||||
"Should have only one retry for internal server errors"
|
||||
retry_count, 3,
|
||||
"Should have 3 retries for internal server errors"
|
||||
);
|
||||
|
||||
// For internal server errors, we only retry once and then give up
|
||||
// Check that retry_state is cleared after the single retry
|
||||
// For internal server errors, we retry 3 times and then give up
|
||||
// Check that retry_state is cleared after all retries
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert!(
|
||||
thread.retry_state.is_none(),
|
||||
"Retry state should be cleared after single retry"
|
||||
"Retry state should be cleared after all retries"
|
||||
);
|
||||
});
|
||||
|
||||
// Verify total attempts (1 initial + 1 retry)
|
||||
// Verify total attempts (1 initial + 3 retries)
|
||||
assert_eq!(
|
||||
*completion_count.lock(),
|
||||
2,
|
||||
"Should have attempted once plus 1 retry"
|
||||
4,
|
||||
"Should have attempted once plus 3 retries"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4393,6 +4541,11 @@ fn main() {{
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Enable Burn Mode to allow retries
|
||||
thread.update(cx, |thread, _| {
|
||||
thread.set_completion_mode(CompletionMode::Burn);
|
||||
});
|
||||
|
||||
// Create model that returns overloaded error
|
||||
let model = Arc::new(ErrorInjector::new(TestError::Overloaded));
|
||||
|
||||
@@ -4479,6 +4632,11 @@ fn main() {{
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Enable Burn Mode to allow retries
|
||||
thread.update(cx, |thread, _| {
|
||||
thread.set_completion_mode(CompletionMode::Burn);
|
||||
});
|
||||
|
||||
// We'll use a wrapper to switch behavior after first failure
|
||||
struct RetryTestModel {
|
||||
inner: Arc<FakeLanguageModel>,
|
||||
@@ -4647,6 +4805,11 @@ fn main() {{
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Enable Burn Mode to allow retries
|
||||
thread.update(cx, |thread, _| {
|
||||
thread.set_completion_mode(CompletionMode::Burn);
|
||||
});
|
||||
|
||||
// Create a model that fails once then succeeds
|
||||
struct FailOnceModel {
|
||||
inner: Arc<FakeLanguageModel>,
|
||||
@@ -4808,6 +4971,11 @@ fn main() {{
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Enable Burn Mode to allow retries
|
||||
thread.update(cx, |thread, _| {
|
||||
thread.set_completion_mode(CompletionMode::Burn);
|
||||
});
|
||||
|
||||
// Create a model that returns rate limit error with retry_after
|
||||
struct RateLimitModel {
|
||||
inner: Arc<FakeLanguageModel>,
|
||||
@@ -5081,6 +5249,79 @@ fn main() {{
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_no_retry_without_burn_mode(cx: &mut TestAppContext) {
|
||||
init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Ensure we're in Normal mode (not Burn mode)
|
||||
thread.update(cx, |thread, _| {
|
||||
thread.set_completion_mode(CompletionMode::Normal);
|
||||
});
|
||||
|
||||
// Track error events
|
||||
let error_events = Arc::new(Mutex::new(Vec::new()));
|
||||
let error_events_clone = error_events.clone();
|
||||
|
||||
let _subscription = thread.update(cx, |_, cx| {
|
||||
cx.subscribe(&thread, move |_, _, event: &ThreadEvent, _| {
|
||||
if let ThreadEvent::ShowError(error) = event {
|
||||
error_events_clone.lock().push(error.clone());
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// Create model that returns overloaded error
|
||||
let model = Arc::new(ErrorInjector::new(TestError::Overloaded));
|
||||
|
||||
// Insert a user message
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.insert_user_message("Hello!", ContextLoadResult::default(), None, vec![], cx);
|
||||
});
|
||||
|
||||
// Start completion
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.send_to_model(model.clone(), CompletionIntent::UserPrompt, None, cx);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
// Verify no retry state was created
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert!(
|
||||
thread.retry_state.is_none(),
|
||||
"Should not have retry state in Normal mode"
|
||||
);
|
||||
});
|
||||
|
||||
// Check that a retryable error was reported
|
||||
let errors = error_events.lock();
|
||||
assert!(!errors.is_empty(), "Should have received an error event");
|
||||
|
||||
if let ThreadError::RetryableError {
|
||||
message: _,
|
||||
can_enable_burn_mode,
|
||||
} = &errors[0]
|
||||
{
|
||||
assert!(
|
||||
*can_enable_burn_mode,
|
||||
"Error should indicate burn mode can be enabled"
|
||||
);
|
||||
} else {
|
||||
panic!("Expected RetryableError, got {:?}", errors[0]);
|
||||
}
|
||||
|
||||
// Verify the thread is no longer generating
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert!(
|
||||
!thread.is_generating(),
|
||||
"Should not be generating after error without retry"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_retry_cancelled_on_stop(cx: &mut TestAppContext) {
|
||||
init_test_settings(cx);
|
||||
@@ -5088,6 +5329,11 @@ fn main() {{
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Enable Burn Mode to allow retries
|
||||
thread.update(cx, |thread, _| {
|
||||
thread.set_completion_mode(CompletionMode::Burn);
|
||||
});
|
||||
|
||||
// Create model that returns overloaded error
|
||||
let model = Arc::new(ErrorInjector::new(TestError::Overloaded));
|
||||
|
||||
@@ -5249,7 +5495,7 @@ fn main() {{
|
||||
let thread = thread_store.update(cx, |store, cx| store.create_thread(cx));
|
||||
let context_store = cx.new(|_cx| ContextStore::new(project.downgrade(), None));
|
||||
|
||||
let provider = Arc::new(FakeLanguageModelProvider);
|
||||
let provider = Arc::new(FakeLanguageModelProvider::default());
|
||||
let model = provider.test_model();
|
||||
let model: Arc<dyn LanguageModel> = Arc::new(model);
|
||||
|
||||
|
||||
@@ -41,6 +41,9 @@ use std::{
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
|
||||
pub static ZED_STATELESS: std::sync::LazyLock<bool> =
|
||||
std::sync::LazyLock::new(|| std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty()));
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum DataType {
|
||||
#[serde(rename = "json")]
|
||||
@@ -874,7 +877,11 @@ impl ThreadsDatabase {
|
||||
|
||||
let needs_migration_from_heed = mdb_path.exists();
|
||||
|
||||
let connection = Connection::open_file(&sqlite_path.to_string_lossy());
|
||||
let connection = if *ZED_STATELESS {
|
||||
Connection::open_memory(Some("THREAD_FALLBACK_DB"))
|
||||
} else {
|
||||
Connection::open_file(&sqlite_path.to_string_lossy())
|
||||
};
|
||||
|
||||
connection.exec(indoc! {"
|
||||
CREATE TABLE IF NOT EXISTS threads (
|
||||
|
||||
@@ -165,7 +165,12 @@ impl ToolUseState {
|
||||
self.pending_tool_uses_by_id.values().collect()
|
||||
}
|
||||
|
||||
pub fn tool_uses_for_message(&self, id: MessageId, cx: &App) -> Vec<ToolUse> {
|
||||
pub fn tool_uses_for_message(
|
||||
&self,
|
||||
id: MessageId,
|
||||
project: &Entity<Project>,
|
||||
cx: &App,
|
||||
) -> Vec<ToolUse> {
|
||||
let Some(tool_uses_for_message) = &self.tool_uses_by_assistant_message.get(&id) else {
|
||||
return Vec::new();
|
||||
};
|
||||
@@ -211,7 +216,10 @@ impl ToolUseState {
|
||||
|
||||
let (icon, needs_confirmation) =
|
||||
if let Some(tool) = self.tools.read(cx).tool(&tool_use.name, cx) {
|
||||
(tool.icon(), tool.needs_confirmation(&tool_use.input, cx))
|
||||
(
|
||||
tool.icon(),
|
||||
tool.needs_confirmation(&tool_use.input, project, cx),
|
||||
)
|
||||
} else {
|
||||
(IconName::Cog, false)
|
||||
};
|
||||
|
||||
@@ -5,6 +5,10 @@ edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[features]
|
||||
test-support = ["acp_thread/test-support", "gpui/test-support", "project/test-support"]
|
||||
e2e = []
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -13,15 +17,41 @@ path = "src/agent_servers.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
acp_thread.workspace = true
|
||||
agent-client-protocol.workspace = true
|
||||
agentic-coding-protocol.workspace = true
|
||||
anyhow.workspace = true
|
||||
collections.workspace = true
|
||||
context_server.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
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
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
strum.workspace = true
|
||||
tempfile.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
watch.workspace = true
|
||||
indoc.workspace = true
|
||||
which.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc.workspace = true
|
||||
nix.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger.workspace = true
|
||||
language.workspace = true
|
||||
indoc.workspace = true
|
||||
acp_thread = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -1,30 +1,82 @@
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
mod claude;
|
||||
mod codex;
|
||||
mod gemini;
|
||||
mod mcp_server;
|
||||
mod settings;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
#[cfg(test)]
|
||||
mod e2e_tests;
|
||||
|
||||
pub use claude::*;
|
||||
pub use codex::*;
|
||||
pub use gemini::*;
|
||||
pub use settings::*;
|
||||
|
||||
use acp_thread::AgentConnection;
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use gpui::{App, AsyncApp, Entity, SharedString};
|
||||
use gpui::{App, AsyncApp, Entity, SharedString, Task};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources, SettingsStore};
|
||||
use util::{ResultExt, paths};
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
AllAgentServersSettings::register(cx);
|
||||
settings::init(cx);
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug)]
|
||||
pub struct AllAgentServersSettings {
|
||||
gemini: Option<AgentServerSettings>,
|
||||
pub trait AgentServer: Send {
|
||||
fn logo(&self) -> ui::IconName;
|
||||
fn name(&self) -> &'static str;
|
||||
fn empty_state_headline(&self) -> &'static str;
|
||||
fn empty_state_message(&self) -> &'static str;
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
// these will go away when old_acp is fully removed
|
||||
root_dir: &Path,
|
||||
project: &Entity<Project>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Rc<dyn AgentConnection>>>;
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug)]
|
||||
pub struct AgentServerSettings {
|
||||
#[serde(flatten)]
|
||||
command: AgentServerCommand,
|
||||
impl std::fmt::Debug for AgentServerCommand {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let filtered_env = self.env.as_ref().map(|env| {
|
||||
env.iter()
|
||||
.map(|(k, v)| {
|
||||
(
|
||||
k,
|
||||
if util::redact::should_redact(k) {
|
||||
"[REDACTED]"
|
||||
} else {
|
||||
v
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
f.debug_struct("AgentServerCommand")
|
||||
.field("path", &self.path)
|
||||
.field("args", &self.args)
|
||||
.field("env", &filtered_env)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
pub enum AgentServerVersion {
|
||||
Supported,
|
||||
Unsupported {
|
||||
error_message: SharedString,
|
||||
upgrade_message: SharedString,
|
||||
upgrade_command: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)]
|
||||
@@ -36,105 +88,34 @@ pub struct AgentServerCommand {
|
||||
pub env: Option<HashMap<String, String>>,
|
||||
}
|
||||
|
||||
pub struct Gemini;
|
||||
|
||||
pub struct AgentServerVersion {
|
||||
pub current_version: SharedString,
|
||||
pub supported: bool,
|
||||
}
|
||||
|
||||
pub trait AgentServer: Send {
|
||||
fn command(
|
||||
&self,
|
||||
impl AgentServerCommand {
|
||||
pub(crate) async fn resolve(
|
||||
path_bin_name: &'static str,
|
||||
extra_args: &[&'static str],
|
||||
settings: Option<AgentServerSettings>,
|
||||
project: &Entity<Project>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> impl Future<Output = Result<AgentServerCommand>>;
|
||||
|
||||
fn version(
|
||||
&self,
|
||||
command: &AgentServerCommand,
|
||||
) -> impl Future<Output = Result<AgentServerVersion>> + Send;
|
||||
}
|
||||
|
||||
const GEMINI_ACP_ARG: &str = "--experimental-acp";
|
||||
|
||||
impl AgentServer for Gemini {
|
||||
async fn command(
|
||||
&self,
|
||||
project: &Entity<Project>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<AgentServerCommand> {
|
||||
let custom_command = cx.read_global(|settings: &SettingsStore, _| {
|
||||
let settings = settings.get::<AllAgentServersSettings>(None);
|
||||
settings
|
||||
.gemini
|
||||
.as_ref()
|
||||
.map(|gemini_settings| AgentServerCommand {
|
||||
path: gemini_settings.command.path.clone(),
|
||||
args: gemini_settings
|
||||
.command
|
||||
.args
|
||||
.iter()
|
||||
.cloned()
|
||||
.chain(std::iter::once(GEMINI_ACP_ARG.into()))
|
||||
.collect(),
|
||||
env: gemini_settings.command.env.clone(),
|
||||
})
|
||||
})?;
|
||||
|
||||
if let Some(custom_command) = custom_command {
|
||||
return Ok(custom_command);
|
||||
}
|
||||
|
||||
if let Some(path) = find_bin_in_path("gemini", project, cx).await {
|
||||
return Ok(AgentServerCommand {
|
||||
path,
|
||||
args: vec![GEMINI_ACP_ARG.into()],
|
||||
env: None,
|
||||
) -> Option<Self> {
|
||||
if let Some(agent_settings) = settings {
|
||||
return Some(Self {
|
||||
path: agent_settings.command.path,
|
||||
args: agent_settings
|
||||
.command
|
||||
.args
|
||||
.into_iter()
|
||||
.chain(extra_args.iter().map(|arg| arg.to_string()))
|
||||
.collect(),
|
||||
env: agent_settings.command.env,
|
||||
});
|
||||
} else {
|
||||
find_bin_in_path(path_bin_name, project, cx)
|
||||
.await
|
||||
.map(|path| Self {
|
||||
path,
|
||||
args: extra_args.iter().map(|arg| arg.to_string()).collect(),
|
||||
env: None,
|
||||
})
|
||||
}
|
||||
|
||||
let (fs, node_runtime) = project.update(cx, |project, _| {
|
||||
(project.fs().clone(), project.node_runtime().cloned())
|
||||
})?;
|
||||
let node_runtime = node_runtime.context("gemini not found on path")?;
|
||||
|
||||
let directory = ::paths::agent_servers_dir().join("gemini");
|
||||
fs.create_dir(&directory).await?;
|
||||
node_runtime
|
||||
.npm_install_packages(&directory, &[("@google/gemini-cli", "latest")])
|
||||
.await?;
|
||||
let path = directory.join("node_modules/.bin/gemini");
|
||||
|
||||
Ok(AgentServerCommand {
|
||||
path,
|
||||
args: vec![GEMINI_ACP_ARG.into()],
|
||||
env: None,
|
||||
})
|
||||
}
|
||||
|
||||
async fn version(&self, command: &AgentServerCommand) -> Result<AgentServerVersion> {
|
||||
let version_fut = util::command::new_smol_command(&command.path)
|
||||
.args(command.args.iter())
|
||||
.arg("--version")
|
||||
.kill_on_drop(true)
|
||||
.output();
|
||||
|
||||
let help_fut = util::command::new_smol_command(&command.path)
|
||||
.args(command.args.iter())
|
||||
.arg("--help")
|
||||
.kill_on_drop(true)
|
||||
.output();
|
||||
|
||||
let (version_output, help_output) = futures::future::join(version_fut, help_fut).await;
|
||||
|
||||
let current_version = String::from_utf8(version_output?.stdout)?.into();
|
||||
let supported = String::from_utf8(help_output?.stdout)?.contains(GEMINI_ACP_ARG);
|
||||
|
||||
Ok(AgentServerVersion {
|
||||
current_version,
|
||||
supported,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,48 +165,3 @@ async fn find_bin_in_path(
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for AgentServerCommand {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let filtered_env = self.env.as_ref().map(|env| {
|
||||
env.iter()
|
||||
.map(|(k, v)| {
|
||||
(
|
||||
k,
|
||||
if util::redact::should_redact(k) {
|
||||
"[REDACTED]"
|
||||
} else {
|
||||
v
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
f.debug_struct("AgentServerCommand")
|
||||
.field("path", &self.path)
|
||||
.field("args", &self.args)
|
||||
.field("env", &filtered_env)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl settings::Settings for AllAgentServersSettings {
|
||||
const KEY: Option<&'static str> = Some("agent_servers");
|
||||
|
||||
type FileContent = Self;
|
||||
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
|
||||
let mut settings = AllAgentServersSettings::default();
|
||||
|
||||
for value in sources.defaults_and_customizations() {
|
||||
if value.gemini.is_some() {
|
||||
settings.gemini = value.gemini.clone();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(settings)
|
||||
}
|
||||
|
||||
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
|
||||
}
|
||||
|
||||
839
crates/agent_servers/src/claude.rs
Normal file
@@ -0,0 +1,839 @@
|
||||
mod mcp_server;
|
||||
pub mod tools;
|
||||
|
||||
use collections::HashMap;
|
||||
use context_server::listener::McpServerTool;
|
||||
use project::Project;
|
||||
use settings::SettingsStore;
|
||||
use smol::process::Child;
|
||||
use std::cell::RefCell;
|
||||
use std::fmt::Display;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use agent_client_protocol as acp;
|
||||
use anyhow::{Result, anyhow};
|
||||
use futures::channel::oneshot;
|
||||
use futures::{AsyncBufReadExt, AsyncWriteExt};
|
||||
use futures::{
|
||||
AsyncRead, AsyncWrite, FutureExt, StreamExt,
|
||||
channel::mpsc::{self, UnboundedReceiver, UnboundedSender},
|
||||
io::BufReader,
|
||||
select_biased,
|
||||
};
|
||||
use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::claude::mcp_server::{ClaudeZedMcpServer, McpConfig};
|
||||
use crate::claude::tools::ClaudeTool;
|
||||
use crate::{AgentServer, AgentServerCommand, AllAgentServersSettings};
|
||||
use acp_thread::{AcpThread, AgentConnection};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ClaudeCode;
|
||||
|
||||
impl AgentServer for ClaudeCode {
|
||||
fn name(&self) -> &'static str {
|
||||
"Claude Code"
|
||||
}
|
||||
|
||||
fn empty_state_headline(&self) -> &'static str {
|
||||
self.name()
|
||||
}
|
||||
|
||||
fn empty_state_message(&self) -> &'static str {
|
||||
"How can I help you today?"
|
||||
}
|
||||
|
||||
fn logo(&self) -> ui::IconName {
|
||||
ui::IconName::AiClaude
|
||||
}
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
_root_dir: &Path,
|
||||
_project: &Entity<Project>,
|
||||
_cx: &mut App,
|
||||
) -> Task<Result<Rc<dyn AgentConnection>>> {
|
||||
let connection = ClaudeAgentConnection {
|
||||
sessions: Default::default(),
|
||||
};
|
||||
|
||||
Task::ready(Ok(Rc::new(connection) as _))
|
||||
}
|
||||
}
|
||||
|
||||
struct ClaudeAgentConnection {
|
||||
sessions: Rc<RefCell<HashMap<acp::SessionId, ClaudeAgentSession>>>,
|
||||
}
|
||||
|
||||
impl AgentConnection for ClaudeAgentConnection {
|
||||
fn name(&self) -> &'static str {
|
||||
ClaudeCode.name()
|
||||
}
|
||||
|
||||
fn new_thread(
|
||||
self: Rc<Self>,
|
||||
project: Entity<Project>,
|
||||
cwd: &Path,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Task<Result<Entity<AcpThread>>> {
|
||||
let cwd = cwd.to_owned();
|
||||
cx.spawn(async move |cx| {
|
||||
let (mut thread_tx, thread_rx) = watch::channel(WeakEntity::new_invalid());
|
||||
let permission_mcp_server = ClaudeZedMcpServer::new(thread_rx.clone(), cx).await?;
|
||||
|
||||
let mut mcp_servers = HashMap::default();
|
||||
mcp_servers.insert(
|
||||
mcp_server::SERVER_NAME.to_string(),
|
||||
permission_mcp_server.server_config()?,
|
||||
);
|
||||
let mcp_config = McpConfig { mcp_servers };
|
||||
|
||||
let mcp_config_file = tempfile::NamedTempFile::new()?;
|
||||
let (mcp_config_file, mcp_config_path) = mcp_config_file.into_parts();
|
||||
|
||||
let mut mcp_config_file = smol::fs::File::from(mcp_config_file);
|
||||
mcp_config_file
|
||||
.write_all(serde_json::to_string(&mcp_config)?.as_bytes())
|
||||
.await?;
|
||||
mcp_config_file.flush().await?;
|
||||
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).claude.clone()
|
||||
})?;
|
||||
|
||||
let Some(command) =
|
||||
AgentServerCommand::resolve("claude", &[], settings, &project, cx).await
|
||||
else {
|
||||
anyhow::bail!("Failed to find claude binary");
|
||||
};
|
||||
|
||||
let (incoming_message_tx, mut incoming_message_rx) = mpsc::unbounded();
|
||||
let (outgoing_tx, outgoing_rx) = mpsc::unbounded();
|
||||
|
||||
let session_id = acp::SessionId(Uuid::new_v4().to_string().into());
|
||||
|
||||
log::trace!("Starting session with id: {}", session_id);
|
||||
|
||||
cx.background_spawn({
|
||||
let session_id = session_id.clone();
|
||||
async move {
|
||||
let mut outgoing_rx = Some(outgoing_rx);
|
||||
|
||||
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);
|
||||
|
||||
ClaudeAgentSession::handle_io(
|
||||
outgoing_rx.take().unwrap(),
|
||||
incoming_message_tx.clone(),
|
||||
child.stdin.take().unwrap(),
|
||||
child.stdout.take().unwrap(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
log::trace!("Stopped (pid: {})", pid);
|
||||
|
||||
drop(mcp_config_path);
|
||||
anyhow::Ok(())
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
let end_turn_tx = Rc::new(RefCell::new(None));
|
||||
let handler_task = cx.spawn({
|
||||
let end_turn_tx = end_turn_tx.clone();
|
||||
let thread_rx = thread_rx.clone();
|
||||
async move |cx| {
|
||||
while let Some(message) = incoming_message_rx.next().await {
|
||||
ClaudeAgentSession::handle_message(
|
||||
thread_rx.clone(),
|
||||
message,
|
||||
end_turn_tx.clone(),
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let thread =
|
||||
cx.new(|cx| AcpThread::new(self.clone(), project, session_id.clone(), cx))?;
|
||||
|
||||
thread_tx.send(thread.downgrade())?;
|
||||
|
||||
let session = ClaudeAgentSession {
|
||||
outgoing_tx,
|
||||
end_turn_tx,
|
||||
_handler_task: handler_task,
|
||||
_mcp_server: Some(permission_mcp_server),
|
||||
};
|
||||
|
||||
self.sessions.borrow_mut().insert(session_id, session);
|
||||
|
||||
Ok(thread)
|
||||
})
|
||||
}
|
||||
|
||||
fn authenticate(&self, _cx: &mut App) -> Task<Result<()>> {
|
||||
Task::ready(Err(anyhow!("Authentication not supported")))
|
||||
}
|
||||
|
||||
fn prompt(&self, params: acp::PromptArguments, cx: &mut App) -> Task<Result<()>> {
|
||||
let sessions = self.sessions.borrow();
|
||||
let Some(session) = sessions.get(¶ms.session_id) else {
|
||||
return Task::ready(Err(anyhow!(
|
||||
"Attempted to send message to nonexistent session {}",
|
||||
params.session_id
|
||||
)));
|
||||
};
|
||||
|
||||
let (tx, rx) = oneshot::channel();
|
||||
session.end_turn_tx.borrow_mut().replace(tx);
|
||||
|
||||
let mut content = String::new();
|
||||
for chunk in params.prompt {
|
||||
match chunk {
|
||||
acp::ContentBlock::Text(text_content) => {
|
||||
content.push_str(&text_content.text);
|
||||
}
|
||||
acp::ContentBlock::ResourceLink(resource_link) => {
|
||||
content.push_str(&format!("@{}", resource_link.uri));
|
||||
}
|
||||
acp::ContentBlock::Audio(_)
|
||||
| acp::ContentBlock::Image(_)
|
||||
| acp::ContentBlock::Resource(_) => {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(err) = session.outgoing_tx.unbounded_send(SdkMessage::User {
|
||||
message: Message {
|
||||
role: Role::User,
|
||||
content: Content::UntaggedText(content),
|
||||
id: None,
|
||||
model: None,
|
||||
stop_reason: None,
|
||||
stop_sequence: None,
|
||||
usage: None,
|
||||
},
|
||||
session_id: Some(params.session_id.to_string()),
|
||||
}) {
|
||||
return Task::ready(Err(anyhow!(err)));
|
||||
}
|
||||
|
||||
cx.foreground_executor().spawn(async move {
|
||||
rx.await??;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) {
|
||||
let sessions = self.sessions.borrow();
|
||||
let Some(session) = sessions.get(&session_id) else {
|
||||
log::warn!("Attempted to cancel nonexistent session {}", session_id);
|
||||
return;
|
||||
};
|
||||
|
||||
session
|
||||
.outgoing_tx
|
||||
.unbounded_send(SdkMessage::new_interrupt_message())
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum ClaudeSessionMode {
|
||||
Start,
|
||||
#[expect(dead_code)]
|
||||
Resume,
|
||||
}
|
||||
|
||||
async fn spawn_claude(
|
||||
command: &AgentServerCommand,
|
||||
mode: ClaudeSessionMode,
|
||||
session_id: acp::SessionId,
|
||||
mcp_config_path: &Path,
|
||||
root_dir: &Path,
|
||||
) -> Result<Child> {
|
||||
let child = util::command::new_smol_command(&command.path)
|
||||
.args([
|
||||
"--input-format",
|
||||
"stream-json",
|
||||
"--output-format",
|
||||
"stream-json",
|
||||
"--print",
|
||||
"--verbose",
|
||||
"--mcp-config",
|
||||
mcp_config_path.to_string_lossy().as_ref(),
|
||||
"--permission-prompt-tool",
|
||||
&format!(
|
||||
"mcp__{}__{}",
|
||||
mcp_server::SERVER_NAME,
|
||||
mcp_server::PermissionTool::NAME,
|
||||
),
|
||||
"--allowedTools",
|
||||
&format!(
|
||||
"mcp__{}__{},mcp__{}__{}",
|
||||
mcp_server::SERVER_NAME,
|
||||
mcp_server::EditTool::NAME,
|
||||
mcp_server::SERVER_NAME,
|
||||
mcp_server::ReadTool::NAME
|
||||
),
|
||||
"--disallowedTools",
|
||||
"Read,Edit",
|
||||
])
|
||||
.args(match mode {
|
||||
ClaudeSessionMode::Start => ["--session-id".to_string(), session_id.to_string()],
|
||||
ClaudeSessionMode::Resume => ["--resume".to_string(), session_id.to_string()],
|
||||
})
|
||||
.args(command.args.iter().map(|arg| arg.as_str()))
|
||||
.current_dir(root_dir)
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::inherit())
|
||||
.kill_on_drop(true)
|
||||
.spawn()?;
|
||||
|
||||
Ok(child)
|
||||
}
|
||||
|
||||
struct ClaudeAgentSession {
|
||||
outgoing_tx: UnboundedSender<SdkMessage>,
|
||||
end_turn_tx: Rc<RefCell<Option<oneshot::Sender<Result<()>>>>>,
|
||||
_mcp_server: Option<ClaudeZedMcpServer>,
|
||||
_handler_task: Task<()>,
|
||||
}
|
||||
|
||||
impl ClaudeAgentSession {
|
||||
async fn handle_message(
|
||||
mut thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
|
||||
message: SdkMessage,
|
||||
end_turn_tx: Rc<RefCell<Option<oneshot::Sender<Result<()>>>>>,
|
||||
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: _,
|
||||
}
|
||||
| SdkMessage::User {
|
||||
message,
|
||||
session_id: _,
|
||||
} => {
|
||||
let Some(thread) = thread_rx
|
||||
.recv()
|
||||
.await
|
||||
.log_err()
|
||||
.and_then(|entity| entity.upgrade())
|
||||
else {
|
||||
log::error!("Received an SDK message but thread is gone");
|
||||
return;
|
||||
};
|
||||
|
||||
for chunk in message.content.chunks() {
|
||||
match chunk {
|
||||
ContentChunk::Text { text } | ContentChunk::UntaggedText(text) => {
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.push_assistant_content_block(text.into(), false, cx)
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
ContentChunk::ToolUse { id, name, input } => {
|
||||
let claude_tool = ClaudeTool::infer(&name, input);
|
||||
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
if let ClaudeTool::TodoWrite(Some(params)) = claude_tool {
|
||||
thread.update_plan(
|
||||
acp::Plan {
|
||||
entries: params
|
||||
.todos
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect(),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
thread.upsert_tool_call(
|
||||
claude_tool.as_acp(acp::ToolCallId(id.into())),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
ContentChunk::ToolResult {
|
||||
content,
|
||||
tool_use_id,
|
||||
} => {
|
||||
let content = content.to_string();
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.update_tool_call(
|
||||
acp::ToolCallUpdate {
|
||||
id: acp::ToolCallId(tool_use_id.into()),
|
||||
fields: acp::ToolCallUpdateFields {
|
||||
status: Some(acp::ToolCallStatus::Completed),
|
||||
content: (!content.is_empty())
|
||||
.then(|| vec![content.into()]),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
ContentChunk::Image
|
||||
| ContentChunk::Document
|
||||
| ContentChunk::Thinking
|
||||
| ContentChunk::RedactedThinking
|
||||
| ContentChunk::WebSearchToolResult => {
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.push_assistant_content_block(
|
||||
format!("Unsupported content: {:?}", chunk).into(),
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SdkMessage::Result {
|
||||
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: {}",
|
||||
result.unwrap_or_else(|| subtype.to_string())
|
||||
)))
|
||||
.ok();
|
||||
} else {
|
||||
end_turn_tx.send(Ok(())).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
SdkMessage::System { .. } | SdkMessage::ControlResponse { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_io(
|
||||
mut outgoing_rx: UnboundedReceiver<SdkMessage>,
|
||||
incoming_tx: UnboundedSender<SdkMessage>,
|
||||
mut outgoing_bytes: impl Unpin + AsyncWrite,
|
||||
incoming_bytes: impl Unpin + AsyncRead,
|
||||
) -> Result<UnboundedReceiver<SdkMessage>> {
|
||||
let mut output_reader = BufReader::new(incoming_bytes);
|
||||
let mut outgoing_line = Vec::new();
|
||||
let mut incoming_line = String::new();
|
||||
loop {
|
||||
select_biased! {
|
||||
message = outgoing_rx.next() => {
|
||||
if let Some(message) = message {
|
||||
outgoing_line.clear();
|
||||
serde_json::to_writer(&mut outgoing_line, &message)?;
|
||||
log::trace!("send: {}", String::from_utf8_lossy(&outgoing_line));
|
||||
outgoing_line.push(b'\n');
|
||||
outgoing_bytes.write_all(&outgoing_line).await.ok();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
bytes_read = output_reader.read_line(&mut incoming_line).fuse() => {
|
||||
if bytes_read? == 0 {
|
||||
break
|
||||
}
|
||||
log::trace!("recv: {}", &incoming_line);
|
||||
match serde_json::from_str::<SdkMessage>(&incoming_line) {
|
||||
Ok(message) => {
|
||||
incoming_tx.unbounded_send(message).log_err();
|
||||
}
|
||||
Err(error) => {
|
||||
log::error!("failed to parse incoming message: {error}. Raw: {incoming_line}");
|
||||
}
|
||||
}
|
||||
incoming_line.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(outgoing_rx)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct Message {
|
||||
role: Role,
|
||||
content: Content,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
model: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
stop_reason: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
stop_sequence: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
usage: Option<Usage>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum Content {
|
||||
UntaggedText(String),
|
||||
Chunks(Vec<ContentChunk>),
|
||||
}
|
||||
|
||||
impl Content {
|
||||
pub fn chunks(self) -> impl Iterator<Item = ContentChunk> {
|
||||
match self {
|
||||
Self::Chunks(chunks) => chunks.into_iter(),
|
||||
Self::UntaggedText(text) => vec![ContentChunk::Text { text: text.clone() }].into_iter(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Content {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Content::UntaggedText(txt) => write!(f, "{}", txt),
|
||||
Content::Chunks(chunks) => {
|
||||
for chunk in chunks {
|
||||
write!(f, "{}", chunk)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
enum ContentChunk {
|
||||
Text {
|
||||
text: String,
|
||||
},
|
||||
ToolUse {
|
||||
id: String,
|
||||
name: String,
|
||||
input: serde_json::Value,
|
||||
},
|
||||
ToolResult {
|
||||
content: Content,
|
||||
tool_use_id: String,
|
||||
},
|
||||
// TODO
|
||||
Image,
|
||||
Document,
|
||||
Thinking,
|
||||
RedactedThinking,
|
||||
WebSearchToolResult,
|
||||
#[serde(untagged)]
|
||||
UntaggedText(String),
|
||||
}
|
||||
|
||||
impl Display for ContentChunk {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ContentChunk::Text { text } => write!(f, "{}", text),
|
||||
ContentChunk::UntaggedText(text) => write!(f, "{}", text),
|
||||
ContentChunk::ToolResult { content, .. } => write!(f, "{}", content),
|
||||
ContentChunk::Image
|
||||
| ContentChunk::Document
|
||||
| ContentChunk::Thinking
|
||||
| ContentChunk::RedactedThinking
|
||||
| ContentChunk::ToolUse { .. }
|
||||
| ContentChunk::WebSearchToolResult => {
|
||||
write!(f, "\n{:?}\n", &self)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct Usage {
|
||||
input_tokens: u32,
|
||||
cache_creation_input_tokens: u32,
|
||||
cache_read_input_tokens: u32,
|
||||
output_tokens: u32,
|
||||
service_tier: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum Role {
|
||||
System,
|
||||
Assistant,
|
||||
User,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct MessageParam {
|
||||
role: Role,
|
||||
content: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
enum SdkMessage {
|
||||
// An assistant message
|
||||
Assistant {
|
||||
message: Message, // from Anthropic SDK
|
||||
#[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,
|
||||
duration_ms: f64,
|
||||
duration_api_ms: f64,
|
||||
is_error: bool,
|
||||
num_turns: i32,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
result: Option<String>,
|
||||
session_id: String,
|
||||
total_cost_usd: f64,
|
||||
},
|
||||
// Emitted as the first message at the start of a conversation
|
||||
System {
|
||||
cwd: String,
|
||||
session_id: String,
|
||||
tools: Vec<String>,
|
||||
model: String,
|
||||
mcp_servers: Vec<McpServer>,
|
||||
#[serde(rename = "apiKeySource")]
|
||||
api_key_source: String,
|
||||
#[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)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum ResultErrorType {
|
||||
Success,
|
||||
ErrorMaxTurns,
|
||||
ErrorDuringExecution,
|
||||
}
|
||||
|
||||
impl Display for ResultErrorType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ResultErrorType::Success => write!(f, "success"),
|
||||
ResultErrorType::ErrorMaxTurns => write!(f, "error_max_turns"),
|
||||
ResultErrorType::ErrorDuringExecution => write!(f, "error_during_execution"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
status: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
enum PermissionMode {
|
||||
Default,
|
||||
AcceptEdits,
|
||||
BypassPermissions,
|
||||
Plan,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
crate::common_e2e_tests!(ClaudeCode, allow_option_id = "allow");
|
||||
|
||||
pub fn local_command() -> AgentServerCommand {
|
||||
AgentServerCommand {
|
||||
path: "claude".into(),
|
||||
args: vec![],
|
||||
env: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_content_untagged_text() {
|
||||
let json = json!("Hello, world!");
|
||||
let content: Content = serde_json::from_value(json).unwrap();
|
||||
match content {
|
||||
Content::UntaggedText(text) => assert_eq!(text, "Hello, world!"),
|
||||
_ => panic!("Expected UntaggedText variant"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_content_chunks() {
|
||||
let json = json!([
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Hello"
|
||||
},
|
||||
{
|
||||
"type": "tool_use",
|
||||
"id": "tool_123",
|
||||
"name": "calculator",
|
||||
"input": {"operation": "add", "a": 1, "b": 2}
|
||||
}
|
||||
]);
|
||||
let content: Content = serde_json::from_value(json).unwrap();
|
||||
match content {
|
||||
Content::Chunks(chunks) => {
|
||||
assert_eq!(chunks.len(), 2);
|
||||
match &chunks[0] {
|
||||
ContentChunk::Text { text } => assert_eq!(text, "Hello"),
|
||||
_ => panic!("Expected Text chunk"),
|
||||
}
|
||||
match &chunks[1] {
|
||||
ContentChunk::ToolUse { id, name, input } => {
|
||||
assert_eq!(id, "tool_123");
|
||||
assert_eq!(name, "calculator");
|
||||
assert_eq!(input["operation"], "add");
|
||||
assert_eq!(input["a"], 1);
|
||||
assert_eq!(input["b"], 2);
|
||||
}
|
||||
_ => panic!("Expected ToolUse chunk"),
|
||||
}
|
||||
}
|
||||
_ => panic!("Expected Chunks variant"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_tool_result_untagged_text() {
|
||||
let json = json!({
|
||||
"type": "tool_result",
|
||||
"content": "Result content",
|
||||
"tool_use_id": "tool_456"
|
||||
});
|
||||
let chunk: ContentChunk = serde_json::from_value(json).unwrap();
|
||||
match chunk {
|
||||
ContentChunk::ToolResult {
|
||||
content,
|
||||
tool_use_id,
|
||||
} => {
|
||||
match content {
|
||||
Content::UntaggedText(text) => assert_eq!(text, "Result content"),
|
||||
_ => panic!("Expected UntaggedText content"),
|
||||
}
|
||||
assert_eq!(tool_use_id, "tool_456");
|
||||
}
|
||||
_ => panic!("Expected ToolResult variant"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_tool_result_chunks() {
|
||||
let json = json!({
|
||||
"type": "tool_result",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Processing complete"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Result: 42"
|
||||
}
|
||||
],
|
||||
"tool_use_id": "tool_789"
|
||||
});
|
||||
let chunk: ContentChunk = serde_json::from_value(json).unwrap();
|
||||
match chunk {
|
||||
ContentChunk::ToolResult {
|
||||
content,
|
||||
tool_use_id,
|
||||
} => {
|
||||
match content {
|
||||
Content::Chunks(chunks) => {
|
||||
assert_eq!(chunks.len(), 2);
|
||||
match &chunks[0] {
|
||||
ContentChunk::Text { text } => assert_eq!(text, "Processing complete"),
|
||||
_ => panic!("Expected Text chunk"),
|
||||
}
|
||||
match &chunks[1] {
|
||||
ContentChunk::Text { text } => assert_eq!(text, "Result: 42"),
|
||||
_ => panic!("Expected Text chunk"),
|
||||
}
|
||||
}
|
||||
_ => panic!("Expected Chunks content"),
|
||||
}
|
||||
assert_eq!(tool_use_id, "tool_789");
|
||||
}
|
||||
_ => panic!("Expected ToolResult variant"),
|
||||
}
|
||||
}
|
||||
}
|
||||
302
crates/agent_servers/src/claude/mcp_server.rs
Normal file
@@ -0,0 +1,302 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::claude::tools::{ClaudeTool, EditToolParams, ReadToolParams};
|
||||
use acp_thread::AcpThread;
|
||||
use agent_client_protocol as acp;
|
||||
use anyhow::{Context, Result};
|
||||
use collections::HashMap;
|
||||
use context_server::listener::{McpServerTool, ToolResponse};
|
||||
use context_server::types::{
|
||||
Implementation, InitializeParams, InitializeResponse, ProtocolVersion, ServerCapabilities,
|
||||
ToolAnnotations, ToolResponseContent, ToolsCapabilities, requests,
|
||||
};
|
||||
use gpui::{App, AsyncApp, Task, WeakEntity};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub struct ClaudeZedMcpServer {
|
||||
server: context_server::listener::McpServer,
|
||||
}
|
||||
|
||||
pub const SERVER_NAME: &str = "zed";
|
||||
|
||||
impl ClaudeZedMcpServer {
|
||||
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(PermissionTool {
|
||||
thread_rx: thread_rx.clone(),
|
||||
});
|
||||
mcp_server.add_tool(ReadTool {
|
||||
thread_rx: thread_rx.clone(),
|
||||
});
|
||||
mcp_server.add_tool(EditTool {
|
||||
thread_rx: thread_rx.clone(),
|
||||
});
|
||||
|
||||
Ok(Self { server: mcp_server })
|
||||
}
|
||||
|
||||
pub fn server_config(&self) -> Result<McpServerConfig> {
|
||||
#[cfg(not(test))]
|
||||
let zed_path = std::env::current_exe()
|
||||
.context("finding current executable path for use in mcp_server")?;
|
||||
|
||||
#[cfg(test)]
|
||||
let zed_path = crate::e2e_tests::get_zed_path();
|
||||
|
||||
Ok(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,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct McpConfig {
|
||||
pub mcp_servers: HashMap<String, McpServerConfig>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct McpServerConfig {
|
||||
pub command: PathBuf,
|
||||
pub args: Vec<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub env: Option<HashMap<String, String>>,
|
||||
}
|
||||
|
||||
// Tools
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PermissionTool {
|
||||
thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, JsonSchema, Debug)]
|
||||
pub struct PermissionToolParams {
|
||||
tool_name: String,
|
||||
input: serde_json::Value,
|
||||
tool_use_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PermissionToolResponse {
|
||||
behavior: PermissionToolBehavior,
|
||||
updated_input: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum PermissionToolBehavior {
|
||||
Allow,
|
||||
Deny,
|
||||
}
|
||||
|
||||
impl McpServerTool for PermissionTool {
|
||||
type Input = PermissionToolParams;
|
||||
type Output = ();
|
||||
|
||||
const NAME: &'static str = "Confirmation";
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Request permission for tool calls"
|
||||
}
|
||||
|
||||
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 claude_tool = ClaudeTool::infer(&input.tool_name, input.input.clone());
|
||||
let tool_call_id = acp::ToolCallId(input.tool_use_id.context("Tool ID required")?.into());
|
||||
let allow_option_id = acp::PermissionOptionId("allow".into());
|
||||
let reject_option_id = acp::PermissionOptionId("reject".into());
|
||||
|
||||
let chosen_option = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.request_tool_call_permission(
|
||||
claude_tool.as_acp(tool_call_id),
|
||||
vec![
|
||||
acp::PermissionOption {
|
||||
id: allow_option_id.clone(),
|
||||
label: "Allow".into(),
|
||||
kind: acp::PermissionOptionKind::AllowOnce,
|
||||
},
|
||||
acp::PermissionOption {
|
||||
id: reject_option_id.clone(),
|
||||
label: "Reject".into(),
|
||||
kind: acp::PermissionOptionKind::RejectOnce,
|
||||
},
|
||||
],
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
let response = if chosen_option == allow_option_id {
|
||||
PermissionToolResponse {
|
||||
behavior: PermissionToolBehavior::Allow,
|
||||
updated_input: input.input,
|
||||
}
|
||||
} else {
|
||||
debug_assert_eq!(chosen_option, reject_option_id);
|
||||
PermissionToolResponse {
|
||||
behavior: PermissionToolBehavior::Deny,
|
||||
updated_input: input.input,
|
||||
}
|
||||
};
|
||||
|
||||
Ok(ToolResponse {
|
||||
content: vec![ToolResponseContent::Text {
|
||||
text: serde_json::to_string(&response)?,
|
||||
}],
|
||||
structured_content: (),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ReadTool {
|
||||
thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
|
||||
}
|
||||
|
||||
impl McpServerTool for ReadTool {
|
||||
type Input = ReadToolParams;
|
||||
type Output = ();
|
||||
|
||||
const NAME: &'static str = "Read";
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Read the contents of a file. In sessions with mcp__zed__Read always use it instead of Read as it contains the most up-to-date contents."
|
||||
}
|
||||
|
||||
fn annotations(&self) -> ToolAnnotations {
|
||||
ToolAnnotations {
|
||||
title: Some("Read file".to_string()),
|
||||
read_only_hint: Some(true),
|
||||
destructive_hint: Some(false),
|
||||
open_world_hint: Some(false),
|
||||
idempotent_hint: None,
|
||||
}
|
||||
}
|
||||
|
||||
async fn run(
|
||||
&self,
|
||||
input: Self::Input,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<ToolResponse<Self::Output>> {
|
||||
let mut thread_rx = self.thread_rx.clone();
|
||||
let Some(thread) = thread_rx.recv().await?.upgrade() else {
|
||||
anyhow::bail!("Thread closed");
|
||||
};
|
||||
|
||||
let content = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.read_text_file(input.abs_path, input.offset, input.limit, false, cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
Ok(ToolResponse {
|
||||
content: vec![ToolResponseContent::Text { text: content }],
|
||||
structured_content: (),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct EditTool {
|
||||
thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
|
||||
}
|
||||
|
||||
impl McpServerTool for EditTool {
|
||||
type Input = EditToolParams;
|
||||
type Output = ();
|
||||
|
||||
const NAME: &'static str = "Edit";
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Edits a file. In sessions with mcp__zed__Edit always use it instead of Edit as it will show the diff to the user better."
|
||||
}
|
||||
|
||||
fn annotations(&self) -> ToolAnnotations {
|
||||
ToolAnnotations {
|
||||
title: Some("Edit file".to_string()),
|
||||
read_only_hint: Some(false),
|
||||
destructive_hint: Some(false),
|
||||
open_world_hint: Some(false),
|
||||
idempotent_hint: Some(false),
|
||||
}
|
||||
}
|
||||
|
||||
async fn run(
|
||||
&self,
|
||||
input: Self::Input,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<ToolResponse<Self::Output>> {
|
||||
let mut thread_rx = self.thread_rx.clone();
|
||||
let Some(thread) = thread_rx.recv().await?.upgrade() else {
|
||||
anyhow::bail!("Thread closed");
|
||||
};
|
||||
|
||||
let content = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.read_text_file(input.abs_path.clone(), None, None, true, cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
let new_content = content.replace(&input.old_text, &input.new_text);
|
||||
if new_content == content {
|
||||
return Err(anyhow::anyhow!("The old_text was not found in the content"));
|
||||
}
|
||||
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.write_text_file(input.abs_path, new_content, cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
Ok(ToolResponse {
|
||||
content: vec![],
|
||||
structured_content: (),
|
||||
})
|
||||
}
|
||||
}
|
||||
675
crates/agent_servers/src/claude/tools.rs
Normal file
@@ -0,0 +1,675 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use agent_client_protocol as acp;
|
||||
use itertools::Itertools;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use util::ResultExt;
|
||||
|
||||
pub enum ClaudeTool {
|
||||
Task(Option<TaskToolParams>),
|
||||
NotebookRead(Option<NotebookReadToolParams>),
|
||||
NotebookEdit(Option<NotebookEditToolParams>),
|
||||
Edit(Option<EditToolParams>),
|
||||
MultiEdit(Option<MultiEditToolParams>),
|
||||
ReadFile(Option<ReadToolParams>),
|
||||
Write(Option<WriteToolParams>),
|
||||
Ls(Option<LsToolParams>),
|
||||
Glob(Option<GlobToolParams>),
|
||||
Grep(Option<GrepToolParams>),
|
||||
Terminal(Option<BashToolParams>),
|
||||
WebFetch(Option<WebFetchToolParams>),
|
||||
WebSearch(Option<WebSearchToolParams>),
|
||||
TodoWrite(Option<TodoWriteToolParams>),
|
||||
ExitPlanMode(Option<ExitPlanModeToolParams>),
|
||||
Other {
|
||||
name: String,
|
||||
input: serde_json::Value,
|
||||
},
|
||||
}
|
||||
|
||||
impl ClaudeTool {
|
||||
pub fn infer(tool_name: &str, input: serde_json::Value) -> Self {
|
||||
match tool_name {
|
||||
// Known tools
|
||||
"mcp__zed__Read" => Self::ReadFile(serde_json::from_value(input).log_err()),
|
||||
"mcp__zed__Edit" => Self::Edit(serde_json::from_value(input).log_err()),
|
||||
"MultiEdit" => Self::MultiEdit(serde_json::from_value(input).log_err()),
|
||||
"Write" => Self::Write(serde_json::from_value(input).log_err()),
|
||||
"LS" => Self::Ls(serde_json::from_value(input).log_err()),
|
||||
"Glob" => Self::Glob(serde_json::from_value(input).log_err()),
|
||||
"Grep" => Self::Grep(serde_json::from_value(input).log_err()),
|
||||
"Bash" => Self::Terminal(serde_json::from_value(input).log_err()),
|
||||
"WebFetch" => Self::WebFetch(serde_json::from_value(input).log_err()),
|
||||
"WebSearch" => Self::WebSearch(serde_json::from_value(input).log_err()),
|
||||
"TodoWrite" => Self::TodoWrite(serde_json::from_value(input).log_err()),
|
||||
"exit_plan_mode" => Self::ExitPlanMode(serde_json::from_value(input).log_err()),
|
||||
"Task" => Self::Task(serde_json::from_value(input).log_err()),
|
||||
"NotebookRead" => Self::NotebookRead(serde_json::from_value(input).log_err()),
|
||||
"NotebookEdit" => Self::NotebookEdit(serde_json::from_value(input).log_err()),
|
||||
// Inferred from name
|
||||
_ => {
|
||||
let tool_name = tool_name.to_lowercase();
|
||||
|
||||
if tool_name.contains("edit") || tool_name.contains("write") {
|
||||
Self::Edit(None)
|
||||
} else if tool_name.contains("terminal") {
|
||||
Self::Terminal(None)
|
||||
} else {
|
||||
Self::Other {
|
||||
name: tool_name.to_string(),
|
||||
input,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn label(&self) -> String {
|
||||
match &self {
|
||||
Self::Task(Some(params)) => params.description.clone(),
|
||||
Self::Task(None) => "Task".into(),
|
||||
Self::NotebookRead(Some(params)) => {
|
||||
format!("Read Notebook {}", params.notebook_path.display())
|
||||
}
|
||||
Self::NotebookRead(None) => "Read Notebook".into(),
|
||||
Self::NotebookEdit(Some(params)) => {
|
||||
format!("Edit Notebook {}", params.notebook_path.display())
|
||||
}
|
||||
Self::NotebookEdit(None) => "Edit Notebook".into(),
|
||||
Self::Terminal(Some(params)) => format!("`{}`", params.command),
|
||||
Self::Terminal(None) => "Terminal".into(),
|
||||
Self::ReadFile(_) => "Read File".into(),
|
||||
Self::Ls(Some(params)) => {
|
||||
format!("List Directory {}", params.path.display())
|
||||
}
|
||||
Self::Ls(None) => "List Directory".into(),
|
||||
Self::Edit(Some(params)) => {
|
||||
format!("Edit {}", params.abs_path.display())
|
||||
}
|
||||
Self::Edit(None) => "Edit".into(),
|
||||
Self::MultiEdit(Some(params)) => {
|
||||
format!("Multi Edit {}", params.file_path.display())
|
||||
}
|
||||
Self::MultiEdit(None) => "Multi Edit".into(),
|
||||
Self::Write(Some(params)) => {
|
||||
format!("Write {}", params.file_path.display())
|
||||
}
|
||||
Self::Write(None) => "Write".into(),
|
||||
Self::Glob(Some(params)) => {
|
||||
format!("Glob `{params}`")
|
||||
}
|
||||
Self::Glob(None) => "Glob".into(),
|
||||
Self::Grep(Some(params)) => format!("`{params}`"),
|
||||
Self::Grep(None) => "Grep".into(),
|
||||
Self::WebFetch(Some(params)) => format!("Fetch {}", params.url),
|
||||
Self::WebFetch(None) => "Fetch".into(),
|
||||
Self::WebSearch(Some(params)) => format!("Web Search: {}", params),
|
||||
Self::WebSearch(None) => "Web Search".into(),
|
||||
Self::TodoWrite(Some(params)) => format!(
|
||||
"Update TODOs: {}",
|
||||
params.todos.iter().map(|todo| &todo.content).join(", ")
|
||||
),
|
||||
Self::TodoWrite(None) => "Update TODOs".into(),
|
||||
Self::ExitPlanMode(_) => "Exit Plan Mode".into(),
|
||||
Self::Other { name, .. } => name.clone(),
|
||||
}
|
||||
}
|
||||
pub fn content(&self) -> Vec<acp::ToolCallContent> {
|
||||
match &self {
|
||||
Self::Other { input, .. } => vec![
|
||||
format!(
|
||||
"```json\n{}```",
|
||||
serde_json::to_string_pretty(&input).unwrap_or("{}".to_string())
|
||||
)
|
||||
.into(),
|
||||
],
|
||||
Self::Task(Some(params)) => vec![params.prompt.clone().into()],
|
||||
Self::NotebookRead(Some(params)) => {
|
||||
vec![params.notebook_path.display().to_string().into()]
|
||||
}
|
||||
Self::NotebookEdit(Some(params)) => vec![params.new_source.clone().into()],
|
||||
Self::Terminal(Some(params)) => vec![
|
||||
format!(
|
||||
"`{}`\n\n{}",
|
||||
params.command,
|
||||
params.description.as_deref().unwrap_or_default()
|
||||
)
|
||||
.into(),
|
||||
],
|
||||
Self::ReadFile(Some(params)) => vec![params.abs_path.display().to_string().into()],
|
||||
Self::Ls(Some(params)) => vec![params.path.display().to_string().into()],
|
||||
Self::Glob(Some(params)) => vec![params.to_string().into()],
|
||||
Self::Grep(Some(params)) => vec![format!("`{params}`").into()],
|
||||
Self::WebFetch(Some(params)) => vec![params.prompt.clone().into()],
|
||||
Self::WebSearch(Some(params)) => vec![params.to_string().into()],
|
||||
Self::TodoWrite(Some(params)) => vec![
|
||||
params
|
||||
.todos
|
||||
.iter()
|
||||
.map(|todo| {
|
||||
format!(
|
||||
"- {} {}: {}",
|
||||
match todo.status {
|
||||
TodoStatus::Completed => "✅",
|
||||
TodoStatus::InProgress => "🚧",
|
||||
TodoStatus::Pending => "⬜",
|
||||
},
|
||||
todo.priority,
|
||||
todo.content
|
||||
)
|
||||
})
|
||||
.join("\n")
|
||||
.into(),
|
||||
],
|
||||
Self::ExitPlanMode(Some(params)) => vec![params.plan.clone().into()],
|
||||
Self::Edit(Some(params)) => vec![acp::ToolCallContent::Diff {
|
||||
diff: acp::Diff {
|
||||
path: params.abs_path.clone(),
|
||||
old_text: Some(params.old_text.clone()),
|
||||
new_text: params.new_text.clone(),
|
||||
},
|
||||
}],
|
||||
Self::Write(Some(params)) => vec![acp::ToolCallContent::Diff {
|
||||
diff: acp::Diff {
|
||||
path: params.file_path.clone(),
|
||||
old_text: None,
|
||||
new_text: params.content.clone(),
|
||||
},
|
||||
}],
|
||||
Self::MultiEdit(Some(params)) => {
|
||||
// todo: show multiple edits in a multibuffer?
|
||||
params
|
||||
.edits
|
||||
.first()
|
||||
.map(|edit| {
|
||||
vec![acp::ToolCallContent::Diff {
|
||||
diff: acp::Diff {
|
||||
path: params.file_path.clone(),
|
||||
old_text: Some(edit.old_string.clone()),
|
||||
new_text: edit.new_string.clone(),
|
||||
},
|
||||
}]
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
Self::Task(None)
|
||||
| Self::NotebookRead(None)
|
||||
| Self::NotebookEdit(None)
|
||||
| Self::Terminal(None)
|
||||
| Self::ReadFile(None)
|
||||
| Self::Ls(None)
|
||||
| Self::Glob(None)
|
||||
| Self::Grep(None)
|
||||
| Self::WebFetch(None)
|
||||
| Self::WebSearch(None)
|
||||
| Self::TodoWrite(None)
|
||||
| Self::ExitPlanMode(None)
|
||||
| Self::Edit(None)
|
||||
| Self::Write(None)
|
||||
| Self::MultiEdit(None) => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> acp::ToolKind {
|
||||
match self {
|
||||
Self::Task(_) => acp::ToolKind::Think,
|
||||
Self::NotebookRead(_) => acp::ToolKind::Read,
|
||||
Self::NotebookEdit(_) => acp::ToolKind::Edit,
|
||||
Self::Edit(_) => acp::ToolKind::Edit,
|
||||
Self::MultiEdit(_) => acp::ToolKind::Edit,
|
||||
Self::Write(_) => acp::ToolKind::Edit,
|
||||
Self::ReadFile(_) => acp::ToolKind::Read,
|
||||
Self::Ls(_) => acp::ToolKind::Search,
|
||||
Self::Glob(_) => acp::ToolKind::Search,
|
||||
Self::Grep(_) => acp::ToolKind::Search,
|
||||
Self::Terminal(_) => acp::ToolKind::Execute,
|
||||
Self::WebSearch(_) => acp::ToolKind::Search,
|
||||
Self::WebFetch(_) => acp::ToolKind::Fetch,
|
||||
Self::TodoWrite(_) => acp::ToolKind::Think,
|
||||
Self::ExitPlanMode(_) => acp::ToolKind::Think,
|
||||
Self::Other { .. } => acp::ToolKind::Other,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn locations(&self) -> Vec<acp::ToolCallLocation> {
|
||||
match &self {
|
||||
Self::Edit(Some(EditToolParams { abs_path, .. })) => vec![acp::ToolCallLocation {
|
||||
path: abs_path.clone(),
|
||||
line: None,
|
||||
}],
|
||||
Self::MultiEdit(Some(MultiEditToolParams { file_path, .. })) => {
|
||||
vec![acp::ToolCallLocation {
|
||||
path: file_path.clone(),
|
||||
line: None,
|
||||
}]
|
||||
}
|
||||
Self::Write(Some(WriteToolParams { file_path, .. })) => {
|
||||
vec![acp::ToolCallLocation {
|
||||
path: file_path.clone(),
|
||||
line: None,
|
||||
}]
|
||||
}
|
||||
Self::ReadFile(Some(ReadToolParams {
|
||||
abs_path, offset, ..
|
||||
})) => vec![acp::ToolCallLocation {
|
||||
path: abs_path.clone(),
|
||||
line: *offset,
|
||||
}],
|
||||
Self::NotebookRead(Some(NotebookReadToolParams { notebook_path, .. })) => {
|
||||
vec![acp::ToolCallLocation {
|
||||
path: notebook_path.clone(),
|
||||
line: None,
|
||||
}]
|
||||
}
|
||||
Self::NotebookEdit(Some(NotebookEditToolParams { notebook_path, .. })) => {
|
||||
vec![acp::ToolCallLocation {
|
||||
path: notebook_path.clone(),
|
||||
line: None,
|
||||
}]
|
||||
}
|
||||
Self::Glob(Some(GlobToolParams {
|
||||
path: Some(path), ..
|
||||
})) => vec![acp::ToolCallLocation {
|
||||
path: path.clone(),
|
||||
line: None,
|
||||
}],
|
||||
Self::Ls(Some(LsToolParams { path, .. })) => vec![acp::ToolCallLocation {
|
||||
path: path.clone(),
|
||||
line: None,
|
||||
}],
|
||||
Self::Grep(Some(GrepToolParams {
|
||||
path: Some(path), ..
|
||||
})) => vec![acp::ToolCallLocation {
|
||||
path: PathBuf::from(path),
|
||||
line: None,
|
||||
}],
|
||||
Self::Task(_)
|
||||
| Self::NotebookRead(None)
|
||||
| Self::NotebookEdit(None)
|
||||
| Self::Edit(None)
|
||||
| Self::MultiEdit(None)
|
||||
| Self::Write(None)
|
||||
| Self::ReadFile(None)
|
||||
| Self::Ls(None)
|
||||
| Self::Glob(_)
|
||||
| Self::Grep(_)
|
||||
| Self::Terminal(_)
|
||||
| Self::WebFetch(_)
|
||||
| Self::WebSearch(_)
|
||||
| Self::TodoWrite(_)
|
||||
| Self::ExitPlanMode(_)
|
||||
| Self::Other { .. } => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_acp(&self, id: acp::ToolCallId) -> acp::ToolCall {
|
||||
acp::ToolCall {
|
||||
id,
|
||||
kind: self.kind(),
|
||||
status: acp::ToolCallStatus::InProgress,
|
||||
label: self.label(),
|
||||
content: self.content(),
|
||||
locations: self.locations(),
|
||||
raw_input: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, JsonSchema, Debug)]
|
||||
pub struct EditToolParams {
|
||||
/// The absolute path to the file to read.
|
||||
pub abs_path: PathBuf,
|
||||
/// The old text to replace (must be unique in the file)
|
||||
pub old_text: String,
|
||||
/// The new text.
|
||||
pub new_text: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, JsonSchema, Debug)]
|
||||
pub struct ReadToolParams {
|
||||
/// The absolute path to the file to read.
|
||||
pub abs_path: PathBuf,
|
||||
/// Which line to start reading from. Omit to start from the beginning.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub offset: Option<u32>,
|
||||
/// How many lines to read. Omit for the whole file.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, JsonSchema, Debug)]
|
||||
pub struct WriteToolParams {
|
||||
/// Absolute path for new file
|
||||
pub file_path: PathBuf,
|
||||
/// File content
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, JsonSchema, Debug)]
|
||||
pub struct BashToolParams {
|
||||
/// Shell command to execute
|
||||
pub command: String,
|
||||
/// 5-10 word description of what command does
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
/// Timeout in ms (max 600000ms/10min, default 120000ms)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub timeout: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, JsonSchema, Debug)]
|
||||
pub struct GlobToolParams {
|
||||
/// Glob pattern like **/*.js or src/**/*.ts
|
||||
pub pattern: String,
|
||||
/// Directory to search in (omit for current directory)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for GlobToolParams {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if let Some(path) = &self.path {
|
||||
write!(f, "{}", path.display())?;
|
||||
}
|
||||
write!(f, "{}", self.pattern)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, JsonSchema, Debug)]
|
||||
pub struct LsToolParams {
|
||||
/// Absolute path to directory
|
||||
pub path: PathBuf,
|
||||
/// Array of glob patterns to ignore
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub ignore: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, JsonSchema, Debug)]
|
||||
pub struct GrepToolParams {
|
||||
/// Regex pattern to search for
|
||||
pub pattern: String,
|
||||
/// File/directory to search (defaults to current directory)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub path: Option<String>,
|
||||
/// "content" (shows lines), "files_with_matches" (default), "count"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_mode: Option<GrepOutputMode>,
|
||||
/// Filter files with glob pattern like "*.js"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub glob: Option<String>,
|
||||
/// File type filter like "js", "py", "rust"
|
||||
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
|
||||
pub file_type: Option<String>,
|
||||
/// Case insensitive search
|
||||
#[serde(rename = "-i", default, skip_serializing_if = "is_false")]
|
||||
pub case_insensitive: bool,
|
||||
/// Show line numbers (content mode only)
|
||||
#[serde(rename = "-n", default, skip_serializing_if = "is_false")]
|
||||
pub line_numbers: bool,
|
||||
/// Lines after match (content mode only)
|
||||
#[serde(rename = "-A", skip_serializing_if = "Option::is_none")]
|
||||
pub after_context: Option<u32>,
|
||||
/// Lines before match (content mode only)
|
||||
#[serde(rename = "-B", skip_serializing_if = "Option::is_none")]
|
||||
pub before_context: Option<u32>,
|
||||
/// Lines before and after match (content mode only)
|
||||
#[serde(rename = "-C", skip_serializing_if = "Option::is_none")]
|
||||
pub context: Option<u32>,
|
||||
/// Enable multiline/cross-line matching
|
||||
#[serde(default, skip_serializing_if = "is_false")]
|
||||
pub multiline: bool,
|
||||
/// Limit output to first N results
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub head_limit: Option<u32>,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for GrepToolParams {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "grep")?;
|
||||
|
||||
// Boolean flags
|
||||
if self.case_insensitive {
|
||||
write!(f, " -i")?;
|
||||
}
|
||||
if self.line_numbers {
|
||||
write!(f, " -n")?;
|
||||
}
|
||||
|
||||
// Context options
|
||||
if let Some(after) = self.after_context {
|
||||
write!(f, " -A {}", after)?;
|
||||
}
|
||||
if let Some(before) = self.before_context {
|
||||
write!(f, " -B {}", before)?;
|
||||
}
|
||||
if let Some(context) = self.context {
|
||||
write!(f, " -C {}", context)?;
|
||||
}
|
||||
|
||||
// Output mode
|
||||
if let Some(mode) = &self.output_mode {
|
||||
match mode {
|
||||
GrepOutputMode::FilesWithMatches => write!(f, " -l")?,
|
||||
GrepOutputMode::Count => write!(f, " -c")?,
|
||||
GrepOutputMode::Content => {} // Default mode
|
||||
}
|
||||
}
|
||||
|
||||
// Head limit
|
||||
if let Some(limit) = self.head_limit {
|
||||
write!(f, " | head -{}", limit)?;
|
||||
}
|
||||
|
||||
// Glob pattern
|
||||
if let Some(glob) = &self.glob {
|
||||
write!(f, " --include=\"{}\"", glob)?;
|
||||
}
|
||||
|
||||
// File type
|
||||
if let Some(file_type) = &self.file_type {
|
||||
write!(f, " --type={}", file_type)?;
|
||||
}
|
||||
|
||||
// Multiline
|
||||
if self.multiline {
|
||||
write!(f, " -P")?; // Perl-compatible regex for multiline
|
||||
}
|
||||
|
||||
// Pattern (escaped if contains special characters)
|
||||
write!(f, " \"{}\"", self.pattern)?;
|
||||
|
||||
// Path
|
||||
if let Some(path) = &self.path {
|
||||
write!(f, " {}", path)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, JsonSchema, strum::Display, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TodoPriority {
|
||||
High,
|
||||
Medium,
|
||||
Low,
|
||||
}
|
||||
|
||||
impl Into<acp::PlanEntryPriority> for TodoPriority {
|
||||
fn into(self) -> acp::PlanEntryPriority {
|
||||
match self {
|
||||
TodoPriority::High => acp::PlanEntryPriority::High,
|
||||
TodoPriority::Medium => acp::PlanEntryPriority::Medium,
|
||||
TodoPriority::Low => acp::PlanEntryPriority::Low,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, JsonSchema, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TodoStatus {
|
||||
Pending,
|
||||
InProgress,
|
||||
Completed,
|
||||
}
|
||||
|
||||
impl Into<acp::PlanEntryStatus> for TodoStatus {
|
||||
fn into(self) -> acp::PlanEntryStatus {
|
||||
match self {
|
||||
TodoStatus::Pending => acp::PlanEntryStatus::Pending,
|
||||
TodoStatus::InProgress => acp::PlanEntryStatus::InProgress,
|
||||
TodoStatus::Completed => acp::PlanEntryStatus::Completed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, JsonSchema, Debug)]
|
||||
pub struct Todo {
|
||||
/// Unique identifier
|
||||
pub id: String,
|
||||
/// Task description
|
||||
pub content: String,
|
||||
/// Priority level of the todo
|
||||
pub priority: TodoPriority,
|
||||
/// Current status of the todo
|
||||
pub status: TodoStatus,
|
||||
}
|
||||
|
||||
impl Into<acp::PlanEntry> for Todo {
|
||||
fn into(self) -> acp::PlanEntry {
|
||||
acp::PlanEntry {
|
||||
content: self.content,
|
||||
priority: self.priority.into(),
|
||||
status: self.status.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, JsonSchema, Debug)]
|
||||
pub struct TodoWriteToolParams {
|
||||
pub todos: Vec<Todo>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, JsonSchema, Debug)]
|
||||
pub struct ExitPlanModeToolParams {
|
||||
/// Implementation plan in markdown format
|
||||
pub plan: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, JsonSchema, Debug)]
|
||||
pub struct TaskToolParams {
|
||||
/// Short 3-5 word description of task
|
||||
pub description: String,
|
||||
/// Detailed task for agent to perform
|
||||
pub prompt: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, JsonSchema, Debug)]
|
||||
pub struct NotebookReadToolParams {
|
||||
/// Absolute path to .ipynb file
|
||||
pub notebook_path: PathBuf,
|
||||
/// Specific cell ID to read
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cell_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, JsonSchema, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CellType {
|
||||
Code,
|
||||
Markdown,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, JsonSchema, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum EditMode {
|
||||
Replace,
|
||||
Insert,
|
||||
Delete,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, JsonSchema, Debug)]
|
||||
pub struct NotebookEditToolParams {
|
||||
/// Absolute path to .ipynb file
|
||||
pub notebook_path: PathBuf,
|
||||
/// New cell content
|
||||
pub new_source: String,
|
||||
/// Cell ID to edit
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cell_id: Option<String>,
|
||||
/// Type of cell (code or markdown)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cell_type: Option<CellType>,
|
||||
/// Edit operation mode
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub edit_mode: Option<EditMode>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, JsonSchema, Debug)]
|
||||
pub struct MultiEditItem {
|
||||
/// The text to search for and replace
|
||||
pub old_string: String,
|
||||
/// The replacement text
|
||||
pub new_string: String,
|
||||
/// Whether to replace all occurrences or just the first
|
||||
#[serde(default, skip_serializing_if = "is_false")]
|
||||
pub replace_all: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, JsonSchema, Debug)]
|
||||
pub struct MultiEditToolParams {
|
||||
/// Absolute path to file
|
||||
pub file_path: PathBuf,
|
||||
/// List of edits to apply
|
||||
pub edits: Vec<MultiEditItem>,
|
||||
}
|
||||
|
||||
fn is_false(v: &bool) -> bool {
|
||||
!*v
|
||||
}
|
||||
|
||||
#[derive(Deserialize, JsonSchema, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GrepOutputMode {
|
||||
Content,
|
||||
FilesWithMatches,
|
||||
Count,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, JsonSchema, Debug)]
|
||||
pub struct WebFetchToolParams {
|
||||
/// Valid URL to fetch
|
||||
#[serde(rename = "url")]
|
||||
pub url: String,
|
||||
/// What to extract from content
|
||||
pub prompt: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, JsonSchema, Debug)]
|
||||
pub struct WebSearchToolParams {
|
||||
/// Search query (min 2 chars)
|
||||
pub query: String,
|
||||
/// Only include these domains
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub allowed_domains: Vec<String>,
|
||||
/// Exclude these domains
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub blocked_domains: Vec<String>,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for WebSearchToolParams {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "\"{}\"", self.query)?;
|
||||
|
||||
if !self.allowed_domains.is_empty() {
|
||||
write!(f, " (allowed: {})", self.allowed_domains.join(", "))?;
|
||||
}
|
||||
|
||||
if !self.blocked_domains.is_empty() {
|
||||
write!(f, " (blocked: {})", self.blocked_domains.join(", "))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
319
crates/agent_servers/src/codex.rs
Normal file
@@ -0,0 +1,319 @@
|
||||
use agent_client_protocol as acp;
|
||||
use anyhow::anyhow;
|
||||
use collections::HashMap;
|
||||
use context_server::listener::McpServerTool;
|
||||
use context_server::types::requests;
|
||||
use context_server::{ContextServer, ContextServerCommand, ContextServerId};
|
||||
use futures::channel::{mpsc, oneshot};
|
||||
use project::Project;
|
||||
use settings::SettingsStore;
|
||||
use smol::stream::StreamExt as _;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::{path::Path, sync::Arc};
|
||||
use util::ResultExt;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use gpui::{App, AppContext as _, AsyncApp, Entity, Task, WeakEntity};
|
||||
|
||||
use crate::mcp_server::ZedMcpServer;
|
||||
use crate::{AgentServer, AgentServerCommand, AllAgentServersSettings, mcp_server};
|
||||
use acp_thread::{AcpThread, AgentConnection};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Codex;
|
||||
|
||||
impl AgentServer for Codex {
|
||||
fn name(&self) -> &'static str {
|
||||
"Codex"
|
||||
}
|
||||
|
||||
fn empty_state_headline(&self) -> &'static str {
|
||||
"Welcome to Codex"
|
||||
}
|
||||
|
||||
fn empty_state_message(&self) -> &'static str {
|
||||
"What can I help with?"
|
||||
}
|
||||
|
||||
fn logo(&self) -> ui::IconName {
|
||||
ui::IconName::AiOpenAi
|
||||
}
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
_root_dir: &Path,
|
||||
project: &Entity<Project>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Rc<dyn AgentConnection>>> {
|
||||
let project = project.clone();
|
||||
let working_directory = project.read(cx).active_project_directory(cx);
|
||||
cx.spawn(async move |cx| {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).codex.clone()
|
||||
})?;
|
||||
|
||||
let Some(command) =
|
||||
AgentServerCommand::resolve("codex", &["mcp"], settings, &project, cx).await
|
||||
else {
|
||||
anyhow::bail!("Failed to find codex binary");
|
||||
};
|
||||
|
||||
let client: Arc<ContextServer> = ContextServer::stdio(
|
||||
ContextServerId("codex-mcp-server".into()),
|
||||
ContextServerCommand {
|
||||
path: command.path,
|
||||
args: command.args,
|
||||
env: command.env,
|
||||
},
|
||||
working_directory,
|
||||
)
|
||||
.into();
|
||||
ContextServer::start(client.clone(), cx).await?;
|
||||
|
||||
let (notification_tx, mut notification_rx) = mpsc::unbounded();
|
||||
client
|
||||
.client()
|
||||
.context("Failed to subscribe")?
|
||||
.on_notification(acp::SESSION_UPDATE_METHOD_NAME, {
|
||||
move |notification, _cx| {
|
||||
let notification_tx = notification_tx.clone();
|
||||
log::trace!(
|
||||
"ACP Notification: {}",
|
||||
serde_json::to_string_pretty(¬ification).unwrap()
|
||||
);
|
||||
|
||||
if let Some(notification) =
|
||||
serde_json::from_value::<acp::SessionNotification>(notification)
|
||||
.log_err()
|
||||
{
|
||||
notification_tx.unbounded_send(notification).ok();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let sessions = Rc::new(RefCell::new(HashMap::default()));
|
||||
|
||||
let notification_handler_task = cx.spawn({
|
||||
let sessions = sessions.clone();
|
||||
async move |cx| {
|
||||
while let Some(notification) = notification_rx.next().await {
|
||||
CodexConnection::handle_session_notification(
|
||||
notification,
|
||||
sessions.clone(),
|
||||
cx,
|
||||
)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let connection = CodexConnection {
|
||||
client,
|
||||
sessions,
|
||||
_notification_handler_task: notification_handler_task,
|
||||
};
|
||||
Ok(Rc::new(connection) as _)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct CodexConnection {
|
||||
client: Arc<context_server::ContextServer>,
|
||||
sessions: Rc<RefCell<HashMap<acp::SessionId, CodexSession>>>,
|
||||
_notification_handler_task: Task<()>,
|
||||
}
|
||||
|
||||
struct CodexSession {
|
||||
thread: WeakEntity<AcpThread>,
|
||||
cancel_tx: Option<oneshot::Sender<()>>,
|
||||
_mcp_server: ZedMcpServer,
|
||||
}
|
||||
|
||||
impl AgentConnection for CodexConnection {
|
||||
fn name(&self) -> &'static str {
|
||||
"Codex"
|
||||
}
|
||||
|
||||
fn new_thread(
|
||||
self: Rc<Self>,
|
||||
project: Entity<Project>,
|
||||
cwd: &Path,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Task<Result<Entity<AcpThread>>> {
|
||||
let client = self.client.client();
|
||||
let sessions = self.sessions.clone();
|
||||
let cwd = cwd.to_path_buf();
|
||||
cx.spawn(async move |cx| {
|
||||
let client = client.context("MCP server is not initialized yet")?;
|
||||
let (mut thread_tx, thread_rx) = watch::channel(WeakEntity::new_invalid());
|
||||
|
||||
let mcp_server = ZedMcpServer::new(thread_rx, cx).await?;
|
||||
|
||||
let response = client
|
||||
.request::<requests::CallTool>(context_server::types::CallToolParams {
|
||||
name: acp::NEW_SESSION_TOOL_NAME.into(),
|
||||
arguments: Some(serde_json::to_value(acp::NewSessionArguments {
|
||||
mcp_servers: [(
|
||||
mcp_server::SERVER_NAME.to_string(),
|
||||
mcp_server.server_config()?,
|
||||
)]
|
||||
.into(),
|
||||
client_tools: acp::ClientTools {
|
||||
request_permission: Some(acp::McpToolId {
|
||||
mcp_server: mcp_server::SERVER_NAME.into(),
|
||||
tool_name: mcp_server::RequestPermissionTool::NAME.into(),
|
||||
}),
|
||||
read_text_file: Some(acp::McpToolId {
|
||||
mcp_server: mcp_server::SERVER_NAME.into(),
|
||||
tool_name: mcp_server::ReadTextFileTool::NAME.into(),
|
||||
}),
|
||||
write_text_file: Some(acp::McpToolId {
|
||||
mcp_server: mcp_server::SERVER_NAME.into(),
|
||||
tool_name: mcp_server::WriteTextFileTool::NAME.into(),
|
||||
}),
|
||||
},
|
||||
cwd,
|
||||
})?),
|
||||
meta: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
if response.is_error.unwrap_or_default() {
|
||||
return Err(anyhow!(response.text_contents()));
|
||||
}
|
||||
|
||||
let result = serde_json::from_value::<acp::NewSessionOutput>(
|
||||
response.structured_content.context("Empty response")?,
|
||||
)?;
|
||||
|
||||
let thread =
|
||||
cx.new(|cx| AcpThread::new(self.clone(), project, result.session_id.clone(), cx))?;
|
||||
|
||||
thread_tx.send(thread.downgrade())?;
|
||||
|
||||
let session = CodexSession {
|
||||
thread: thread.downgrade(),
|
||||
cancel_tx: None,
|
||||
_mcp_server: mcp_server,
|
||||
};
|
||||
sessions.borrow_mut().insert(result.session_id, session);
|
||||
|
||||
Ok(thread)
|
||||
})
|
||||
}
|
||||
|
||||
fn authenticate(&self, _cx: &mut App) -> Task<Result<()>> {
|
||||
Task::ready(Err(anyhow!("Authentication not supported")))
|
||||
}
|
||||
|
||||
fn prompt(
|
||||
&self,
|
||||
params: agent_client_protocol::PromptArguments,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<()>> {
|
||||
let client = self.client.client();
|
||||
let sessions = self.sessions.clone();
|
||||
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let client = client.context("MCP server is not initialized yet")?;
|
||||
|
||||
let (new_cancel_tx, cancel_rx) = oneshot::channel();
|
||||
{
|
||||
let mut sessions = sessions.borrow_mut();
|
||||
let session = sessions
|
||||
.get_mut(¶ms.session_id)
|
||||
.context("Session not found")?;
|
||||
session.cancel_tx.replace(new_cancel_tx);
|
||||
}
|
||||
|
||||
let result = client
|
||||
.request_with::<requests::CallTool>(
|
||||
context_server::types::CallToolParams {
|
||||
name: acp::PROMPT_TOOL_NAME.into(),
|
||||
arguments: Some(serde_json::to_value(params)?),
|
||||
meta: None,
|
||||
},
|
||||
Some(cancel_rx),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Err(err) = &result
|
||||
&& err.is::<context_server::client::RequestCanceled>()
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let response = result?;
|
||||
|
||||
if response.is_error.unwrap_or_default() {
|
||||
return Err(anyhow!(response.text_contents()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn cancel(&self, session_id: &agent_client_protocol::SessionId, _cx: &mut App) {
|
||||
let mut sessions = self.sessions.borrow_mut();
|
||||
|
||||
if let Some(cancel_tx) = sessions
|
||||
.get_mut(session_id)
|
||||
.and_then(|session| session.cancel_tx.take())
|
||||
{
|
||||
cancel_tx.send(()).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CodexConnection {
|
||||
pub fn handle_session_notification(
|
||||
notification: acp::SessionNotification,
|
||||
threads: Rc<RefCell<HashMap<acp::SessionId, CodexSession>>>,
|
||||
cx: &mut AsyncApp,
|
||||
) {
|
||||
let threads = threads.borrow();
|
||||
let Some(thread) = threads
|
||||
.get(¬ification.session_id)
|
||||
.and_then(|session| session.thread.upgrade())
|
||||
else {
|
||||
log::error!(
|
||||
"Thread not found for session ID: {}",
|
||||
notification.session_id
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.handle_session_update(notification.update, cx)
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for CodexConnection {
|
||||
fn drop(&mut self) {
|
||||
self.client.stop().log_err();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
use crate::AgentServerCommand;
|
||||
use std::path::Path;
|
||||
|
||||
crate::common_e2e_tests!(Codex, allow_option_id = "approve");
|
||||
|
||||
pub fn local_command() -> AgentServerCommand {
|
||||
let cli_path = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../../codex/codex-rs/target/debug/codex");
|
||||
|
||||
AgentServerCommand {
|
||||
path: cli_path,
|
||||
args: vec![],
|
||||
env: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
459
crates/agent_servers/src/e2e_tests.rs
Normal file
@@ -0,0 +1,459 @@
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crate::{AgentServer, AgentServerSettings, AllAgentServersSettings};
|
||||
use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus};
|
||||
use agent_client_protocol as acp;
|
||||
|
||||
use futures::{FutureExt, StreamExt, channel::mpsc, select};
|
||||
use gpui::{Entity, TestAppContext};
|
||||
use indoc::indoc;
|
||||
use project::{FakeFs, Project};
|
||||
use settings::{Settings, SettingsStore};
|
||||
use util::path;
|
||||
|
||||
pub async fn test_basic(server: impl AgentServer + 'static, cx: &mut TestAppContext) {
|
||||
let fs = init_test(cx).await;
|
||||
let project = Project::test(fs, [], cx).await;
|
||||
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
|
||||
|
||||
thread
|
||||
.update(cx, |thread, cx| thread.send_raw("Hello from Zed!", cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert!(
|
||||
thread.entries().len() >= 2,
|
||||
"Expected at least 2 entries. Got: {:?}",
|
||||
thread.entries()
|
||||
);
|
||||
assert!(matches!(
|
||||
thread.entries()[0],
|
||||
AgentThreadEntry::UserMessage(_)
|
||||
));
|
||||
assert!(matches!(
|
||||
thread.entries()[1],
|
||||
AgentThreadEntry::AssistantMessage(_)
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn test_path_mentions(server: impl AgentServer + 'static, cx: &mut TestAppContext) {
|
||||
let _fs = init_test(cx).await;
|
||||
|
||||
let tempdir = tempfile::tempdir().unwrap();
|
||||
std::fs::write(
|
||||
tempdir.path().join("foo.rs"),
|
||||
indoc! {"
|
||||
fn main() {
|
||||
println!(\"Hello, world!\");
|
||||
}
|
||||
"},
|
||||
)
|
||||
.expect("failed to write file");
|
||||
let project = Project::example([tempdir.path()], &mut cx.to_async()).await;
|
||||
let thread = new_test_thread(server, project.clone(), tempdir.path(), cx).await;
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(
|
||||
vec![
|
||||
acp::ContentBlock::Text(acp::TextContent {
|
||||
text: "Read the file ".into(),
|
||||
annotations: None,
|
||||
}),
|
||||
acp::ContentBlock::ResourceLink(acp::ResourceLink {
|
||||
uri: "foo.rs".into(),
|
||||
name: "foo.rs".into(),
|
||||
annotations: None,
|
||||
description: None,
|
||||
mime_type: None,
|
||||
size: None,
|
||||
title: None,
|
||||
}),
|
||||
acp::ContentBlock::Text(acp::TextContent {
|
||||
text: " and tell me what the content of the println! is".into(),
|
||||
annotations: None,
|
||||
}),
|
||||
],
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
thread.read_with(cx, |thread, cx| {
|
||||
assert!(matches!(
|
||||
thread.entries()[0],
|
||||
AgentThreadEntry::UserMessage(_)
|
||||
));
|
||||
let assistant_message = &thread
|
||||
.entries()
|
||||
.iter()
|
||||
.rev()
|
||||
.find_map(|entry| match entry {
|
||||
AgentThreadEntry::AssistantMessage(msg) => Some(msg),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
assistant_message.to_markdown(cx).contains("Hello, world!"),
|
||||
"unexpected assistant message: {:?}",
|
||||
assistant_message.to_markdown(cx)
|
||||
);
|
||||
});
|
||||
|
||||
drop(tempdir);
|
||||
}
|
||||
|
||||
pub async fn test_tool_call(server: impl AgentServer + 'static, cx: &mut TestAppContext) {
|
||||
let _fs = init_test(cx).await;
|
||||
|
||||
let tempdir = tempfile::tempdir().unwrap();
|
||||
let foo_path = tempdir.path().join("foo");
|
||||
std::fs::write(&foo_path, "Lorem ipsum dolor").expect("failed to write file");
|
||||
|
||||
let project = Project::example([tempdir.path()], &mut cx.to_async()).await;
|
||||
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
|
||||
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send_raw(
|
||||
&format!("Read {} and tell me what you see.", foo_path.display()),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
thread.read_with(cx, |thread, _cx| {
|
||||
assert!(thread.entries().iter().any(|entry| {
|
||||
matches!(
|
||||
entry,
|
||||
AgentThreadEntry::ToolCall(ToolCall {
|
||||
status: ToolCallStatus::Allowed { .. },
|
||||
..
|
||||
})
|
||||
)
|
||||
}));
|
||||
assert!(
|
||||
thread
|
||||
.entries()
|
||||
.iter()
|
||||
.any(|entry| { matches!(entry, AgentThreadEntry::AssistantMessage(_)) })
|
||||
);
|
||||
});
|
||||
|
||||
drop(tempdir);
|
||||
}
|
||||
|
||||
pub async fn test_tool_call_with_confirmation(
|
||||
server: impl AgentServer + 'static,
|
||||
allow_option_id: acp::PermissionOptionId,
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
let fs = init_test(cx).await;
|
||||
let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await;
|
||||
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
|
||||
let full_turn = thread.update(cx, |thread, cx| {
|
||||
thread.send_raw(
|
||||
r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
run_until_first_tool_call(
|
||||
&thread,
|
||||
|entry| {
|
||||
matches!(
|
||||
entry,
|
||||
AgentThreadEntry::ToolCall(ToolCall {
|
||||
status: ToolCallStatus::WaitingForConfirmation { .. },
|
||||
..
|
||||
})
|
||||
)
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
let tool_call_id = thread.read_with(cx, |thread, cx| {
|
||||
let AgentThreadEntry::ToolCall(ToolCall {
|
||||
id,
|
||||
label,
|
||||
status: ToolCallStatus::WaitingForConfirmation { .. },
|
||||
..
|
||||
}) = &thread
|
||||
.entries()
|
||||
.iter()
|
||||
.find(|entry| matches!(entry, AgentThreadEntry::ToolCall(_)))
|
||||
.unwrap()
|
||||
else {
|
||||
panic!();
|
||||
};
|
||||
|
||||
let label = label.read(cx).source();
|
||||
assert!(label.contains("touch"), "Got: {}", label);
|
||||
|
||||
id.clone()
|
||||
});
|
||||
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.authorize_tool_call(
|
||||
tool_call_id,
|
||||
allow_option_id,
|
||||
acp::PermissionOptionKind::AllowOnce,
|
||||
cx,
|
||||
);
|
||||
|
||||
assert!(thread.entries().iter().any(|entry| matches!(
|
||||
entry,
|
||||
AgentThreadEntry::ToolCall(ToolCall {
|
||||
status: ToolCallStatus::Allowed { .. },
|
||||
..
|
||||
})
|
||||
)));
|
||||
});
|
||||
|
||||
full_turn.await.unwrap();
|
||||
|
||||
thread.read_with(cx, |thread, cx| {
|
||||
let AgentThreadEntry::ToolCall(ToolCall {
|
||||
content,
|
||||
status: ToolCallStatus::Allowed { .. },
|
||||
..
|
||||
}) = thread
|
||||
.entries()
|
||||
.iter()
|
||||
.find(|entry| matches!(entry, AgentThreadEntry::ToolCall(_)))
|
||||
.unwrap()
|
||||
else {
|
||||
panic!();
|
||||
};
|
||||
|
||||
assert!(
|
||||
content.iter().any(|c| c.to_markdown(cx).contains("Hello")),
|
||||
"Expected content to contain 'Hello'"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppContext) {
|
||||
let fs = init_test(cx).await;
|
||||
|
||||
let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await;
|
||||
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
|
||||
let full_turn = thread.update(cx, |thread, cx| {
|
||||
thread.send_raw(
|
||||
r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let first_tool_call_ix = run_until_first_tool_call(
|
||||
&thread,
|
||||
|entry| {
|
||||
matches!(
|
||||
entry,
|
||||
AgentThreadEntry::ToolCall(ToolCall {
|
||||
status: ToolCallStatus::WaitingForConfirmation { .. },
|
||||
..
|
||||
})
|
||||
)
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
thread.read_with(cx, |thread, cx| {
|
||||
let AgentThreadEntry::ToolCall(ToolCall {
|
||||
id,
|
||||
label,
|
||||
status: ToolCallStatus::WaitingForConfirmation { .. },
|
||||
..
|
||||
}) = &thread.entries()[first_tool_call_ix]
|
||||
else {
|
||||
panic!("{:?}", thread.entries()[1]);
|
||||
};
|
||||
|
||||
let label = label.read(cx).source();
|
||||
assert!(label.contains("touch"), "Got: {}", label);
|
||||
|
||||
id.clone()
|
||||
});
|
||||
|
||||
let _ = thread.update(cx, |thread, cx| thread.cancel(cx));
|
||||
full_turn.await.unwrap();
|
||||
thread.read_with(cx, |thread, _| {
|
||||
let AgentThreadEntry::ToolCall(ToolCall {
|
||||
status: ToolCallStatus::Canceled,
|
||||
..
|
||||
}) = &thread.entries()[first_tool_call_ix]
|
||||
else {
|
||||
panic!();
|
||||
};
|
||||
});
|
||||
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send_raw(r#"Stop running and say goodbye to me."#, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert!(matches!(
|
||||
&thread.entries().last().unwrap(),
|
||||
AgentThreadEntry::AssistantMessage(..),
|
||||
))
|
||||
});
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! common_e2e_tests {
|
||||
($server:expr, allow_option_id = $allow_option_id:expr) => {
|
||||
mod common_e2e {
|
||||
use super::*;
|
||||
|
||||
#[::gpui::test]
|
||||
#[cfg_attr(not(feature = "e2e"), ignore)]
|
||||
async fn basic(cx: &mut ::gpui::TestAppContext) {
|
||||
$crate::e2e_tests::test_basic($server, cx).await;
|
||||
}
|
||||
|
||||
#[::gpui::test]
|
||||
#[cfg_attr(not(feature = "e2e"), ignore)]
|
||||
async fn path_mentions(cx: &mut ::gpui::TestAppContext) {
|
||||
$crate::e2e_tests::test_path_mentions($server, cx).await;
|
||||
}
|
||||
|
||||
#[::gpui::test]
|
||||
#[cfg_attr(not(feature = "e2e"), ignore)]
|
||||
async fn tool_call(cx: &mut ::gpui::TestAppContext) {
|
||||
$crate::e2e_tests::test_tool_call($server, cx).await;
|
||||
}
|
||||
|
||||
#[::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,
|
||||
::agent_client_protocol::PermissionOptionId($allow_option_id.into()),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[::gpui::test]
|
||||
#[cfg_attr(not(feature = "e2e"), ignore)]
|
||||
async fn cancel(cx: &mut ::gpui::TestAppContext) {
|
||||
$crate::e2e_tests::test_cancel($server, cx).await;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
|
||||
env_logger::try_init().ok();
|
||||
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
Project::init_settings(cx);
|
||||
language::init(cx);
|
||||
crate::settings::init(cx);
|
||||
|
||||
crate::AllAgentServersSettings::override_global(
|
||||
AllAgentServersSettings {
|
||||
claude: Some(AgentServerSettings {
|
||||
command: crate::claude::tests::local_command(),
|
||||
}),
|
||||
gemini: Some(AgentServerSettings {
|
||||
command: crate::gemini::tests::local_command(),
|
||||
}),
|
||||
codex: Some(AgentServerSettings {
|
||||
command: crate::codex::tests::local_command(),
|
||||
}),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
cx.executor().allow_parking();
|
||||
|
||||
FakeFs::new(cx.executor())
|
||||
}
|
||||
|
||||
pub async fn new_test_thread(
|
||||
server: impl AgentServer + 'static,
|
||||
project: Entity<Project>,
|
||||
current_dir: impl AsRef<Path>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> Entity<AcpThread> {
|
||||
let connection = cx
|
||||
.update(|cx| server.connect(current_dir.as_ref(), &project, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let thread = connection
|
||||
.new_thread(project.clone(), current_dir.as_ref(), &mut cx.to_async())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
thread
|
||||
}
|
||||
|
||||
pub async fn run_until_first_tool_call(
|
||||
thread: &Entity<AcpThread>,
|
||||
wait_until: impl Fn(&AgentThreadEntry) -> bool + 'static,
|
||||
cx: &mut TestAppContext,
|
||||
) -> usize {
|
||||
let (mut tx, mut rx) = mpsc::channel::<usize>(1);
|
||||
|
||||
let subscription = cx.update(|cx| {
|
||||
cx.subscribe(thread, move |thread, _, cx| {
|
||||
for (ix, entry) in thread.read(cx).entries().iter().enumerate() {
|
||||
if wait_until(entry) {
|
||||
return tx.try_send(ix).unwrap();
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
select! {
|
||||
// We have to use a smol timer here because
|
||||
// cx.background_executor().timer isn't real in the test context
|
||||
_ = futures::FutureExt::fuse(smol::Timer::after(Duration::from_secs(20))) => {
|
||||
panic!("Timeout waiting for tool call")
|
||||
}
|
||||
ix = rx.next().fuse() => {
|
||||
drop(subscription);
|
||||
ix.unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
206
crates/agent_servers/src/gemini.rs
Normal file
@@ -0,0 +1,206 @@
|
||||
use anyhow::anyhow;
|
||||
use std::cell::RefCell;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::{AgentServer, AgentServerCommand, AgentServerVersion};
|
||||
use acp_thread::{AgentConnection, LoadError, OldAcpAgentConnection, OldAcpClientDelegate};
|
||||
use agentic_coding_protocol as acp_old;
|
||||
use anyhow::{Context as _, Result};
|
||||
use gpui::{AppContext as _, AsyncApp, Entity, Task, WeakEntity};
|
||||
use project::Project;
|
||||
use settings::SettingsStore;
|
||||
use ui::App;
|
||||
|
||||
use crate::AllAgentServersSettings;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Gemini;
|
||||
|
||||
const ACP_ARG: &str = "--experimental-acp";
|
||||
|
||||
impl AgentServer for Gemini {
|
||||
fn name(&self) -> &'static str {
|
||||
"Gemini"
|
||||
}
|
||||
|
||||
fn empty_state_headline(&self) -> &'static str {
|
||||
"Welcome to Gemini"
|
||||
}
|
||||
|
||||
fn empty_state_message(&self) -> &'static str {
|
||||
"Ask questions, edit files, run commands.\nBe specific for the best results."
|
||||
}
|
||||
|
||||
fn logo(&self) -> ui::IconName {
|
||||
ui::IconName::AiGemini
|
||||
}
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
root_dir: &Path,
|
||||
project: &Entity<Project>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Rc<dyn AgentConnection>>> {
|
||||
let root_dir = root_dir.to_path_buf();
|
||||
let project = project.clone();
|
||||
let this = self.clone();
|
||||
let name = self.name();
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let command = this.command(&project, cx).await?;
|
||||
|
||||
let mut child = util::command::new_smol_command(&command.path)
|
||||
.args(command.args.iter())
|
||||
.current_dir(root_dir)
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::inherit())
|
||||
.kill_on_drop(true)
|
||||
.spawn()?;
|
||||
|
||||
let stdin = child.stdin.take().unwrap();
|
||||
let stdout = child.stdout.take().unwrap();
|
||||
|
||||
let foreground_executor = cx.foreground_executor().clone();
|
||||
|
||||
let thread_rc = Rc::new(RefCell::new(WeakEntity::new_invalid()));
|
||||
|
||||
let (connection, io_fut) = acp_old::AgentConnection::connect_to_agent(
|
||||
OldAcpClientDelegate::new(thread_rc.clone(), cx.clone()),
|
||||
stdin,
|
||||
stdout,
|
||||
move |fut| foreground_executor.spawn(fut).detach(),
|
||||
);
|
||||
|
||||
let io_task = cx.background_spawn(async move {
|
||||
io_fut.await.log_err();
|
||||
});
|
||||
|
||||
let child_status = cx.background_spawn(async move {
|
||||
let result = match child.status().await {
|
||||
Err(e) => Err(anyhow!(e)),
|
||||
Ok(result) if result.success() => Ok(()),
|
||||
Ok(result) => {
|
||||
if let Some(AgentServerVersion::Unsupported {
|
||||
error_message,
|
||||
upgrade_message,
|
||||
upgrade_command,
|
||||
}) = this.version(&command).await.log_err()
|
||||
{
|
||||
Err(anyhow!(LoadError::Unsupported {
|
||||
error_message,
|
||||
upgrade_message,
|
||||
upgrade_command
|
||||
}))
|
||||
} else {
|
||||
Err(anyhow!(LoadError::Exited(result.code().unwrap_or(-127))))
|
||||
}
|
||||
}
|
||||
};
|
||||
drop(io_task);
|
||||
result
|
||||
});
|
||||
|
||||
let connection: Rc<dyn AgentConnection> = Rc::new(OldAcpAgentConnection {
|
||||
name,
|
||||
connection,
|
||||
child_status,
|
||||
current_thread: thread_rc,
|
||||
});
|
||||
|
||||
Ok(connection)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Gemini {
|
||||
async fn command(
|
||||
&self,
|
||||
project: &Entity<Project>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<AgentServerCommand> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).gemini.clone()
|
||||
})?;
|
||||
|
||||
if let Some(command) =
|
||||
AgentServerCommand::resolve("gemini", &[ACP_ARG], settings, &project, cx).await
|
||||
{
|
||||
return Ok(command);
|
||||
};
|
||||
|
||||
let (fs, node_runtime) = project.update(cx, |project, _| {
|
||||
(project.fs().clone(), project.node_runtime().cloned())
|
||||
})?;
|
||||
let node_runtime = node_runtime.context("gemini not found on path")?;
|
||||
|
||||
let directory = ::paths::agent_servers_dir().join("gemini");
|
||||
fs.create_dir(&directory).await?;
|
||||
node_runtime
|
||||
.npm_install_packages(&directory, &[("@google/gemini-cli", "latest")])
|
||||
.await?;
|
||||
let path = directory.join("node_modules/.bin/gemini");
|
||||
|
||||
Ok(AgentServerCommand {
|
||||
path,
|
||||
args: vec![ACP_ARG.into()],
|
||||
env: None,
|
||||
})
|
||||
}
|
||||
|
||||
async fn version(&self, command: &AgentServerCommand) -> Result<AgentServerVersion> {
|
||||
let version_fut = util::command::new_smol_command(&command.path)
|
||||
.args(command.args.iter())
|
||||
.arg("--version")
|
||||
.kill_on_drop(true)
|
||||
.output();
|
||||
|
||||
let help_fut = util::command::new_smol_command(&command.path)
|
||||
.args(command.args.iter())
|
||||
.arg("--help")
|
||||
.kill_on_drop(true)
|
||||
.output();
|
||||
|
||||
let (version_output, help_output) = futures::future::join(version_fut, help_fut).await;
|
||||
|
||||
let current_version = String::from_utf8(version_output?.stdout)?;
|
||||
let supported = String::from_utf8(help_output?.stdout)?.contains(ACP_ARG);
|
||||
|
||||
if supported {
|
||||
Ok(AgentServerVersion::Supported)
|
||||
} else {
|
||||
Ok(AgentServerVersion::Unsupported {
|
||||
error_message: format!(
|
||||
"Your installed version of Gemini {} doesn't support the Agentic Coding Protocol (ACP).",
|
||||
current_version
|
||||
).into(),
|
||||
upgrade_message: "Upgrade Gemini to Latest".into(),
|
||||
upgrade_command: "npm install -g @google/gemini-cli@latest".into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
use crate::AgentServerCommand;
|
||||
use std::path::Path;
|
||||
|
||||
crate::common_e2e_tests!(Gemini, allow_option_id = "0");
|
||||
|
||||
pub fn local_command() -> AgentServerCommand {
|
||||
let cli_path = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../../gemini-cli/packages/cli")
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
AgentServerCommand {
|
||||
path: "node".into(),
|
||||
args: vec![cli_path, ACP_ARG.into()],
|
||||
env: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
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: (),
|
||||
})
|
||||
}
|
||||
}
|
||||
54
crates/agent_servers/src/settings.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use crate::AgentServerCommand;
|
||||
use anyhow::Result;
|
||||
use gpui::App;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
AllAgentServersSettings::register(cx);
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug)]
|
||||
pub struct AllAgentServersSettings {
|
||||
pub gemini: Option<AgentServerSettings>,
|
||||
pub claude: Option<AgentServerSettings>,
|
||||
pub codex: Option<AgentServerSettings>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug)]
|
||||
pub struct AgentServerSettings {
|
||||
#[serde(flatten)]
|
||||
pub command: AgentServerCommand,
|
||||
}
|
||||
|
||||
impl settings::Settings for AllAgentServersSettings {
|
||||
const KEY: Option<&'static str> = Some("agent_servers");
|
||||
|
||||
type FileContent = Self;
|
||||
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
|
||||
let mut settings = AllAgentServersSettings::default();
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ path = "src/agent_settings.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
gpui.workspace = true
|
||||
language_model.workspace = true
|
||||
@@ -20,7 +21,6 @@ schemars.workspace = true
|
||||
serde.workspace = true
|
||||
settings.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
fs.workspace = true
|
||||
|
||||
@@ -69,6 +69,7 @@ pub struct AgentSettings {
|
||||
pub enable_feedback: bool,
|
||||
pub expand_edit_card: bool,
|
||||
pub expand_terminal_card: bool,
|
||||
pub use_modifier_to_send: bool,
|
||||
}
|
||||
|
||||
impl AgentSettings {
|
||||
@@ -174,6 +175,10 @@ impl AgentSettingsContent {
|
||||
self.single_file_review = Some(allow);
|
||||
}
|
||||
|
||||
pub fn set_use_modifier_to_send(&mut self, always_use: bool) {
|
||||
self.use_modifier_to_send = Some(always_use);
|
||||
}
|
||||
|
||||
pub fn set_profile(&mut self, profile_id: AgentProfileId) {
|
||||
self.default_profile = Some(profile_id);
|
||||
}
|
||||
@@ -301,6 +306,10 @@ pub struct AgentSettingsContent {
|
||||
///
|
||||
/// Default: true
|
||||
expand_terminal_card: Option<bool>,
|
||||
/// Whether to always use cmd-enter (or ctrl-enter on Linux) to send messages in the agent panel.
|
||||
///
|
||||
/// Default: false
|
||||
use_modifier_to_send: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
|
||||
@@ -312,11 +321,11 @@ pub enum CompletionMode {
|
||||
Burn,
|
||||
}
|
||||
|
||||
impl From<CompletionMode> for zed_llm_client::CompletionMode {
|
||||
impl From<CompletionMode> for cloud_llm_client::CompletionMode {
|
||||
fn from(value: CompletionMode) -> Self {
|
||||
match value {
|
||||
CompletionMode::Normal => zed_llm_client::CompletionMode::Normal,
|
||||
CompletionMode::Burn => zed_llm_client::CompletionMode::Max,
|
||||
CompletionMode::Normal => cloud_llm_client::CompletionMode::Normal,
|
||||
CompletionMode::Burn => cloud_llm_client::CompletionMode::Max,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -456,6 +465,10 @@ impl Settings for AgentSettings {
|
||||
&mut settings.expand_terminal_card,
|
||||
value.expand_terminal_card,
|
||||
);
|
||||
merge(
|
||||
&mut settings.use_modifier_to_send,
|
||||
value.use_modifier_to_send,
|
||||
);
|
||||
|
||||
settings
|
||||
.model_parameters
|
||||
|
||||
@@ -16,11 +16,12 @@ doctest = false
|
||||
test-support = ["gpui/test-support", "language/test-support"]
|
||||
|
||||
[dependencies]
|
||||
acp.workspace = true
|
||||
acp_thread.workspace = true
|
||||
agent-client-protocol.workspace = true
|
||||
agent.workspace = true
|
||||
agentic-coding-protocol.workspace = true
|
||||
agent_settings.workspace = true
|
||||
agent_servers.workspace = true
|
||||
agent_settings.workspace = true
|
||||
ai_onboarding.workspace = true
|
||||
anyhow.workspace = true
|
||||
assistant_context.workspace = true
|
||||
assistant_slash_command.workspace = true
|
||||
@@ -30,7 +31,9 @@ audio.workspace = true
|
||||
buffer_diff.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
component.workspace = true
|
||||
context_server.workspace = true
|
||||
db.workspace = true
|
||||
@@ -44,14 +47,15 @@ futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
html_to_markdown.workspace = true
|
||||
indoc.workspace = true
|
||||
http_client.workspace = true
|
||||
indexed_docs.workspace = true
|
||||
indoc.workspace = true
|
||||
inventory.workspace = true
|
||||
itertools.workspace = true
|
||||
jsonschema.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
language_models.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
markdown.workspace = true
|
||||
@@ -86,6 +90,7 @@ theme.workspace = true
|
||||
time.workspace = true
|
||||
time_format.workspace = true
|
||||
ui.workspace = true
|
||||
ui_input.workspace = true
|
||||
urlencoding.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
@@ -93,7 +98,6 @@ watch.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
assistant_tools.workspace = true
|
||||
|
||||
@@ -14,6 +14,7 @@ use agent_settings::{AgentSettings, NotifyWhenAgentWaiting};
|
||||
use anyhow::Context as _;
|
||||
use assistant_tool::ToolUseStatus;
|
||||
use audio::{Audio, Sound};
|
||||
use cloud_llm_client::CompletionIntent;
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::actions::{MoveUp, Paste};
|
||||
use editor::scroll::Autoscroll;
|
||||
@@ -52,7 +53,6 @@ use util::ResultExt as _;
|
||||
use util::markdown::MarkdownCodeBlock;
|
||||
use workspace::{CollaboratorId, Workspace};
|
||||
use zed_actions::assistant::OpenRulesLibrary;
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
const CODEBLOCK_CONTAINER_GROUP: &str = "codeblock_container";
|
||||
const EDIT_PREVIOUS_MESSAGE_MIN_LINES: usize = 1;
|
||||
@@ -1036,7 +1036,7 @@ impl ActiveThread {
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
self.last_error = Some(ThreadError::Message {
|
||||
header: "Error interacting with language model".into(),
|
||||
header: "Error".into(),
|
||||
message: error_message.into(),
|
||||
});
|
||||
}
|
||||
@@ -3202,7 +3202,10 @@ impl ActiveThread {
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.rounded_b_lg()
|
||||
.child(
|
||||
LoadingLabel::new("Waiting for Confirmation").size(LabelSize::Small)
|
||||
div()
|
||||
.min_w(rems_from_px(145.))
|
||||
.child(LoadingLabel::new("Waiting for Confirmation").size(LabelSize::Small)
|
||||
)
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -3247,7 +3250,6 @@ impl ActiveThread {
|
||||
},
|
||||
))
|
||||
})
|
||||
.child(ui::Divider::vertical())
|
||||
.child({
|
||||
let tool_id = tool_use.id.clone();
|
||||
Button::new("allow-tool-action", "Allow")
|
||||
@@ -3722,8 +3724,11 @@ pub(crate) fn open_context(
|
||||
|
||||
AgentContextHandle::Thread(thread_context) => workspace.update(cx, |workspace, cx| {
|
||||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.open_thread(thread_context.thread.clone(), window, cx);
|
||||
let thread = thread_context.thread.clone();
|
||||
window.defer(cx, move |window, cx| {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.open_thread(thread, window, cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
}),
|
||||
@@ -3731,8 +3736,11 @@ pub(crate) fn open_context(
|
||||
AgentContextHandle::TextThread(text_thread_context) => {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.open_prompt_editor(text_thread_context.context.clone(), window, cx)
|
||||
let context = text_thread_context.context.clone();
|
||||
window.defer(cx, move |window, cx| {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.open_prompt_editor(context, window, cx)
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
@@ -3887,7 +3895,7 @@ mod tests {
|
||||
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
|
||||
registry.set_default_model(
|
||||
Some(ConfiguredModel {
|
||||
provider: Arc::new(FakeLanguageModelProvider),
|
||||
provider: Arc::new(FakeLanguageModelProvider::default()),
|
||||
model,
|
||||
}),
|
||||
cx,
|
||||
@@ -3971,7 +3979,7 @@ mod tests {
|
||||
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
|
||||
registry.set_default_model(
|
||||
Some(ConfiguredModel {
|
||||
provider: Arc::new(FakeLanguageModelProvider),
|
||||
provider: Arc::new(FakeLanguageModelProvider::default()),
|
||||
model: model.clone(),
|
||||
}),
|
||||
cx,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
mod add_llm_provider_modal;
|
||||
mod configure_context_server_modal;
|
||||
mod manage_profiles_modal;
|
||||
mod tool_picker;
|
||||
@@ -6,6 +7,7 @@ use std::{sync::Arc, time::Duration};
|
||||
|
||||
use agent_settings::AgentSettings;
|
||||
use assistant_tool::{ToolSource, ToolWorkingSet};
|
||||
use cloud_llm_client::Plan;
|
||||
use collections::HashMap;
|
||||
use context_server::ContextServerId;
|
||||
use extension::ExtensionManifest;
|
||||
@@ -24,11 +26,10 @@ use project::{
|
||||
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
|
||||
project_settings::{ContextServerSettings, ProjectSettings},
|
||||
};
|
||||
use proto::Plan;
|
||||
use settings::{Settings, update_settings_file};
|
||||
use ui::{
|
||||
Chip, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu,
|
||||
Scrollbar, ScrollbarState, Switch, SwitchColor, Tooltip, prelude::*,
|
||||
Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip, prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use workspace::Workspace;
|
||||
@@ -37,7 +38,10 @@ use zed_actions::ExtensionCategoryFilter;
|
||||
pub(crate) use configure_context_server_modal::ConfigureContextServerModal;
|
||||
pub(crate) use manage_profiles_modal::ManageProfilesModal;
|
||||
|
||||
use crate::AddContextServer;
|
||||
use crate::{
|
||||
AddContextServer,
|
||||
agent_configuration::add_llm_provider_modal::{AddLlmProviderModal, LlmCompatibleProvider},
|
||||
};
|
||||
|
||||
pub struct AgentConfiguration {
|
||||
fs: Arc<dyn Fs>,
|
||||
@@ -176,12 +180,20 @@ impl AgentConfiguration {
|
||||
let current_plan = if is_zed_provider {
|
||||
self.workspace
|
||||
.upgrade()
|
||||
.and_then(|workspace| workspace.read(cx).user_store().read(cx).current_plan())
|
||||
.and_then(|workspace| workspace.read(cx).user_store().read(cx).plan())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let is_signed_in = self
|
||||
.workspace
|
||||
.read_with(cx, |workspace, _| {
|
||||
workspace.client().status().borrow().is_connected()
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
v_flex()
|
||||
.w_full()
|
||||
.when(is_expanded, |this| this.mb_2())
|
||||
.child(
|
||||
div()
|
||||
@@ -212,6 +224,7 @@ impl AgentConfiguration {
|
||||
.hover(|hover| hover.bg(cx.theme().colors().element_hover))
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.child(
|
||||
Icon::new(provider.icon())
|
||||
@@ -220,14 +233,15 @@ impl AgentConfiguration {
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new(provider_name.clone())
|
||||
.size(LabelSize::Large),
|
||||
)
|
||||
.map(|this| {
|
||||
if is_zed_provider {
|
||||
this.gap_2().child(
|
||||
if is_zed_provider && is_signed_in {
|
||||
this.child(
|
||||
self.render_zed_plan_info(current_plan, cx),
|
||||
)
|
||||
} else {
|
||||
@@ -303,21 +317,78 @@ impl AgentConfiguration {
|
||||
let providers = LanguageModelRegistry::read_global(cx).providers();
|
||||
|
||||
v_flex()
|
||||
.w_full()
|
||||
.child(
|
||||
v_flex()
|
||||
h_flex()
|
||||
.p(DynamicSpacing::Base16.rems(cx))
|
||||
.pr(DynamicSpacing::Base20.rems(cx))
|
||||
.pb_0()
|
||||
.mb_2p5()
|
||||
.gap_0p5()
|
||||
.child(Headline::new("LLM Providers"))
|
||||
.items_start()
|
||||
.justify_between()
|
||||
.child(
|
||||
Label::new("Add at least one provider to use AI-powered features.")
|
||||
.color(Color::Muted),
|
||||
v_flex()
|
||||
.w_full()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(Headline::new("LLM Providers"))
|
||||
.child(
|
||||
PopoverMenu::new("add-provider-popover")
|
||||
.trigger(
|
||||
Button::new("add-provider", "Add Provider")
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon(IconName::Plus)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.label_size(LabelSize::Small),
|
||||
)
|
||||
.anchor(gpui::Corner::TopRight)
|
||||
.menu({
|
||||
let workspace = self.workspace.clone();
|
||||
move |window, cx| {
|
||||
Some(ContextMenu::build(
|
||||
window,
|
||||
cx,
|
||||
|menu, _window, _cx| {
|
||||
menu.header("Compatible APIs").entry(
|
||||
"OpenAI",
|
||||
None,
|
||||
{
|
||||
let workspace =
|
||||
workspace.clone();
|
||||
move |window, cx| {
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
AddLlmProviderModal::toggle(
|
||||
LlmCompatibleProvider::OpenAi,
|
||||
workspace,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
))
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Label::new("Add at least one provider to use AI-powered features.")
|
||||
.color(Color::Muted),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.pl(DynamicSpacing::Base08.rems(cx))
|
||||
.pr(DynamicSpacing::Base20.rems(cx))
|
||||
.children(
|
||||
@@ -330,119 +401,80 @@ impl AgentConfiguration {
|
||||
|
||||
fn render_command_permission(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let always_allow_tool_actions = AgentSettings::get_global(cx).always_allow_tool_actions;
|
||||
let fs = self.fs.clone();
|
||||
|
||||
h_flex()
|
||||
.gap_4()
|
||||
.justify_between()
|
||||
.flex_wrap()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.max_w_5_6()
|
||||
.child(Label::new("Allow running editing tools without asking for confirmation"))
|
||||
.child(
|
||||
Label::new(
|
||||
"The agent can perform potentially destructive actions without asking for your confirmation.",
|
||||
)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Switch::new(
|
||||
"always-allow-tool-actions-switch",
|
||||
always_allow_tool_actions.into(),
|
||||
)
|
||||
.color(SwitchColor::Accent)
|
||||
.on_click({
|
||||
let fs = self.fs.clone();
|
||||
move |state, _window, cx| {
|
||||
let allow = state == &ToggleState::Selected;
|
||||
update_settings_file::<AgentSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _| {
|
||||
settings.set_always_allow_tool_actions(allow);
|
||||
},
|
||||
);
|
||||
}
|
||||
}),
|
||||
)
|
||||
SwitchField::new(
|
||||
"always-allow-tool-actions-switch",
|
||||
"Allow running commands without asking for confirmation",
|
||||
Some(
|
||||
"The agent can perform potentially destructive actions without asking for your confirmation.".into(),
|
||||
),
|
||||
always_allow_tool_actions,
|
||||
move |state, _window, cx| {
|
||||
let allow = state == &ToggleState::Selected;
|
||||
update_settings_file::<AgentSettings>(fs.clone(), cx, move |settings, _| {
|
||||
settings.set_always_allow_tool_actions(allow);
|
||||
});
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn render_single_file_review(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let single_file_review = AgentSettings::get_global(cx).single_file_review;
|
||||
let fs = self.fs.clone();
|
||||
|
||||
h_flex()
|
||||
.gap_4()
|
||||
.justify_between()
|
||||
.flex_wrap()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.max_w_5_6()
|
||||
.child(Label::new("Enable single-file agent reviews"))
|
||||
.child(
|
||||
Label::new(
|
||||
"Agent edits are also displayed in single-file editors for review.",
|
||||
)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Switch::new("single-file-review-switch", single_file_review.into())
|
||||
.color(SwitchColor::Accent)
|
||||
.on_click({
|
||||
let fs = self.fs.clone();
|
||||
move |state, _window, cx| {
|
||||
let allow = state == &ToggleState::Selected;
|
||||
update_settings_file::<AgentSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _| {
|
||||
settings.set_single_file_review(allow);
|
||||
},
|
||||
);
|
||||
}
|
||||
}),
|
||||
)
|
||||
SwitchField::new(
|
||||
"single-file-review",
|
||||
"Enable single-file agent reviews",
|
||||
Some("Agent edits are also displayed in single-file editors for review.".into()),
|
||||
single_file_review,
|
||||
move |state, _window, cx| {
|
||||
let allow = state == &ToggleState::Selected;
|
||||
update_settings_file::<AgentSettings>(fs.clone(), cx, move |settings, _| {
|
||||
settings.set_single_file_review(allow);
|
||||
});
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn render_sound_notification(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let play_sound_when_agent_done = AgentSettings::get_global(cx).play_sound_when_agent_done;
|
||||
let fs = self.fs.clone();
|
||||
|
||||
h_flex()
|
||||
.gap_4()
|
||||
.justify_between()
|
||||
.flex_wrap()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.max_w_5_6()
|
||||
.child(Label::new("Play sound when finished generating"))
|
||||
.child(
|
||||
Label::new(
|
||||
"Hear a notification sound when the agent is done generating changes or needs your input.",
|
||||
)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Switch::new("play-sound-notification-switch", play_sound_when_agent_done.into())
|
||||
.color(SwitchColor::Accent)
|
||||
.on_click({
|
||||
let fs = self.fs.clone();
|
||||
move |state, _window, cx| {
|
||||
let allow = state == &ToggleState::Selected;
|
||||
update_settings_file::<AgentSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _| {
|
||||
settings.set_play_sound_when_agent_done(allow);
|
||||
},
|
||||
);
|
||||
}
|
||||
}),
|
||||
)
|
||||
SwitchField::new(
|
||||
"sound-notification",
|
||||
"Play sound when finished generating",
|
||||
Some(
|
||||
"Hear a notification sound when the agent is done generating changes or needs your input.".into(),
|
||||
),
|
||||
play_sound_when_agent_done,
|
||||
move |state, _window, cx| {
|
||||
let allow = state == &ToggleState::Selected;
|
||||
update_settings_file::<AgentSettings>(fs.clone(), cx, move |settings, _| {
|
||||
settings.set_play_sound_when_agent_done(allow);
|
||||
});
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn render_modifier_to_send(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let use_modifier_to_send = AgentSettings::get_global(cx).use_modifier_to_send;
|
||||
let fs = self.fs.clone();
|
||||
|
||||
SwitchField::new(
|
||||
"modifier-send",
|
||||
"Use modifier to submit a message",
|
||||
Some(
|
||||
"Make a modifier (cmd-enter on macOS, ctrl-enter on Linux) required to send messages.".into(),
|
||||
),
|
||||
use_modifier_to_send,
|
||||
move |state, _window, cx| {
|
||||
let allow = state == &ToggleState::Selected;
|
||||
update_settings_file::<AgentSettings>(fs.clone(), cx, move |settings, _| {
|
||||
settings.set_use_modifier_to_send(allow);
|
||||
});
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn render_general_settings_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
@@ -456,6 +488,7 @@ impl AgentConfiguration {
|
||||
.child(self.render_command_permission(cx))
|
||||
.child(self.render_single_file_review(cx))
|
||||
.child(self.render_sound_notification(cx))
|
||||
.child(self.render_modifier_to_send(cx))
|
||||
}
|
||||
|
||||
fn render_zed_plan_info(&self, plan: Option<Plan>, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
@@ -475,7 +508,7 @@ impl AgentConfiguration {
|
||||
.blend(cx.theme().colors().text_accent.opacity(0.2));
|
||||
|
||||
let (plan_name, label_color, bg_color) = match plan {
|
||||
Plan::Free => ("Free", Color::Default, free_chip_bg),
|
||||
Plan::ZedFree => ("Free", Color::Default, free_chip_bg),
|
||||
Plan::ZedProTrial => ("Pro Trial", Color::Accent, pro_chip_bg),
|
||||
Plan::ZedPro => ("Pro", Color::Accent, pro_chip_bg),
|
||||
};
|
||||
|
||||
@@ -0,0 +1,639 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use collections::HashSet;
|
||||
use fs::Fs;
|
||||
use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, Task};
|
||||
use language_model::LanguageModelRegistry;
|
||||
use language_models::{
|
||||
AllLanguageModelSettings, OpenAiCompatibleSettingsContent,
|
||||
provider::open_ai_compatible::AvailableModel,
|
||||
};
|
||||
use settings::update_settings_file;
|
||||
use ui::{Banner, KeyBinding, Modal, ModalFooter, ModalHeader, Section, prelude::*};
|
||||
use ui_input::SingleLineInput;
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum LlmCompatibleProvider {
|
||||
OpenAi,
|
||||
}
|
||||
|
||||
impl LlmCompatibleProvider {
|
||||
fn name(&self) -> &'static str {
|
||||
match self {
|
||||
LlmCompatibleProvider::OpenAi => "OpenAI",
|
||||
}
|
||||
}
|
||||
|
||||
fn api_url(&self) -> &'static str {
|
||||
match self {
|
||||
LlmCompatibleProvider::OpenAi => "https://api.openai.com/v1",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AddLlmProviderInput {
|
||||
provider_name: Entity<SingleLineInput>,
|
||||
api_url: Entity<SingleLineInput>,
|
||||
api_key: Entity<SingleLineInput>,
|
||||
models: Vec<ModelInput>,
|
||||
}
|
||||
|
||||
impl AddLlmProviderInput {
|
||||
fn new(provider: LlmCompatibleProvider, window: &mut Window, cx: &mut App) -> Self {
|
||||
let provider_name = single_line_input("Provider Name", provider.name(), None, window, cx);
|
||||
let api_url = single_line_input("API URL", provider.api_url(), None, window, cx);
|
||||
let api_key = single_line_input(
|
||||
"API Key",
|
||||
"000000000000000000000000000000000000000000000000",
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
Self {
|
||||
provider_name,
|
||||
api_url,
|
||||
api_key,
|
||||
models: vec![ModelInput::new(window, cx)],
|
||||
}
|
||||
}
|
||||
|
||||
fn add_model(&mut self, window: &mut Window, cx: &mut App) {
|
||||
self.models.push(ModelInput::new(window, cx));
|
||||
}
|
||||
|
||||
fn remove_model(&mut self, index: usize) {
|
||||
self.models.remove(index);
|
||||
}
|
||||
}
|
||||
|
||||
struct ModelInput {
|
||||
name: Entity<SingleLineInput>,
|
||||
max_completion_tokens: Entity<SingleLineInput>,
|
||||
max_output_tokens: Entity<SingleLineInput>,
|
||||
max_tokens: Entity<SingleLineInput>,
|
||||
}
|
||||
|
||||
impl ModelInput {
|
||||
fn new(window: &mut Window, cx: &mut App) -> Self {
|
||||
let model_name = single_line_input(
|
||||
"Model Name",
|
||||
"e.g. gpt-4o, claude-opus-4, gemini-2.5-pro",
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let max_completion_tokens = single_line_input(
|
||||
"Max Completion Tokens",
|
||||
"200000",
|
||||
Some("200000"),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let max_output_tokens = single_line_input(
|
||||
"Max Output Tokens",
|
||||
"Max Output Tokens",
|
||||
Some("32000"),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let max_tokens = single_line_input("Max Tokens", "Max Tokens", Some("200000"), window, cx);
|
||||
Self {
|
||||
name: model_name,
|
||||
max_completion_tokens,
|
||||
max_output_tokens,
|
||||
max_tokens,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(&self, cx: &App) -> Result<AvailableModel, SharedString> {
|
||||
let name = self.name.read(cx).text(cx);
|
||||
if name.is_empty() {
|
||||
return Err(SharedString::from("Model Name cannot be empty"));
|
||||
}
|
||||
Ok(AvailableModel {
|
||||
name,
|
||||
display_name: None,
|
||||
max_completion_tokens: Some(
|
||||
self.max_completion_tokens
|
||||
.read(cx)
|
||||
.text(cx)
|
||||
.parse::<u64>()
|
||||
.map_err(|_| SharedString::from("Max Completion Tokens must be a number"))?,
|
||||
),
|
||||
max_output_tokens: Some(
|
||||
self.max_output_tokens
|
||||
.read(cx)
|
||||
.text(cx)
|
||||
.parse::<u64>()
|
||||
.map_err(|_| SharedString::from("Max Output Tokens must be a number"))?,
|
||||
),
|
||||
max_tokens: self
|
||||
.max_tokens
|
||||
.read(cx)
|
||||
.text(cx)
|
||||
.parse::<u64>()
|
||||
.map_err(|_| SharedString::from("Max Tokens must be a number"))?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn single_line_input(
|
||||
label: impl Into<SharedString>,
|
||||
placeholder: impl Into<SharedString>,
|
||||
text: Option<&str>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<SingleLineInput> {
|
||||
cx.new(|cx| {
|
||||
let input = SingleLineInput::new(window, cx, placeholder).label(label);
|
||||
if let Some(text) = text {
|
||||
input
|
||||
.editor()
|
||||
.update(cx, |editor, cx| editor.set_text(text, window, cx));
|
||||
}
|
||||
input
|
||||
})
|
||||
}
|
||||
|
||||
fn save_provider_to_settings(
|
||||
input: &AddLlmProviderInput,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<(), SharedString>> {
|
||||
let provider_name: Arc<str> = input.provider_name.read(cx).text(cx).into();
|
||||
if provider_name.is_empty() {
|
||||
return Task::ready(Err("Provider Name cannot be empty".into()));
|
||||
}
|
||||
|
||||
if LanguageModelRegistry::read_global(cx)
|
||||
.providers()
|
||||
.iter()
|
||||
.any(|provider| {
|
||||
provider.id().0.as_ref() == provider_name.as_ref()
|
||||
|| provider.name().0.as_ref() == provider_name.as_ref()
|
||||
})
|
||||
{
|
||||
return Task::ready(Err(
|
||||
"Provider Name is already taken by another provider".into()
|
||||
));
|
||||
}
|
||||
|
||||
let api_url = input.api_url.read(cx).text(cx);
|
||||
if api_url.is_empty() {
|
||||
return Task::ready(Err("API URL cannot be empty".into()));
|
||||
}
|
||||
|
||||
let api_key = input.api_key.read(cx).text(cx);
|
||||
if api_key.is_empty() {
|
||||
return Task::ready(Err("API Key cannot be empty".into()));
|
||||
}
|
||||
|
||||
let mut models = Vec::new();
|
||||
let mut model_names: HashSet<String> = HashSet::default();
|
||||
for model in &input.models {
|
||||
match model.parse(cx) {
|
||||
Ok(model) => {
|
||||
if !model_names.insert(model.name.clone()) {
|
||||
return Task::ready(Err("Model Names must be unique".into()));
|
||||
}
|
||||
models.push(model)
|
||||
}
|
||||
Err(err) => return Task::ready(Err(err)),
|
||||
}
|
||||
}
|
||||
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
let task = cx.write_credentials(&api_url, "Bearer", api_key.as_bytes());
|
||||
cx.spawn(async move |cx| {
|
||||
task.await
|
||||
.map_err(|_| "Failed to write API key to keychain")?;
|
||||
cx.update(|cx| {
|
||||
update_settings_file::<AllLanguageModelSettings>(fs, cx, |settings, _cx| {
|
||||
settings.openai_compatible.get_or_insert_default().insert(
|
||||
provider_name,
|
||||
OpenAiCompatibleSettingsContent {
|
||||
api_url,
|
||||
available_models: models,
|
||||
},
|
||||
);
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub struct AddLlmProviderModal {
|
||||
provider: LlmCompatibleProvider,
|
||||
input: AddLlmProviderInput,
|
||||
focus_handle: FocusHandle,
|
||||
last_error: Option<SharedString>,
|
||||
}
|
||||
|
||||
impl AddLlmProviderModal {
|
||||
pub fn toggle(
|
||||
provider: LlmCompatibleProvider,
|
||||
workspace: &mut Workspace,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
workspace.toggle_modal(window, cx, |window, cx| Self::new(provider, window, cx));
|
||||
}
|
||||
|
||||
fn new(provider: LlmCompatibleProvider, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
Self {
|
||||
input: AddLlmProviderInput::new(provider, window, cx),
|
||||
provider,
|
||||
last_error: None,
|
||||
focus_handle: cx.focus_handle(),
|
||||
}
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &menu::Confirm, _: &mut Window, cx: &mut Context<Self>) {
|
||||
let task = save_provider_to_settings(&self.input, cx);
|
||||
cx.spawn(async move |this, cx| {
|
||||
let result = task.await;
|
||||
this.update(cx, |this, cx| match result {
|
||||
Ok(_) => {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
Err(error) => {
|
||||
this.last_error = Some(error);
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
fn render_section(&self) -> Section {
|
||||
Section::new()
|
||||
.child(self.input.provider_name.clone())
|
||||
.child(self.input.api_url.clone())
|
||||
.child(self.input.api_key.clone())
|
||||
}
|
||||
|
||||
fn render_model_section(&self, cx: &mut Context<Self>) -> Section {
|
||||
Section::new().child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(Label::new("Models").size(LabelSize::Small))
|
||||
.child(
|
||||
Button::new("add-model", "Add Model")
|
||||
.icon(IconName::Plus)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.input.add_model(window, cx);
|
||||
cx.notify();
|
||||
})),
|
||||
),
|
||||
)
|
||||
.children(
|
||||
self.input
|
||||
.models
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, _)| self.render_model(ix, cx)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_model(&self, ix: usize, cx: &mut Context<Self>) -> impl IntoElement + use<> {
|
||||
let has_more_than_one_model = self.input.models.len() > 1;
|
||||
let model = &self.input.models[ix];
|
||||
|
||||
v_flex()
|
||||
.p_2()
|
||||
.gap_2()
|
||||
.rounded_sm()
|
||||
.border_1()
|
||||
.border_dashed()
|
||||
.border_color(cx.theme().colors().border.opacity(0.6))
|
||||
.bg(cx.theme().colors().element_active.opacity(0.15))
|
||||
.child(model.name.clone())
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(model.max_completion_tokens.clone())
|
||||
.child(model.max_output_tokens.clone()),
|
||||
)
|
||||
.child(model.max_tokens.clone())
|
||||
.when(has_more_than_one_model, |this| {
|
||||
this.child(
|
||||
Button::new(("remove-model", ix), "Remove Model")
|
||||
.icon(IconName::Trash)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.label_size(LabelSize::Small)
|
||||
.style(ButtonStyle::Outlined)
|
||||
.full_width()
|
||||
.on_click(cx.listener(move |this, _, _window, cx| {
|
||||
this.input.remove_model(ix);
|
||||
cx.notify();
|
||||
})),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for AddLlmProviderModal {}
|
||||
|
||||
impl Focusable for AddLlmProviderModal {
|
||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl ModalView for AddLlmProviderModal {}
|
||||
|
||||
impl Render for AddLlmProviderModal {
|
||||
fn render(&mut self, window: &mut ui::Window, cx: &mut ui::Context<Self>) -> impl IntoElement {
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
|
||||
div()
|
||||
.id("add-llm-provider-modal")
|
||||
.key_context("AddLlmProviderModal")
|
||||
.w(rems(34.))
|
||||
.elevation_3(cx)
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.capture_any_mouse_down(cx.listener(|this, _, window, cx| {
|
||||
this.focus_handle(cx).focus(window);
|
||||
}))
|
||||
.child(
|
||||
Modal::new("configure-context-server", None)
|
||||
.header(ModalHeader::new().headline("Add LLM Provider").description(
|
||||
match self.provider {
|
||||
LlmCompatibleProvider::OpenAi => {
|
||||
"This provider will use an OpenAI compatible API."
|
||||
}
|
||||
},
|
||||
))
|
||||
.when_some(self.last_error.clone(), |this, error| {
|
||||
this.section(
|
||||
Section::new().child(
|
||||
Banner::new()
|
||||
.severity(ui::Severity::Warning)
|
||||
.child(div().text_xs().child(error)),
|
||||
),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
v_flex()
|
||||
.id("modal_content")
|
||||
.max_h_128()
|
||||
.overflow_y_scroll()
|
||||
.gap_2()
|
||||
.child(self.render_section())
|
||||
.child(self.render_model_section(cx)),
|
||||
)
|
||||
.footer(
|
||||
ModalFooter::new().end_slot(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Button::new("cancel", "Cancel")
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(
|
||||
&menu::Cancel,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(12.))),
|
||||
)
|
||||
.on_click(cx.listener(|this, _event, window, cx| {
|
||||
this.cancel(&menu::Cancel, window, cx)
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("save-server", "Save Provider")
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(
|
||||
&menu::Confirm,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(12.))),
|
||||
)
|
||||
.on_click(cx.listener(|this, _event, window, cx| {
|
||||
this.confirm(&menu::Confirm, window, cx)
|
||||
})),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use editor::EditorSettings;
|
||||
use fs::FakeFs;
|
||||
use gpui::{TestAppContext, VisualTestContext};
|
||||
use language::language_settings;
|
||||
use language_model::{
|
||||
LanguageModelProviderId, LanguageModelProviderName,
|
||||
fake_provider::FakeLanguageModelProvider,
|
||||
};
|
||||
use project::Project;
|
||||
use settings::{Settings as _, SettingsStore};
|
||||
use util::path;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_save_provider_invalid_inputs(cx: &mut TestAppContext) {
|
||||
let cx = setup_test(cx).await;
|
||||
|
||||
assert_eq!(
|
||||
save_provider_validation_errors("", "someurl", "somekey", vec![], cx,).await,
|
||||
Some("Provider Name cannot be empty".into())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
save_provider_validation_errors("someprovider", "", "somekey", vec![], cx,).await,
|
||||
Some("API URL cannot be empty".into())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
save_provider_validation_errors("someprovider", "someurl", "", vec![], cx,).await,
|
||||
Some("API Key cannot be empty".into())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
save_provider_validation_errors(
|
||||
"someprovider",
|
||||
"someurl",
|
||||
"somekey",
|
||||
vec![("", "200000", "200000", "32000")],
|
||||
cx,
|
||||
)
|
||||
.await,
|
||||
Some("Model Name cannot be empty".into())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
save_provider_validation_errors(
|
||||
"someprovider",
|
||||
"someurl",
|
||||
"somekey",
|
||||
vec![("somemodel", "abc", "200000", "32000")],
|
||||
cx,
|
||||
)
|
||||
.await,
|
||||
Some("Max Tokens must be a number".into())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
save_provider_validation_errors(
|
||||
"someprovider",
|
||||
"someurl",
|
||||
"somekey",
|
||||
vec![("somemodel", "200000", "abc", "32000")],
|
||||
cx,
|
||||
)
|
||||
.await,
|
||||
Some("Max Completion Tokens must be a number".into())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
save_provider_validation_errors(
|
||||
"someprovider",
|
||||
"someurl",
|
||||
"somekey",
|
||||
vec![("somemodel", "200000", "200000", "abc")],
|
||||
cx,
|
||||
)
|
||||
.await,
|
||||
Some("Max Output Tokens must be a number".into())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
save_provider_validation_errors(
|
||||
"someprovider",
|
||||
"someurl",
|
||||
"somekey",
|
||||
vec![
|
||||
("somemodel", "200000", "200000", "32000"),
|
||||
("somemodel", "200000", "200000", "32000"),
|
||||
],
|
||||
cx,
|
||||
)
|
||||
.await,
|
||||
Some("Model Names must be unique".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_save_provider_name_conflict(cx: &mut TestAppContext) {
|
||||
let cx = setup_test(cx).await;
|
||||
|
||||
cx.update(|_window, cx| {
|
||||
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
|
||||
registry.register_provider(
|
||||
FakeLanguageModelProvider::new(
|
||||
LanguageModelProviderId::new("someprovider"),
|
||||
LanguageModelProviderName::new("Some Provider"),
|
||||
),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
save_provider_validation_errors(
|
||||
"someprovider",
|
||||
"someurl",
|
||||
"someapikey",
|
||||
vec![("somemodel", "200000", "200000", "32000")],
|
||||
cx,
|
||||
)
|
||||
.await,
|
||||
Some("Provider Name is already taken by another provider".into())
|
||||
);
|
||||
}
|
||||
|
||||
async fn setup_test(cx: &mut TestAppContext) -> &mut VisualTestContext {
|
||||
cx.update(|cx| {
|
||||
let store = SettingsStore::test(cx);
|
||||
cx.set_global(store);
|
||||
workspace::init_settings(cx);
|
||||
Project::init_settings(cx);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
language_settings::init(cx);
|
||||
EditorSettings::register(cx);
|
||||
language_model::init_settings(cx);
|
||||
language_models::init_settings(cx);
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
cx.update(|cx| <dyn Fs>::set_global(fs.clone(), cx));
|
||||
let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
|
||||
let (_, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
|
||||
cx
|
||||
}
|
||||
|
||||
async fn save_provider_validation_errors(
|
||||
provider_name: &str,
|
||||
api_url: &str,
|
||||
api_key: &str,
|
||||
models: Vec<(&str, &str, &str, &str)>,
|
||||
cx: &mut VisualTestContext,
|
||||
) -> Option<SharedString> {
|
||||
fn set_text(
|
||||
input: &Entity<SingleLineInput>,
|
||||
text: &str,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
input.update(cx, |input, cx| {
|
||||
input.editor().update(cx, |editor, cx| {
|
||||
editor.set_text(text, window, cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let task = cx.update(|window, cx| {
|
||||
let mut input = AddLlmProviderInput::new(LlmCompatibleProvider::OpenAi, window, cx);
|
||||
set_text(&input.provider_name, provider_name, window, cx);
|
||||
set_text(&input.api_url, api_url, window, cx);
|
||||
set_text(&input.api_key, api_key, window, cx);
|
||||
|
||||
for (i, (name, max_tokens, max_completion_tokens, max_output_tokens)) in
|
||||
models.iter().enumerate()
|
||||
{
|
||||
if i >= input.models.len() {
|
||||
input.models.push(ModelInput::new(window, cx));
|
||||
}
|
||||
let model = &mut input.models[i];
|
||||
set_text(&model.name, name, window, cx);
|
||||
set_text(&model.max_tokens, max_tokens, window, cx);
|
||||
set_text(
|
||||
&model.max_completion_tokens,
|
||||
max_completion_tokens,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
set_text(&model.max_output_tokens, max_output_tokens, window, cx);
|
||||
}
|
||||
save_provider_to_settings(&input, cx)
|
||||
});
|
||||
|
||||
task.await.err()
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{Arc, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
@@ -188,7 +189,7 @@ fn context_server_input(existing: Option<(ContextServerId, ContextServerCommand)
|
||||
}
|
||||
None => (
|
||||
"some-mcp-server".to_string(),
|
||||
"".to_string(),
|
||||
PathBuf::new(),
|
||||
"[]".to_string(),
|
||||
"{}".to_string(),
|
||||
),
|
||||
@@ -199,13 +200,14 @@ fn context_server_input(existing: Option<(ContextServerId, ContextServerCommand)
|
||||
/// The name of your MCP server
|
||||
"{name}": {{
|
||||
/// The command which runs the MCP server
|
||||
"command": "{command}",
|
||||
"command": "{}",
|
||||
/// The arguments to pass to the MCP server
|
||||
"args": {args},
|
||||
/// The environment variables to set
|
||||
"env": {env}
|
||||
}}
|
||||
}}"#
|
||||
}}"#,
|
||||
command.display()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{Keep, KeepAll, OpenAgentDiff, Reject, RejectAll};
|
||||
use acp::{AcpThread, AcpThreadEvent};
|
||||
use acp_thread::{AcpThread, AcpThreadEvent};
|
||||
use agent::{Thread, ThreadEvent, ThreadSummary};
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::Result;
|
||||
@@ -30,7 +30,7 @@ use std::{
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use ui::{IconButtonShape, KeyBinding, Tooltip, prelude::*, vertical_divider};
|
||||
use ui::{ButtonSize, IconButtonShape, KeyBinding, Tooltip, prelude::*, vertical_divider};
|
||||
use util::ResultExt;
|
||||
use workspace::{
|
||||
Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
|
||||
@@ -81,7 +81,7 @@ impl AgentDiffThread {
|
||||
match self {
|
||||
AgentDiffThread::Native(thread) => thread.read(cx).is_generating(),
|
||||
AgentDiffThread::AcpThread(thread) => {
|
||||
thread.read(cx).status() == acp::ThreadStatus::Generating
|
||||
thread.read(cx).status() == acp_thread::ThreadStatus::Generating
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -791,22 +791,32 @@ fn render_diff_hunk_controls(
|
||||
) -> AnyElement {
|
||||
let editor = editor.clone();
|
||||
|
||||
h_flex()
|
||||
// Get controls positioning from editor state
|
||||
let controls_above = editor.read(cx).diff_hunk_controls_above();
|
||||
|
||||
let mut container = h_flex()
|
||||
.h(line_height)
|
||||
.mr_0p5()
|
||||
.gap_1()
|
||||
.px_0p5()
|
||||
.pb_1()
|
||||
.py_0p5()
|
||||
.border_x_1()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_b_md()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.gap_1()
|
||||
.block_mouse_except_scroll()
|
||||
.shadow_md()
|
||||
.shadow_md();
|
||||
|
||||
if controls_above {
|
||||
container = container.border_t_1().rounded_t_md();
|
||||
} else {
|
||||
container = container.border_b_1().rounded_b_md();
|
||||
}
|
||||
|
||||
container
|
||||
.children(vec![
|
||||
Button::new(("reject", row as u64), "Reject")
|
||||
.size(ButtonSize::Compact)
|
||||
.disabled(is_created_file)
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(
|
||||
@@ -835,6 +845,7 @@ fn render_diff_hunk_controls(
|
||||
}
|
||||
}),
|
||||
Button::new(("keep", row as u64), "Keep")
|
||||
.size(ButtonSize::Compact)
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(&Keep, &editor.read(cx).focus_handle(cx), window, cx)
|
||||
.map(|kb| kb.size(rems_from_px(12.))),
|
||||
@@ -1506,8 +1517,7 @@ impl AgentDiff {
|
||||
.read(cx)
|
||||
.entries()
|
||||
.last()
|
||||
.and_then(|entry| entry.diff())
|
||||
.is_some()
|
||||
.map_or(false, |entry| entry.diffs().next().is_some())
|
||||
{
|
||||
self.update_reviewing_editors(workspace, window, cx);
|
||||
}
|
||||
@@ -1517,12 +1527,14 @@ impl AgentDiff {
|
||||
.read(cx)
|
||||
.entries()
|
||||
.get(*ix)
|
||||
.and_then(|entry| entry.diff())
|
||||
.is_some()
|
||||
.map_or(false, |entry| entry.diffs().next().is_some())
|
||||
{
|
||||
self.update_reviewing_editors(workspace, window, cx);
|
||||
}
|
||||
}
|
||||
AcpThreadEvent::Stopped
|
||||
| AcpThreadEvent::ToolAuthorizationRequired
|
||||
| AcpThreadEvent::Error => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use crate::{
|
||||
ModelUsageContext,
|
||||
language_model_selector::{
|
||||
LanguageModelSelector, ToggleModelSelector, language_model_selector,
|
||||
},
|
||||
language_model_selector::{LanguageModelSelector, language_model_selector},
|
||||
};
|
||||
use agent_settings::AgentSettings;
|
||||
use fs::Fs;
|
||||
@@ -12,6 +10,7 @@ use picker::popover_menu::PickerPopoverMenu;
|
||||
use settings::update_settings_file;
|
||||
use std::sync::Arc;
|
||||
use ui::{ButtonLike, PopoverMenuHandle, Tooltip, prelude::*};
|
||||
use zed_actions::agent::ToggleModelSelector;
|
||||
|
||||
pub struct AgentModelSelector {
|
||||
selector: Entity<LanguageModelSelector>,
|
||||
@@ -96,22 +95,18 @@ impl Render for AgentModelSelector {
|
||||
let model_name = model
|
||||
.as_ref()
|
||||
.map(|model| model.model.name().0)
|
||||
.unwrap_or_else(|| SharedString::from("No model selected"));
|
||||
let provider_icon = model
|
||||
.as_ref()
|
||||
.map(|model| model.provider.icon())
|
||||
.unwrap_or_else(|| IconName::Ai);
|
||||
.unwrap_or_else(|| SharedString::from("Select a Model"));
|
||||
|
||||
let provider_icon = model.as_ref().map(|model| model.provider.icon());
|
||||
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
PickerPopoverMenu::new(
|
||||
self.selector.clone(),
|
||||
ButtonLike::new("active-model")
|
||||
.child(
|
||||
Icon::new(provider_icon)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
)
|
||||
.when_some(provider_icon, |this, icon| {
|
||||
this.child(Icon::new(icon).color(Color::Muted).size(IconSize::XSmall))
|
||||
})
|
||||
.child(
|
||||
Label::new(model_name)
|
||||
.color(Color::Muted)
|
||||
|
||||
@@ -25,12 +25,14 @@ mod thread_history;
|
||||
mod tool_compatibility;
|
||||
mod ui;
|
||||
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use agent::{Thread, ThreadId};
|
||||
use agent_settings::{AgentProfileId, AgentSettings, LanguageModelSelection};
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use client::Client;
|
||||
use client::{Client, DisableAiSettings};
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use feature_flags::FeatureFlagAppExt as _;
|
||||
use fs::Fs;
|
||||
use gpui::{Action, App, Entity, actions};
|
||||
@@ -40,8 +42,9 @@ use language_model::{
|
||||
};
|
||||
use prompt_store::PromptBuilder;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings as _, SettingsStore};
|
||||
use std::any::TypeId;
|
||||
|
||||
pub use crate::active_thread::ActiveThread;
|
||||
use crate::agent_configuration::{ConfigureContextServerModal, ManageProfilesModal};
|
||||
@@ -51,14 +54,13 @@ use crate::slash_command_settings::SlashCommandSettings;
|
||||
pub use agent_diff::{AgentDiffPane, AgentDiffToolbar};
|
||||
pub use text_thread_editor::{AgentPanelDelegate, TextThreadEditor};
|
||||
pub use ui::preview::{all_agent_previews, get_agent_preview};
|
||||
use zed_actions;
|
||||
|
||||
actions!(
|
||||
agent,
|
||||
[
|
||||
/// Creates a new text-based conversation thread.
|
||||
NewTextThread,
|
||||
/// Creates a new external agent conversation thread.
|
||||
NewAcpThread,
|
||||
/// Toggles the context picker interface for adding files, symbols, or other context.
|
||||
ToggleContextPicker,
|
||||
/// Toggles the navigation menu for switching between threads and views.
|
||||
@@ -133,6 +135,34 @@ pub struct NewThread {
|
||||
from_thread_id: Option<ThreadId>,
|
||||
}
|
||||
|
||||
/// Creates a new external agent conversation thread.
|
||||
#[derive(Default, Clone, PartialEq, Deserialize, JsonSchema, Action)]
|
||||
#[action(namespace = agent)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct NewExternalAgentThread {
|
||||
/// Which agent to use for the conversation.
|
||||
agent: Option<ExternalAgent>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum ExternalAgent {
|
||||
#[default]
|
||||
Gemini,
|
||||
ClaudeCode,
|
||||
Codex,
|
||||
}
|
||||
|
||||
impl ExternalAgent {
|
||||
pub fn server(&self) -> Rc<dyn agent_servers::AgentServer> {
|
||||
match self {
|
||||
ExternalAgent::Gemini => Rc::new(agent_servers::Gemini),
|
||||
ExternalAgent::ClaudeCode => Rc::new(agent_servers::ClaudeCode),
|
||||
ExternalAgent::Codex => Rc::new(agent_servers::Codex),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens the profile management interface for configuring agent tools and settings.
|
||||
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
|
||||
#[action(namespace = agent)]
|
||||
@@ -216,6 +246,69 @@ pub fn init(
|
||||
})
|
||||
.detach();
|
||||
cx.observe_new(ManageProfilesModal::register).detach();
|
||||
|
||||
// Update command palette filter based on AI settings
|
||||
update_command_palette_filter(cx);
|
||||
|
||||
// Watch for settings changes
|
||||
cx.observe_global::<SettingsStore>(|app_cx| {
|
||||
// When settings change, update the command palette filter
|
||||
update_command_palette_filter(app_cx);
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn update_command_palette_filter(cx: &mut App) {
|
||||
let disable_ai = DisableAiSettings::get_global(cx).disable_ai;
|
||||
CommandPaletteFilter::update_global(cx, |filter, _| {
|
||||
if disable_ai {
|
||||
filter.hide_namespace("agent");
|
||||
filter.hide_namespace("assistant");
|
||||
filter.hide_namespace("copilot");
|
||||
filter.hide_namespace("supermaven");
|
||||
filter.hide_namespace("zed_predict_onboarding");
|
||||
filter.hide_namespace("edit_prediction");
|
||||
|
||||
use editor::actions::{
|
||||
AcceptEditPrediction, AcceptPartialEditPrediction, NextEditPrediction,
|
||||
PreviousEditPrediction, ShowEditPrediction, ToggleEditPrediction,
|
||||
};
|
||||
let edit_prediction_actions = [
|
||||
TypeId::of::<AcceptEditPrediction>(),
|
||||
TypeId::of::<AcceptPartialEditPrediction>(),
|
||||
TypeId::of::<ShowEditPrediction>(),
|
||||
TypeId::of::<NextEditPrediction>(),
|
||||
TypeId::of::<PreviousEditPrediction>(),
|
||||
TypeId::of::<ToggleEditPrediction>(),
|
||||
];
|
||||
filter.hide_action_types(&edit_prediction_actions);
|
||||
filter.hide_action_types(&[TypeId::of::<zed_actions::OpenZedPredictOnboarding>()]);
|
||||
} else {
|
||||
filter.show_namespace("agent");
|
||||
filter.show_namespace("assistant");
|
||||
filter.show_namespace("copilot");
|
||||
filter.show_namespace("zed_predict_onboarding");
|
||||
|
||||
filter.show_namespace("edit_prediction");
|
||||
|
||||
use editor::actions::{
|
||||
AcceptEditPrediction, AcceptPartialEditPrediction, NextEditPrediction,
|
||||
PreviousEditPrediction, ShowEditPrediction, ToggleEditPrediction,
|
||||
};
|
||||
let edit_prediction_actions = [
|
||||
TypeId::of::<AcceptEditPrediction>(),
|
||||
TypeId::of::<AcceptPartialEditPrediction>(),
|
||||
TypeId::of::<ShowEditPrediction>(),
|
||||
TypeId::of::<NextEditPrediction>(),
|
||||
TypeId::of::<PreviousEditPrediction>(),
|
||||
TypeId::of::<ToggleEditPrediction>(),
|
||||
];
|
||||
filter.show_action_types(edit_prediction_actions.iter());
|
||||
|
||||
filter
|
||||
.show_action_types([TypeId::of::<zed_actions::OpenZedPredictOnboarding>()].iter());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn init_language_model_settings(cx: &mut App) {
|
||||
|
||||
@@ -6,6 +6,7 @@ use agent::{
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::{Context as _, Result};
|
||||
use client::telemetry::Telemetry;
|
||||
use cloud_llm_client::CompletionIntent;
|
||||
use collections::HashSet;
|
||||
use editor::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint};
|
||||
use futures::{
|
||||
@@ -35,7 +36,6 @@ use std::{
|
||||
};
|
||||
use streaming_diff::{CharOperation, LineDiff, LineOperation, StreamingDiff};
|
||||
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
pub struct BufferCodegen {
|
||||
alternatives: Vec<Entity<CodegenAlternative>>,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#![allow(unused, dead_code)]
|
||||
|
||||
use client::{ModelRequestUsage, RequestUsage};
|
||||
use cloud_llm_client::{Plan, UsageLimit};
|
||||
use gpui::Global;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use ui::prelude::*;
|
||||
use zed_llm_client::{Plan, UsageLimit};
|
||||
|
||||
/// Debug only: Used for testing various account states
|
||||
///
|
||||
|
||||
@@ -16,7 +16,7 @@ use agent::{
|
||||
};
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::{Context as _, Result};
|
||||
use client::telemetry::Telemetry;
|
||||
use client::{DisableAiSettings, telemetry::Telemetry};
|
||||
use collections::{HashMap, HashSet, VecDeque, hash_map};
|
||||
use editor::SelectionEffects;
|
||||
use editor::{
|
||||
@@ -48,7 +48,7 @@ use text::{OffsetRangeExt, ToPoint as _};
|
||||
use ui::prelude::*;
|
||||
use util::{RangeExt, ResultExt, maybe};
|
||||
use workspace::{ItemHandle, Toast, Workspace, dock::Panel, notifications::NotificationId};
|
||||
use zed_actions::agent::OpenConfiguration;
|
||||
use zed_actions::agent::OpenSettings;
|
||||
|
||||
pub fn init(
|
||||
fs: Arc<dyn Fs>,
|
||||
@@ -57,6 +57,17 @@ pub fn init(
|
||||
cx: &mut App,
|
||||
) {
|
||||
cx.set_global(InlineAssistant::new(fs, prompt_builder, telemetry));
|
||||
|
||||
cx.observe_global::<SettingsStore>(|cx| {
|
||||
if DisableAiSettings::get_global(cx).disable_ai {
|
||||
// Hide any active inline assist UI when AI is disabled
|
||||
InlineAssistant::update_global(cx, |assistant, cx| {
|
||||
assistant.cancel_all_active_completions(cx);
|
||||
});
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.observe_new(|_workspace: &mut Workspace, window, cx| {
|
||||
let Some(window) = window else {
|
||||
return;
|
||||
@@ -141,6 +152,26 @@ impl InlineAssistant {
|
||||
.detach();
|
||||
}
|
||||
|
||||
/// Hides all active inline assists when AI is disabled
|
||||
pub fn cancel_all_active_completions(&mut self, cx: &mut App) {
|
||||
// Cancel all active completions in editors
|
||||
for (editor_handle, _) in self.assists_by_editor.iter() {
|
||||
if let Some(editor) = editor_handle.upgrade() {
|
||||
let windows = cx.windows();
|
||||
if !windows.is_empty() {
|
||||
let window = windows[0];
|
||||
let _ = window.update(cx, |_, window, cx| {
|
||||
editor.update(cx, |editor, cx| {
|
||||
if editor.has_active_inline_completion() {
|
||||
editor.cancel(&Default::default(), window, cx);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_workspace_event(
|
||||
&mut self,
|
||||
workspace: Entity<Workspace>,
|
||||
@@ -176,7 +207,7 @@ impl InlineAssistant {
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let is_assistant2_enabled = true;
|
||||
let is_assistant2_enabled = !DisableAiSettings::get_global(cx).disable_ai;
|
||||
|
||||
if let Some(editor) = item.act_as::<Editor>(cx) {
|
||||
editor.update(cx, |editor, cx| {
|
||||
@@ -199,6 +230,13 @@ impl InlineAssistant {
|
||||
cx,
|
||||
);
|
||||
|
||||
if DisableAiSettings::get_global(cx).disable_ai {
|
||||
// Cancel any active completions
|
||||
if editor.has_active_inline_completion() {
|
||||
editor.cancel(&Default::default(), window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the Assistant1 code action provider, as it still might be registered.
|
||||
editor.remove_code_action_provider("assistant".into(), window, cx);
|
||||
} else {
|
||||
@@ -219,7 +257,7 @@ impl InlineAssistant {
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
let settings = AgentSettings::get_global(cx);
|
||||
if !settings.enabled {
|
||||
if !settings.enabled || DisableAiSettings::get_global(cx).disable_ai {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -307,7 +345,7 @@ impl InlineAssistant {
|
||||
if let Some(answer) = answer {
|
||||
if answer == 0 {
|
||||
cx.update(|window, cx| {
|
||||
window.dispatch_action(Box::new(OpenConfiguration), cx)
|
||||
window.dispatch_action(Box::new(OpenSettings), cx)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ use crate::agent_model_selector::AgentModelSelector;
|
||||
use crate::buffer_codegen::BufferCodegen;
|
||||
use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider};
|
||||
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
|
||||
use crate::language_model_selector::ToggleModelSelector;
|
||||
use crate::message_editor::{ContextCreasesAddon, extract_message_creases, insert_message_creases};
|
||||
use crate::terminal_codegen::TerminalCodegen;
|
||||
use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext};
|
||||
@@ -38,6 +37,7 @@ use ui::{
|
||||
CheckboxWithLabel, IconButtonShape, KeyBinding, Popover, PopoverMenuHandle, Tooltip, prelude::*,
|
||||
};
|
||||
use workspace::Workspace;
|
||||
use zed_actions::agent::ToggleModelSelector;
|
||||
|
||||
pub struct PromptEditor<T> {
|
||||
pub editor: Entity<Editor>,
|
||||
|
||||
@@ -3,9 +3,7 @@ use std::{cmp::Reverse, sync::Arc};
|
||||
use collections::{HashSet, IndexMap};
|
||||
use feature_flags::ZedProFeatureFlag;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
|
||||
use gpui::{
|
||||
Action, AnyElement, App, BackgroundExecutor, DismissEvent, Subscription, Task, actions,
|
||||
};
|
||||
use gpui::{Action, AnyElement, App, BackgroundExecutor, DismissEvent, Subscription, Task};
|
||||
use language_model::{
|
||||
AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelProviderId,
|
||||
LanguageModelRegistry,
|
||||
@@ -15,15 +13,6 @@ use picker::{Picker, PickerDelegate};
|
||||
use proto::Plan;
|
||||
use ui::{ListItem, ListItemSpacing, prelude::*};
|
||||
|
||||
actions!(
|
||||
agent,
|
||||
[
|
||||
/// Toggles the language model selector dropdown.
|
||||
#[action(deprecated_aliases = ["assistant::ToggleModelSelector", "assistant2::ToggleModelSelector"])]
|
||||
ToggleModelSelector
|
||||
]
|
||||
);
|
||||
|
||||
const TRY_ZED_PRO_URL: &str = "https://zed.dev/pro";
|
||||
|
||||
type OnModelChanged = Arc<dyn Fn(Arc<dyn LanguageModel>, &mut App) + 'static>;
|
||||
@@ -587,7 +576,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(
|
||||
zed_actions::agent::OpenConfiguration.boxed_clone(),
|
||||
zed_actions::agent::OpenSettings.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
|
||||
@@ -4,19 +4,20 @@ use std::sync::Arc;
|
||||
|
||||
use crate::agent_diff::AgentDiffThread;
|
||||
use crate::agent_model_selector::AgentModelSelector;
|
||||
use crate::language_model_selector::ToggleModelSelector;
|
||||
use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip};
|
||||
use crate::ui::{
|
||||
MaxModeTooltip,
|
||||
preview::{AgentPreview, UsageCallout},
|
||||
};
|
||||
use agent::history_store::HistoryStore;
|
||||
use agent::{
|
||||
context::{AgentContextKey, ContextLoadResult, load_context},
|
||||
context_store::ContextStoreEvent,
|
||||
};
|
||||
use agent_settings::{AgentSettings, CompletionMode};
|
||||
use ai_onboarding::ApiKeysWithProviders;
|
||||
use buffer_diff::BufferDiff;
|
||||
use client::UserStore;
|
||||
use cloud_llm_client::CompletionIntent;
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::actions::{MoveUp, Paste};
|
||||
use editor::display_map::CreaseId;
|
||||
@@ -29,17 +30,18 @@ use fs::Fs;
|
||||
use futures::future::Shared;
|
||||
use futures::{FutureExt as _, future};
|
||||
use gpui::{
|
||||
Animation, AnimationExt, App, Entity, EventEmitter, Focusable, Subscription, Task, TextStyle,
|
||||
WeakEntity, linear_color_stop, linear_gradient, point, pulsating_between,
|
||||
Animation, AnimationExt, App, Entity, EventEmitter, Focusable, IntoElement, KeyContext,
|
||||
Subscription, Task, TextStyle, WeakEntity, linear_color_stop, linear_gradient, point,
|
||||
pulsating_between,
|
||||
};
|
||||
use language::{Buffer, Language, Point};
|
||||
use language_model::{
|
||||
ConfiguredModel, LanguageModelRequestMessage, MessageContent, ZED_CLOUD_PROVIDER_ID,
|
||||
ConfiguredModel, LanguageModelRegistry, LanguageModelRequestMessage, MessageContent,
|
||||
ZED_CLOUD_PROVIDER_ID,
|
||||
};
|
||||
use multi_buffer;
|
||||
use project::Project;
|
||||
use prompt_store::PromptStore;
|
||||
use proto::Plan;
|
||||
use settings::Settings;
|
||||
use std::time::Duration;
|
||||
use theme::ThemeSettings;
|
||||
@@ -49,7 +51,7 @@ use ui::{
|
||||
use util::ResultExt as _;
|
||||
use workspace::{CollaboratorId, Workspace};
|
||||
use zed_actions::agent::Chat;
|
||||
use zed_llm_client::CompletionIntent;
|
||||
use zed_actions::agent::ToggleModelSelector;
|
||||
|
||||
use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention};
|
||||
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
|
||||
@@ -65,6 +67,9 @@ use agent::{
|
||||
thread_store::{TextThreadStore, ThreadStore},
|
||||
};
|
||||
|
||||
pub const MIN_EDITOR_LINES: usize = 4;
|
||||
pub const MAX_EDITOR_LINES: usize = 8;
|
||||
|
||||
#[derive(RegisterComponent)]
|
||||
pub struct MessageEditor {
|
||||
thread: Entity<Thread>,
|
||||
@@ -72,9 +77,9 @@ pub struct MessageEditor {
|
||||
editor: Entity<Editor>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
user_store: Entity<UserStore>,
|
||||
context_store: Entity<ContextStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
history_store: Option<WeakEntity<HistoryStore>>,
|
||||
context_strip: Entity<ContextStrip>,
|
||||
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||
model_selector: Entity<AgentModelSelector>,
|
||||
@@ -88,9 +93,6 @@ pub struct MessageEditor {
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
const MIN_EDITOR_LINES: usize = 4;
|
||||
const MAX_EDITOR_LINES: usize = 8;
|
||||
|
||||
pub(crate) fn create_editor(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
context_store: WeakEntity<ContextStore>,
|
||||
@@ -132,6 +134,7 @@ pub(crate) fn create_editor(
|
||||
placement: Some(ContextMenuPlacement::Above),
|
||||
});
|
||||
editor.register_addon(ContextCreasesAddon::new());
|
||||
editor.register_addon(MessageEditorAddon::new());
|
||||
editor
|
||||
});
|
||||
|
||||
@@ -153,11 +156,11 @@ impl MessageEditor {
|
||||
pub fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
user_store: Entity<UserStore>,
|
||||
context_store: Entity<ContextStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
thread_store: WeakEntity<ThreadStore>,
|
||||
text_thread_store: WeakEntity<TextThreadStore>,
|
||||
history_store: Option<WeakEntity<HistoryStore>>,
|
||||
thread: Entity<Thread>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
@@ -224,12 +227,12 @@ impl MessageEditor {
|
||||
Self {
|
||||
editor: editor.clone(),
|
||||
project: thread.read(cx).project().clone(),
|
||||
user_store,
|
||||
thread,
|
||||
incompatible_tools_state: incompatible_tools.clone(),
|
||||
workspace,
|
||||
context_store,
|
||||
prompt_store,
|
||||
history_store,
|
||||
context_strip,
|
||||
context_picker_menu_handle,
|
||||
load_context_task: None,
|
||||
@@ -609,7 +612,11 @@ impl MessageEditor {
|
||||
)
|
||||
}
|
||||
|
||||
fn render_follow_toggle(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn render_follow_toggle(
|
||||
&self,
|
||||
is_model_selected: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let following = self
|
||||
.workspace
|
||||
.read_with(cx, |workspace, _| {
|
||||
@@ -618,6 +625,7 @@ impl MessageEditor {
|
||||
.unwrap_or(false);
|
||||
|
||||
IconButton::new("follow-agent", IconName::Crosshair)
|
||||
.disabled(!is_model_selected)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.toggle_state(following)
|
||||
@@ -705,11 +713,11 @@ impl MessageEditor {
|
||||
cx.listener(|this, _: &RejectAll, window, cx| this.handle_reject_all(window, cx)),
|
||||
)
|
||||
.capture_action(cx.listener(Self::paste))
|
||||
.gap_2()
|
||||
.p_2()
|
||||
.bg(editor_bg_color)
|
||||
.gap_2()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(editor_bg_color)
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
@@ -786,7 +794,7 @@ impl MessageEditor {
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.child(self.render_follow_toggle(cx))
|
||||
.child(self.render_follow_toggle(is_model_selected, cx))
|
||||
.children(self.render_burn_mode_toggle(cx)),
|
||||
)
|
||||
.child(
|
||||
@@ -902,6 +910,10 @@ impl MessageEditor {
|
||||
.on_click({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |_event, window, cx| {
|
||||
telemetry::event!(
|
||||
"Agent Message Sent",
|
||||
agent = "zed",
|
||||
);
|
||||
focus_handle.dispatch_action(
|
||||
&Chat, window, cx,
|
||||
);
|
||||
@@ -1270,24 +1282,12 @@ impl MessageEditor {
|
||||
return None;
|
||||
}
|
||||
|
||||
let user_store = self.user_store.read(cx);
|
||||
|
||||
let ubb_enable = user_store
|
||||
.usage_based_billing_enabled()
|
||||
.map_or(false, |enabled| enabled);
|
||||
|
||||
if ubb_enable {
|
||||
let user_store = self.project.read(cx).user_store().read(cx);
|
||||
if user_store.is_usage_based_billing_enabled() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let plan = user_store
|
||||
.current_plan()
|
||||
.map(|plan| match plan {
|
||||
Plan::Free => zed_llm_client::Plan::ZedFree,
|
||||
Plan::ZedPro => zed_llm_client::Plan::ZedPro,
|
||||
Plan::ZedProTrial => zed_llm_client::Plan::ZedProTrial,
|
||||
})
|
||||
.unwrap_or(zed_llm_client::Plan::ZedFree);
|
||||
let plan = user_store.plan().unwrap_or(cloud_llm_client::Plan::ZedFree);
|
||||
|
||||
let usage = user_store.model_request_usage()?;
|
||||
|
||||
@@ -1489,6 +1489,31 @@ pub struct ContextCreasesAddon {
|
||||
_subscription: Option<Subscription>,
|
||||
}
|
||||
|
||||
pub struct MessageEditorAddon {}
|
||||
|
||||
impl MessageEditorAddon {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl Addon for MessageEditorAddon {
|
||||
fn to_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
|
||||
Some(self)
|
||||
}
|
||||
|
||||
fn extend_key_context(&self, key_context: &mut KeyContext, cx: &App) {
|
||||
let settings = agent_settings::AgentSettings::get_global(cx);
|
||||
if settings.use_modifier_to_send {
|
||||
key_context.add("use_modifier_to_send");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Addon for ContextCreasesAddon {
|
||||
fn to_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
@@ -1624,9 +1649,38 @@ impl Render for MessageEditor {
|
||||
|
||||
let line_height = TextSize::Small.rems(cx).to_pixels(window.rem_size()) * 1.5;
|
||||
|
||||
let has_configured_providers = LanguageModelRegistry::read_global(cx)
|
||||
.providers()
|
||||
.iter()
|
||||
.filter(|provider| {
|
||||
provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
|
||||
})
|
||||
.count()
|
||||
> 0;
|
||||
|
||||
let is_signed_out = self
|
||||
.workspace
|
||||
.read_with(cx, |workspace, _| {
|
||||
workspace.client().status().borrow().is_signed_out()
|
||||
})
|
||||
.unwrap_or(true);
|
||||
|
||||
let has_history = self
|
||||
.history_store
|
||||
.as_ref()
|
||||
.and_then(|hs| hs.update(cx, |hs, cx| hs.entries(cx).len() > 0).ok())
|
||||
.unwrap_or(false)
|
||||
|| self
|
||||
.thread
|
||||
.read_with(cx, |thread, _| thread.messages().len() > 0);
|
||||
|
||||
v_flex()
|
||||
.size_full()
|
||||
.bg(cx.theme().colors().panel_background)
|
||||
.when(
|
||||
!has_history && is_signed_out && has_configured_providers,
|
||||
|this| this.child(cx.new(ApiKeysWithProviders::new)),
|
||||
)
|
||||
.when(changed_buffers.len() > 0, |parent| {
|
||||
parent.child(self.render_edits_bar(&changed_buffers, window, cx))
|
||||
})
|
||||
@@ -1698,7 +1752,6 @@ impl AgentPreview for MessageEditor {
|
||||
) -> Option<AnyElement> {
|
||||
if let Some(workspace) = workspace.upgrade() {
|
||||
let fs = workspace.read(cx).app_state().fs.clone();
|
||||
let user_store = workspace.read(cx).app_state().user_store.clone();
|
||||
let project = workspace.read(cx).project().clone();
|
||||
let weak_project = project.downgrade();
|
||||
let context_store = cx.new(|_cx| ContextStore::new(weak_project, None));
|
||||
@@ -1711,11 +1764,11 @@ impl AgentPreview for MessageEditor {
|
||||
MessageEditor::new(
|
||||
fs,
|
||||
workspace.downgrade(),
|
||||
user_store,
|
||||
context_store,
|
||||
None,
|
||||
thread_store.downgrade(),
|
||||
text_thread_store.downgrade(),
|
||||
None,
|
||||
thread,
|
||||
window,
|
||||
cx,
|
||||
|
||||
@@ -10,6 +10,7 @@ use agent::{
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::{Context as _, Result};
|
||||
use client::telemetry::Telemetry;
|
||||
use cloud_llm_client::CompletionIntent;
|
||||
use collections::{HashMap, VecDeque};
|
||||
use editor::{MultiBuffer, actions::SelectAll};
|
||||
use fs::Fs;
|
||||
@@ -27,7 +28,6 @@ use terminal_view::TerminalView;
|
||||
use ui::prelude::*;
|
||||
use util::ResultExt;
|
||||
use workspace::{Toast, Workspace, notifications::NotificationId};
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
pub fn init(
|
||||
fs: Arc<dyn Fs>,
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use crate::{
|
||||
burn_mode_tooltip::BurnModeTooltip,
|
||||
language_model_selector::{
|
||||
LanguageModelSelector, ToggleModelSelector, language_model_selector,
|
||||
},
|
||||
language_model_selector::{LanguageModelSelector, language_model_selector},
|
||||
};
|
||||
use agent_settings::{AgentSettings, CompletionMode};
|
||||
use anyhow::Result;
|
||||
@@ -38,8 +36,7 @@ use language::{
|
||||
language_settings::{SoftWrap, all_language_settings},
|
||||
};
|
||||
use language_model::{
|
||||
ConfigurationError, LanguageModelExt, LanguageModelImage, LanguageModelProviderTosView,
|
||||
LanguageModelRegistry, Role,
|
||||
ConfigurationError, LanguageModelExt, LanguageModelImage, LanguageModelRegistry, Role,
|
||||
};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use picker::{Picker, popover_menu::PickerPopoverMenu};
|
||||
@@ -74,6 +71,7 @@ use workspace::{
|
||||
pane,
|
||||
searchable::{SearchEvent, SearchableItem},
|
||||
};
|
||||
use zed_actions::agent::ToggleModelSelector;
|
||||
|
||||
use crate::{slash_command::SlashCommandCompletionProvider, slash_command_picker};
|
||||
use assistant_context::{
|
||||
@@ -1895,108 +1893,6 @@ impl TextThreadEditor {
|
||||
.update(cx, |context, cx| context.summarize(true, cx));
|
||||
}
|
||||
|
||||
fn render_notice(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
||||
// This was previously gated behind the `zed-pro` feature flag. Since we
|
||||
// aren't planning to ship that right now, we're just hard-coding this
|
||||
// value to not show the nudge.
|
||||
let nudge = Some(false);
|
||||
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
|
||||
if nudge.map_or(false, |value| value) {
|
||||
Some(
|
||||
h_flex()
|
||||
.p_3()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_3()
|
||||
.child(Icon::new(IconName::ZedAssistant).color(Color::Accent))
|
||||
.child(Label::new("Zed AI is here! Get started by signing in →")),
|
||||
)
|
||||
.child(
|
||||
Button::new("sign-in", "Sign in")
|
||||
.size(ButtonSize::Compact)
|
||||
.style(ButtonStyle::Filled)
|
||||
.on_click(cx.listener(|this, _event, _window, cx| {
|
||||
let client = this
|
||||
.workspace
|
||||
.read_with(cx, |workspace, _| workspace.client().clone())
|
||||
.log_err();
|
||||
|
||||
if let Some(client) = client {
|
||||
cx.spawn(async move |context_editor, cx| {
|
||||
match client.authenticate_and_connect(true, cx).await {
|
||||
util::ConnectionResult::Timeout => {
|
||||
log::error!("Authentication timeout")
|
||||
}
|
||||
util::ConnectionResult::ConnectionReset => {
|
||||
log::error!("Connection reset")
|
||||
}
|
||||
util::ConnectionResult::Result(r) => {
|
||||
if r.log_err().is_some() {
|
||||
context_editor
|
||||
.update(cx, |_, cx| cx.notify())
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach()
|
||||
}
|
||||
})),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
} else if let Some(configuration_error) =
|
||||
model_registry.configuration_error(model_registry.default_model(), cx)
|
||||
{
|
||||
Some(
|
||||
h_flex()
|
||||
.px_3()
|
||||
.py_2()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_3()
|
||||
.child(
|
||||
Icon::new(IconName::Warning)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Warning),
|
||||
)
|
||||
.child(Label::new(configuration_error.to_string())),
|
||||
)
|
||||
.child(
|
||||
Button::new("open-configuration", "Configure Providers")
|
||||
.size(ButtonSize::Compact)
|
||||
.icon(Some(IconName::SlidersVertical))
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_position(IconPosition::Start)
|
||||
.style(ButtonStyle::Filled)
|
||||
.on_click({
|
||||
let focus_handle = self.focus_handle(cx).clone();
|
||||
move |_event, window, cx| {
|
||||
focus_handle.dispatch_action(
|
||||
&zed_actions::agent::OpenConfiguration,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn render_send_button(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let focus_handle = self.focus_handle(cx).clone();
|
||||
|
||||
@@ -2128,12 +2024,13 @@ impl TextThreadEditor {
|
||||
.map(|default| default.model);
|
||||
let model_name = match active_model {
|
||||
Some(model) => model.name().0,
|
||||
None => SharedString::from("No model selected"),
|
||||
None => SharedString::from("Select Model"),
|
||||
};
|
||||
|
||||
let active_provider = LanguageModelRegistry::read_global(cx)
|
||||
.default_model()
|
||||
.map(|default| default.provider);
|
||||
|
||||
let provider_icon = match active_provider {
|
||||
Some(provider) => provider.icon(),
|
||||
None => IconName::Ai,
|
||||
@@ -2581,20 +2478,7 @@ impl EventEmitter<SearchEvent> for TextThreadEditor {}
|
||||
|
||||
impl Render for TextThreadEditor {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let provider = LanguageModelRegistry::read_global(cx)
|
||||
.default_model()
|
||||
.map(|default| default.provider);
|
||||
|
||||
let accept_terms = if self.show_accept_terms {
|
||||
provider.as_ref().and_then(|provider| {
|
||||
provider.render_accept_terms(LanguageModelProviderTosView::PromptEditorPopup, cx)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let language_model_selector = self.language_model_selector_menu_handle.clone();
|
||||
let burn_mode_toggle = self.render_burn_mode_toggle(cx);
|
||||
|
||||
v_flex()
|
||||
.key_context("ContextEditor")
|
||||
@@ -2611,28 +2495,12 @@ impl Render for TextThreadEditor {
|
||||
language_model_selector.toggle(window, cx);
|
||||
})
|
||||
.size_full()
|
||||
.children(self.render_notice(cx))
|
||||
.child(
|
||||
div()
|
||||
.flex_grow()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(self.editor.clone()),
|
||||
)
|
||||
.when_some(accept_terms, |this, element| {
|
||||
this.child(
|
||||
div()
|
||||
.absolute()
|
||||
.right_3()
|
||||
.bottom_12()
|
||||
.max_w_96()
|
||||
.py_2()
|
||||
.px_3()
|
||||
.elevation_2(cx)
|
||||
.bg(cx.theme().colors().surface_background)
|
||||
.occlude()
|
||||
.child(element),
|
||||
)
|
||||
})
|
||||
.children(self.render_last_error(cx))
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -2649,7 +2517,7 @@ impl Render for TextThreadEditor {
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(self.render_inject_context_menu(cx))
|
||||
.when_some(burn_mode_toggle, |this, element| this.child(element)),
|
||||
.children(self.render_burn_mode_toggle(cx)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
mod agent_notification;
|
||||
mod burn_mode_tooltip;
|
||||
mod context_pill;
|
||||
mod end_trial_upsell;
|
||||
mod new_thread_button;
|
||||
mod onboarding_modal;
|
||||
pub mod preview;
|
||||
mod upsell;
|
||||
|
||||
pub use agent_notification::*;
|
||||
pub use burn_mode_tooltip::*;
|
||||
pub use context_pill::*;
|
||||
pub use end_trial_upsell::*;
|
||||
pub use new_thread_button::*;
|
||||
pub use onboarding_modal::*;
|
||||
|
||||
123
crates/agent_ui/src/ui/end_trial_upsell.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use ai_onboarding::{AgentPanelOnboardingCard, BulletItem};
|
||||
use client::zed_urls;
|
||||
use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
|
||||
use ui::{Divider, List, Tooltip, prelude::*};
|
||||
|
||||
#[derive(IntoElement, RegisterComponent)]
|
||||
pub struct EndTrialUpsell {
|
||||
dismiss_upsell: Arc<dyn Fn(&mut Window, &mut App)>,
|
||||
}
|
||||
|
||||
impl EndTrialUpsell {
|
||||
pub fn new(dismiss_upsell: Arc<dyn Fn(&mut Window, &mut App)>) -> Self {
|
||||
Self { dismiss_upsell }
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for EndTrialUpsell {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let pro_section = v_flex()
|
||||
.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",
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
Button::new("cta-button", "Upgrade to Zed Pro")
|
||||
.full_width()
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
.on_click(move |_, _window, cx| {
|
||||
telemetry::event!("Upgrade To Pro Clicked", state = "end-of-trial");
|
||||
cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))
|
||||
}),
|
||||
);
|
||||
|
||||
let free_section = v_flex()
|
||||
.mt_1p5()
|
||||
.gap_1()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Label::new("Free")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
)
|
||||
.child(
|
||||
Label::new("(Current Plan)")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Custom(cx.theme().colors().text_muted.opacity(0.6)))
|
||||
.buffer_font(cx),
|
||||
)
|
||||
.child(Divider::horizontal()),
|
||||
)
|
||||
.child(
|
||||
List::new()
|
||||
.child(BulletItem::new("50 prompts with the Claude models"))
|
||||
.child(BulletItem::new("2,000 accepted edit predictions")),
|
||||
);
|
||||
|
||||
AgentPanelOnboardingCard::new()
|
||||
.child(Headline::new("Your Zed Pro Trial has expired"))
|
||||
.child(
|
||||
Label::new("You've been automatically reset to the Free plan.")
|
||||
.color(Color::Muted)
|
||||
.mb_2(),
|
||||
)
|
||||
.child(pro_section)
|
||||
.child(free_section)
|
||||
.child(
|
||||
h_flex().absolute().top_4().right_4().child(
|
||||
IconButton::new("dismiss_onboarding", IconName::Close)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::text("Dismiss"))
|
||||
.on_click({
|
||||
let callback = self.dismiss_upsell.clone();
|
||||
move |_, window, cx| {
|
||||
telemetry::event!("Banner Dismissed", source = "AI Onboarding");
|
||||
callback(window, cx)
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for EndTrialUpsell {
|
||||
fn scope() -> ComponentScope {
|
||||
ComponentScope::Agent
|
||||
}
|
||||
|
||||
fn sort_name() -> &'static str {
|
||||
"AgentEndTrialUpsell"
|
||||
}
|
||||
|
||||
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
|
||||
Some(
|
||||
v_flex()
|
||||
.p_4()
|
||||
.gap_4()
|
||||
.child(EndTrialUpsell {
|
||||
dismiss_upsell: Arc::new(|_, _| {}),
|
||||
})
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}
|
||||