Compare commits
262 Commits
git-graph
...
spawn-suba
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7273d8e76d | ||
|
|
7098952a1c | ||
|
|
bd5569b338 | ||
|
|
be1f824a35 | ||
|
|
f21cec7cb1 | ||
|
|
93d79f3862 | ||
|
|
4896f477e2 | ||
|
|
d07818b20f | ||
|
|
c1317baebe | ||
|
|
3f11cbd62c | ||
|
|
bcebe76e53 | ||
|
|
0466db66cd | ||
|
|
420254cff1 | ||
|
|
8b9fa1581c | ||
|
|
914b0117fb | ||
|
|
005a85e57b | ||
|
|
935a7cc310 | ||
|
|
4573a59777 | ||
|
|
7ba6f39e82 | ||
|
|
73b37e9774 | ||
|
|
1104ac7f7c | ||
|
|
da0960bab6 | ||
|
|
81519ae923 | ||
|
|
5f054e8d9c | ||
|
|
37e4f7e9b5 | ||
|
|
5f451c89e0 | ||
|
|
0362e301f7 | ||
|
|
37bd27b2a8 | ||
|
|
775548e93c | ||
|
|
90d7ccfd5d | ||
|
|
68295ba371 | ||
|
|
5152fd898e | ||
|
|
4e482288cb | ||
|
|
30deb22ab7 | ||
|
|
f358b9531a | ||
|
|
ba24ac7aae | ||
|
|
2178ad6b91 | ||
|
|
c3b0860909 | ||
|
|
33b71aea64 | ||
|
|
4109c9dde7 | ||
|
|
9ec147db67 | ||
|
|
9c32c29238 | ||
|
|
a176a8c47e | ||
|
|
9d4d37a514 | ||
|
|
81d8fb930a | ||
|
|
65e9001791 | ||
|
|
ebd5a50cce | ||
|
|
f760233704 | ||
|
|
a1dbfd0d77 | ||
|
|
8ef37e8577 | ||
|
|
6016d0b8c6 | ||
|
|
ee2a4a9d37 | ||
|
|
829b1b5661 | ||
|
|
c7d248329b | ||
|
|
b17b097204 | ||
|
|
dfdad947e1 | ||
|
|
3b2ccaff6f | ||
|
|
a60e0a178f | ||
|
|
f8561b4cb9 | ||
|
|
7a4de734c6 | ||
|
|
b8d0da97fa | ||
|
|
870159e7e8 | ||
|
|
0ead4668d2 | ||
|
|
b52f907a8e | ||
|
|
4096bc55be | ||
|
|
97f6cdac81 | ||
|
|
5987dff7e4 | ||
|
|
eceece8ce5 | ||
|
|
faef5c9eac | ||
|
|
47a6bd22e4 | ||
|
|
c7a1852e36 | ||
|
|
ee6469d60e | ||
|
|
9e11aaec51 | ||
|
|
fb574d8869 | ||
|
|
523f093c8e | ||
|
|
2441dc3f66 | ||
|
|
969e9a6707 | ||
|
|
dbab71e348 | ||
|
|
c75d880983 | ||
|
|
3076c4ee4e | ||
|
|
0410b2340c | ||
|
|
7d7ca129db | ||
|
|
7cd483321b | ||
|
|
d4f965724c | ||
|
|
0d891bd3e5 | ||
|
|
1b29725a60 | ||
|
|
79dfae2464 | ||
|
|
e1063743e8 | ||
|
|
3cc21a01ef | ||
|
|
0a5955a464 | ||
|
|
34122aeb21 | ||
|
|
6401ac0725 | ||
|
|
c20cbba0eb | ||
|
|
f2f3d9faf6 | ||
|
|
b922019221 | ||
|
|
d52defe35a | ||
|
|
79a8985a8e | ||
|
|
03216c9800 | ||
|
|
632bd378ba | ||
|
|
b71ef540fc | ||
|
|
158ebdc580 | ||
|
|
f4c3a6c236 | ||
|
|
6eb198cabf | ||
|
|
07bf685fee | ||
|
|
a6b7af3cbd | ||
|
|
7889aaf3fb | ||
|
|
3bf57dc779 | ||
|
|
a3ac595737 | ||
|
|
63bfb6131f | ||
|
|
5fe7fd97bd | ||
|
|
a61c14cf3b | ||
|
|
c996934b57 | ||
|
|
5805f62f18 | ||
|
|
bd481dea48 | ||
|
|
59b01651e1 | ||
|
|
3e8d55739c | ||
|
|
8fb2bde2c9 | ||
|
|
886832281d | ||
|
|
b633de66f7 | ||
|
|
2f63543380 | ||
|
|
79d4f7d33d | ||
|
|
693b978c8d | ||
|
|
dd13c95158 | ||
|
|
a78ffdafa9 | ||
|
|
c952de4bfb | ||
|
|
75c71a9fc5 | ||
|
|
213c1b210b | ||
|
|
be57307a6f | ||
|
|
38f4e21fe8 | ||
|
|
6067436e9b | ||
|
|
54c4302cdb | ||
|
|
3db2d03bb3 | ||
|
|
63918b8955 | ||
|
|
82535a5481 | ||
|
|
c2c8b4b9fb | ||
|
|
6cab835003 | ||
|
|
0c47984a19 | ||
|
|
b8e40e6fdb | ||
|
|
d7da5d3efd | ||
|
|
86aa9abc90 | ||
|
|
a51585d2da | ||
|
|
26b261a336 | ||
|
|
f80ef9a3c5 | ||
|
|
13594bd97e | ||
|
|
e9073eceeb | ||
|
|
00169e0ae2 | ||
|
|
6cc947f654 | ||
|
|
f2cc24c5fa | ||
|
|
488fa02547 | ||
|
|
dad6481e02 | ||
|
|
0283bfb049 | ||
|
|
56daba28d4 | ||
|
|
6e0ecbcb07 | ||
|
|
4754422ef4 | ||
|
|
e860252185 | ||
|
|
fad06dd00c | ||
|
|
329ec645da | ||
|
|
e1d236eaf0 | ||
|
|
60f4aa333b | ||
|
|
a698f1bf63 | ||
|
|
636d11ebec | ||
|
|
4d0e760b04 | ||
|
|
8bd4d866b9 | ||
|
|
47c30b6da7 | ||
|
|
18d344e118 | ||
|
|
610cc1b138 | ||
|
|
a07ea1a272 | ||
|
|
e03fa114a7 | ||
|
|
17db7b0e99 | ||
|
|
1afe29422b | ||
|
|
a8aa7622b7 | ||
|
|
a66854e435 | ||
|
|
12073e10f8 | ||
|
|
1186b50ca4 | ||
|
|
65130a9ca9 | ||
|
|
23d18fde8c | ||
|
|
332c0d03d1 | ||
|
|
b871130220 | ||
|
|
0a1e5f93a0 | ||
|
|
8d0fff688f | ||
|
|
717d898692 | ||
|
|
1cd7563f04 | ||
|
|
fc6ca38989 | ||
|
|
1029a8fbaf | ||
|
|
07748b7bae | ||
|
|
37f2ac24b8 | ||
|
|
b5a0a3322d | ||
|
|
eb7da26d19 | ||
|
|
9c099e7ed3 | ||
|
|
7669b05268 | ||
|
|
2098b67304 | ||
|
|
5a6198cc39 | ||
|
|
cda78c12ab | ||
|
|
f4378672b8 | ||
|
|
ecb8d3d4dd | ||
|
|
95dbc0efc2 | ||
|
|
8572c19a02 | ||
|
|
045c14593f | ||
|
|
0ff3b68a5e | ||
|
|
a6b9524d78 | ||
|
|
7ed5d42696 | ||
|
|
25d74480aa | ||
|
|
37077a8ebb | ||
|
|
7c4a85f5f1 | ||
|
|
d21628c349 | ||
|
|
9e628505f3 | ||
|
|
3a84ec38ac | ||
|
|
a61bf33fb0 | ||
|
|
d83201256d | ||
|
|
8ee85eab3c | ||
|
|
5b309ef986 | ||
|
|
326ebb5230 | ||
|
|
f5babf96e1 | ||
|
|
f48aa252f8 | ||
|
|
4106c8a188 | ||
|
|
21f7e6a9e6 | ||
|
|
dd431631b4 | ||
|
|
511e51c80e | ||
|
|
0a816cbc87 | ||
|
|
b1333b53ad | ||
|
|
30597a0cba | ||
|
|
a8e2dc2f25 | ||
|
|
fd2094fa19 | ||
|
|
22f1655f8f | ||
|
|
7cbe25fda5 | ||
|
|
728f09f3f4 | ||
|
|
4353b8ecd5 | ||
|
|
736a712387 | ||
|
|
3180f44477 | ||
|
|
5dd8561b06 | ||
|
|
bfab0b71e0 | ||
|
|
04d920016f | ||
|
|
20fa9983ad | ||
|
|
dd57d97bb6 | ||
|
|
d5a437d22f | ||
|
|
a524071dd9 | ||
|
|
1471105643 | ||
|
|
f05ee8a24d | ||
|
|
4d0cada8f4 | ||
|
|
abf90cc274 | ||
|
|
b79d92d1c6 | ||
|
|
660234fed2 | ||
|
|
2b02b60317 | ||
|
|
9d49c1ffda | ||
|
|
6253b1d220 | ||
|
|
4e75f0f3ab | ||
|
|
0b4f72e549 | ||
|
|
dc5f54eaf9 | ||
|
|
ba807a3c46 | ||
|
|
45829b3380 | ||
|
|
631e3dd272 | ||
|
|
8d44bcd4f9 | ||
|
|
1888106664 | ||
|
|
c005adb09c | ||
|
|
6b2d1f153d | ||
|
|
22e1bcccad | ||
|
|
bb591f1e65 | ||
|
|
3d6cc3dc79 | ||
|
|
464d4f72eb | ||
|
|
f4892559f0 | ||
|
|
387059c6b2 | ||
|
|
4a382b2797 |
16
.github/ISSUE_TEMPLATE/10_bug_report.yml
vendored
16
.github/ISSUE_TEMPLATE/10_bug_report.yml
vendored
@@ -75,6 +75,22 @@ body:
|
|||||||
</details>
|
</details>
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Relevant Keymap
|
||||||
|
description: |
|
||||||
|
Open the command palette in Zed, then type “zed: open keymap file” and copy/paste the file's contents.
|
||||||
|
value: |
|
||||||
|
<details><summary>keymap.json</summary>
|
||||||
|
|
||||||
|
<!-- Paste your keymap file inside the code block. -->
|
||||||
|
```json
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: (for AI issues) Model provider details
|
label: (for AI issues) Model provider details
|
||||||
|
|||||||
25
.github/workflows/after_release.yml
vendored
25
.github/workflows/after_release.yml
vendored
@@ -5,13 +5,27 @@ on:
|
|||||||
release:
|
release:
|
||||||
types:
|
types:
|
||||||
- published
|
- published
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag_name:
|
||||||
|
description: tag_name
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
prerelease:
|
||||||
|
description: prerelease
|
||||||
|
required: true
|
||||||
|
type: boolean
|
||||||
|
body:
|
||||||
|
description: body
|
||||||
|
type: string
|
||||||
|
default: ''
|
||||||
jobs:
|
jobs:
|
||||||
rebuild_releases_page:
|
rebuild_releases_page:
|
||||||
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
|
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
|
||||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||||
steps:
|
steps:
|
||||||
- name: after_release::rebuild_releases_page::refresh_cloud_releases
|
- name: after_release::rebuild_releases_page::refresh_cloud_releases
|
||||||
run: curl -fX POST https://cloud.zed.dev/releases/refresh?expect_tag=${{ github.event.release.tag_name }}
|
run: curl -fX POST https://cloud.zed.dev/releases/refresh?expect_tag=${{ github.event.release.tag_name || inputs.tag_name }}
|
||||||
shell: bash -euxo pipefail {0}
|
shell: bash -euxo pipefail {0}
|
||||||
- name: after_release::rebuild_releases_page::redeploy_zed_dev
|
- name: after_release::rebuild_releases_page::redeploy_zed_dev
|
||||||
run: npm exec --yes -- vercel@37 --token="$VERCEL_TOKEN" --scope zed-industries redeploy https://zed.dev
|
run: npm exec --yes -- vercel@37 --token="$VERCEL_TOKEN" --scope zed-industries redeploy https://zed.dev
|
||||||
@@ -27,7 +41,7 @@ jobs:
|
|||||||
- id: get-release-url
|
- id: get-release-url
|
||||||
name: after_release::post_to_discord::get_release_url
|
name: after_release::post_to_discord::get_release_url
|
||||||
run: |
|
run: |
|
||||||
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
|
if [ "${{ github.event.release.prerelease || inputs.prerelease }}" == "true" ]; then
|
||||||
URL="https://zed.dev/releases/preview"
|
URL="https://zed.dev/releases/preview"
|
||||||
else
|
else
|
||||||
URL="https://zed.dev/releases/stable"
|
URL="https://zed.dev/releases/stable"
|
||||||
@@ -40,9 +54,9 @@ jobs:
|
|||||||
uses: 2428392/gh-truncate-string-action@b3ff790d21cf42af3ca7579146eedb93c8fb0757
|
uses: 2428392/gh-truncate-string-action@b3ff790d21cf42af3ca7579146eedb93c8fb0757
|
||||||
with:
|
with:
|
||||||
stringToTruncate: |
|
stringToTruncate: |
|
||||||
📣 Zed [${{ github.event.release.tag_name }}](<${{ steps.get-release-url.outputs.URL }}>) was just released!
|
📣 Zed [${{ github.event.release.tag_name || inputs.tag_name }}](<${{ steps.get-release-url.outputs.URL }}>) was just released!
|
||||||
|
|
||||||
${{ github.event.release.body }}
|
${{ github.event.release.body || inputs.body }}
|
||||||
maxLength: 2000
|
maxLength: 2000
|
||||||
truncationSymbol: '...'
|
truncationSymbol: '...'
|
||||||
- name: after_release::post_to_discord::discord_webhook_action
|
- name: after_release::post_to_discord::discord_webhook_action
|
||||||
@@ -56,7 +70,7 @@ jobs:
|
|||||||
- id: set-package-name
|
- id: set-package-name
|
||||||
name: after_release::publish_winget::set_package_name
|
name: after_release::publish_winget::set_package_name
|
||||||
run: |
|
run: |
|
||||||
if ("${{ github.event.release.prerelease }}" -eq "true") {
|
if ("${{ github.event.release.prerelease || inputs.prerelease }}" -eq "true") {
|
||||||
$PACKAGE_NAME = "ZedIndustries.Zed.Preview"
|
$PACKAGE_NAME = "ZedIndustries.Zed.Preview"
|
||||||
} else {
|
} else {
|
||||||
$PACKAGE_NAME = "ZedIndustries.Zed"
|
$PACKAGE_NAME = "ZedIndustries.Zed"
|
||||||
@@ -68,6 +82,7 @@ jobs:
|
|||||||
uses: vedantmgoyal9/winget-releaser@19e706d4c9121098010096f9c495a70a7518b30f
|
uses: vedantmgoyal9/winget-releaser@19e706d4c9121098010096f9c495a70a7518b30f
|
||||||
with:
|
with:
|
||||||
identifier: ${{ steps.set-package-name.outputs.PACKAGE_NAME }}
|
identifier: ${{ steps.set-package-name.outputs.PACKAGE_NAME }}
|
||||||
|
release-tag: ${{ github.event.release.tag_name || inputs.tag_name }}
|
||||||
max-versions-to-keep: 5
|
max-versions-to-keep: 5
|
||||||
token: ${{ secrets.WINGET_TOKEN }}
|
token: ${{ secrets.WINGET_TOKEN }}
|
||||||
create_sentry_release:
|
create_sentry_release:
|
||||||
|
|||||||
83
.github/workflows/autofix_pr.yml
vendored
Normal file
83
.github/workflows/autofix_pr.yml
vendored
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# Generated from xtask::workflows::autofix_pr
|
||||||
|
# Rebuild with `cargo xtask workflows`.
|
||||||
|
name: autofix_pr
|
||||||
|
run-name: 'autofix PR #${{ inputs.pr_number }}'
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
pr_number:
|
||||||
|
description: pr_number
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
jobs:
|
||||||
|
run_autofix:
|
||||||
|
runs-on: namespace-profile-16x32-ubuntu-2204
|
||||||
|
steps:
|
||||||
|
- id: get-app-token
|
||||||
|
name: autofix_pr::run_autofix::authenticate_as_zippy
|
||||||
|
uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
|
||||||
|
private-key: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }}
|
||||||
|
- name: steps::checkout_repo_with_token
|
||||||
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||||
|
with:
|
||||||
|
clean: false
|
||||||
|
token: ${{ steps.get-app-token.outputs.token }}
|
||||||
|
- name: autofix_pr::run_autofix::checkout_pr
|
||||||
|
run: gh pr checkout ${{ inputs.pr_number }}
|
||||||
|
shell: bash -euxo pipefail {0}
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ steps.get-app-token.outputs.token }}
|
||||||
|
- name: steps::setup_cargo_config
|
||||||
|
run: |
|
||||||
|
mkdir -p ./../.cargo
|
||||||
|
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
|
||||||
|
shell: bash -euxo pipefail {0}
|
||||||
|
- name: steps::cache_rust_dependencies_namespace
|
||||||
|
uses: namespacelabs/nscloud-cache-action@v1
|
||||||
|
with:
|
||||||
|
cache: rust
|
||||||
|
- name: steps::setup_linux
|
||||||
|
run: ./script/linux
|
||||||
|
shell: bash -euxo pipefail {0}
|
||||||
|
- name: steps::install_mold
|
||||||
|
run: ./script/install-mold
|
||||||
|
shell: bash -euxo pipefail {0}
|
||||||
|
- name: steps::download_wasi_sdk
|
||||||
|
run: ./script/download-wasi-sdk
|
||||||
|
shell: bash -euxo pipefail {0}
|
||||||
|
- name: steps::setup_pnpm
|
||||||
|
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2
|
||||||
|
with:
|
||||||
|
version: '9'
|
||||||
|
- name: autofix_pr::run_autofix::run_prettier_fix
|
||||||
|
run: ./script/prettier --write
|
||||||
|
shell: bash -euxo pipefail {0}
|
||||||
|
- name: autofix_pr::run_autofix::run_cargo_fmt
|
||||||
|
run: cargo fmt --all
|
||||||
|
shell: bash -euxo pipefail {0}
|
||||||
|
- name: autofix_pr::run_autofix::run_clippy_fix
|
||||||
|
run: cargo clippy --workspace --release --all-targets --all-features --fix --allow-dirty --allow-staged
|
||||||
|
shell: bash -euxo pipefail {0}
|
||||||
|
- name: autofix_pr::run_autofix::commit_and_push
|
||||||
|
run: |
|
||||||
|
if git diff --quiet; then
|
||||||
|
echo "No changes to commit"
|
||||||
|
else
|
||||||
|
git add -A
|
||||||
|
git commit -m "Autofix"
|
||||||
|
git push
|
||||||
|
fi
|
||||||
|
shell: bash -euxo pipefail {0}
|
||||||
|
env:
|
||||||
|
GIT_COMMITTER_NAME: Zed Zippy
|
||||||
|
GIT_COMMITTER_EMAIL: 234243425+zed-zippy[bot]@users.noreply.github.com
|
||||||
|
GIT_AUTHOR_NAME: Zed Zippy
|
||||||
|
GIT_AUTHOR_EMAIL: 234243425+zed-zippy[bot]@users.noreply.github.com
|
||||||
|
GITHUB_TOKEN: ${{ steps.get-app-token.outputs.token }}
|
||||||
|
- name: steps::cleanup_cargo_config
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
rm -rf ./../.cargo
|
||||||
|
shell: bash -euxo pipefail {0}
|
||||||
1
.github/workflows/extension_bump.yml
vendored
1
.github/workflows/extension_bump.yml
vendored
@@ -113,6 +113,7 @@ jobs:
|
|||||||
delete-branch: true
|
delete-branch: true
|
||||||
token: ${{ steps.generate-token.outputs.token }}
|
token: ${{ steps.generate-token.outputs.token }}
|
||||||
sign-commits: true
|
sign-commits: true
|
||||||
|
assignees: ${{ github.actor }}
|
||||||
timeout-minutes: 1
|
timeout-minutes: 1
|
||||||
create_version_label:
|
create_version_label:
|
||||||
needs:
|
needs:
|
||||||
|
|||||||
2
.github/workflows/run_tests.yml
vendored
2
.github/workflows/run_tests.yml
vendored
@@ -497,6 +497,8 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GIT_AUTHOR_NAME: Protobuf Action
|
GIT_AUTHOR_NAME: Protobuf Action
|
||||||
GIT_AUTHOR_EMAIL: ci@zed.dev
|
GIT_AUTHOR_EMAIL: ci@zed.dev
|
||||||
|
GIT_COMMITTER_NAME: Protobuf Action
|
||||||
|
GIT_COMMITTER_EMAIL: ci@zed.dev
|
||||||
steps:
|
steps:
|
||||||
- name: steps::checkout_repo
|
- name: steps::checkout_repo
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -8,6 +8,7 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
.blob_store
|
.blob_store
|
||||||
.build
|
.build
|
||||||
|
.claude/settings.local.json
|
||||||
.envrc
|
.envrc
|
||||||
.flatpak-builder
|
.flatpak-builder
|
||||||
.idea
|
.idea
|
||||||
@@ -41,4 +42,4 @@ xcuserdata/
|
|||||||
.env.secret.toml
|
.env.secret.toml
|
||||||
|
|
||||||
# `nix build` output
|
# `nix build` output
|
||||||
/result
|
/result
|
||||||
|
|||||||
6
.rules
6
.rules
@@ -26,6 +26,12 @@
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# Timers in tests
|
||||||
|
|
||||||
|
* In GPUI tests, prefer GPUI executor timers over `smol::Timer::after(...)` when you need timeouts, delays, or to drive `run_until_parked()`:
|
||||||
|
- Use `cx.background_executor().timer(duration).await` (or `cx.background_executor.timer(duration).await` in `TestAppContext`) so the work is scheduled on GPUI's dispatcher.
|
||||||
|
- Avoid `smol::Timer::after(...)` for test timeouts when you rely on `run_until_parked()`, because it may not be tracked by GPUI's scheduler and can lead to "nothing left to run" when pumping.
|
||||||
|
|
||||||
# GPUI
|
# GPUI
|
||||||
|
|
||||||
GPUI is a UI framework which also provides primitives for state and concurrency management.
|
GPUI is a UI framework which also provides primitives for state and concurrency management.
|
||||||
|
|||||||
@@ -15,15 +15,17 @@ with the community to improve the product in ways we haven't thought of (or had
|
|||||||
|
|
||||||
In particular we love PRs that are:
|
In particular we love PRs that are:
|
||||||
|
|
||||||
- Fixes to existing bugs and issues.
|
- Fixing or extending the docs.
|
||||||
- Small enhancements to existing features, particularly to make them work for more people.
|
- Fixing bugs.
|
||||||
|
- Small enhancements to existing features to make them work for more people (making things work on more platforms/modes/whatever).
|
||||||
- Small extra features, like keybindings or actions you miss from other editors or extensions.
|
- Small extra features, like keybindings or actions you miss from other editors or extensions.
|
||||||
- Work towards shipping larger features on our roadmap.
|
- Part of a Community Program like [Let's Git Together](https://github.com/zed-industries/zed/issues/41541).
|
||||||
|
|
||||||
If you're looking for concrete ideas:
|
If you're looking for concrete ideas:
|
||||||
|
|
||||||
- Our [top-ranking issues](https://github.com/zed-industries/zed/issues/5393) based on votes by the community.
|
- [Curated board of issues](https://github.com/orgs/zed-industries/projects/69) suitable for everyone from first-time contributors to seasoned community champions.
|
||||||
- Our [public roadmap](https://zed.dev/roadmap) contains a rough outline of our near-term priorities for Zed.
|
- [Triaged bugs with confirmed steps to reproduce](https://github.com/zed-industries/zed/issues?q=is%3Aissue%20state%3Aopen%20type%3ABug%20label%3Astate%3Areproducible).
|
||||||
|
- [Area labels](https://github.com/zed-industries/zed/labels?q=area%3A*) to browse bugs in a specific part of the product you care about (after clicking on an area label, add type:Bug to the search).
|
||||||
|
|
||||||
## Sending changes
|
## Sending changes
|
||||||
|
|
||||||
@@ -37,9 +39,17 @@ like, sorry).
|
|||||||
Although we will take a look, we tend to only merge about half the PRs that are
|
Although we will take a look, we tend to only merge about half the PRs that are
|
||||||
submitted. If you'd like your PR to have the best chance of being merged:
|
submitted. If you'd like your PR to have the best chance of being merged:
|
||||||
|
|
||||||
- Include a clear description of what you're solving, and why it's important to you.
|
- Make sure the change is **desired**: we're always happy to accept bugfixes,
|
||||||
- Include tests.
|
but features should be confirmed with us first if you aim to avoid wasted
|
||||||
- If it changes the UI, attach screenshots or screen recordings.
|
effort. If there isn't already a GitHub issue for your feature with staff
|
||||||
|
confirmation that we want it, start with a GitHub discussion rather than a PR.
|
||||||
|
- Include a clear description of **what you're solving**, and why it's important.
|
||||||
|
- Include **tests**.
|
||||||
|
- If it changes the UI, attach **screenshots** or screen recordings.
|
||||||
|
- Make the PR about **one thing only**, e.g. if it's a bugfix, don't add two
|
||||||
|
features and a refactoring on top of that.
|
||||||
|
- Keep AI assistance under your judgement and responsibility: it's unlikely
|
||||||
|
we'll merge a vibe-coded PR that the author doesn't understand.
|
||||||
|
|
||||||
The internal advice for reviewers is as follows:
|
The internal advice for reviewers is as follows:
|
||||||
|
|
||||||
@@ -50,10 +60,9 @@ The internal advice for reviewers is as follows:
|
|||||||
If you need more feedback from us: the best way is to be responsive to
|
If you need more feedback from us: the best way is to be responsive to
|
||||||
Github comments, or to offer up time to pair with us.
|
Github comments, or to offer up time to pair with us.
|
||||||
|
|
||||||
If you are making a larger change, or need advice on how to finish the change
|
If you need help deciding how to fix a bug, or finish implementing a feature
|
||||||
you're making, please open the PR early. We would love to help you get
|
that we've agreed we want, please open a PR early so we can discuss how to make
|
||||||
things right, and it's often easier to see how to solve a problem before the
|
the change with code in hand.
|
||||||
diff gets too big.
|
|
||||||
|
|
||||||
## Things we will (probably) not merge
|
## Things we will (probably) not merge
|
||||||
|
|
||||||
@@ -61,11 +70,11 @@ Although there are few hard and fast rules, typically we don't merge:
|
|||||||
|
|
||||||
- Anything that can be provided by an extension. For example a new language, or theme. For adding themes or support for a new language to Zed, check out our [docs on developing extensions](https://zed.dev/docs/extensions/developing-extensions).
|
- Anything that can be provided by an extension. For example a new language, or theme. For adding themes or support for a new language to Zed, check out our [docs on developing extensions](https://zed.dev/docs/extensions/developing-extensions).
|
||||||
- New file icons. Zed's default icon theme consists of icons that are hand-designed to fit together in a cohesive manner, please don't submit PRs with off-the-shelf SVGs.
|
- New file icons. Zed's default icon theme consists of icons that are hand-designed to fit together in a cohesive manner, please don't submit PRs with off-the-shelf SVGs.
|
||||||
|
- Features where (in our subjective opinion) the extra complexity isn't worth it for the number of people who will benefit.
|
||||||
- Giant refactorings.
|
- Giant refactorings.
|
||||||
- Non-trivial changes with no tests.
|
- Non-trivial changes with no tests.
|
||||||
- Stylistic code changes that do not alter any app logic. Reducing allocations, removing `.unwrap()`s, fixing typos is great; making code "more readable" — maybe not so much.
|
- Stylistic code changes that do not alter any app logic. Reducing allocations, removing `.unwrap()`s, fixing typos is great; making code "more readable" — maybe not so much.
|
||||||
- Features where (in our subjective opinion) the extra complexity isn't worth it for the number of people who will benefit.
|
- Anything that seems AI-generated without understanding the output.
|
||||||
- Anything that seems completely AI generated.
|
|
||||||
|
|
||||||
## Bird's-eye view of Zed
|
## Bird's-eye view of Zed
|
||||||
|
|
||||||
|
|||||||
452
Cargo.lock
generated
452
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
17
Cargo.toml
17
Cargo.toml
@@ -9,6 +9,7 @@ members = [
|
|||||||
"crates/agent_servers",
|
"crates/agent_servers",
|
||||||
"crates/agent_settings",
|
"crates/agent_settings",
|
||||||
"crates/agent_ui",
|
"crates/agent_ui",
|
||||||
|
"crates/agent_ui_v2",
|
||||||
"crates/ai_onboarding",
|
"crates/ai_onboarding",
|
||||||
"crates/anthropic",
|
"crates/anthropic",
|
||||||
"crates/askpass",
|
"crates/askpass",
|
||||||
@@ -32,7 +33,6 @@ members = [
|
|||||||
"crates/cloud_api_client",
|
"crates/cloud_api_client",
|
||||||
"crates/cloud_api_types",
|
"crates/cloud_api_types",
|
||||||
"crates/cloud_llm_client",
|
"crates/cloud_llm_client",
|
||||||
"crates/cloud_zeta2_prompt",
|
|
||||||
"crates/collab",
|
"crates/collab",
|
||||||
"crates/collab_ui",
|
"crates/collab_ui",
|
||||||
"crates/collections",
|
"crates/collections",
|
||||||
@@ -202,6 +202,7 @@ members = [
|
|||||||
"crates/zed_actions",
|
"crates/zed_actions",
|
||||||
"crates/zed_env_vars",
|
"crates/zed_env_vars",
|
||||||
"crates/edit_prediction_cli",
|
"crates/edit_prediction_cli",
|
||||||
|
"crates/zeta_prompt",
|
||||||
"crates/zlog",
|
"crates/zlog",
|
||||||
"crates/zlog_settings",
|
"crates/zlog_settings",
|
||||||
"crates/ztracing",
|
"crates/ztracing",
|
||||||
@@ -242,6 +243,7 @@ action_log = { path = "crates/action_log" }
|
|||||||
agent = { path = "crates/agent" }
|
agent = { path = "crates/agent" }
|
||||||
activity_indicator = { path = "crates/activity_indicator" }
|
activity_indicator = { path = "crates/activity_indicator" }
|
||||||
agent_ui = { path = "crates/agent_ui" }
|
agent_ui = { path = "crates/agent_ui" }
|
||||||
|
agent_ui_v2 = { path = "crates/agent_ui_v2" }
|
||||||
agent_settings = { path = "crates/agent_settings" }
|
agent_settings = { path = "crates/agent_settings" }
|
||||||
agent_servers = { path = "crates/agent_servers" }
|
agent_servers = { path = "crates/agent_servers" }
|
||||||
ai_onboarding = { path = "crates/ai_onboarding" }
|
ai_onboarding = { path = "crates/ai_onboarding" }
|
||||||
@@ -266,7 +268,6 @@ clock = { path = "crates/clock" }
|
|||||||
cloud_api_client = { path = "crates/cloud_api_client" }
|
cloud_api_client = { path = "crates/cloud_api_client" }
|
||||||
cloud_api_types = { path = "crates/cloud_api_types" }
|
cloud_api_types = { path = "crates/cloud_api_types" }
|
||||||
cloud_llm_client = { path = "crates/cloud_llm_client" }
|
cloud_llm_client = { path = "crates/cloud_llm_client" }
|
||||||
cloud_zeta2_prompt = { path = "crates/cloud_zeta2_prompt" }
|
|
||||||
collab_ui = { path = "crates/collab_ui" }
|
collab_ui = { path = "crates/collab_ui" }
|
||||||
collections = { path = "crates/collections", version = "0.1.0" }
|
collections = { path = "crates/collections", version = "0.1.0" }
|
||||||
command_palette = { path = "crates/command_palette" }
|
command_palette = { path = "crates/command_palette" }
|
||||||
@@ -425,6 +426,7 @@ zed = { path = "crates/zed" }
|
|||||||
zed_actions = { path = "crates/zed_actions" }
|
zed_actions = { path = "crates/zed_actions" }
|
||||||
zed_env_vars = { path = "crates/zed_env_vars" }
|
zed_env_vars = { path = "crates/zed_env_vars" }
|
||||||
edit_prediction = { path = "crates/edit_prediction" }
|
edit_prediction = { path = "crates/edit_prediction" }
|
||||||
|
zeta_prompt = { path = "crates/zeta_prompt" }
|
||||||
zlog = { path = "crates/zlog" }
|
zlog = { path = "crates/zlog" }
|
||||||
zlog_settings = { path = "crates/zlog_settings" }
|
zlog_settings = { path = "crates/zlog_settings" }
|
||||||
ztracing = { path = "crates/ztracing" }
|
ztracing = { path = "crates/ztracing" }
|
||||||
@@ -631,7 +633,7 @@ shellexpand = "2.1.0"
|
|||||||
shlex = "1.3.0"
|
shlex = "1.3.0"
|
||||||
simplelog = "0.12.2"
|
simplelog = "0.12.2"
|
||||||
slotmap = "1.0.6"
|
slotmap = "1.0.6"
|
||||||
smallvec = { version = "1.6", features = ["union"] }
|
smallvec = { version = "1.6", features = ["union", "const_new"] }
|
||||||
smol = "2.0"
|
smol = "2.0"
|
||||||
sqlformat = "0.2"
|
sqlformat = "0.2"
|
||||||
stacksafe = "0.1"
|
stacksafe = "0.1"
|
||||||
@@ -657,10 +659,11 @@ time = { version = "0.3", features = [
|
|||||||
tiny_http = "0.8"
|
tiny_http = "0.8"
|
||||||
tokio = { version = "1" }
|
tokio = { version = "1" }
|
||||||
tokio-tungstenite = { version = "0.26", features = ["__rustls-tls"] }
|
tokio-tungstenite = { version = "0.26", features = ["__rustls-tls"] }
|
||||||
|
tokio-socks = { version = "0.5.2", default-features = false, features = ["futures-io", "tokio"] }
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
toml_edit = { version = "0.22", default-features = false, features = ["display", "parse", "serde"] }
|
toml_edit = { version = "0.22", default-features = false, features = ["display", "parse", "serde"] }
|
||||||
tower-http = "0.4.4"
|
tower-http = "0.4.4"
|
||||||
tree-sitter = { version = "0.25.10", features = ["wasm"] }
|
tree-sitter = { version = "0.26", features = ["wasm"] }
|
||||||
tree-sitter-bash = "0.25.1"
|
tree-sitter-bash = "0.25.1"
|
||||||
tree-sitter-c = "0.23"
|
tree-sitter-c = "0.23"
|
||||||
tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "5cb9b693cfd7bfacab1d9ff4acac1a4150700609" }
|
tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "5cb9b693cfd7bfacab1d9ff4acac1a4150700609" }
|
||||||
@@ -694,7 +697,7 @@ uuid = { version = "1.1.2", features = ["v4", "v5", "v7", "serde"] }
|
|||||||
walkdir = "2.5"
|
walkdir = "2.5"
|
||||||
wasm-encoder = "0.221"
|
wasm-encoder = "0.221"
|
||||||
wasmparser = "0.221"
|
wasmparser = "0.221"
|
||||||
wasmtime = { version = "29", default-features = false, features = [
|
wasmtime = { version = "33", default-features = false, features = [
|
||||||
"async",
|
"async",
|
||||||
"demangle",
|
"demangle",
|
||||||
"runtime",
|
"runtime",
|
||||||
@@ -703,7 +706,7 @@ wasmtime = { version = "29", default-features = false, features = [
|
|||||||
"incremental-cache",
|
"incremental-cache",
|
||||||
"parallel-compilation",
|
"parallel-compilation",
|
||||||
] }
|
] }
|
||||||
wasmtime-wasi = "29"
|
wasmtime-wasi = "33"
|
||||||
wax = "0.6"
|
wax = "0.6"
|
||||||
which = "6.0.0"
|
which = "6.0.0"
|
||||||
windows-core = "0.61"
|
windows-core = "0.61"
|
||||||
@@ -854,8 +857,6 @@ unexpected_cfgs = { level = "allow" }
|
|||||||
dbg_macro = "deny"
|
dbg_macro = "deny"
|
||||||
todo = "deny"
|
todo = "deny"
|
||||||
|
|
||||||
# This is not a style lint, see https://github.com/rust-lang/rust-clippy/pull/15454
|
|
||||||
# Remove when the lint gets promoted to `suspicious`.
|
|
||||||
declare_interior_mutable_const = "deny"
|
declare_interior_mutable_const = "deny"
|
||||||
|
|
||||||
redundant_clone = "deny"
|
redundant_clone = "deny"
|
||||||
|
|||||||
@@ -34,8 +34,4 @@ RUN apt-get update; \
|
|||||||
linux-perf binutils
|
linux-perf binutils
|
||||||
WORKDIR app
|
WORKDIR app
|
||||||
COPY --from=builder /app/collab /app/collab
|
COPY --from=builder /app/collab /app/collab
|
||||||
COPY --from=builder /app/crates/collab/migrations /app/migrations
|
|
||||||
COPY --from=builder /app/crates/collab/migrations_llm /app/migrations_llm
|
|
||||||
ENV MIGRATIONS_PATH=/app/migrations
|
|
||||||
ENV LLM_DATABASE_MIGRATIONS_PATH=/app/migrations_llm
|
|
||||||
ENTRYPOINT ["/app/collab"]
|
ENTRYPOINT ["/app/collab"]
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ Welcome to Zed, a high-performance, multiplayer code editor from the creators of
|
|||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
On macOS, Linux, and Windows you can [download Zed directly](https://zed.dev/download) or [install Zed via your local package manager](https://zed.dev/docs/linux#installing-via-a-package-manager).
|
On macOS, Linux, and Windows you can [download Zed directly](https://zed.dev/download) or install Zed via your local package manager ([macOS](https://zed.dev/docs/installation#macos)/[Linux](https://zed.dev/docs/linux#installing-via-a-package-manager)/[Windows](https://zed.dev/docs/windows#package-managers)).
|
||||||
|
|
||||||
Other platforms are not yet available:
|
Other platforms are not yet available:
|
||||||
|
|
||||||
|
|||||||
5
assets/icons/box.svg
Normal file
5
assets/icons/box.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 d="M13.3996 5.59852C13.3994 5.3881 13.3439 5.18144 13.2386 4.99926C13.1333 4.81709 12.9819 4.66581 12.7997 4.56059L8.59996 2.16076C8.41755 2.05544 8.21063 2 8 2C7.78937 2 7.58246 2.05544 7.40004 2.16076L3.20033 4.56059C3.0181 4.66581 2.86674 4.81709 2.76144 4.99926C2.65613 5.18144 2.60059 5.3881 2.60037 5.59852V10.3982C2.60059 10.6086 2.65613 10.8153 2.76144 10.9975C2.86674 11.1796 3.0181 11.3309 3.20033 11.4361L7.40004 13.836C7.58246 13.9413 7.78937 13.9967 8 13.9967C8.21063 13.9967 8.41755 13.9413 8.59996 13.836L12.7997 11.4361C12.9819 11.3309 13.1333 11.1796 13.2386 10.9975C13.3439 10.8153 13.3994 10.6086 13.3996 10.3982V5.59852Z" stroke="white" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M2.78033 4.99857L7.99998 7.99836L13.2196 4.99857" stroke="white" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M8 13.9979V7.99829" stroke="white" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
5
assets/icons/zed_agent_two.svg
Normal file
5
assets/icons/zed_agent_two.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 d="M6.2224 1.32129L5.2036 4.41875C5.15145 4.57727 5.06282 4.72134 4.94481 4.83934C4.82681 4.95735 4.68274 5.04598 4.52422 5.09813L1.42676 6.11693L4.52422 7.13574C4.68274 7.18788 4.82681 7.27652 4.94481 7.39453C5.06282 7.51253 5.15145 7.6566 5.2036 7.81512L6.2224 10.9126L7.24121 7.81512C7.29335 7.6566 7.38199 7.51253 7.5 7.39453C7.618 7.27652 7.76207 7.18788 7.9206 7.13574L11.018 6.11693L7.9206 5.09813C7.76207 5.04598 7.618 4.95735 7.5 4.83934C7.38199 4.72134 7.29335 4.57727 7.24121 4.41875L6.2224 1.32129Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M9.76681 13.9373C9.76681 13.6048 9.95997 13.3083 10.5126 12.7917L11.8872 11.4978C12.3545 11.0575 12.5612 10.77 12.5612 10.4735C12.5612 10.1411 12.3185 9.91643 11.9681 9.91643C11.6986 9.91643 11.5054 10.0242 11.2673 10.3208C10.9933 10.6622 10.7956 10.779 10.4946 10.779C10.0633 10.779 9.75781 10.4915 9.75781 10.0916C9.75781 9.21559 10.8136 8.44287 12.067 8.44287C13.3743 8.44287 14.3492 9.22907 14.3492 10.2848C14.3492 10.9452 13.9988 11.5742 13.2845 12.2077L12.2242 13.1511V13.223H13.7292C14.2503 13.223 14.5738 13.5015 14.5738 13.9552C14.5738 14.4089 14.2593 14.6785 13.7292 14.6785H10.5979C10.1037 14.6785 9.76681 14.3775 9.76681 13.9373Z" fill="black"/>
|
||||||
|
<path d="M12.8994 1.32129V4.00482M11.5576 2.66302H14.2412" stroke="black" stroke-opacity="0.75" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -10,12 +10,12 @@
|
|||||||
"context": "Workspace",
|
"context": "Workspace",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
// "shift shift": "file_finder::Toggle"
|
// "shift shift": "file_finder::Toggle"
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && vim_mode == insert",
|
"context": "Editor && vim_mode == insert",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
// "j k": "vim::NormalBefore"
|
// "j k": "vim::NormalBefore"
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -4,15 +4,15 @@
|
|||||||
"bindings": {
|
"bindings": {
|
||||||
"ctrl-shift-f5": "workspace::Reload", // window:reload
|
"ctrl-shift-f5": "workspace::Reload", // window:reload
|
||||||
"ctrl-k ctrl-n": "workspace::ActivatePreviousPane", // window:focus-next-pane
|
"ctrl-k ctrl-n": "workspace::ActivatePreviousPane", // window:focus-next-pane
|
||||||
"ctrl-k ctrl-p": "workspace::ActivateNextPane" // window:focus-previous-pane
|
"ctrl-k ctrl-p": "workspace::ActivateNextPane", // window:focus-previous-pane
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor",
|
"context": "Editor",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"ctrl-k ctrl-u": "editor::ConvertToUpperCase", // editor:upper-case
|
"ctrl-k ctrl-u": "editor::ConvertToUpperCase", // editor:upper-case
|
||||||
"ctrl-k ctrl-l": "editor::ConvertToLowerCase" // editor:lower-case
|
"ctrl-k ctrl-l": "editor::ConvertToLowerCase", // editor:lower-case
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && mode == full",
|
"context": "Editor && mode == full",
|
||||||
@@ -32,8 +32,8 @@
|
|||||||
"ctrl-down": "editor::MoveLineDown", // editor:move-line-down
|
"ctrl-down": "editor::MoveLineDown", // editor:move-line-down
|
||||||
"ctrl-\\": "workspace::ToggleLeftDock", // tree-view:toggle
|
"ctrl-\\": "workspace::ToggleLeftDock", // tree-view:toggle
|
||||||
"ctrl-shift-m": "markdown::OpenPreviewToTheSide", // markdown-preview:toggle
|
"ctrl-shift-m": "markdown::OpenPreviewToTheSide", // markdown-preview:toggle
|
||||||
"ctrl-r": "outline::Toggle" // symbols-view:toggle-project-symbols
|
"ctrl-r": "outline::Toggle", // symbols-view:toggle-project-symbols
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "BufferSearchBar",
|
"context": "BufferSearchBar",
|
||||||
@@ -41,8 +41,8 @@
|
|||||||
"f3": ["editor::SelectNext", { "replace_newest": true }], // find-and-replace:find-next
|
"f3": ["editor::SelectNext", { "replace_newest": true }], // find-and-replace:find-next
|
||||||
"shift-f3": ["editor::SelectPrevious", { "replace_newest": true }], //find-and-replace:find-previous
|
"shift-f3": ["editor::SelectPrevious", { "replace_newest": true }], //find-and-replace:find-previous
|
||||||
"ctrl-f3": "search::SelectNextMatch", // find-and-replace:find-next-selected
|
"ctrl-f3": "search::SelectNextMatch", // find-and-replace:find-next-selected
|
||||||
"ctrl-shift-f3": "search::SelectPreviousMatch" // find-and-replace:find-previous-selected
|
"ctrl-shift-f3": "search::SelectPreviousMatch", // find-and-replace:find-previous-selected
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Workspace",
|
"context": "Workspace",
|
||||||
@@ -50,8 +50,8 @@
|
|||||||
"ctrl-\\": "workspace::ToggleLeftDock", // tree-view:toggle
|
"ctrl-\\": "workspace::ToggleLeftDock", // tree-view:toggle
|
||||||
"ctrl-k ctrl-b": "workspace::ToggleLeftDock", // tree-view:toggle
|
"ctrl-k ctrl-b": "workspace::ToggleLeftDock", // tree-view:toggle
|
||||||
"ctrl-t": "file_finder::Toggle", // fuzzy-finder:toggle-file-finder
|
"ctrl-t": "file_finder::Toggle", // fuzzy-finder:toggle-file-finder
|
||||||
"ctrl-r": "project_symbols::Toggle" // symbols-view:toggle-project-symbols
|
"ctrl-r": "project_symbols::Toggle", // symbols-view:toggle-project-symbols
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Pane",
|
"context": "Pane",
|
||||||
@@ -65,8 +65,8 @@
|
|||||||
"ctrl-6": ["pane::ActivateItem", 5], // tree-view:open-selected-entry-in-pane-6
|
"ctrl-6": ["pane::ActivateItem", 5], // tree-view:open-selected-entry-in-pane-6
|
||||||
"ctrl-7": ["pane::ActivateItem", 6], // tree-view:open-selected-entry-in-pane-7
|
"ctrl-7": ["pane::ActivateItem", 6], // tree-view:open-selected-entry-in-pane-7
|
||||||
"ctrl-8": ["pane::ActivateItem", 7], // tree-view:open-selected-entry-in-pane-8
|
"ctrl-8": ["pane::ActivateItem", 7], // tree-view:open-selected-entry-in-pane-8
|
||||||
"ctrl-9": ["pane::ActivateItem", 8] // tree-view:open-selected-entry-in-pane-9
|
"ctrl-9": ["pane::ActivateItem", 8], // tree-view:open-selected-entry-in-pane-9
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "ProjectPanel",
|
"context": "ProjectPanel",
|
||||||
@@ -75,8 +75,8 @@
|
|||||||
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
|
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
|
||||||
"ctrl-x": "project_panel::Cut", // tree-view:cut
|
"ctrl-x": "project_panel::Cut", // tree-view:cut
|
||||||
"ctrl-c": "project_panel::Copy", // tree-view:copy
|
"ctrl-c": "project_panel::Copy", // tree-view:copy
|
||||||
"ctrl-v": "project_panel::Paste" // tree-view:paste
|
"ctrl-v": "project_panel::Paste", // tree-view:paste
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "ProjectPanel && not_editing",
|
"context": "ProjectPanel && not_editing",
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
"d": "project_panel::Duplicate", // tree-view:duplicate
|
"d": "project_panel::Duplicate", // tree-view:duplicate
|
||||||
"home": "menu::SelectFirst", // core:move-to-top
|
"home": "menu::SelectFirst", // core:move-to-top
|
||||||
"end": "menu::SelectLast", // core:move-to-bottom
|
"end": "menu::SelectLast", // core:move-to-bottom
|
||||||
"shift-a": "project_panel::NewDirectory" // tree-view:add-folder
|
"shift-a": "project_panel::NewDirectory", // tree-view:add-folder
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
"ctrl-shift-i": "agent::ToggleFocus",
|
"ctrl-shift-i": "agent::ToggleFocus",
|
||||||
"ctrl-l": "agent::ToggleFocus",
|
"ctrl-l": "agent::ToggleFocus",
|
||||||
"ctrl-shift-l": "agent::ToggleFocus",
|
"ctrl-shift-l": "agent::ToggleFocus",
|
||||||
"ctrl-shift-j": "agent::OpenSettings"
|
"ctrl-shift-j": "agent::OpenSettings",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && mode == full",
|
"context": "Editor && mode == full",
|
||||||
@@ -20,18 +20,18 @@
|
|||||||
"ctrl-shift-l": "agent::AddSelectionToThread", // In cursor uses "Ask" mode
|
"ctrl-shift-l": "agent::AddSelectionToThread", // In cursor uses "Ask" mode
|
||||||
"ctrl-l": "agent::AddSelectionToThread", // In cursor uses "Agent" mode
|
"ctrl-l": "agent::AddSelectionToThread", // In cursor uses "Agent" mode
|
||||||
"ctrl-k": "assistant::InlineAssist",
|
"ctrl-k": "assistant::InlineAssist",
|
||||||
"ctrl-shift-k": "assistant::InsertIntoEditor"
|
"ctrl-shift-k": "assistant::InsertIntoEditor",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "InlineAssistEditor",
|
"context": "InlineAssistEditor",
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"ctrl-shift-backspace": "editor::Cancel"
|
"ctrl-shift-backspace": "editor::Cancel",
|
||||||
// "alt-enter": // Quick Question
|
// "alt-enter": // Quick Question
|
||||||
// "ctrl-shift-enter": // Full File Context
|
// "ctrl-shift-enter": // Full File Context
|
||||||
// "ctrl-shift-k": // Toggle input focus (editor <> inline assist)
|
// "ctrl-shift-k": // Toggle input focus (editor <> inline assist)
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "AgentPanel || ContextEditor || (MessageEditor > Editor)",
|
"context": "AgentPanel || ContextEditor || (MessageEditor > Editor)",
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
"ctrl-shift-backspace": "editor::Cancel",
|
"ctrl-shift-backspace": "editor::Cancel",
|
||||||
"ctrl-r": "agent::NewThread",
|
"ctrl-r": "agent::NewThread",
|
||||||
"ctrl-shift-v": "editor::Paste",
|
"ctrl-shift-v": "editor::Paste",
|
||||||
"ctrl-shift-k": "assistant::InsertIntoEditor"
|
"ctrl-shift-k": "assistant::InsertIntoEditor",
|
||||||
// "escape": "agent::ToggleFocus"
|
// "escape": "agent::ToggleFocus"
|
||||||
///// Enable when Zed supports multiple thread tabs
|
///// Enable when Zed supports multiple thread tabs
|
||||||
// "ctrl-t": // new thread tab
|
// "ctrl-t": // new thread tab
|
||||||
@@ -56,28 +56,29 @@
|
|||||||
///// Enable if Zed adds support for keyboard navigation of thread elements
|
///// Enable if Zed adds support for keyboard navigation of thread elements
|
||||||
// "tab": // cycle to next message
|
// "tab": // cycle to next message
|
||||||
// "shift-tab": // cycle to previous message
|
// "shift-tab": // cycle to previous message
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && editor_agent_diff",
|
"context": "Editor && editor_agent_diff",
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"ctrl-enter": "agent::KeepAll",
|
"ctrl-enter": "agent::KeepAll",
|
||||||
"ctrl-backspace": "agent::RejectAll"
|
"ctrl-backspace": "agent::RejectAll",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && mode == full && edit_prediction",
|
"context": "Editor && mode == full && edit_prediction",
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"ctrl-right": "editor::AcceptPartialEditPrediction"
|
"ctrl-right": "editor::AcceptNextWordEditPrediction",
|
||||||
}
|
"ctrl-down": "editor::AcceptNextLineEditPrediction",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Terminal",
|
"context": "Terminal",
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"ctrl-k": "assistant::InlineAssist"
|
"ctrl-k": "assistant::InlineAssist",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"ctrl-g": "menu::Cancel"
|
"ctrl-g": "menu::Cancel",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Workaround to avoid falling back to default bindings.
|
// Workaround to avoid falling back to default bindings.
|
||||||
@@ -18,8 +18,8 @@
|
|||||||
"ctrl-g": null, // currently activates `go_to_line::Toggle` when there is nothing to cancel
|
"ctrl-g": null, // currently activates `go_to_line::Toggle` when there is nothing to cancel
|
||||||
"ctrl-x": null, // currently activates `editor::Cut` if no following key is pressed for 1 second
|
"ctrl-x": null, // currently activates `editor::Cut` if no following key is pressed for 1 second
|
||||||
"ctrl-p": null, // currently activates `file_finder::Toggle` when the cursor is on the first character of the buffer
|
"ctrl-p": null, // currently activates `file_finder::Toggle` when the cursor is on the first character of the buffer
|
||||||
"ctrl-n": null // currently activates `workspace::NewFile` when the cursor is on the last character of the buffer
|
"ctrl-n": null, // currently activates `workspace::NewFile` when the cursor is on the last character of the buffer
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor",
|
"context": "Editor",
|
||||||
@@ -82,8 +82,8 @@
|
|||||||
"ctrl-s": "buffer_search::Deploy", // isearch-forward
|
"ctrl-s": "buffer_search::Deploy", // isearch-forward
|
||||||
"ctrl-r": "buffer_search::Deploy", // isearch-backward
|
"ctrl-r": "buffer_search::Deploy", // isearch-backward
|
||||||
"alt-^": "editor::JoinLines", // join-line
|
"alt-^": "editor::JoinLines", // join-line
|
||||||
"alt-q": "editor::Rewrap" // fill-paragraph
|
"alt-q": "editor::Rewrap", // fill-paragraph
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && selection_mode", // region selection
|
"context": "Editor && selection_mode", // region selection
|
||||||
@@ -119,22 +119,22 @@
|
|||||||
"alt->": "editor::SelectToEnd",
|
"alt->": "editor::SelectToEnd",
|
||||||
"ctrl-home": "editor::SelectToBeginning",
|
"ctrl-home": "editor::SelectToBeginning",
|
||||||
"ctrl-end": "editor::SelectToEnd",
|
"ctrl-end": "editor::SelectToEnd",
|
||||||
"ctrl-g": "editor::Cancel"
|
"ctrl-g": "editor::Cancel",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && (showing_code_actions || showing_completions)",
|
"context": "Editor && (showing_code_actions || showing_completions)",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"ctrl-p": "editor::ContextMenuPrevious",
|
"ctrl-p": "editor::ContextMenuPrevious",
|
||||||
"ctrl-n": "editor::ContextMenuNext"
|
"ctrl-n": "editor::ContextMenuNext",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && showing_signature_help && !showing_completions",
|
"context": "Editor && showing_signature_help && !showing_completions",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"ctrl-p": "editor::SignatureHelpPrevious",
|
"ctrl-p": "editor::SignatureHelpPrevious",
|
||||||
"ctrl-n": "editor::SignatureHelpNext"
|
"ctrl-n": "editor::SignatureHelpNext",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
// Example setting for using emacs-style tab
|
// Example setting for using emacs-style tab
|
||||||
// (i.e. indent the current line / selection or perform symbol completion depending on context)
|
// (i.e. indent the current line / selection or perform symbol completion depending on context)
|
||||||
@@ -164,8 +164,8 @@
|
|||||||
"ctrl-x ctrl-f": "file_finder::Toggle", // find-file
|
"ctrl-x ctrl-f": "file_finder::Toggle", // find-file
|
||||||
"ctrl-x ctrl-s": "workspace::Save", // save-buffer
|
"ctrl-x ctrl-s": "workspace::Save", // save-buffer
|
||||||
"ctrl-x ctrl-w": "workspace::SaveAs", // write-file
|
"ctrl-x ctrl-w": "workspace::SaveAs", // write-file
|
||||||
"ctrl-x s": "workspace::SaveAll" // save-some-buffers
|
"ctrl-x s": "workspace::SaveAll", // save-some-buffers
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Workaround to enable using native emacs from the Zed terminal.
|
// Workaround to enable using native emacs from the Zed terminal.
|
||||||
@@ -185,22 +185,22 @@
|
|||||||
"ctrl-x ctrl-f": null, // find-file
|
"ctrl-x ctrl-f": null, // find-file
|
||||||
"ctrl-x ctrl-s": null, // save-buffer
|
"ctrl-x ctrl-s": null, // save-buffer
|
||||||
"ctrl-x ctrl-w": null, // write-file
|
"ctrl-x ctrl-w": null, // write-file
|
||||||
"ctrl-x s": null // save-some-buffers
|
"ctrl-x s": null, // save-some-buffers
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "BufferSearchBar > Editor",
|
"context": "BufferSearchBar > Editor",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"ctrl-s": "search::SelectNextMatch",
|
"ctrl-s": "search::SelectNextMatch",
|
||||||
"ctrl-r": "search::SelectPreviousMatch",
|
"ctrl-r": "search::SelectPreviousMatch",
|
||||||
"ctrl-g": "buffer_search::Dismiss"
|
"ctrl-g": "buffer_search::Dismiss",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Pane",
|
"context": "Pane",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"ctrl-alt-left": "pane::GoBack",
|
"ctrl-alt-left": "pane::GoBack",
|
||||||
"ctrl-alt-right": "pane::GoForward"
|
"ctrl-alt-right": "pane::GoForward",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -13,8 +13,8 @@
|
|||||||
"shift-f8": "debugger::StepOut",
|
"shift-f8": "debugger::StepOut",
|
||||||
"f9": "debugger::Continue",
|
"f9": "debugger::Continue",
|
||||||
"shift-f9": "debugger::Start",
|
"shift-f9": "debugger::Start",
|
||||||
"alt-shift-f9": "debugger::Start"
|
"alt-shift-f9": "debugger::Start",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor",
|
"context": "Editor",
|
||||||
@@ -62,28 +62,30 @@
|
|||||||
"ctrl-shift-end": "editor::SelectToEnd",
|
"ctrl-shift-end": "editor::SelectToEnd",
|
||||||
"ctrl-f8": "editor::ToggleBreakpoint",
|
"ctrl-f8": "editor::ToggleBreakpoint",
|
||||||
"ctrl-shift-f8": "editor::EditLogBreakpoint",
|
"ctrl-shift-f8": "editor::EditLogBreakpoint",
|
||||||
"ctrl-shift-u": "editor::ToggleCase"
|
"ctrl-shift-u": "editor::ToggleCase",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && mode == full",
|
"context": "Editor && mode == full",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"ctrl-f12": "outline::Toggle",
|
"ctrl-f12": "outline::Toggle",
|
||||||
"ctrl-r": ["buffer_search::Deploy", { "replace_enabled": true }],
|
"ctrl-r": ["buffer_search::Deploy", { "replace_enabled": true }],
|
||||||
|
"ctrl-e": "file_finder::Toggle",
|
||||||
"ctrl-shift-n": "file_finder::Toggle",
|
"ctrl-shift-n": "file_finder::Toggle",
|
||||||
|
"ctrl-alt-n": "file_finder::Toggle",
|
||||||
"ctrl-g": "go_to_line::Toggle",
|
"ctrl-g": "go_to_line::Toggle",
|
||||||
"alt-enter": "editor::ToggleCodeActions",
|
"alt-enter": "editor::ToggleCodeActions",
|
||||||
"ctrl-space": "editor::ShowCompletions",
|
"ctrl-space": "editor::ShowCompletions",
|
||||||
"ctrl-q": "editor::Hover",
|
"ctrl-q": "editor::Hover",
|
||||||
"ctrl-p": "editor::ShowSignatureHelp",
|
"ctrl-p": "editor::ShowSignatureHelp",
|
||||||
"ctrl-\\": "assistant::InlineAssist"
|
"ctrl-\\": "assistant::InlineAssist",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "BufferSearchBar",
|
"context": "BufferSearchBar",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"shift-enter": "search::SelectPreviousMatch"
|
"shift-enter": "search::SelectPreviousMatch",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "BufferSearchBar || ProjectSearchBar",
|
"context": "BufferSearchBar || ProjectSearchBar",
|
||||||
@@ -91,8 +93,8 @@
|
|||||||
"alt-c": "search::ToggleCaseSensitive",
|
"alt-c": "search::ToggleCaseSensitive",
|
||||||
"alt-e": "search::ToggleSelection",
|
"alt-e": "search::ToggleSelection",
|
||||||
"alt-x": "search::ToggleRegex",
|
"alt-x": "search::ToggleRegex",
|
||||||
"alt-w": "search::ToggleWholeWord"
|
"alt-w": "search::ToggleWholeWord",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Workspace",
|
"context": "Workspace",
|
||||||
@@ -105,8 +107,8 @@
|
|||||||
"ctrl-e": "file_finder::Toggle",
|
"ctrl-e": "file_finder::Toggle",
|
||||||
"ctrl-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor
|
"ctrl-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor
|
||||||
"ctrl-shift-n": "file_finder::Toggle",
|
"ctrl-shift-n": "file_finder::Toggle",
|
||||||
"ctrl-n": "project_symbols::Toggle",
|
|
||||||
"ctrl-alt-n": "file_finder::Toggle",
|
"ctrl-alt-n": "file_finder::Toggle",
|
||||||
|
"ctrl-n": "project_symbols::Toggle",
|
||||||
"ctrl-shift-a": "command_palette::Toggle",
|
"ctrl-shift-a": "command_palette::Toggle",
|
||||||
"shift shift": "command_palette::Toggle",
|
"shift shift": "command_palette::Toggle",
|
||||||
"ctrl-alt-shift-n": "project_symbols::Toggle",
|
"ctrl-alt-shift-n": "project_symbols::Toggle",
|
||||||
@@ -114,8 +116,8 @@
|
|||||||
"alt-1": "project_panel::ToggleFocus",
|
"alt-1": "project_panel::ToggleFocus",
|
||||||
"alt-5": "debug_panel::ToggleFocus",
|
"alt-5": "debug_panel::ToggleFocus",
|
||||||
"alt-6": "diagnostics::Deploy",
|
"alt-6": "diagnostics::Deploy",
|
||||||
"alt-7": "outline_panel::ToggleFocus"
|
"alt-7": "outline_panel::ToggleFocus",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Pane", // this is to override the default Pane mappings to switch tabs
|
"context": "Pane", // this is to override the default Pane mappings to switch tabs
|
||||||
@@ -129,15 +131,15 @@
|
|||||||
"alt-7": "outline_panel::ToggleFocus",
|
"alt-7": "outline_panel::ToggleFocus",
|
||||||
"alt-8": null, // Services (bottom dock)
|
"alt-8": null, // Services (bottom dock)
|
||||||
"alt-9": null, // Git History (bottom dock)
|
"alt-9": null, // Git History (bottom dock)
|
||||||
"alt-0": "git_panel::ToggleFocus"
|
"alt-0": "git_panel::ToggleFocus",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Workspace || Editor",
|
"context": "Workspace || Editor",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"alt-f12": "terminal_panel::Toggle",
|
"alt-f12": "terminal_panel::Toggle",
|
||||||
"ctrl-shift-k": "git::Push"
|
"ctrl-shift-k": "git::Push",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Pane",
|
"context": "Pane",
|
||||||
@@ -145,8 +147,8 @@
|
|||||||
"ctrl-alt-left": "pane::GoBack",
|
"ctrl-alt-left": "pane::GoBack",
|
||||||
"ctrl-alt-right": "pane::GoForward",
|
"ctrl-alt-right": "pane::GoForward",
|
||||||
"alt-left": "pane::ActivatePreviousItem",
|
"alt-left": "pane::ActivatePreviousItem",
|
||||||
"alt-right": "pane::ActivateNextItem"
|
"alt-right": "pane::ActivateNextItem",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "ProjectPanel",
|
"context": "ProjectPanel",
|
||||||
@@ -156,8 +158,8 @@
|
|||||||
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
|
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
|
||||||
"delete": ["project_panel::Trash", { "skip_prompt": false }],
|
"delete": ["project_panel::Trash", { "skip_prompt": false }],
|
||||||
"shift-delete": ["project_panel::Delete", { "skip_prompt": false }],
|
"shift-delete": ["project_panel::Delete", { "skip_prompt": false }],
|
||||||
"shift-f6": "project_panel::Rename"
|
"shift-f6": "project_panel::Rename",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Terminal",
|
"context": "Terminal",
|
||||||
@@ -167,8 +169,8 @@
|
|||||||
"ctrl-up": "terminal::ScrollLineUp",
|
"ctrl-up": "terminal::ScrollLineUp",
|
||||||
"ctrl-down": "terminal::ScrollLineDown",
|
"ctrl-down": "terminal::ScrollLineDown",
|
||||||
"shift-pageup": "terminal::ScrollPageUp",
|
"shift-pageup": "terminal::ScrollPageUp",
|
||||||
"shift-pagedown": "terminal::ScrollPageDown"
|
"shift-pagedown": "terminal::ScrollPageDown",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{ "context": "GitPanel", "bindings": { "alt-0": "workspace::CloseActiveDock" } },
|
{ "context": "GitPanel", "bindings": { "alt-0": "workspace::CloseActiveDock" } },
|
||||||
{ "context": "ProjectPanel", "bindings": { "alt-1": "workspace::CloseActiveDock" } },
|
{ "context": "ProjectPanel", "bindings": { "alt-1": "workspace::CloseActiveDock" } },
|
||||||
@@ -179,7 +181,7 @@
|
|||||||
"context": "Dock || Workspace || OutlinePanel || ProjectPanel || CollabPanel || (Editor && mode == auto_height)",
|
"context": "Dock || Workspace || OutlinePanel || ProjectPanel || CollabPanel || (Editor && mode == auto_height)",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"escape": "editor::ToggleFocus",
|
"escape": "editor::ToggleFocus",
|
||||||
"shift-escape": "workspace::CloseActiveDock"
|
"shift-escape": "workspace::CloseActiveDock",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -22,8 +22,8 @@
|
|||||||
"ctrl-^": ["workspace::MoveItemToPane", { "destination": 5 }],
|
"ctrl-^": ["workspace::MoveItemToPane", { "destination": 5 }],
|
||||||
"ctrl-&": ["workspace::MoveItemToPane", { "destination": 6 }],
|
"ctrl-&": ["workspace::MoveItemToPane", { "destination": 6 }],
|
||||||
"ctrl-*": ["workspace::MoveItemToPane", { "destination": 7 }],
|
"ctrl-*": ["workspace::MoveItemToPane", { "destination": 7 }],
|
||||||
"ctrl-(": ["workspace::MoveItemToPane", { "destination": 8 }]
|
"ctrl-(": ["workspace::MoveItemToPane", { "destination": 8 }],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor",
|
"context": "Editor",
|
||||||
@@ -55,20 +55,20 @@
|
|||||||
"alt-right": "editor::MoveToNextSubwordEnd",
|
"alt-right": "editor::MoveToNextSubwordEnd",
|
||||||
"alt-left": "editor::MoveToPreviousSubwordStart",
|
"alt-left": "editor::MoveToPreviousSubwordStart",
|
||||||
"alt-shift-right": "editor::SelectToNextSubwordEnd",
|
"alt-shift-right": "editor::SelectToNextSubwordEnd",
|
||||||
"alt-shift-left": "editor::SelectToPreviousSubwordStart"
|
"alt-shift-left": "editor::SelectToPreviousSubwordStart",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && mode == full",
|
"context": "Editor && mode == full",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"ctrl-r": "outline::Toggle"
|
"ctrl-r": "outline::Toggle",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && !agent_diff",
|
"context": "Editor && !agent_diff",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"ctrl-k ctrl-z": "git::Restore"
|
"ctrl-k ctrl-z": "git::Restore",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Pane",
|
"context": "Pane",
|
||||||
@@ -83,15 +83,15 @@
|
|||||||
"alt-6": ["pane::ActivateItem", 5],
|
"alt-6": ["pane::ActivateItem", 5],
|
||||||
"alt-7": ["pane::ActivateItem", 6],
|
"alt-7": ["pane::ActivateItem", 6],
|
||||||
"alt-8": ["pane::ActivateItem", 7],
|
"alt-8": ["pane::ActivateItem", 7],
|
||||||
"alt-9": "pane::ActivateLastItem"
|
"alt-9": "pane::ActivateLastItem",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Workspace",
|
"context": "Workspace",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"ctrl-k ctrl-b": "workspace::ToggleLeftDock",
|
"ctrl-k ctrl-b": "workspace::ToggleLeftDock",
|
||||||
// "ctrl-0": "project_panel::ToggleFocus", // normally resets zoom
|
// "ctrl-0": "project_panel::ToggleFocus", // normally resets zoom
|
||||||
"shift-ctrl-r": "project_symbols::Toggle"
|
"shift-ctrl-r": "project_symbols::Toggle",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -4,16 +4,16 @@
|
|||||||
"bindings": {
|
"bindings": {
|
||||||
"ctrl-alt-cmd-l": "workspace::Reload",
|
"ctrl-alt-cmd-l": "workspace::Reload",
|
||||||
"cmd-k cmd-p": "workspace::ActivatePreviousPane",
|
"cmd-k cmd-p": "workspace::ActivatePreviousPane",
|
||||||
"cmd-k cmd-n": "workspace::ActivateNextPane"
|
"cmd-k cmd-n": "workspace::ActivateNextPane",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor",
|
"context": "Editor",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"cmd-shift-backspace": "editor::DeleteToBeginningOfLine",
|
"cmd-shift-backspace": "editor::DeleteToBeginningOfLine",
|
||||||
"cmd-k cmd-u": "editor::ConvertToUpperCase",
|
"cmd-k cmd-u": "editor::ConvertToUpperCase",
|
||||||
"cmd-k cmd-l": "editor::ConvertToLowerCase"
|
"cmd-k cmd-l": "editor::ConvertToLowerCase",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && mode == full",
|
"context": "Editor && mode == full",
|
||||||
@@ -33,8 +33,8 @@
|
|||||||
"ctrl-cmd-down": "editor::MoveLineDown",
|
"ctrl-cmd-down": "editor::MoveLineDown",
|
||||||
"cmd-\\": "workspace::ToggleLeftDock",
|
"cmd-\\": "workspace::ToggleLeftDock",
|
||||||
"ctrl-shift-m": "markdown::OpenPreviewToTheSide",
|
"ctrl-shift-m": "markdown::OpenPreviewToTheSide",
|
||||||
"cmd-r": "outline::Toggle"
|
"cmd-r": "outline::Toggle",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "BufferSearchBar",
|
"context": "BufferSearchBar",
|
||||||
@@ -42,8 +42,8 @@
|
|||||||
"cmd-g": ["editor::SelectNext", { "replace_newest": true }],
|
"cmd-g": ["editor::SelectNext", { "replace_newest": true }],
|
||||||
"cmd-shift-g": ["editor::SelectPrevious", { "replace_newest": true }],
|
"cmd-shift-g": ["editor::SelectPrevious", { "replace_newest": true }],
|
||||||
"cmd-f3": "search::SelectNextMatch",
|
"cmd-f3": "search::SelectNextMatch",
|
||||||
"cmd-shift-f3": "search::SelectPreviousMatch"
|
"cmd-shift-f3": "search::SelectPreviousMatch",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Workspace",
|
"context": "Workspace",
|
||||||
@@ -51,8 +51,8 @@
|
|||||||
"cmd-\\": "workspace::ToggleLeftDock",
|
"cmd-\\": "workspace::ToggleLeftDock",
|
||||||
"cmd-k cmd-b": "workspace::ToggleLeftDock",
|
"cmd-k cmd-b": "workspace::ToggleLeftDock",
|
||||||
"cmd-t": "file_finder::Toggle",
|
"cmd-t": "file_finder::Toggle",
|
||||||
"cmd-shift-r": "project_symbols::Toggle"
|
"cmd-shift-r": "project_symbols::Toggle",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Pane",
|
"context": "Pane",
|
||||||
@@ -67,8 +67,8 @@
|
|||||||
"cmd-6": ["pane::ActivateItem", 5],
|
"cmd-6": ["pane::ActivateItem", 5],
|
||||||
"cmd-7": ["pane::ActivateItem", 6],
|
"cmd-7": ["pane::ActivateItem", 6],
|
||||||
"cmd-8": ["pane::ActivateItem", 7],
|
"cmd-8": ["pane::ActivateItem", 7],
|
||||||
"cmd-9": "pane::ActivateLastItem"
|
"cmd-9": "pane::ActivateLastItem",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "ProjectPanel",
|
"context": "ProjectPanel",
|
||||||
@@ -77,8 +77,8 @@
|
|||||||
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
|
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
|
||||||
"cmd-x": "project_panel::Cut",
|
"cmd-x": "project_panel::Cut",
|
||||||
"cmd-c": "project_panel::Copy",
|
"cmd-c": "project_panel::Copy",
|
||||||
"cmd-v": "project_panel::Paste"
|
"cmd-v": "project_panel::Paste",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "ProjectPanel && not_editing",
|
"context": "ProjectPanel && not_editing",
|
||||||
@@ -92,7 +92,7 @@
|
|||||||
"d": "project_panel::Duplicate",
|
"d": "project_panel::Duplicate",
|
||||||
"home": "menu::SelectFirst",
|
"home": "menu::SelectFirst",
|
||||||
"end": "menu::SelectLast",
|
"end": "menu::SelectLast",
|
||||||
"shift-a": "project_panel::NewDirectory"
|
"shift-a": "project_panel::NewDirectory",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
"cmd-shift-i": "agent::ToggleFocus",
|
"cmd-shift-i": "agent::ToggleFocus",
|
||||||
"cmd-l": "agent::ToggleFocus",
|
"cmd-l": "agent::ToggleFocus",
|
||||||
"cmd-shift-l": "agent::ToggleFocus",
|
"cmd-shift-l": "agent::ToggleFocus",
|
||||||
"cmd-shift-j": "agent::OpenSettings"
|
"cmd-shift-j": "agent::OpenSettings",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && mode == full",
|
"context": "Editor && mode == full",
|
||||||
@@ -20,19 +20,19 @@
|
|||||||
"cmd-shift-l": "agent::AddSelectionToThread", // In cursor uses "Ask" mode
|
"cmd-shift-l": "agent::AddSelectionToThread", // In cursor uses "Ask" mode
|
||||||
"cmd-l": "agent::AddSelectionToThread", // In cursor uses "Agent" mode
|
"cmd-l": "agent::AddSelectionToThread", // In cursor uses "Agent" mode
|
||||||
"cmd-k": "assistant::InlineAssist",
|
"cmd-k": "assistant::InlineAssist",
|
||||||
"cmd-shift-k": "assistant::InsertIntoEditor"
|
"cmd-shift-k": "assistant::InsertIntoEditor",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "InlineAssistEditor",
|
"context": "InlineAssistEditor",
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"cmd-shift-backspace": "editor::Cancel",
|
"cmd-shift-backspace": "editor::Cancel",
|
||||||
"cmd-enter": "menu::Confirm"
|
"cmd-enter": "menu::Confirm",
|
||||||
// "alt-enter": // Quick Question
|
// "alt-enter": // Quick Question
|
||||||
// "cmd-shift-enter": // Full File Context
|
// "cmd-shift-enter": // Full File Context
|
||||||
// "cmd-shift-k": // Toggle input focus (editor <> inline assist)
|
// "cmd-shift-k": // Toggle input focus (editor <> inline assist)
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "AgentPanel || ContextEditor || (MessageEditor > Editor)",
|
"context": "AgentPanel || ContextEditor || (MessageEditor > Editor)",
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
"cmd-shift-backspace": "editor::Cancel",
|
"cmd-shift-backspace": "editor::Cancel",
|
||||||
"cmd-r": "agent::NewThread",
|
"cmd-r": "agent::NewThread",
|
||||||
"cmd-shift-v": "editor::Paste",
|
"cmd-shift-v": "editor::Paste",
|
||||||
"cmd-shift-k": "assistant::InsertIntoEditor"
|
"cmd-shift-k": "assistant::InsertIntoEditor",
|
||||||
// "escape": "agent::ToggleFocus"
|
// "escape": "agent::ToggleFocus"
|
||||||
///// Enable when Zed supports multiple thread tabs
|
///// Enable when Zed supports multiple thread tabs
|
||||||
// "cmd-t": // new thread tab
|
// "cmd-t": // new thread tab
|
||||||
@@ -57,28 +57,29 @@
|
|||||||
///// Enable if Zed adds support for keyboard navigation of thread elements
|
///// Enable if Zed adds support for keyboard navigation of thread elements
|
||||||
// "tab": // cycle to next message
|
// "tab": // cycle to next message
|
||||||
// "shift-tab": // cycle to previous message
|
// "shift-tab": // cycle to previous message
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && editor_agent_diff",
|
"context": "Editor && editor_agent_diff",
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"cmd-enter": "agent::KeepAll",
|
"cmd-enter": "agent::KeepAll",
|
||||||
"cmd-backspace": "agent::RejectAll"
|
"cmd-backspace": "agent::RejectAll",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && mode == full && edit_prediction",
|
"context": "Editor && mode == full && edit_prediction",
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"cmd-right": "editor::AcceptPartialEditPrediction"
|
"cmd-right": "editor::AcceptNextWordEditPrediction",
|
||||||
}
|
"cmd-down": "editor::AcceptNextLineEditPrediction",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Terminal",
|
"context": "Terminal",
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"cmd-k": "assistant::InlineAssist"
|
"cmd-k": "assistant::InlineAssist",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
{
|
{
|
||||||
"context": "!GitPanel",
|
"context": "!GitPanel",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"ctrl-g": "menu::Cancel"
|
"ctrl-g": "menu::Cancel",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Workaround to avoid falling back to default bindings.
|
// Workaround to avoid falling back to default bindings.
|
||||||
@@ -15,8 +15,8 @@
|
|||||||
// NOTE: must be declared before the `Editor` override.
|
// NOTE: must be declared before the `Editor` override.
|
||||||
"context": "Editor",
|
"context": "Editor",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"ctrl-g": null // currently activates `go_to_line::Toggle` when there is nothing to cancel
|
"ctrl-g": null, // currently activates `go_to_line::Toggle` when there is nothing to cancel
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor",
|
"context": "Editor",
|
||||||
@@ -79,8 +79,8 @@
|
|||||||
"ctrl-s": "buffer_search::Deploy", // isearch-forward
|
"ctrl-s": "buffer_search::Deploy", // isearch-forward
|
||||||
"ctrl-r": "buffer_search::Deploy", // isearch-backward
|
"ctrl-r": "buffer_search::Deploy", // isearch-backward
|
||||||
"alt-^": "editor::JoinLines", // join-line
|
"alt-^": "editor::JoinLines", // join-line
|
||||||
"alt-q": "editor::Rewrap" // fill-paragraph
|
"alt-q": "editor::Rewrap", // fill-paragraph
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && selection_mode", // region selection
|
"context": "Editor && selection_mode", // region selection
|
||||||
@@ -116,22 +116,22 @@
|
|||||||
"alt->": "editor::SelectToEnd",
|
"alt->": "editor::SelectToEnd",
|
||||||
"ctrl-home": "editor::SelectToBeginning",
|
"ctrl-home": "editor::SelectToBeginning",
|
||||||
"ctrl-end": "editor::SelectToEnd",
|
"ctrl-end": "editor::SelectToEnd",
|
||||||
"ctrl-g": "editor::Cancel"
|
"ctrl-g": "editor::Cancel",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && (showing_code_actions || showing_completions)",
|
"context": "Editor && (showing_code_actions || showing_completions)",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"ctrl-p": "editor::ContextMenuPrevious",
|
"ctrl-p": "editor::ContextMenuPrevious",
|
||||||
"ctrl-n": "editor::ContextMenuNext"
|
"ctrl-n": "editor::ContextMenuNext",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && showing_signature_help && !showing_completions",
|
"context": "Editor && showing_signature_help && !showing_completions",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"ctrl-p": "editor::SignatureHelpPrevious",
|
"ctrl-p": "editor::SignatureHelpPrevious",
|
||||||
"ctrl-n": "editor::SignatureHelpNext"
|
"ctrl-n": "editor::SignatureHelpNext",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
// Example setting for using emacs-style tab
|
// Example setting for using emacs-style tab
|
||||||
// (i.e. indent the current line / selection or perform symbol completion depending on context)
|
// (i.e. indent the current line / selection or perform symbol completion depending on context)
|
||||||
@@ -161,8 +161,8 @@
|
|||||||
"ctrl-x ctrl-f": "file_finder::Toggle", // find-file
|
"ctrl-x ctrl-f": "file_finder::Toggle", // find-file
|
||||||
"ctrl-x ctrl-s": "workspace::Save", // save-buffer
|
"ctrl-x ctrl-s": "workspace::Save", // save-buffer
|
||||||
"ctrl-x ctrl-w": "workspace::SaveAs", // write-file
|
"ctrl-x ctrl-w": "workspace::SaveAs", // write-file
|
||||||
"ctrl-x s": "workspace::SaveAll" // save-some-buffers
|
"ctrl-x s": "workspace::SaveAll", // save-some-buffers
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Workaround to enable using native emacs from the Zed terminal.
|
// Workaround to enable using native emacs from the Zed terminal.
|
||||||
@@ -182,22 +182,22 @@
|
|||||||
"ctrl-x ctrl-f": null, // find-file
|
"ctrl-x ctrl-f": null, // find-file
|
||||||
"ctrl-x ctrl-s": null, // save-buffer
|
"ctrl-x ctrl-s": null, // save-buffer
|
||||||
"ctrl-x ctrl-w": null, // write-file
|
"ctrl-x ctrl-w": null, // write-file
|
||||||
"ctrl-x s": null // save-some-buffers
|
"ctrl-x s": null, // save-some-buffers
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "BufferSearchBar > Editor",
|
"context": "BufferSearchBar > Editor",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"ctrl-s": "search::SelectNextMatch",
|
"ctrl-s": "search::SelectNextMatch",
|
||||||
"ctrl-r": "search::SelectPreviousMatch",
|
"ctrl-r": "search::SelectPreviousMatch",
|
||||||
"ctrl-g": "buffer_search::Dismiss"
|
"ctrl-g": "buffer_search::Dismiss",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Pane",
|
"context": "Pane",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"ctrl-alt-left": "pane::GoBack",
|
"ctrl-alt-left": "pane::GoBack",
|
||||||
"ctrl-alt-right": "pane::GoForward"
|
"ctrl-alt-right": "pane::GoForward",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -13,8 +13,8 @@
|
|||||||
"shift-f8": "debugger::StepOut",
|
"shift-f8": "debugger::StepOut",
|
||||||
"f9": "debugger::Continue",
|
"f9": "debugger::Continue",
|
||||||
"shift-f9": "debugger::Start",
|
"shift-f9": "debugger::Start",
|
||||||
"alt-shift-f9": "debugger::Start"
|
"alt-shift-f9": "debugger::Start",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor",
|
"context": "Editor",
|
||||||
@@ -60,28 +60,30 @@
|
|||||||
"cmd-shift-end": "editor::SelectToEnd",
|
"cmd-shift-end": "editor::SelectToEnd",
|
||||||
"ctrl-f8": "editor::ToggleBreakpoint",
|
"ctrl-f8": "editor::ToggleBreakpoint",
|
||||||
"ctrl-shift-f8": "editor::EditLogBreakpoint",
|
"ctrl-shift-f8": "editor::EditLogBreakpoint",
|
||||||
"cmd-shift-u": "editor::ToggleCase"
|
"cmd-shift-u": "editor::ToggleCase",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && mode == full",
|
"context": "Editor && mode == full",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"cmd-f12": "outline::Toggle",
|
"cmd-f12": "outline::Toggle",
|
||||||
"cmd-r": ["buffer_search::Deploy", { "replace_enabled": true }],
|
"cmd-r": ["buffer_search::Deploy", { "replace_enabled": true }],
|
||||||
"cmd-shift-o": "file_finder::Toggle",
|
|
||||||
"cmd-l": "go_to_line::Toggle",
|
"cmd-l": "go_to_line::Toggle",
|
||||||
|
"cmd-e": "file_finder::Toggle",
|
||||||
|
"cmd-shift-o": "file_finder::Toggle",
|
||||||
|
"cmd-shift-n": "file_finder::Toggle",
|
||||||
"alt-enter": "editor::ToggleCodeActions",
|
"alt-enter": "editor::ToggleCodeActions",
|
||||||
"ctrl-space": "editor::ShowCompletions",
|
"ctrl-space": "editor::ShowCompletions",
|
||||||
"cmd-j": "editor::Hover",
|
"cmd-j": "editor::Hover",
|
||||||
"cmd-p": "editor::ShowSignatureHelp",
|
"cmd-p": "editor::ShowSignatureHelp",
|
||||||
"cmd-\\": "assistant::InlineAssist"
|
"cmd-\\": "assistant::InlineAssist",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "BufferSearchBar",
|
"context": "BufferSearchBar",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"shift-enter": "search::SelectPreviousMatch"
|
"shift-enter": "search::SelectPreviousMatch",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "BufferSearchBar || ProjectSearchBar",
|
"context": "BufferSearchBar || ProjectSearchBar",
|
||||||
@@ -93,8 +95,8 @@
|
|||||||
"ctrl-alt-c": "search::ToggleCaseSensitive",
|
"ctrl-alt-c": "search::ToggleCaseSensitive",
|
||||||
"ctrl-alt-e": "search::ToggleSelection",
|
"ctrl-alt-e": "search::ToggleSelection",
|
||||||
"ctrl-alt-w": "search::ToggleWholeWord",
|
"ctrl-alt-w": "search::ToggleWholeWord",
|
||||||
"ctrl-alt-x": "search::ToggleRegex"
|
"ctrl-alt-x": "search::ToggleRegex",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Workspace",
|
"context": "Workspace",
|
||||||
@@ -116,8 +118,8 @@
|
|||||||
"cmd-1": "project_panel::ToggleFocus",
|
"cmd-1": "project_panel::ToggleFocus",
|
||||||
"cmd-5": "debug_panel::ToggleFocus",
|
"cmd-5": "debug_panel::ToggleFocus",
|
||||||
"cmd-6": "diagnostics::Deploy",
|
"cmd-6": "diagnostics::Deploy",
|
||||||
"cmd-7": "outline_panel::ToggleFocus"
|
"cmd-7": "outline_panel::ToggleFocus",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Pane", // this is to override the default Pane mappings to switch tabs
|
"context": "Pane", // this is to override the default Pane mappings to switch tabs
|
||||||
@@ -131,15 +133,15 @@
|
|||||||
"cmd-7": "outline_panel::ToggleFocus",
|
"cmd-7": "outline_panel::ToggleFocus",
|
||||||
"cmd-8": null, // Services (bottom dock)
|
"cmd-8": null, // Services (bottom dock)
|
||||||
"cmd-9": null, // Git History (bottom dock)
|
"cmd-9": null, // Git History (bottom dock)
|
||||||
"cmd-0": "git_panel::ToggleFocus"
|
"cmd-0": "git_panel::ToggleFocus",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Workspace || Editor",
|
"context": "Workspace || Editor",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"alt-f12": "terminal_panel::Toggle",
|
"alt-f12": "terminal_panel::Toggle",
|
||||||
"cmd-shift-k": "git::Push"
|
"cmd-shift-k": "git::Push",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Pane",
|
"context": "Pane",
|
||||||
@@ -147,8 +149,8 @@
|
|||||||
"cmd-alt-left": "pane::GoBack",
|
"cmd-alt-left": "pane::GoBack",
|
||||||
"cmd-alt-right": "pane::GoForward",
|
"cmd-alt-right": "pane::GoForward",
|
||||||
"alt-left": "pane::ActivatePreviousItem",
|
"alt-left": "pane::ActivatePreviousItem",
|
||||||
"alt-right": "pane::ActivateNextItem"
|
"alt-right": "pane::ActivateNextItem",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "ProjectPanel",
|
"context": "ProjectPanel",
|
||||||
@@ -159,8 +161,8 @@
|
|||||||
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
|
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
|
||||||
"delete": ["project_panel::Trash", { "skip_prompt": false }],
|
"delete": ["project_panel::Trash", { "skip_prompt": false }],
|
||||||
"shift-delete": ["project_panel::Delete", { "skip_prompt": false }],
|
"shift-delete": ["project_panel::Delete", { "skip_prompt": false }],
|
||||||
"shift-f6": "project_panel::Rename"
|
"shift-f6": "project_panel::Rename",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Terminal",
|
"context": "Terminal",
|
||||||
@@ -170,8 +172,8 @@
|
|||||||
"cmd-up": "terminal::ScrollLineUp",
|
"cmd-up": "terminal::ScrollLineUp",
|
||||||
"cmd-down": "terminal::ScrollLineDown",
|
"cmd-down": "terminal::ScrollLineDown",
|
||||||
"shift-pageup": "terminal::ScrollPageUp",
|
"shift-pageup": "terminal::ScrollPageUp",
|
||||||
"shift-pagedown": "terminal::ScrollPageDown"
|
"shift-pagedown": "terminal::ScrollPageDown",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{ "context": "GitPanel", "bindings": { "cmd-0": "workspace::CloseActiveDock" } },
|
{ "context": "GitPanel", "bindings": { "cmd-0": "workspace::CloseActiveDock" } },
|
||||||
{ "context": "ProjectPanel", "bindings": { "cmd-1": "workspace::CloseActiveDock" } },
|
{ "context": "ProjectPanel", "bindings": { "cmd-1": "workspace::CloseActiveDock" } },
|
||||||
@@ -182,7 +184,7 @@
|
|||||||
"context": "Dock || Workspace || OutlinePanel || ProjectPanel || CollabPanel || (Editor && mode == auto_height)",
|
"context": "Dock || Workspace || OutlinePanel || ProjectPanel || CollabPanel || (Editor && mode == auto_height)",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"escape": "editor::ToggleFocus",
|
"escape": "editor::ToggleFocus",
|
||||||
"shift-escape": "workspace::CloseActiveDock"
|
"shift-escape": "workspace::CloseActiveDock",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -22,8 +22,8 @@
|
|||||||
"ctrl-^": ["workspace::MoveItemToPane", { "destination": 5 }],
|
"ctrl-^": ["workspace::MoveItemToPane", { "destination": 5 }],
|
||||||
"ctrl-&": ["workspace::MoveItemToPane", { "destination": 6 }],
|
"ctrl-&": ["workspace::MoveItemToPane", { "destination": 6 }],
|
||||||
"ctrl-*": ["workspace::MoveItemToPane", { "destination": 7 }],
|
"ctrl-*": ["workspace::MoveItemToPane", { "destination": 7 }],
|
||||||
"ctrl-(": ["workspace::MoveItemToPane", { "destination": 8 }]
|
"ctrl-(": ["workspace::MoveItemToPane", { "destination": 8 }],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor",
|
"context": "Editor",
|
||||||
@@ -57,20 +57,20 @@
|
|||||||
"ctrl-right": "editor::MoveToNextSubwordEnd",
|
"ctrl-right": "editor::MoveToNextSubwordEnd",
|
||||||
"ctrl-left": "editor::MoveToPreviousSubwordStart",
|
"ctrl-left": "editor::MoveToPreviousSubwordStart",
|
||||||
"ctrl-shift-right": "editor::SelectToNextSubwordEnd",
|
"ctrl-shift-right": "editor::SelectToNextSubwordEnd",
|
||||||
"ctrl-shift-left": "editor::SelectToPreviousSubwordStart"
|
"ctrl-shift-left": "editor::SelectToPreviousSubwordStart",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && mode == full",
|
"context": "Editor && mode == full",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"cmd-r": "outline::Toggle"
|
"cmd-r": "outline::Toggle",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && !agent_diff",
|
"context": "Editor && !agent_diff",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"cmd-k cmd-z": "git::Restore"
|
"cmd-k cmd-z": "git::Restore",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Pane",
|
"context": "Pane",
|
||||||
@@ -85,8 +85,8 @@
|
|||||||
"cmd-6": ["pane::ActivateItem", 5],
|
"cmd-6": ["pane::ActivateItem", 5],
|
||||||
"cmd-7": ["pane::ActivateItem", 6],
|
"cmd-7": ["pane::ActivateItem", 6],
|
||||||
"cmd-8": ["pane::ActivateItem", 7],
|
"cmd-8": ["pane::ActivateItem", 7],
|
||||||
"cmd-9": "pane::ActivateLastItem"
|
"cmd-9": "pane::ActivateLastItem",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Workspace",
|
"context": "Workspace",
|
||||||
@@ -95,7 +95,7 @@
|
|||||||
"cmd-t": "file_finder::Toggle",
|
"cmd-t": "file_finder::Toggle",
|
||||||
"shift-cmd-r": "project_symbols::Toggle",
|
"shift-cmd-r": "project_symbols::Toggle",
|
||||||
// Currently busted: https://github.com/zed-industries/feedback/issues/898
|
// Currently busted: https://github.com/zed-industries/feedback/issues/898
|
||||||
"ctrl-0": "project_panel::ToggleFocus"
|
"ctrl-0": "project_panel::ToggleFocus",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
{
|
{
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"cmd-shift-o": "projects::OpenRecent",
|
"cmd-shift-o": "projects::OpenRecent",
|
||||||
"cmd-alt-tab": "project_panel::ToggleFocus"
|
"cmd-alt-tab": "project_panel::ToggleFocus",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && mode == full",
|
"context": "Editor && mode == full",
|
||||||
@@ -15,8 +15,8 @@
|
|||||||
"cmd-enter": "editor::NewlineBelow",
|
"cmd-enter": "editor::NewlineBelow",
|
||||||
"cmd-alt-enter": "editor::NewlineAbove",
|
"cmd-alt-enter": "editor::NewlineAbove",
|
||||||
"cmd-shift-l": "editor::SelectLine",
|
"cmd-shift-l": "editor::SelectLine",
|
||||||
"cmd-shift-t": "outline::Toggle"
|
"cmd-shift-t": "outline::Toggle",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor",
|
"context": "Editor",
|
||||||
@@ -41,30 +41,30 @@
|
|||||||
"ctrl-u": "editor::ConvertToUpperCase",
|
"ctrl-u": "editor::ConvertToUpperCase",
|
||||||
"ctrl-shift-u": "editor::ConvertToLowerCase",
|
"ctrl-shift-u": "editor::ConvertToLowerCase",
|
||||||
"ctrl-alt-u": "editor::ConvertToUpperCamelCase",
|
"ctrl-alt-u": "editor::ConvertToUpperCamelCase",
|
||||||
"ctrl-_": "editor::ConvertToSnakeCase"
|
"ctrl-_": "editor::ConvertToSnakeCase",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "BufferSearchBar",
|
"context": "BufferSearchBar",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"ctrl-s": "search::SelectNextMatch",
|
"ctrl-s": "search::SelectNextMatch",
|
||||||
"ctrl-shift-s": "search::SelectPreviousMatch"
|
"ctrl-shift-s": "search::SelectPreviousMatch",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Workspace",
|
"context": "Workspace",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"cmd-alt-ctrl-d": "workspace::ToggleLeftDock",
|
"cmd-alt-ctrl-d": "workspace::ToggleLeftDock",
|
||||||
"cmd-t": "file_finder::Toggle",
|
"cmd-t": "file_finder::Toggle",
|
||||||
"cmd-shift-t": "project_symbols::Toggle"
|
"cmd-shift-t": "project_symbols::Toggle",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Pane",
|
"context": "Pane",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"alt-cmd-r": "search::ToggleRegex",
|
"alt-cmd-r": "search::ToggleRegex",
|
||||||
"ctrl-tab": "project_panel::ToggleFocus"
|
"ctrl-tab": "project_panel::ToggleFocus",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "ProjectPanel",
|
"context": "ProjectPanel",
|
||||||
@@ -75,11 +75,11 @@
|
|||||||
"return": "project_panel::Rename",
|
"return": "project_panel::Rename",
|
||||||
"cmd-c": "project_panel::Copy",
|
"cmd-c": "project_panel::Copy",
|
||||||
"cmd-v": "project_panel::Paste",
|
"cmd-v": "project_panel::Paste",
|
||||||
"cmd-alt-c": "project_panel::CopyPath"
|
"cmd-alt-c": "project_panel::CopyPath",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Dock",
|
"context": "Dock",
|
||||||
"bindings": {}
|
"bindings": {},
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
"backspace": "editor::Backspace",
|
"backspace": "editor::Backspace",
|
||||||
"delete": "editor::Delete",
|
"delete": "editor::Delete",
|
||||||
"left": "editor::MoveLeft",
|
"left": "editor::MoveLeft",
|
||||||
"right": "editor::MoveRight"
|
"right": "editor::MoveRight",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -180,10 +180,9 @@
|
|||||||
"ctrl-w g shift-d": "editor::GoToTypeDefinitionSplit",
|
"ctrl-w g shift-d": "editor::GoToTypeDefinitionSplit",
|
||||||
"ctrl-w space": "editor::OpenExcerptsSplit",
|
"ctrl-w space": "editor::OpenExcerptsSplit",
|
||||||
"ctrl-w g space": "editor::OpenExcerptsSplit",
|
"ctrl-w g space": "editor::OpenExcerptsSplit",
|
||||||
"ctrl-6": "pane::AlternateFile",
|
|
||||||
"ctrl-^": "pane::AlternateFile",
|
"ctrl-^": "pane::AlternateFile",
|
||||||
".": "vim::Repeat"
|
".": "vim::Repeat",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "vim_mode == normal || vim_mode == visual || vim_mode == operator",
|
"context": "vim_mode == normal || vim_mode == visual || vim_mode == operator",
|
||||||
@@ -224,8 +223,8 @@
|
|||||||
"] r": "vim::GoToNextReference",
|
"] r": "vim::GoToNextReference",
|
||||||
// tree-sitter related commands
|
// tree-sitter related commands
|
||||||
"[ x": "vim::SelectLargerSyntaxNode",
|
"[ x": "vim::SelectLargerSyntaxNode",
|
||||||
"] x": "vim::SelectSmallerSyntaxNode"
|
"] x": "vim::SelectSmallerSyntaxNode",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "vim_mode == normal",
|
"context": "vim_mode == normal",
|
||||||
@@ -262,16 +261,16 @@
|
|||||||
"[ d": "editor::GoToPreviousDiagnostic",
|
"[ d": "editor::GoToPreviousDiagnostic",
|
||||||
"] c": "editor::GoToHunk",
|
"] c": "editor::GoToHunk",
|
||||||
"[ c": "editor::GoToPreviousHunk",
|
"[ c": "editor::GoToPreviousHunk",
|
||||||
"g c": "vim::PushToggleComments"
|
"g c": "vim::PushToggleComments",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "VimControl && VimCount",
|
"context": "VimControl && VimCount",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"0": ["vim::Number", 0],
|
"0": ["vim::Number", 0],
|
||||||
":": "vim::CountCommand",
|
":": "vim::CountCommand",
|
||||||
"%": "vim::GoToPercentage"
|
"%": "vim::GoToPercentage",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "vim_mode == visual",
|
"context": "vim_mode == visual",
|
||||||
@@ -323,8 +322,8 @@
|
|||||||
"g w": "vim::Rewrap",
|
"g w": "vim::Rewrap",
|
||||||
"g ?": "vim::ConvertToRot13",
|
"g ?": "vim::ConvertToRot13",
|
||||||
// "g ?": "vim::ConvertToRot47",
|
// "g ?": "vim::ConvertToRot47",
|
||||||
"\"": "vim::PushRegister"
|
"\"": "vim::PushRegister",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "vim_mode == helix_select",
|
"context": "vim_mode == helix_select",
|
||||||
@@ -344,8 +343,8 @@
|
|||||||
"ctrl-pageup": "pane::ActivatePreviousItem",
|
"ctrl-pageup": "pane::ActivatePreviousItem",
|
||||||
"ctrl-pagedown": "pane::ActivateNextItem",
|
"ctrl-pagedown": "pane::ActivateNextItem",
|
||||||
".": "vim::Repeat",
|
".": "vim::Repeat",
|
||||||
"alt-.": "vim::RepeatFind"
|
"alt-.": "vim::RepeatFind",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "vim_mode == insert",
|
"context": "vim_mode == insert",
|
||||||
@@ -375,8 +374,8 @@
|
|||||||
"ctrl-r": "vim::PushRegister",
|
"ctrl-r": "vim::PushRegister",
|
||||||
"insert": "vim::ToggleReplace",
|
"insert": "vim::ToggleReplace",
|
||||||
"ctrl-o": "vim::TemporaryNormal",
|
"ctrl-o": "vim::TemporaryNormal",
|
||||||
"ctrl-s": "editor::ShowSignatureHelp"
|
"ctrl-s": "editor::ShowSignatureHelp",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "showing_completions",
|
"context": "showing_completions",
|
||||||
@@ -384,8 +383,8 @@
|
|||||||
"ctrl-d": "vim::ScrollDown",
|
"ctrl-d": "vim::ScrollDown",
|
||||||
"ctrl-u": "vim::ScrollUp",
|
"ctrl-u": "vim::ScrollUp",
|
||||||
"ctrl-e": "vim::LineDown",
|
"ctrl-e": "vim::LineDown",
|
||||||
"ctrl-y": "vim::LineUp"
|
"ctrl-y": "vim::LineUp",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "(vim_mode == normal || vim_mode == helix_normal) && !menu",
|
"context": "(vim_mode == normal || vim_mode == helix_normal) && !menu",
|
||||||
@@ -410,23 +409,31 @@
|
|||||||
"shift-s": "vim::SubstituteLine",
|
"shift-s": "vim::SubstituteLine",
|
||||||
"\"": "vim::PushRegister",
|
"\"": "vim::PushRegister",
|
||||||
"ctrl-pagedown": "pane::ActivateNextItem",
|
"ctrl-pagedown": "pane::ActivateNextItem",
|
||||||
"ctrl-pageup": "pane::ActivatePreviousItem"
|
"ctrl-pageup": "pane::ActivatePreviousItem",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "VimControl && vim_mode == helix_normal && !menu",
|
"context": "VimControl && vim_mode == helix_normal && !menu",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
|
"j": ["vim::Down", { "display_lines": true }],
|
||||||
|
"down": ["vim::Down", { "display_lines": true }],
|
||||||
|
"k": ["vim::Up", { "display_lines": true }],
|
||||||
|
"up": ["vim::Up", { "display_lines": true }],
|
||||||
|
"g j": "vim::Down",
|
||||||
|
"g down": "vim::Down",
|
||||||
|
"g k": "vim::Up",
|
||||||
|
"g up": "vim::Up",
|
||||||
"escape": "vim::SwitchToHelixNormalMode",
|
"escape": "vim::SwitchToHelixNormalMode",
|
||||||
"i": "vim::HelixInsert",
|
"i": "vim::HelixInsert",
|
||||||
"a": "vim::HelixAppend",
|
"a": "vim::HelixAppend",
|
||||||
"ctrl-[": "editor::Cancel"
|
"ctrl-[": "editor::Cancel",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "vim_mode == helix_select && !menu",
|
"context": "vim_mode == helix_select && !menu",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"escape": "vim::SwitchToHelixNormalMode"
|
"escape": "vim::SwitchToHelixNormalMode",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "(vim_mode == helix_normal || vim_mode == helix_select) && !menu",
|
"context": "(vim_mode == helix_normal || vim_mode == helix_select) && !menu",
|
||||||
@@ -446,9 +453,9 @@
|
|||||||
"shift-r": "editor::Paste",
|
"shift-r": "editor::Paste",
|
||||||
"`": "vim::ConvertToLowerCase",
|
"`": "vim::ConvertToLowerCase",
|
||||||
"alt-`": "vim::ConvertToUpperCase",
|
"alt-`": "vim::ConvertToUpperCase",
|
||||||
"insert": "vim::InsertBefore",
|
"insert": "vim::InsertBefore", // not a helix default
|
||||||
"shift-u": "editor::Redo",
|
"shift-u": "editor::Redo",
|
||||||
"ctrl-r": "vim::Redo",
|
"ctrl-r": "vim::Redo", // not a helix default
|
||||||
"y": "vim::HelixYank",
|
"y": "vim::HelixYank",
|
||||||
"p": "vim::HelixPaste",
|
"p": "vim::HelixPaste",
|
||||||
"shift-p": ["vim::HelixPaste", { "before": true }],
|
"shift-p": ["vim::HelixPaste", { "before": true }],
|
||||||
@@ -477,6 +484,7 @@
|
|||||||
"alt-p": "editor::SelectPreviousSyntaxNode",
|
"alt-p": "editor::SelectPreviousSyntaxNode",
|
||||||
"alt-n": "editor::SelectNextSyntaxNode",
|
"alt-n": "editor::SelectNextSyntaxNode",
|
||||||
|
|
||||||
|
// Search
|
||||||
"n": "vim::HelixSelectNext",
|
"n": "vim::HelixSelectNext",
|
||||||
"shift-n": "vim::HelixSelectPrevious",
|
"shift-n": "vim::HelixSelectPrevious",
|
||||||
|
|
||||||
@@ -484,27 +492,32 @@
|
|||||||
"g e": "vim::EndOfDocument",
|
"g e": "vim::EndOfDocument",
|
||||||
"g h": "vim::StartOfLine",
|
"g h": "vim::StartOfLine",
|
||||||
"g l": "vim::EndOfLine",
|
"g l": "vim::EndOfLine",
|
||||||
"g s": "vim::FirstNonWhitespace", // "g s" default behavior is "space s"
|
"g s": "vim::FirstNonWhitespace",
|
||||||
"g t": "vim::WindowTop",
|
"g t": "vim::WindowTop",
|
||||||
"g c": "vim::WindowMiddle",
|
"g c": "vim::WindowMiddle",
|
||||||
"g b": "vim::WindowBottom",
|
"g b": "vim::WindowBottom",
|
||||||
"g r": "editor::FindAllReferences", // zed specific
|
"g r": "editor::FindAllReferences",
|
||||||
"g n": "pane::ActivateNextItem",
|
"g n": "pane::ActivateNextItem",
|
||||||
"shift-l": "pane::ActivateNextItem",
|
"shift-l": "pane::ActivateNextItem", // not a helix default
|
||||||
"g p": "pane::ActivatePreviousItem",
|
"g p": "pane::ActivatePreviousItem",
|
||||||
"shift-h": "pane::ActivatePreviousItem",
|
"shift-h": "pane::ActivatePreviousItem", // not a helix default
|
||||||
"g .": "vim::HelixGotoLastModification", // go to last modification
|
"g .": "vim::HelixGotoLastModification",
|
||||||
|
"g o": "editor::ToggleSelectedDiffHunks", // Zed specific
|
||||||
|
"g shift-o": "git::ToggleStaged", // Zed specific
|
||||||
|
"g shift-r": "git::Restore", // Zed specific
|
||||||
|
"g u": "git::StageAndNext", // Zed specific
|
||||||
|
"g shift-u": "git::UnstageAndNext", // Zed specific
|
||||||
|
|
||||||
// Window mode
|
// Window mode
|
||||||
"space w h": "workspace::ActivatePaneLeft",
|
|
||||||
"space w l": "workspace::ActivatePaneRight",
|
|
||||||
"space w k": "workspace::ActivatePaneUp",
|
|
||||||
"space w j": "workspace::ActivatePaneDown",
|
|
||||||
"space w q": "pane::CloseActiveItem",
|
|
||||||
"space w s": "pane::SplitRight",
|
|
||||||
"space w r": "pane::SplitRight",
|
|
||||||
"space w v": "pane::SplitDown",
|
"space w v": "pane::SplitDown",
|
||||||
"space w d": "pane::SplitDown",
|
"space w s": "pane::SplitRight",
|
||||||
|
"space w h": "workspace::ActivatePaneLeft",
|
||||||
|
"space w j": "workspace::ActivatePaneDown",
|
||||||
|
"space w k": "workspace::ActivatePaneUp",
|
||||||
|
"space w l": "workspace::ActivatePaneRight",
|
||||||
|
"space w q": "pane::CloseActiveItem",
|
||||||
|
"space w r": "pane::SplitRight", // not a helix default
|
||||||
|
"space w d": "pane::SplitDown", // not a helix default
|
||||||
|
|
||||||
// Space mode
|
// Space mode
|
||||||
"space f": "file_finder::Toggle",
|
"space f": "file_finder::Toggle",
|
||||||
@@ -518,6 +531,7 @@
|
|||||||
"space c": "editor::ToggleComments",
|
"space c": "editor::ToggleComments",
|
||||||
"space p": "editor::Paste",
|
"space p": "editor::Paste",
|
||||||
"space y": "editor::Copy",
|
"space y": "editor::Copy",
|
||||||
|
"space /": "pane::DeploySearch",
|
||||||
|
|
||||||
// Other
|
// Other
|
||||||
":": "command_palette::Toggle",
|
":": "command_palette::Toggle",
|
||||||
@@ -525,24 +539,22 @@
|
|||||||
"]": ["vim::PushHelixNext", { "around": true }],
|
"]": ["vim::PushHelixNext", { "around": true }],
|
||||||
"[": ["vim::PushHelixPrevious", { "around": true }],
|
"[": ["vim::PushHelixPrevious", { "around": true }],
|
||||||
"g q": "vim::PushRewrap",
|
"g q": "vim::PushRewrap",
|
||||||
"g w": "vim::PushRewrap"
|
"g w": "vim::PushRewrap", // not a helix default & clashes with helix `goto_word`
|
||||||
// "tab": "pane::ActivateNextItem",
|
},
|
||||||
// "shift-tab": "pane::ActivatePrevItem",
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "vim_mode == insert && !(showing_code_actions || showing_completions)",
|
"context": "vim_mode == insert && !(showing_code_actions || showing_completions)",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"ctrl-p": "editor::ShowWordCompletions",
|
"ctrl-p": "editor::ShowWordCompletions",
|
||||||
"ctrl-n": "editor::ShowWordCompletions"
|
"ctrl-n": "editor::ShowWordCompletions",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "(vim_mode == insert || vim_mode == normal) && showing_signature_help && !showing_completions",
|
"context": "(vim_mode == insert || vim_mode == normal) && showing_signature_help && !showing_completions",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"ctrl-p": "editor::SignatureHelpPrevious",
|
"ctrl-p": "editor::SignatureHelpPrevious",
|
||||||
"ctrl-n": "editor::SignatureHelpNext"
|
"ctrl-n": "editor::SignatureHelpNext",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "vim_mode == replace",
|
"context": "vim_mode == replace",
|
||||||
@@ -558,8 +570,8 @@
|
|||||||
"backspace": "vim::UndoReplace",
|
"backspace": "vim::UndoReplace",
|
||||||
"tab": "vim::Tab",
|
"tab": "vim::Tab",
|
||||||
"enter": "vim::Enter",
|
"enter": "vim::Enter",
|
||||||
"insert": "vim::InsertBefore"
|
"insert": "vim::InsertBefore",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "vim_mode == waiting",
|
"context": "vim_mode == waiting",
|
||||||
@@ -571,14 +583,14 @@
|
|||||||
"escape": "vim::ClearOperators",
|
"escape": "vim::ClearOperators",
|
||||||
"ctrl-k": ["vim::PushDigraph", {}],
|
"ctrl-k": ["vim::PushDigraph", {}],
|
||||||
"ctrl-v": ["vim::PushLiteral", {}],
|
"ctrl-v": ["vim::PushLiteral", {}],
|
||||||
"ctrl-q": ["vim::PushLiteral", {}]
|
"ctrl-q": ["vim::PushLiteral", {}],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && vim_mode == waiting && (vim_operator == ys || vim_operator == cs)",
|
"context": "Editor && vim_mode == waiting && (vim_operator == ys || vim_operator == cs)",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"escape": "vim::SwitchToNormalMode"
|
"escape": "vim::SwitchToNormalMode",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "vim_mode == operator",
|
"context": "vim_mode == operator",
|
||||||
@@ -586,8 +598,8 @@
|
|||||||
"ctrl-c": "vim::ClearOperators",
|
"ctrl-c": "vim::ClearOperators",
|
||||||
"ctrl-[": "vim::ClearOperators",
|
"ctrl-[": "vim::ClearOperators",
|
||||||
"escape": "vim::ClearOperators",
|
"escape": "vim::ClearOperators",
|
||||||
"g c": "vim::Comment"
|
"g c": "vim::Comment",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "vim_operator == a || vim_operator == i || vim_operator == cs || vim_operator == helix_next || vim_operator == helix_previous",
|
"context": "vim_operator == a || vim_operator == i || vim_operator == cs || vim_operator == helix_next || vim_operator == helix_previous",
|
||||||
@@ -624,14 +636,14 @@
|
|||||||
"shift-i": ["vim::IndentObj", { "include_below": true }],
|
"shift-i": ["vim::IndentObj", { "include_below": true }],
|
||||||
"f": "vim::Method",
|
"f": "vim::Method",
|
||||||
"c": "vim::Class",
|
"c": "vim::Class",
|
||||||
"e": "vim::EntireFile"
|
"e": "vim::EntireFile",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "vim_operator == helix_m",
|
"context": "vim_operator == helix_m",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"m": "vim::Matching"
|
"m": "vim::Matching",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "vim_operator == helix_next",
|
"context": "vim_operator == helix_next",
|
||||||
@@ -648,8 +660,8 @@
|
|||||||
"x": "editor::SelectSmallerSyntaxNode",
|
"x": "editor::SelectSmallerSyntaxNode",
|
||||||
"d": "editor::GoToDiagnostic",
|
"d": "editor::GoToDiagnostic",
|
||||||
"c": "editor::GoToHunk",
|
"c": "editor::GoToHunk",
|
||||||
"space": "vim::InsertEmptyLineBelow"
|
"space": "vim::InsertEmptyLineBelow",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "vim_operator == helix_previous",
|
"context": "vim_operator == helix_previous",
|
||||||
@@ -666,8 +678,8 @@
|
|||||||
"x": "editor::SelectLargerSyntaxNode",
|
"x": "editor::SelectLargerSyntaxNode",
|
||||||
"d": "editor::GoToPreviousDiagnostic",
|
"d": "editor::GoToPreviousDiagnostic",
|
||||||
"c": "editor::GoToPreviousHunk",
|
"c": "editor::GoToPreviousHunk",
|
||||||
"space": "vim::InsertEmptyLineAbove"
|
"space": "vim::InsertEmptyLineAbove",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "vim_operator == c",
|
"context": "vim_operator == c",
|
||||||
@@ -675,8 +687,8 @@
|
|||||||
"c": "vim::CurrentLine",
|
"c": "vim::CurrentLine",
|
||||||
"x": "vim::Exchange",
|
"x": "vim::Exchange",
|
||||||
"d": "editor::Rename", // zed specific
|
"d": "editor::Rename", // zed specific
|
||||||
"s": ["vim::PushChangeSurrounds", {}]
|
"s": ["vim::PushChangeSurrounds", {}],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "vim_operator == d",
|
"context": "vim_operator == d",
|
||||||
@@ -688,36 +700,36 @@
|
|||||||
"shift-o": "git::ToggleStaged",
|
"shift-o": "git::ToggleStaged",
|
||||||
"p": "git::Restore", // "d p"
|
"p": "git::Restore", // "d p"
|
||||||
"u": "git::StageAndNext", // "d u"
|
"u": "git::StageAndNext", // "d u"
|
||||||
"shift-u": "git::UnstageAndNext" // "d shift-u"
|
"shift-u": "git::UnstageAndNext", // "d shift-u"
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "vim_operator == gu",
|
"context": "vim_operator == gu",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"g u": "vim::CurrentLine",
|
"g u": "vim::CurrentLine",
|
||||||
"u": "vim::CurrentLine"
|
"u": "vim::CurrentLine",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "vim_operator == gU",
|
"context": "vim_operator == gU",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"g shift-u": "vim::CurrentLine",
|
"g shift-u": "vim::CurrentLine",
|
||||||
"shift-u": "vim::CurrentLine"
|
"shift-u": "vim::CurrentLine",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "vim_operator == g~",
|
"context": "vim_operator == g~",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"g ~": "vim::CurrentLine",
|
"g ~": "vim::CurrentLine",
|
||||||
"~": "vim::CurrentLine"
|
"~": "vim::CurrentLine",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "vim_operator == g?",
|
"context": "vim_operator == g?",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"g ?": "vim::CurrentLine",
|
"g ?": "vim::CurrentLine",
|
||||||
"?": "vim::CurrentLine"
|
"?": "vim::CurrentLine",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "vim_operator == gq",
|
"context": "vim_operator == gq",
|
||||||
@@ -725,66 +737,66 @@
|
|||||||
"g q": "vim::CurrentLine",
|
"g q": "vim::CurrentLine",
|
||||||
"q": "vim::CurrentLine",
|
"q": "vim::CurrentLine",
|
||||||
"g w": "vim::CurrentLine",
|
"g w": "vim::CurrentLine",
|
||||||
"w": "vim::CurrentLine"
|
"w": "vim::CurrentLine",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "vim_operator == y",
|
"context": "vim_operator == y",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"y": "vim::CurrentLine",
|
"y": "vim::CurrentLine",
|
||||||
"v": "vim::PushForcedMotion",
|
"v": "vim::PushForcedMotion",
|
||||||
"s": ["vim::PushAddSurrounds", {}]
|
"s": ["vim::PushAddSurrounds", {}],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "vim_operator == ys",
|
"context": "vim_operator == ys",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"s": "vim::CurrentLine"
|
"s": "vim::CurrentLine",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "vim_operator == >",
|
"context": "vim_operator == >",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
">": "vim::CurrentLine"
|
">": "vim::CurrentLine",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "vim_operator == <",
|
"context": "vim_operator == <",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"<": "vim::CurrentLine"
|
"<": "vim::CurrentLine",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "vim_operator == eq",
|
"context": "vim_operator == eq",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"=": "vim::CurrentLine"
|
"=": "vim::CurrentLine",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "vim_operator == sh",
|
"context": "vim_operator == sh",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"!": "vim::CurrentLine"
|
"!": "vim::CurrentLine",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "vim_operator == gc",
|
"context": "vim_operator == gc",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"c": "vim::CurrentLine"
|
"c": "vim::CurrentLine",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "vim_operator == gR",
|
"context": "vim_operator == gR",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"r": "vim::CurrentLine",
|
"r": "vim::CurrentLine",
|
||||||
"shift-r": "vim::CurrentLine"
|
"shift-r": "vim::CurrentLine",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "vim_operator == cx",
|
"context": "vim_operator == cx",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"x": "vim::CurrentLine",
|
"x": "vim::CurrentLine",
|
||||||
"c": "vim::ClearExchange"
|
"c": "vim::ClearExchange",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "vim_mode == literal",
|
"context": "vim_mode == literal",
|
||||||
@@ -826,15 +838,15 @@
|
|||||||
"tab": ["vim::Literal", ["tab", "\u0009"]],
|
"tab": ["vim::Literal", ["tab", "\u0009"]],
|
||||||
// zed extensions:
|
// zed extensions:
|
||||||
"backspace": ["vim::Literal", ["backspace", "\u0008"]],
|
"backspace": ["vim::Literal", ["backspace", "\u0008"]],
|
||||||
"delete": ["vim::Literal", ["delete", "\u007F"]]
|
"delete": ["vim::Literal", ["delete", "\u007F"]],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "BufferSearchBar && !in_replace",
|
"context": "BufferSearchBar && !in_replace",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"enter": "vim::SearchSubmit",
|
"enter": "vim::SearchSubmit",
|
||||||
"escape": "buffer_search::Dismiss"
|
"escape": "buffer_search::Dismiss",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "VimControl && !menu || !Editor && !Terminal",
|
"context": "VimControl && !menu || !Editor && !Terminal",
|
||||||
@@ -895,15 +907,19 @@
|
|||||||
"ctrl-w ctrl-n": "workspace::NewFileSplitHorizontal",
|
"ctrl-w ctrl-n": "workspace::NewFileSplitHorizontal",
|
||||||
"ctrl-w n": "workspace::NewFileSplitHorizontal",
|
"ctrl-w n": "workspace::NewFileSplitHorizontal",
|
||||||
"g t": "vim::GoToTab",
|
"g t": "vim::GoToTab",
|
||||||
"g shift-t": "vim::GoToPreviousTab"
|
"g shift-t": "vim::GoToPreviousTab",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "!Editor && !Terminal",
|
"context": "!Editor && !Terminal",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
":": "command_palette::Toggle",
|
":": "command_palette::Toggle",
|
||||||
"g /": "pane::DeploySearch"
|
"g /": "pane::DeploySearch",
|
||||||
}
|
"] b": "pane::ActivateNextItem",
|
||||||
|
"[ b": "pane::ActivatePreviousItem",
|
||||||
|
"] shift-b": "pane::ActivateLastItem",
|
||||||
|
"[ shift-b": ["pane::ActivateItem", 0],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// netrw compatibility
|
// netrw compatibility
|
||||||
@@ -953,17 +969,45 @@
|
|||||||
"6": ["vim::Number", 6],
|
"6": ["vim::Number", 6],
|
||||||
"7": ["vim::Number", 7],
|
"7": ["vim::Number", 7],
|
||||||
"8": ["vim::Number", 8],
|
"8": ["vim::Number", 8],
|
||||||
"9": ["vim::Number", 9]
|
"9": ["vim::Number", 9],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "OutlinePanel && not_editing",
|
"context": "OutlinePanel && not_editing",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"j": "menu::SelectNext",
|
"h": "outline_panel::CollapseSelectedEntry",
|
||||||
"k": "menu::SelectPrevious",
|
"j": "vim::MenuSelectNext",
|
||||||
|
"k": "vim::MenuSelectPrevious",
|
||||||
|
"down": "vim::MenuSelectNext",
|
||||||
|
"up": "vim::MenuSelectPrevious",
|
||||||
|
"l": "outline_panel::ExpandSelectedEntry",
|
||||||
"shift-g": "menu::SelectLast",
|
"shift-g": "menu::SelectLast",
|
||||||
"g g": "menu::SelectFirst"
|
"g g": "menu::SelectFirst",
|
||||||
}
|
"-": "outline_panel::SelectParent",
|
||||||
|
"enter": "editor::ToggleFocus",
|
||||||
|
"/": "menu::Cancel",
|
||||||
|
"ctrl-u": "outline_panel::ScrollUp",
|
||||||
|
"ctrl-d": "outline_panel::ScrollDown",
|
||||||
|
"z t": "outline_panel::ScrollCursorTop",
|
||||||
|
"z z": "outline_panel::ScrollCursorCenter",
|
||||||
|
"z b": "outline_panel::ScrollCursorBottom",
|
||||||
|
"0": ["vim::Number", 0],
|
||||||
|
"1": ["vim::Number", 1],
|
||||||
|
"2": ["vim::Number", 2],
|
||||||
|
"3": ["vim::Number", 3],
|
||||||
|
"4": ["vim::Number", 4],
|
||||||
|
"5": ["vim::Number", 5],
|
||||||
|
"6": ["vim::Number", 6],
|
||||||
|
"7": ["vim::Number", 7],
|
||||||
|
"8": ["vim::Number", 8],
|
||||||
|
"9": ["vim::Number", 9],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "OutlinePanel && editing",
|
||||||
|
"bindings": {
|
||||||
|
"enter": "menu::Cancel",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "GitPanel && ChangesList",
|
"context": "GitPanel && ChangesList",
|
||||||
@@ -978,8 +1022,8 @@
|
|||||||
"x": "git::ToggleStaged",
|
"x": "git::ToggleStaged",
|
||||||
"shift-x": "git::StageAll",
|
"shift-x": "git::StageAll",
|
||||||
"g x": "git::StageRange",
|
"g x": "git::StageRange",
|
||||||
"shift-u": "git::UnstageAll"
|
"shift-u": "git::UnstageAll",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && mode == auto_height && VimControl",
|
"context": "Editor && mode == auto_height && VimControl",
|
||||||
@@ -990,8 +1034,8 @@
|
|||||||
"#": null,
|
"#": null,
|
||||||
"*": null,
|
"*": null,
|
||||||
"n": null,
|
"n": null,
|
||||||
"shift-n": null
|
"shift-n": null,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Picker > Editor",
|
"context": "Picker > Editor",
|
||||||
@@ -1000,29 +1044,29 @@
|
|||||||
"ctrl-u": "editor::DeleteToBeginningOfLine",
|
"ctrl-u": "editor::DeleteToBeginningOfLine",
|
||||||
"ctrl-w": "editor::DeleteToPreviousWordStart",
|
"ctrl-w": "editor::DeleteToPreviousWordStart",
|
||||||
"ctrl-p": "menu::SelectPrevious",
|
"ctrl-p": "menu::SelectPrevious",
|
||||||
"ctrl-n": "menu::SelectNext"
|
"ctrl-n": "menu::SelectNext",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "GitCommit > Editor && VimControl && vim_mode == normal",
|
"context": "GitCommit > Editor && VimControl && vim_mode == normal",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"ctrl-c": "menu::Cancel",
|
"ctrl-c": "menu::Cancel",
|
||||||
"escape": "menu::Cancel"
|
"escape": "menu::Cancel",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && edit_prediction",
|
"context": "Editor && edit_prediction",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
// This is identical to the binding in the base keymap, but the vim bindings above to
|
// This is identical to the binding in the base keymap, but the vim bindings above to
|
||||||
// "vim::Tab" shadow it, so it needs to be bound again.
|
// "vim::Tab" shadow it, so it needs to be bound again.
|
||||||
"tab": "editor::AcceptEditPrediction"
|
"tab": "editor::AcceptEditPrediction",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "MessageEditor > Editor && VimControl",
|
"context": "MessageEditor > Editor && VimControl",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"enter": "agent::Chat"
|
"enter": "agent::Chat",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "os != macos && Editor && edit_prediction_conflict",
|
"context": "os != macos && Editor && edit_prediction_conflict",
|
||||||
@@ -1030,8 +1074,8 @@
|
|||||||
// alt-l is provided as an alternative to tab/alt-tab. and will be displayed in the UI. This
|
// alt-l is provided as an alternative to tab/alt-tab. and will be displayed in the UI. This
|
||||||
// is because alt-tab may not be available, as it is often used for window switching on Linux
|
// is because alt-tab may not be available, as it is often used for window switching on Linux
|
||||||
// and Windows.
|
// and Windows.
|
||||||
"alt-l": "editor::AcceptEditPrediction"
|
"alt-l": "editor::AcceptEditPrediction",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "SettingsWindow > NavigationMenu && !search",
|
"context": "SettingsWindow > NavigationMenu && !search",
|
||||||
@@ -1041,7 +1085,16 @@
|
|||||||
"k": "settings_editor::FocusPreviousNavEntry",
|
"k": "settings_editor::FocusPreviousNavEntry",
|
||||||
"j": "settings_editor::FocusNextNavEntry",
|
"j": "settings_editor::FocusNextNavEntry",
|
||||||
"g g": "settings_editor::FocusFirstNavEntry",
|
"g g": "settings_editor::FocusFirstNavEntry",
|
||||||
"shift-g": "settings_editor::FocusLastNavEntry"
|
"shift-g": "settings_editor::FocusLastNavEntry",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
"context": "MarkdownPreview",
|
||||||
|
"bindings": {
|
||||||
|
"ctrl-u": "markdown::ScrollPageUp",
|
||||||
|
"ctrl-d": "markdown::ScrollPageDown",
|
||||||
|
"ctrl-y": "markdown::ScrollUp",
|
||||||
|
"ctrl-e": "markdown::ScrollDown",
|
||||||
|
},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -39,6 +39,5 @@ Only make changes that are necessary to fulfill the prompt, leave everything els
|
|||||||
|
|
||||||
Start at the indentation level in the original file in the rewritten {{content_type}}.
|
Start at the indentation level in the original file in the rewritten {{content_type}}.
|
||||||
|
|
||||||
You must use one of the provided tools to make the rewrite or to provide an explanation as to why the user's request cannot be fulfilled. It is an error if
|
IMPORTANT: You MUST use one of the provided tools to make the rewrite or to provide an explanation as to why the user's request cannot be fulfilled. You MUST NOT send back unstructured text. If you need to make a statement or ask a question you MUST use one of the tools to do so.
|
||||||
you simply send back unstructured text. If you need to make a statement or ask a question you must use one of the tools to do so.
|
|
||||||
It is an error if you try to make a change that cannot be made simply by editing the rewrite_section.
|
It is an error if you try to make a change that cannot be made simply by editing the rewrite_section.
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"theme": {
|
"theme": {
|
||||||
"mode": "system",
|
"mode": "system",
|
||||||
"light": "One Light",
|
"light": "One Light",
|
||||||
"dark": "One Dark"
|
"dark": "One Dark",
|
||||||
},
|
},
|
||||||
"icon_theme": "Zed (Default)",
|
"icon_theme": "Zed (Default)",
|
||||||
// The name of a base set of key bindings to use.
|
// The name of a base set of key bindings to use.
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
// Features that can be globally enabled or disabled
|
// Features that can be globally enabled or disabled
|
||||||
"features": {
|
"features": {
|
||||||
// Which edit prediction provider to use.
|
// Which edit prediction provider to use.
|
||||||
"edit_prediction_provider": "zed"
|
"edit_prediction_provider": "zed",
|
||||||
},
|
},
|
||||||
// The name of a font to use for rendering text in the editor
|
// The name of a font to use for rendering text in the editor
|
||||||
// ".ZedMono" currently aliases to Lilex
|
// ".ZedMono" currently aliases to Lilex
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
// The OpenType features to enable for text in the UI
|
// The OpenType features to enable for text in the UI
|
||||||
"ui_font_features": {
|
"ui_font_features": {
|
||||||
// Disable ligatures:
|
// Disable ligatures:
|
||||||
"calt": false
|
"calt": false,
|
||||||
},
|
},
|
||||||
// The weight of the UI font in standard CSS units from 100 to 900.
|
// The weight of the UI font in standard CSS units from 100 to 900.
|
||||||
"ui_font_weight": 400,
|
"ui_font_weight": 400,
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
"border_size": 0.0,
|
"border_size": 0.0,
|
||||||
// Opacity of the inactive panes. 0 means transparent, 1 means opaque.
|
// Opacity of the inactive panes. 0 means transparent, 1 means opaque.
|
||||||
// Values are clamped to the [0.0, 1.0] range.
|
// Values are clamped to the [0.0, 1.0] range.
|
||||||
"inactive_opacity": 1.0
|
"inactive_opacity": 1.0,
|
||||||
},
|
},
|
||||||
// Layout mode of the bottom dock. Defaults to "contained"
|
// Layout mode of the bottom dock. Defaults to "contained"
|
||||||
// choices: contained, full, left_aligned, right_aligned
|
// choices: contained, full, left_aligned, right_aligned
|
||||||
@@ -103,12 +103,12 @@
|
|||||||
"left_padding": 0.2,
|
"left_padding": 0.2,
|
||||||
// The relative width of the right padding of the central pane from the
|
// The relative width of the right padding of the central pane from the
|
||||||
// workspace when the centered layout is used.
|
// workspace when the centered layout is used.
|
||||||
"right_padding": 0.2
|
"right_padding": 0.2,
|
||||||
},
|
},
|
||||||
// Image viewer settings
|
// Image viewer settings
|
||||||
"image_viewer": {
|
"image_viewer": {
|
||||||
// The unit for image file sizes: "binary" (KiB, MiB) or decimal (KB, MB)
|
// The unit for image file sizes: "binary" (KiB, MiB) or decimal (KB, MB)
|
||||||
"unit": "binary"
|
"unit": "binary",
|
||||||
},
|
},
|
||||||
// Determines the modifier to be used to add multiple cursors with the mouse. The open hover link mouse gestures will adapt such that it do not conflict with the multicursor modifier.
|
// Determines the modifier to be used to add multiple cursors with the mouse. The open hover link mouse gestures will adapt such that it do not conflict with the multicursor modifier.
|
||||||
//
|
//
|
||||||
@@ -296,7 +296,7 @@
|
|||||||
// When true, enables drag and drop text selection in buffer.
|
// When true, enables drag and drop text selection in buffer.
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
// The delay in milliseconds that must elapse before drag and drop is allowed. Otherwise, a new text selection is created.
|
// The delay in milliseconds that must elapse before drag and drop is allowed. Otherwise, a new text selection is created.
|
||||||
"delay": 300
|
"delay": 300,
|
||||||
},
|
},
|
||||||
// What to do when go to definition yields no results.
|
// What to do when go to definition yields no results.
|
||||||
//
|
//
|
||||||
@@ -400,14 +400,14 @@
|
|||||||
// Visible characters used to render whitespace when show_whitespaces is enabled.
|
// Visible characters used to render whitespace when show_whitespaces is enabled.
|
||||||
"whitespace_map": {
|
"whitespace_map": {
|
||||||
"space": "•",
|
"space": "•",
|
||||||
"tab": "→"
|
"tab": "→",
|
||||||
},
|
},
|
||||||
// Settings related to calls in Zed
|
// Settings related to calls in Zed
|
||||||
"calls": {
|
"calls": {
|
||||||
// Join calls with the microphone live by default
|
// Join calls with the microphone live by default
|
||||||
"mute_on_join": false,
|
"mute_on_join": false,
|
||||||
// Share your project when you are the first to join a channel
|
// Share your project when you are the first to join a channel
|
||||||
"share_on_join": false
|
"share_on_join": false,
|
||||||
},
|
},
|
||||||
// Toolbar related settings
|
// Toolbar related settings
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
@@ -420,7 +420,7 @@
|
|||||||
// Whether to show agent review buttons in the editor toolbar.
|
// Whether to show agent review buttons in the editor toolbar.
|
||||||
"agent_review": true,
|
"agent_review": true,
|
||||||
// Whether to show code action buttons in the editor toolbar.
|
// Whether to show code action buttons in the editor toolbar.
|
||||||
"code_actions": false
|
"code_actions": false,
|
||||||
},
|
},
|
||||||
// Whether to allow windows to tab together based on the user’s tabbing preference (macOS only).
|
// Whether to allow windows to tab together based on the user’s tabbing preference (macOS only).
|
||||||
"use_system_window_tabs": false,
|
"use_system_window_tabs": false,
|
||||||
@@ -436,10 +436,12 @@
|
|||||||
"show_onboarding_banner": true,
|
"show_onboarding_banner": true,
|
||||||
// Whether to show user picture in the titlebar.
|
// Whether to show user picture in the titlebar.
|
||||||
"show_user_picture": true,
|
"show_user_picture": true,
|
||||||
|
// Whether to show the user menu in the titlebar.
|
||||||
|
"show_user_menu": true,
|
||||||
// Whether to show the sign in button in the titlebar.
|
// Whether to show the sign in button in the titlebar.
|
||||||
"show_sign_in": true,
|
"show_sign_in": true,
|
||||||
// Whether to show the menus in the titlebar.
|
// Whether to show the menus in the titlebar.
|
||||||
"show_menus": false
|
"show_menus": false,
|
||||||
},
|
},
|
||||||
"audio": {
|
"audio": {
|
||||||
// Opt into the new audio system.
|
// Opt into the new audio system.
|
||||||
@@ -472,7 +474,7 @@
|
|||||||
// the future we will migrate by setting this to false
|
// the future we will migrate by setting this to false
|
||||||
//
|
//
|
||||||
// You need to rejoin a call for this setting to apply
|
// You need to rejoin a call for this setting to apply
|
||||||
"experimental.legacy_audio_compatible": true
|
"experimental.legacy_audio_compatible": true,
|
||||||
},
|
},
|
||||||
// Scrollbar related settings
|
// Scrollbar related settings
|
||||||
"scrollbar": {
|
"scrollbar": {
|
||||||
@@ -511,8 +513,8 @@
|
|||||||
// When false, forcefully disables the horizontal scrollbar. Otherwise, obey other settings.
|
// When false, forcefully disables the horizontal scrollbar. Otherwise, obey other settings.
|
||||||
"horizontal": true,
|
"horizontal": true,
|
||||||
// When false, forcefully disables the vertical scrollbar. Otherwise, obey other settings.
|
// When false, forcefully disables the vertical scrollbar. Otherwise, obey other settings.
|
||||||
"vertical": true
|
"vertical": true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
// Minimap related settings
|
// Minimap related settings
|
||||||
"minimap": {
|
"minimap": {
|
||||||
@@ -560,7 +562,7 @@
|
|||||||
// 3. "gutter" or "none" to not highlight the current line in the minimap.
|
// 3. "gutter" or "none" to not highlight the current line in the minimap.
|
||||||
"current_line_highlight": null,
|
"current_line_highlight": null,
|
||||||
// Maximum number of columns to display in the minimap.
|
// Maximum number of columns to display in the minimap.
|
||||||
"max_width_columns": 80
|
"max_width_columns": 80,
|
||||||
},
|
},
|
||||||
// Enable middle-click paste on Linux.
|
// Enable middle-click paste on Linux.
|
||||||
"middle_click_paste": true,
|
"middle_click_paste": true,
|
||||||
@@ -583,7 +585,7 @@
|
|||||||
// Whether to show fold buttons in the gutter.
|
// Whether to show fold buttons in the gutter.
|
||||||
"folds": true,
|
"folds": true,
|
||||||
// Minimum number of characters to reserve space for in the gutter.
|
// Minimum number of characters to reserve space for in the gutter.
|
||||||
"min_line_number_digits": 4
|
"min_line_number_digits": 4,
|
||||||
},
|
},
|
||||||
"indent_guides": {
|
"indent_guides": {
|
||||||
// Whether to show indent guides in the editor.
|
// Whether to show indent guides in the editor.
|
||||||
@@ -604,7 +606,7 @@
|
|||||||
//
|
//
|
||||||
// 1. "disabled"
|
// 1. "disabled"
|
||||||
// 2. "indent_aware"
|
// 2. "indent_aware"
|
||||||
"background_coloring": "disabled"
|
"background_coloring": "disabled",
|
||||||
},
|
},
|
||||||
// Whether the editor will scroll beyond the last line.
|
// Whether the editor will scroll beyond the last line.
|
||||||
"scroll_beyond_last_line": "one_page",
|
"scroll_beyond_last_line": "one_page",
|
||||||
@@ -623,7 +625,7 @@
|
|||||||
"fast_scroll_sensitivity": 4.0,
|
"fast_scroll_sensitivity": 4.0,
|
||||||
"sticky_scroll": {
|
"sticky_scroll": {
|
||||||
// Whether to stick scopes to the top of the editor.
|
// Whether to stick scopes to the top of the editor.
|
||||||
"enabled": false
|
"enabled": false,
|
||||||
},
|
},
|
||||||
"relative_line_numbers": "disabled",
|
"relative_line_numbers": "disabled",
|
||||||
// If 'search_wrap' is disabled, search result do not wrap around the end of the file.
|
// If 'search_wrap' is disabled, search result do not wrap around the end of the file.
|
||||||
@@ -641,7 +643,7 @@
|
|||||||
// Whether to interpret the search query as a regular expression.
|
// Whether to interpret the search query as a regular expression.
|
||||||
"regex": false,
|
"regex": false,
|
||||||
// Whether to center the cursor on each search match when navigating.
|
// Whether to center the cursor on each search match when navigating.
|
||||||
"center_on_match": false
|
"center_on_match": false,
|
||||||
},
|
},
|
||||||
// When to populate a new search's query based on the text under the cursor.
|
// When to populate a new search's query based on the text under the cursor.
|
||||||
// This setting can take the following three values:
|
// This setting can take the following three values:
|
||||||
@@ -684,8 +686,8 @@
|
|||||||
"shift": false,
|
"shift": false,
|
||||||
"alt": false,
|
"alt": false,
|
||||||
"platform": false,
|
"platform": false,
|
||||||
"function": false
|
"function": false,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
// Whether to resize all the panels in a dock when resizing the dock.
|
// Whether to resize all the panels in a dock when resizing the dock.
|
||||||
// Can be a combination of "left", "right" and "bottom".
|
// Can be a combination of "left", "right" and "bottom".
|
||||||
@@ -733,7 +735,7 @@
|
|||||||
// "always"
|
// "always"
|
||||||
// 5. Never show the scrollbar:
|
// 5. Never show the scrollbar:
|
||||||
// "never"
|
// "never"
|
||||||
"show": null
|
"show": null,
|
||||||
},
|
},
|
||||||
// Which files containing diagnostic errors/warnings to mark in the project panel.
|
// Which files containing diagnostic errors/warnings to mark in the project panel.
|
||||||
// This setting can take the following three values:
|
// This setting can take the following three values:
|
||||||
@@ -756,7 +758,7 @@
|
|||||||
// "always"
|
// "always"
|
||||||
// 2. Never show indent guides:
|
// 2. Never show indent guides:
|
||||||
// "never"
|
// "never"
|
||||||
"show": "always"
|
"show": "always",
|
||||||
},
|
},
|
||||||
// Sort order for entries in the project panel.
|
// Sort order for entries in the project panel.
|
||||||
// This setting can take three values:
|
// This setting can take three values:
|
||||||
@@ -781,8 +783,8 @@
|
|||||||
// Whether to automatically open files after pasting or duplicating them.
|
// Whether to automatically open files after pasting or duplicating them.
|
||||||
"on_paste": true,
|
"on_paste": true,
|
||||||
// Whether to automatically open files dropped from external sources.
|
// Whether to automatically open files dropped from external sources.
|
||||||
"on_drop": true
|
"on_drop": true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
"outline_panel": {
|
"outline_panel": {
|
||||||
// Whether to show the outline panel button in the status bar
|
// Whether to show the outline panel button in the status bar
|
||||||
@@ -815,7 +817,7 @@
|
|||||||
// "always"
|
// "always"
|
||||||
// 2. Never show indent guides:
|
// 2. Never show indent guides:
|
||||||
// "never"
|
// "never"
|
||||||
"show": "always"
|
"show": "always",
|
||||||
},
|
},
|
||||||
// Scrollbar-related settings
|
// Scrollbar-related settings
|
||||||
"scrollbar": {
|
"scrollbar": {
|
||||||
@@ -832,11 +834,11 @@
|
|||||||
// "always"
|
// "always"
|
||||||
// 5. Never show the scrollbar:
|
// 5. Never show the scrollbar:
|
||||||
// "never"
|
// "never"
|
||||||
"show": null
|
"show": null,
|
||||||
},
|
},
|
||||||
// Default depth to expand outline items in the current file.
|
// 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.
|
// 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
|
"expand_outlines_with_depth": 100,
|
||||||
},
|
},
|
||||||
"collaboration_panel": {
|
"collaboration_panel": {
|
||||||
// Whether to show the collaboration panel button in the status bar.
|
// Whether to show the collaboration panel button in the status bar.
|
||||||
@@ -844,7 +846,7 @@
|
|||||||
// Where to dock the collaboration panel. Can be 'left' or 'right'.
|
// Where to dock the collaboration panel. Can be 'left' or 'right'.
|
||||||
"dock": "left",
|
"dock": "left",
|
||||||
// Default width of the collaboration panel.
|
// Default width of the collaboration panel.
|
||||||
"default_width": 240
|
"default_width": 240,
|
||||||
},
|
},
|
||||||
"git_panel": {
|
"git_panel": {
|
||||||
// Whether to show the git panel button in the status bar.
|
// Whether to show the git panel button in the status bar.
|
||||||
@@ -870,18 +872,22 @@
|
|||||||
//
|
//
|
||||||
// Default: false
|
// Default: false
|
||||||
"collapse_untracked_diff": false,
|
"collapse_untracked_diff": false,
|
||||||
|
/// Whether to show entries with tree or flat view in the panel
|
||||||
|
///
|
||||||
|
/// Default: false
|
||||||
|
"tree_view": false,
|
||||||
"scrollbar": {
|
"scrollbar": {
|
||||||
// When to show the scrollbar in the git panel.
|
// When to show the scrollbar in the git panel.
|
||||||
//
|
//
|
||||||
// Choices: always, auto, never, system
|
// Choices: always, auto, never, system
|
||||||
// Default: inherits editor scrollbar settings
|
// Default: inherits editor scrollbar settings
|
||||||
// "show": null
|
// "show": null
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
"message_editor": {
|
"message_editor": {
|
||||||
// Whether to automatically replace emoji shortcodes with emoji characters.
|
// Whether to automatically replace emoji shortcodes with emoji characters.
|
||||||
// For example: typing `:wave:` gets replaced with `👋`.
|
// For example: typing `:wave:` gets replaced with `👋`.
|
||||||
"auto_replace_emoji_shortcode": true
|
"auto_replace_emoji_shortcode": true,
|
||||||
},
|
},
|
||||||
"notification_panel": {
|
"notification_panel": {
|
||||||
// Whether to show the notification panel button in the status bar.
|
// Whether to show the notification panel button in the status bar.
|
||||||
@@ -889,9 +895,11 @@
|
|||||||
// Where to dock the notification panel. Can be 'left' or 'right'.
|
// Where to dock the notification panel. Can be 'left' or 'right'.
|
||||||
"dock": "right",
|
"dock": "right",
|
||||||
// Default width of the notification panel.
|
// Default width of the notification panel.
|
||||||
"default_width": 380
|
"default_width": 380,
|
||||||
},
|
},
|
||||||
"agent": {
|
"agent": {
|
||||||
|
// Whether the inline assistant should use streaming tools, when available
|
||||||
|
"inline_assistant_use_streaming_tools": true,
|
||||||
// Whether the agent is enabled.
|
// Whether the agent is enabled.
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
// What completion mode to start new threads in, if available. Can be 'normal' or 'burn'.
|
// What completion mode to start new threads in, if available. Can be 'normal' or 'burn'.
|
||||||
@@ -900,6 +908,8 @@
|
|||||||
"button": true,
|
"button": true,
|
||||||
// Where to dock the agent panel. Can be 'left', 'right' or 'bottom'.
|
// Where to dock the agent panel. Can be 'left', 'right' or 'bottom'.
|
||||||
"dock": "right",
|
"dock": "right",
|
||||||
|
// Where to dock the agents panel. Can be 'left' or 'right'.
|
||||||
|
"agents_panel_dock": "left",
|
||||||
// Default width when the agent panel is docked to the left or right.
|
// Default width when the agent panel is docked to the left or right.
|
||||||
"default_width": 640,
|
"default_width": 640,
|
||||||
// Default height when the agent panel is docked to the bottom.
|
// Default height when the agent panel is docked to the bottom.
|
||||||
@@ -911,7 +921,7 @@
|
|||||||
// The provider to use.
|
// The provider to use.
|
||||||
"provider": "zed.dev",
|
"provider": "zed.dev",
|
||||||
// The model to use.
|
// The model to use.
|
||||||
"model": "claude-sonnet-4"
|
"model": "claude-sonnet-4",
|
||||||
},
|
},
|
||||||
// Additional parameters for language model requests. When making a request to a model, parameters will be taken
|
// Additional parameters for language model requests. When making a request to a model, parameters will be taken
|
||||||
// from the last entry in this list that matches the model's provider and name. In each entry, both provider
|
// from the last entry in this list that matches the model's provider and name. In each entry, both provider
|
||||||
@@ -962,12 +972,14 @@
|
|||||||
"now": true,
|
"now": true,
|
||||||
"find_path": true,
|
"find_path": true,
|
||||||
"read_file": true,
|
"read_file": true,
|
||||||
|
"restore_file_from_disk": true,
|
||||||
|
"save_file": true,
|
||||||
"open": true,
|
"open": true,
|
||||||
"grep": true,
|
"grep": true,
|
||||||
"terminal": true,
|
"terminal": true,
|
||||||
"thinking": true,
|
"thinking": true,
|
||||||
"web_search": true
|
"web_search": true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
"ask": {
|
"ask": {
|
||||||
"name": "Ask",
|
"name": "Ask",
|
||||||
@@ -984,14 +996,14 @@
|
|||||||
"open": true,
|
"open": true,
|
||||||
"grep": true,
|
"grep": true,
|
||||||
"thinking": true,
|
"thinking": true,
|
||||||
"web_search": true
|
"web_search": true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
"minimal": {
|
"minimal": {
|
||||||
"name": "Minimal",
|
"name": "Minimal",
|
||||||
"enable_all_context_servers": false,
|
"enable_all_context_servers": false,
|
||||||
"tools": {}
|
"tools": {},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
// Where to show notifications when the agent has either completed
|
// Where to show notifications when the agent has either completed
|
||||||
// its response, or else needs confirmation before it can run a
|
// its response, or else needs confirmation before it can run a
|
||||||
@@ -1020,7 +1032,7 @@
|
|||||||
// Minimum number of lines to display in the agent message editor.
|
// Minimum number of lines to display in the agent message editor.
|
||||||
//
|
//
|
||||||
// Default: 4
|
// Default: 4
|
||||||
"message_editor_min_lines": 4
|
"message_editor_min_lines": 4,
|
||||||
},
|
},
|
||||||
// Whether the screen sharing icon is shown in the os status bar.
|
// Whether the screen sharing icon is shown in the os status bar.
|
||||||
"show_call_status_icon": true,
|
"show_call_status_icon": true,
|
||||||
@@ -1055,7 +1067,7 @@
|
|||||||
// Whether or not to show the navigation history buttons.
|
// Whether or not to show the navigation history buttons.
|
||||||
"show_nav_history_buttons": true,
|
"show_nav_history_buttons": true,
|
||||||
// Whether or not to show the tab bar buttons.
|
// Whether or not to show the tab bar buttons.
|
||||||
"show_tab_bar_buttons": true
|
"show_tab_bar_buttons": true,
|
||||||
},
|
},
|
||||||
// Settings related to the editor's tabs
|
// Settings related to the editor's tabs
|
||||||
"tabs": {
|
"tabs": {
|
||||||
@@ -1094,7 +1106,7 @@
|
|||||||
// "errors"
|
// "errors"
|
||||||
// 3. Mark files with errors and warnings:
|
// 3. Mark files with errors and warnings:
|
||||||
// "all"
|
// "all"
|
||||||
"show_diagnostics": "off"
|
"show_diagnostics": "off",
|
||||||
},
|
},
|
||||||
// Settings related to preview tabs.
|
// Settings related to preview tabs.
|
||||||
"preview_tabs": {
|
"preview_tabs": {
|
||||||
@@ -1115,7 +1127,7 @@
|
|||||||
"enable_preview_file_from_code_navigation": true,
|
"enable_preview_file_from_code_navigation": true,
|
||||||
// Whether to keep tabs in preview mode when code navigation is used to navigate away from them.
|
// Whether to keep tabs in preview mode when code navigation is used to navigate away from them.
|
||||||
// If `enable_preview_file_from_code_navigation` or `enable_preview_multibuffer_from_code_navigation` is also true, the new tab may replace the existing one.
|
// If `enable_preview_file_from_code_navigation` or `enable_preview_multibuffer_from_code_navigation` is also true, the new tab may replace the existing one.
|
||||||
"enable_keep_preview_on_code_navigation": false
|
"enable_keep_preview_on_code_navigation": false,
|
||||||
},
|
},
|
||||||
// Settings related to the file finder.
|
// Settings related to the file finder.
|
||||||
"file_finder": {
|
"file_finder": {
|
||||||
@@ -1159,7 +1171,7 @@
|
|||||||
// * "all": Use all gitignored files
|
// * "all": Use all gitignored files
|
||||||
// * "indexed": Use only the files Zed had indexed
|
// * "indexed": Use only the files Zed had indexed
|
||||||
// * "smart": Be smart and search for ignored when called from a gitignored worktree
|
// * "smart": Be smart and search for ignored when called from a gitignored worktree
|
||||||
"include_ignored": "smart"
|
"include_ignored": "smart",
|
||||||
},
|
},
|
||||||
// Whether or not to remove any trailing whitespace from lines of a buffer
|
// Whether or not to remove any trailing whitespace from lines of a buffer
|
||||||
// before saving it.
|
// before saving it.
|
||||||
@@ -1230,7 +1242,7 @@
|
|||||||
// Send debug info like crash reports.
|
// Send debug info like crash reports.
|
||||||
"diagnostics": true,
|
"diagnostics": true,
|
||||||
// Send anonymized usage data like what languages you're using Zed with.
|
// Send anonymized usage data like what languages you're using Zed with.
|
||||||
"metrics": true
|
"metrics": true,
|
||||||
},
|
},
|
||||||
// Whether to disable all AI features in Zed.
|
// Whether to disable all AI features in Zed.
|
||||||
//
|
//
|
||||||
@@ -1264,7 +1276,7 @@
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
// Minimum time to wait before pulling diagnostics from the language server(s).
|
// Minimum time to wait before pulling diagnostics from the language server(s).
|
||||||
// 0 turns the debounce off.
|
// 0 turns the debounce off.
|
||||||
"debounce_ms": 50
|
"debounce_ms": 50,
|
||||||
},
|
},
|
||||||
// Settings for inline diagnostics
|
// Settings for inline diagnostics
|
||||||
"inline": {
|
"inline": {
|
||||||
@@ -1282,8 +1294,8 @@
|
|||||||
"min_column": 0,
|
"min_column": 0,
|
||||||
// The minimum severity of the diagnostics to show inline.
|
// The minimum severity of the diagnostics to show inline.
|
||||||
// Inherits editor's diagnostics' max severity settings when `null`.
|
// Inherits editor's diagnostics' max severity settings when `null`.
|
||||||
"max_severity": null
|
"max_severity": null,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
// Files or globs of files that will be excluded by Zed entirely. They will be skipped during file
|
// Files or globs of files that will be excluded by Zed entirely. They will be skipped during file
|
||||||
// scans, file searches, and not be displayed in the project file tree. Takes precedence over `file_scan_inclusions`.
|
// scans, file searches, and not be displayed in the project file tree. Takes precedence over `file_scan_inclusions`.
|
||||||
@@ -1297,7 +1309,7 @@
|
|||||||
"**/.DS_Store",
|
"**/.DS_Store",
|
||||||
"**/Thumbs.db",
|
"**/Thumbs.db",
|
||||||
"**/.classpath",
|
"**/.classpath",
|
||||||
"**/.settings"
|
"**/.settings",
|
||||||
],
|
],
|
||||||
// Files or globs of files that will be included by Zed, even when ignored by git. This is useful
|
// Files or globs of files that will be included by Zed, even when ignored by git. This is useful
|
||||||
// for files that are not tracked by git, but are still important to your project. Note that globs
|
// for files that are not tracked by git, but are still important to your project. Note that globs
|
||||||
@@ -1332,14 +1344,14 @@
|
|||||||
// Whether or not to display the git commit summary on the same line.
|
// Whether or not to display the git commit summary on the same line.
|
||||||
"show_commit_summary": false,
|
"show_commit_summary": false,
|
||||||
// The minimum column number to show the inline blame information at
|
// The minimum column number to show the inline blame information at
|
||||||
"min_column": 0
|
"min_column": 0,
|
||||||
},
|
},
|
||||||
"blame": {
|
"blame": {
|
||||||
"show_avatar": true
|
"show_avatar": true,
|
||||||
},
|
},
|
||||||
// Control which information is shown in the branch picker.
|
// Control which information is shown in the branch picker.
|
||||||
"branch_picker": {
|
"branch_picker": {
|
||||||
"show_author_name": true
|
"show_author_name": true,
|
||||||
},
|
},
|
||||||
// How git hunks are displayed visually in the editor.
|
// How git hunks are displayed visually in the editor.
|
||||||
// This setting can take two values:
|
// This setting can take two values:
|
||||||
@@ -1351,7 +1363,7 @@
|
|||||||
"hunk_style": "staged_hollow",
|
"hunk_style": "staged_hollow",
|
||||||
// Should the name or path be displayed first in the git view.
|
// Should the name or path be displayed first in the git view.
|
||||||
// "path_style": "file_name_first" or "file_path_first"
|
// "path_style": "file_name_first" or "file_path_first"
|
||||||
"path_style": "file_name_first"
|
"path_style": "file_name_first",
|
||||||
},
|
},
|
||||||
// The list of custom Git hosting providers.
|
// The list of custom Git hosting providers.
|
||||||
"git_hosting_providers": [
|
"git_hosting_providers": [
|
||||||
@@ -1385,7 +1397,7 @@
|
|||||||
"**/secrets.yml",
|
"**/secrets.yml",
|
||||||
"**/.zed/settings.json", // zed project settings
|
"**/.zed/settings.json", // zed project settings
|
||||||
"/**/zed/settings.json", // zed user settings
|
"/**/zed/settings.json", // zed user settings
|
||||||
"/**/zed/keymap.json"
|
"/**/zed/keymap.json",
|
||||||
],
|
],
|
||||||
// When to show edit predictions previews in buffer.
|
// When to show edit predictions previews in buffer.
|
||||||
// This setting takes two possible values:
|
// This setting takes two possible values:
|
||||||
@@ -1403,15 +1415,16 @@
|
|||||||
"copilot": {
|
"copilot": {
|
||||||
"enterprise_uri": null,
|
"enterprise_uri": null,
|
||||||
"proxy": null,
|
"proxy": null,
|
||||||
"proxy_no_verify": null
|
"proxy_no_verify": null,
|
||||||
},
|
},
|
||||||
"codestral": {
|
"codestral": {
|
||||||
"model": null,
|
"api_url": "https://codestral.mistral.ai",
|
||||||
"max_tokens": null
|
"model": "codestral-latest",
|
||||||
|
"max_tokens": 150,
|
||||||
},
|
},
|
||||||
// Whether edit predictions are enabled when editing text threads in the agent panel.
|
// Whether edit predictions are enabled when editing text threads in the agent panel.
|
||||||
// This setting has no effect if globally disabled.
|
// This setting has no effect if globally disabled.
|
||||||
"enabled_in_text_threads": true
|
"enabled_in_text_threads": true,
|
||||||
},
|
},
|
||||||
// Settings specific to journaling
|
// Settings specific to journaling
|
||||||
"journal": {
|
"journal": {
|
||||||
@@ -1421,7 +1434,7 @@
|
|||||||
// May take 2 values:
|
// May take 2 values:
|
||||||
// 1. hour12
|
// 1. hour12
|
||||||
// 2. hour24
|
// 2. hour24
|
||||||
"hour_format": "hour12"
|
"hour_format": "hour12",
|
||||||
},
|
},
|
||||||
// Status bar-related settings.
|
// Status bar-related settings.
|
||||||
"status_bar": {
|
"status_bar": {
|
||||||
@@ -1432,7 +1445,7 @@
|
|||||||
// Whether to show the cursor position button in the status bar.
|
// Whether to show the cursor position button in the status bar.
|
||||||
"cursor_position_button": true,
|
"cursor_position_button": true,
|
||||||
// Whether to show active line endings button in the status bar.
|
// Whether to show active line endings button in the status bar.
|
||||||
"line_endings_button": false
|
"line_endings_button": false,
|
||||||
},
|
},
|
||||||
// Settings specific to the terminal
|
// Settings specific to the terminal
|
||||||
"terminal": {
|
"terminal": {
|
||||||
@@ -1553,8 +1566,8 @@
|
|||||||
// Preferred Conda manager to use when activating Conda environments.
|
// Preferred Conda manager to use when activating Conda environments.
|
||||||
// Values: "auto", "conda", "mamba", "micromamba"
|
// Values: "auto", "conda", "mamba", "micromamba"
|
||||||
// Default: "auto"
|
// Default: "auto"
|
||||||
"conda_manager": "auto"
|
"conda_manager": "auto",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
// Whether to display the terminal title in its toolbar's breadcrumbs.
|
// Whether to display the terminal title in its toolbar's breadcrumbs.
|
||||||
@@ -1562,7 +1575,7 @@
|
|||||||
//
|
//
|
||||||
// The shell running in the terminal needs to be configured to emit the title.
|
// The shell running in the terminal needs to be configured to emit the title.
|
||||||
// Example: `echo -e "\e]2;New Title\007";`
|
// Example: `echo -e "\e]2;New Title\007";`
|
||||||
"breadcrumbs": false
|
"breadcrumbs": false,
|
||||||
},
|
},
|
||||||
// Scrollbar-related settings
|
// Scrollbar-related settings
|
||||||
"scrollbar": {
|
"scrollbar": {
|
||||||
@@ -1579,7 +1592,7 @@
|
|||||||
// "always"
|
// "always"
|
||||||
// 5. Never show the scrollbar:
|
// 5. Never show the scrollbar:
|
||||||
// "never"
|
// "never"
|
||||||
"show": null
|
"show": null,
|
||||||
},
|
},
|
||||||
// Set the terminal's font size. If this option is not included,
|
// Set the terminal's font size. If this option is not included,
|
||||||
// the terminal will default to matching the buffer's font size.
|
// the terminal will default to matching the buffer's font size.
|
||||||
@@ -1642,30 +1655,26 @@
|
|||||||
// surrounding symbols or quotes
|
// surrounding symbols or quotes
|
||||||
[
|
[
|
||||||
"(?x)",
|
"(?x)",
|
||||||
"# optionally starts with 0-2 opening prefix symbols",
|
"(?<path>",
|
||||||
"[({\\[<]{0,2}",
|
" (",
|
||||||
"# which may be followed by an opening quote",
|
" # multi-char path: first char (not opening delimiter or space)",
|
||||||
"(?<quote>[\"'`])?",
|
" [^({\\[<\"'`\\ ]",
|
||||||
"# `path` is the shortest sequence of any non-space character",
|
" # middle chars: non-space, and colon/paren only if not followed by digit/paren",
|
||||||
"(?<link>(?<path>[^ ]+?",
|
" ([^\\ :(]|[:(][^0-9()])*",
|
||||||
" # which may end with a line and optionally a column,",
|
" # last char: not closing delimiter or colon",
|
||||||
" (?<line_column>:+[0-9]+(:[0-9]+)?|:?\\([0-9]+([,:][0-9]+)?\\))?",
|
" [^()}\\]>\"'`.,;:\\ ]",
|
||||||
"))",
|
" |",
|
||||||
"# which must be followed by a matching quote",
|
" # single-char path: not delimiter, punctuation, or space",
|
||||||
"(?(<quote>)\\k<quote>)",
|
" [^(){}\\[\\]<>\"'`.,;:\\ ]",
|
||||||
"# and optionally a single closing symbol",
|
" )",
|
||||||
"[)}\\]>]?",
|
" # optional line/column suffix (included in path for PathWithPosition::parse_str)",
|
||||||
"# if line/column matched, may be followed by a description",
|
" (:+[0-9]+(:[0-9]+)?|:?\\([0-9]+([,:]?[0-9]+)?\\))?",
|
||||||
"(?(<line_column>):[^ 0-9][^ ]*)?",
|
")",
|
||||||
"# which may be followed by trailing punctuation",
|
],
|
||||||
"[.,:)}\\]>]*",
|
|
||||||
"# and always includes trailing whitespace or end of line",
|
|
||||||
"([ ]+|$)"
|
|
||||||
]
|
|
||||||
],
|
],
|
||||||
// Timeout for hover and Cmd-click path hyperlink discovery in milliseconds. Specifying a
|
// Timeout for hover and Cmd-click path hyperlink discovery in milliseconds. Specifying a
|
||||||
// timeout of `0` will disable path hyperlinking in terminal.
|
// timeout of `0` will disable path hyperlinking in terminal.
|
||||||
"path_hyperlink_timeout_ms": 1
|
"path_hyperlink_timeout_ms": 1,
|
||||||
},
|
},
|
||||||
"code_actions_on_format": {},
|
"code_actions_on_format": {},
|
||||||
// Settings related to running tasks.
|
// Settings related to running tasks.
|
||||||
@@ -1681,7 +1690,7 @@
|
|||||||
// * Zed task from history (e.g. one-off task was spawned before)
|
// * Zed task from history (e.g. one-off task was spawned before)
|
||||||
//
|
//
|
||||||
// Default: true
|
// Default: true
|
||||||
"prefer_lsp": true
|
"prefer_lsp": true,
|
||||||
},
|
},
|
||||||
// An object whose keys are language names, and whose values
|
// An object whose keys are language names, and whose values
|
||||||
// are arrays of filenames or extensions of files that should
|
// are arrays of filenames or extensions of files that should
|
||||||
@@ -1698,7 +1707,7 @@
|
|||||||
"file_types": {
|
"file_types": {
|
||||||
"JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json", "**/.vscode/**/*.json", "tsconfig*.json"],
|
"JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json", "**/.vscode/**/*.json", "tsconfig*.json"],
|
||||||
"Markdown": [".rules", ".cursorrules", ".windsurfrules", ".clinerules"],
|
"Markdown": [".rules", ".cursorrules", ".windsurfrules", ".clinerules"],
|
||||||
"Shell Script": [".env.*"]
|
"Shell Script": [".env.*"],
|
||||||
},
|
},
|
||||||
// Settings for which version of Node.js and NPM to use when installing
|
// Settings for which version of Node.js and NPM to use when installing
|
||||||
// language servers and Copilot.
|
// language servers and Copilot.
|
||||||
@@ -1714,14 +1723,14 @@
|
|||||||
// `path`, but not `npm_path`, Zed will assume that `npm` is located at
|
// `path`, but not `npm_path`, Zed will assume that `npm` is located at
|
||||||
// `${path}/../npm`.
|
// `${path}/../npm`.
|
||||||
"path": null,
|
"path": null,
|
||||||
"npm_path": null
|
"npm_path": null,
|
||||||
},
|
},
|
||||||
// The extensions that Zed should automatically install on startup.
|
// The extensions that Zed should automatically install on startup.
|
||||||
//
|
//
|
||||||
// If you don't want any of these extensions, add this field to your settings
|
// If you don't want any of these extensions, add this field to your settings
|
||||||
// and change the value to `false`.
|
// and change the value to `false`.
|
||||||
"auto_install_extensions": {
|
"auto_install_extensions": {
|
||||||
"html": true
|
"html": true,
|
||||||
},
|
},
|
||||||
// The capabilities granted to extensions.
|
// The capabilities granted to extensions.
|
||||||
//
|
//
|
||||||
@@ -1729,7 +1738,7 @@
|
|||||||
"granted_extension_capabilities": [
|
"granted_extension_capabilities": [
|
||||||
{ "kind": "process:exec", "command": "*", "args": ["**"] },
|
{ "kind": "process:exec", "command": "*", "args": ["**"] },
|
||||||
{ "kind": "download_file", "host": "*", "path": ["**"] },
|
{ "kind": "download_file", "host": "*", "path": ["**"] },
|
||||||
{ "kind": "npm:install", "package": "*" }
|
{ "kind": "npm:install", "package": "*" },
|
||||||
],
|
],
|
||||||
// Controls how completions are processed for this language.
|
// Controls how completions are processed for this language.
|
||||||
"completions": {
|
"completions": {
|
||||||
@@ -1780,7 +1789,7 @@
|
|||||||
// 4. "replace_suffix"
|
// 4. "replace_suffix"
|
||||||
// Behaves like `"replace"` if the text after the cursor is a suffix of the completion, and like
|
// Behaves like `"replace"` if the text after the cursor is a suffix of the completion, and like
|
||||||
// `"insert"` otherwise.
|
// `"insert"` otherwise.
|
||||||
"lsp_insert_mode": "replace_suffix"
|
"lsp_insert_mode": "replace_suffix",
|
||||||
},
|
},
|
||||||
// Different settings for specific languages.
|
// Different settings for specific languages.
|
||||||
"languages": {
|
"languages": {
|
||||||
@@ -1788,113 +1797,116 @@
|
|||||||
"language_servers": ["astro-language-server", "..."],
|
"language_servers": ["astro-language-server", "..."],
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"allowed": true,
|
"allowed": true,
|
||||||
"plugins": ["prettier-plugin-astro"]
|
"plugins": ["prettier-plugin-astro"],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
"Blade": {
|
"Blade": {
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"allowed": true
|
"allowed": true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
"C": {
|
"C": {
|
||||||
"format_on_save": "off",
|
"format_on_save": "off",
|
||||||
"use_on_type_format": false,
|
"use_on_type_format": false,
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"allowed": false
|
"allowed": false,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
"C++": {
|
"C++": {
|
||||||
"format_on_save": "off",
|
"format_on_save": "off",
|
||||||
"use_on_type_format": false,
|
"use_on_type_format": false,
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"allowed": false
|
"allowed": false,
|
||||||
}
|
},
|
||||||
|
},
|
||||||
|
"CSharp": {
|
||||||
|
"language_servers": ["roslyn", "!omnisharp", "..."],
|
||||||
},
|
},
|
||||||
"CSS": {
|
"CSS": {
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"allowed": true
|
"allowed": true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
"Dart": {
|
"Dart": {
|
||||||
"tab_size": 2
|
"tab_size": 2,
|
||||||
},
|
},
|
||||||
"Diff": {
|
"Diff": {
|
||||||
"show_edit_predictions": false,
|
"show_edit_predictions": false,
|
||||||
"remove_trailing_whitespace_on_save": false,
|
"remove_trailing_whitespace_on_save": false,
|
||||||
"ensure_final_newline_on_save": false
|
"ensure_final_newline_on_save": false,
|
||||||
},
|
},
|
||||||
"Elixir": {
|
"Elixir": {
|
||||||
"language_servers": ["elixir-ls", "!expert", "!next-ls", "!lexical", "..."]
|
"language_servers": ["elixir-ls", "!expert", "!next-ls", "!lexical", "..."],
|
||||||
},
|
},
|
||||||
"Elm": {
|
"Elm": {
|
||||||
"tab_size": 4
|
"tab_size": 4,
|
||||||
},
|
},
|
||||||
"Erlang": {
|
"Erlang": {
|
||||||
"language_servers": ["erlang-ls", "!elp", "..."]
|
"language_servers": ["erlang-ls", "!elp", "..."],
|
||||||
},
|
},
|
||||||
"Git Commit": {
|
"Git Commit": {
|
||||||
"allow_rewrap": "anywhere",
|
"allow_rewrap": "anywhere",
|
||||||
"soft_wrap": "editor_width",
|
"soft_wrap": "editor_width",
|
||||||
"preferred_line_length": 72
|
"preferred_line_length": 72,
|
||||||
},
|
},
|
||||||
"Go": {
|
"Go": {
|
||||||
"hard_tabs": true,
|
"hard_tabs": true,
|
||||||
"code_actions_on_format": {
|
"code_actions_on_format": {
|
||||||
"source.organizeImports": true
|
"source.organizeImports": true,
|
||||||
},
|
},
|
||||||
"debuggers": ["Delve"]
|
"debuggers": ["Delve"],
|
||||||
},
|
},
|
||||||
"GraphQL": {
|
"GraphQL": {
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"allowed": true
|
"allowed": true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
"HEEX": {
|
"HEEX": {
|
||||||
"language_servers": ["elixir-ls", "!expert", "!next-ls", "!lexical", "..."]
|
"language_servers": ["elixir-ls", "!expert", "!next-ls", "!lexical", "..."],
|
||||||
},
|
},
|
||||||
"HTML": {
|
"HTML": {
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"allowed": true
|
"allowed": true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
"HTML+ERB": {
|
"HTML+ERB": {
|
||||||
"language_servers": ["herb", "!ruby-lsp", "..."]
|
"language_servers": ["herb", "!ruby-lsp", "..."],
|
||||||
},
|
},
|
||||||
"Java": {
|
"Java": {
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"allowed": true,
|
"allowed": true,
|
||||||
"plugins": ["prettier-plugin-java"]
|
"plugins": ["prettier-plugin-java"],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
"JavaScript": {
|
"JavaScript": {
|
||||||
"language_servers": ["!typescript-language-server", "vtsls", "..."],
|
"language_servers": ["!typescript-language-server", "vtsls", "..."],
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"allowed": true
|
"allowed": true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
"JSON": {
|
"JSON": {
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"allowed": true
|
"allowed": true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
"JSONC": {
|
"JSONC": {
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"allowed": true
|
"allowed": true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
"JS+ERB": {
|
"JS+ERB": {
|
||||||
"language_servers": ["!ruby-lsp", "..."]
|
"language_servers": ["!ruby-lsp", "..."],
|
||||||
},
|
},
|
||||||
"Kotlin": {
|
"Kotlin": {
|
||||||
"language_servers": ["!kotlin-language-server", "kotlin-lsp", "..."]
|
"language_servers": ["!kotlin-language-server", "kotlin-lsp", "..."],
|
||||||
},
|
},
|
||||||
"LaTeX": {
|
"LaTeX": {
|
||||||
"formatter": "language_server",
|
"formatter": "language_server",
|
||||||
"language_servers": ["texlab", "..."],
|
"language_servers": ["texlab", "..."],
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"allowed": true,
|
"allowed": true,
|
||||||
"plugins": ["prettier-plugin-latex"]
|
"plugins": ["prettier-plugin-latex"],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
"Markdown": {
|
"Markdown": {
|
||||||
"format_on_save": "off",
|
"format_on_save": "off",
|
||||||
@@ -1902,136 +1914,145 @@
|
|||||||
"remove_trailing_whitespace_on_save": false,
|
"remove_trailing_whitespace_on_save": false,
|
||||||
"allow_rewrap": "anywhere",
|
"allow_rewrap": "anywhere",
|
||||||
"soft_wrap": "editor_width",
|
"soft_wrap": "editor_width",
|
||||||
|
"completions": {
|
||||||
|
"words": "disabled",
|
||||||
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"allowed": true
|
"allowed": true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
"PHP": {
|
"PHP": {
|
||||||
"language_servers": ["phpactor", "!intelephense", "!phptools", "..."],
|
"language_servers": ["phpactor", "!intelephense", "!phptools", "..."],
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"allowed": true,
|
"allowed": true,
|
||||||
"plugins": ["@prettier/plugin-php"],
|
"plugins": ["@prettier/plugin-php"],
|
||||||
"parser": "php"
|
"parser": "php",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
"Plain Text": {
|
"Plain Text": {
|
||||||
"allow_rewrap": "anywhere",
|
"allow_rewrap": "anywhere",
|
||||||
"soft_wrap": "editor_width"
|
"soft_wrap": "editor_width",
|
||||||
|
"completions": {
|
||||||
|
"words": "disabled",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"Proto": {
|
||||||
|
"language_servers": ["buf", "!protols", "!protobuf-language-server", "..."],
|
||||||
},
|
},
|
||||||
"Python": {
|
"Python": {
|
||||||
"code_actions_on_format": {
|
"code_actions_on_format": {
|
||||||
"source.organizeImports.ruff": true
|
"source.organizeImports.ruff": true,
|
||||||
},
|
},
|
||||||
"formatter": {
|
"formatter": {
|
||||||
"language_server": {
|
"language_server": {
|
||||||
"name": "ruff"
|
"name": "ruff",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
"debuggers": ["Debugpy"],
|
"debuggers": ["Debugpy"],
|
||||||
"language_servers": ["basedpyright", "ruff", "!ty", "!pyrefly", "!pyright", "!pylsp", "..."]
|
"language_servers": ["basedpyright", "ruff", "!ty", "!pyrefly", "!pyright", "!pylsp", "..."],
|
||||||
},
|
},
|
||||||
"Ruby": {
|
"Ruby": {
|
||||||
"language_servers": ["solargraph", "!ruby-lsp", "!rubocop", "!sorbet", "!steep", "..."]
|
"language_servers": ["solargraph", "!ruby-lsp", "!rubocop", "!sorbet", "!steep", "..."],
|
||||||
},
|
},
|
||||||
"Rust": {
|
"Rust": {
|
||||||
"debuggers": ["CodeLLDB"]
|
"debuggers": ["CodeLLDB"],
|
||||||
},
|
},
|
||||||
"SCSS": {
|
"SCSS": {
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"allowed": true
|
"allowed": true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
"Starlark": {
|
"Starlark": {
|
||||||
"language_servers": ["starpls", "!buck2-lsp", "..."]
|
"language_servers": ["starpls", "!buck2-lsp", "..."],
|
||||||
},
|
},
|
||||||
"Svelte": {
|
"Svelte": {
|
||||||
"language_servers": ["svelte-language-server", "..."],
|
"language_servers": ["svelte-language-server", "..."],
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"allowed": true,
|
"allowed": true,
|
||||||
"plugins": ["prettier-plugin-svelte"]
|
"plugins": ["prettier-plugin-svelte"],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
"TSX": {
|
"TSX": {
|
||||||
"language_servers": ["!typescript-language-server", "vtsls", "..."],
|
"language_servers": ["!typescript-language-server", "vtsls", "..."],
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"allowed": true
|
"allowed": true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
"Twig": {
|
"Twig": {
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"allowed": true
|
"allowed": true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
"TypeScript": {
|
"TypeScript": {
|
||||||
"language_servers": ["!typescript-language-server", "vtsls", "..."],
|
"language_servers": ["!typescript-language-server", "vtsls", "..."],
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"allowed": true
|
"allowed": true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
"SystemVerilog": {
|
"SystemVerilog": {
|
||||||
"format_on_save": "off",
|
"format_on_save": "off",
|
||||||
"language_servers": ["!slang", "..."],
|
"language_servers": ["!slang", "..."],
|
||||||
"use_on_type_format": false
|
"use_on_type_format": false,
|
||||||
},
|
},
|
||||||
"Vue.js": {
|
"Vue.js": {
|
||||||
"language_servers": ["vue-language-server", "vtsls", "..."],
|
"language_servers": ["vue-language-server", "vtsls", "..."],
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"allowed": true
|
"allowed": true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
"XML": {
|
"XML": {
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"allowed": true,
|
"allowed": true,
|
||||||
"plugins": ["@prettier/plugin-xml"]
|
"plugins": ["@prettier/plugin-xml"],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
"YAML": {
|
"YAML": {
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"allowed": true
|
"allowed": true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
"YAML+ERB": {
|
"YAML+ERB": {
|
||||||
"language_servers": ["!ruby-lsp", "..."]
|
"language_servers": ["!ruby-lsp", "..."],
|
||||||
},
|
},
|
||||||
"Zig": {
|
"Zig": {
|
||||||
"language_servers": ["zls", "..."]
|
"language_servers": ["zls", "..."],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
// Different settings for specific language models.
|
// Different settings for specific language models.
|
||||||
"language_models": {
|
"language_models": {
|
||||||
"anthropic": {
|
"anthropic": {
|
||||||
"api_url": "https://api.anthropic.com"
|
"api_url": "https://api.anthropic.com",
|
||||||
},
|
},
|
||||||
"bedrock": {},
|
"bedrock": {},
|
||||||
"google": {
|
"google": {
|
||||||
"api_url": "https://generativelanguage.googleapis.com"
|
"api_url": "https://generativelanguage.googleapis.com",
|
||||||
},
|
},
|
||||||
"ollama": {
|
"ollama": {
|
||||||
"api_url": "http://localhost:11434"
|
"api_url": "http://localhost:11434",
|
||||||
},
|
},
|
||||||
"openai": {
|
"openai": {
|
||||||
"api_url": "https://api.openai.com/v1"
|
"api_url": "https://api.openai.com/v1",
|
||||||
},
|
},
|
||||||
"openai_compatible": {},
|
"openai_compatible": {},
|
||||||
"open_router": {
|
"open_router": {
|
||||||
"api_url": "https://openrouter.ai/api/v1"
|
"api_url": "https://openrouter.ai/api/v1",
|
||||||
},
|
},
|
||||||
"lmstudio": {
|
"lmstudio": {
|
||||||
"api_url": "http://localhost:1234/api/v0"
|
"api_url": "http://localhost:1234/api/v0",
|
||||||
},
|
},
|
||||||
"deepseek": {
|
"deepseek": {
|
||||||
"api_url": "https://api.deepseek.com/v1"
|
"api_url": "https://api.deepseek.com/v1",
|
||||||
},
|
},
|
||||||
"mistral": {
|
"mistral": {
|
||||||
"api_url": "https://api.mistral.ai/v1"
|
"api_url": "https://api.mistral.ai/v1",
|
||||||
},
|
},
|
||||||
"vercel": {
|
"vercel": {
|
||||||
"api_url": "https://api.v0.dev/v1"
|
"api_url": "https://api.v0.dev/v1",
|
||||||
},
|
},
|
||||||
"x_ai": {
|
"x_ai": {
|
||||||
"api_url": "https://api.x.ai/v1"
|
"api_url": "https://api.x.ai/v1",
|
||||||
},
|
},
|
||||||
"zed.dev": {}
|
"zed.dev": {},
|
||||||
},
|
},
|
||||||
"session": {
|
"session": {
|
||||||
// Whether or not to restore unsaved buffers on restart.
|
// Whether or not to restore unsaved buffers on restart.
|
||||||
@@ -2040,7 +2061,13 @@
|
|||||||
// dirty files when closing the application.
|
// dirty files when closing the application.
|
||||||
//
|
//
|
||||||
// Default: true
|
// Default: true
|
||||||
"restore_unsaved_buffers": true
|
"restore_unsaved_buffers": true,
|
||||||
|
// Whether or not to skip worktree trust checks.
|
||||||
|
// When trusted, project settings are synchronized automatically,
|
||||||
|
// language and MCP servers are downloaded and started automatically.
|
||||||
|
//
|
||||||
|
// Default: false
|
||||||
|
"trust_all_worktrees": false,
|
||||||
},
|
},
|
||||||
// Zed's Prettier integration settings.
|
// Zed's Prettier integration settings.
|
||||||
// Allows to enable/disable formatting with Prettier
|
// Allows to enable/disable formatting with Prettier
|
||||||
@@ -2058,11 +2085,11 @@
|
|||||||
// "singleQuote": true
|
// "singleQuote": true
|
||||||
// Forces Prettier integration to use a specific parser name when formatting files with the language
|
// Forces Prettier integration to use a specific parser name when formatting files with the language
|
||||||
// when set to a non-empty string.
|
// when set to a non-empty string.
|
||||||
"parser": ""
|
"parser": "",
|
||||||
},
|
},
|
||||||
// Settings for auto-closing of JSX tags.
|
// Settings for auto-closing of JSX tags.
|
||||||
"jsx_tag_auto_close": {
|
"jsx_tag_auto_close": {
|
||||||
"enabled": true
|
"enabled": true,
|
||||||
},
|
},
|
||||||
// LSP Specific settings.
|
// LSP Specific settings.
|
||||||
"lsp": {
|
"lsp": {
|
||||||
@@ -2083,19 +2110,19 @@
|
|||||||
// Specify the DAP name as a key here.
|
// Specify the DAP name as a key here.
|
||||||
"CodeLLDB": {
|
"CodeLLDB": {
|
||||||
"env": {
|
"env": {
|
||||||
"RUST_LOG": "info"
|
"RUST_LOG": "info",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
// Common language server settings.
|
// Common language server settings.
|
||||||
"global_lsp_settings": {
|
"global_lsp_settings": {
|
||||||
// Whether to show the LSP servers button in the status bar.
|
// Whether to show the LSP servers button in the status bar.
|
||||||
"button": true
|
"button": true,
|
||||||
},
|
},
|
||||||
// Jupyter settings
|
// Jupyter settings
|
||||||
"jupyter": {
|
"jupyter": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"kernel_selections": {}
|
"kernel_selections": {},
|
||||||
// Specify the language name as the key and the kernel name as the value.
|
// Specify the language name as the key and the kernel name as the value.
|
||||||
// "kernel_selections": {
|
// "kernel_selections": {
|
||||||
// "python": "conda-base"
|
// "python": "conda-base"
|
||||||
@@ -2109,7 +2136,7 @@
|
|||||||
"max_columns": 128,
|
"max_columns": 128,
|
||||||
// Maximum number of lines to keep in REPL's scrollback buffer.
|
// Maximum number of lines to keep in REPL's scrollback buffer.
|
||||||
// Clamped with [4, 256] range.
|
// Clamped with [4, 256] range.
|
||||||
"max_lines": 32
|
"max_lines": 32,
|
||||||
},
|
},
|
||||||
// Vim settings
|
// Vim settings
|
||||||
"vim": {
|
"vim": {
|
||||||
@@ -2123,7 +2150,7 @@
|
|||||||
// Specify the mode as the key and the shape as the value.
|
// Specify the mode as the key and the shape as the value.
|
||||||
// The mode can be one of the following: "normal", "replace", "insert", "visual".
|
// The mode can be one of the following: "normal", "replace", "insert", "visual".
|
||||||
// The shape can be one of the following: "block", "bar", "underline", "hollow".
|
// The shape can be one of the following: "block", "bar", "underline", "hollow".
|
||||||
"cursor_shape": {}
|
"cursor_shape": {},
|
||||||
},
|
},
|
||||||
// The server to connect to. If the environment variable
|
// The server to connect to. If the environment variable
|
||||||
// ZED_SERVER_URL is set, it will override this setting.
|
// ZED_SERVER_URL is set, it will override this setting.
|
||||||
@@ -2156,9 +2183,9 @@
|
|||||||
"windows": {
|
"windows": {
|
||||||
"languages": {
|
"languages": {
|
||||||
"PHP": {
|
"PHP": {
|
||||||
"language_servers": ["intelephense", "!phpactor", "!phptools", "..."]
|
"language_servers": ["intelephense", "!phpactor", "!phptools", "..."],
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
// Whether to show full labels in line indicator or short ones
|
// Whether to show full labels in line indicator or short ones
|
||||||
//
|
//
|
||||||
@@ -2217,7 +2244,7 @@
|
|||||||
"dock": "bottom",
|
"dock": "bottom",
|
||||||
"log_dap_communications": true,
|
"log_dap_communications": true,
|
||||||
"format_dap_log_messages": true,
|
"format_dap_log_messages": true,
|
||||||
"button": true
|
"button": true,
|
||||||
},
|
},
|
||||||
// Configures any number of settings profiles that are temporarily applied on
|
// Configures any number of settings profiles that are temporarily applied on
|
||||||
// top of your existing user settings when selected from
|
// top of your existing user settings when selected from
|
||||||
@@ -2244,5 +2271,5 @@
|
|||||||
// Useful for filtering out noisy logs or enabling more verbose logging.
|
// Useful for filtering out noisy logs or enabling more verbose logging.
|
||||||
//
|
//
|
||||||
// Example: {"log": {"client": "warn"}}
|
// Example: {"log": {"client": "warn"}}
|
||||||
"log": {}
|
"log": {},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"adapter": "Debugpy",
|
"adapter": "Debugpy",
|
||||||
"program": "$ZED_FILE",
|
"program": "$ZED_FILE",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"cwd": "$ZED_WORKTREE_ROOT"
|
"cwd": "$ZED_WORKTREE_ROOT",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Debug active JavaScript file",
|
"label": "Debug active JavaScript file",
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
"program": "$ZED_FILE",
|
"program": "$ZED_FILE",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"cwd": "$ZED_WORKTREE_ROOT",
|
"cwd": "$ZED_WORKTREE_ROOT",
|
||||||
"type": "pwa-node"
|
"type": "pwa-node",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "JavaScript debug terminal",
|
"label": "JavaScript debug terminal",
|
||||||
@@ -24,6 +24,6 @@
|
|||||||
"request": "launch",
|
"request": "launch",
|
||||||
"cwd": "$ZED_WORKTREE_ROOT",
|
"cwd": "$ZED_WORKTREE_ROOT",
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
"type": "pwa-node"
|
"type": "pwa-node",
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -3,5 +3,5 @@
|
|||||||
// For a full list of overridable settings, and general information on settings,
|
// For a full list of overridable settings, and general information on settings,
|
||||||
// see the documentation: https://zed.dev/docs/configuring-zed#settings-files
|
// see the documentation: https://zed.dev/docs/configuring-zed#settings-files
|
||||||
{
|
{
|
||||||
"lsp": {}
|
"lsp": {},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,8 +47,8 @@
|
|||||||
// Whether to show the task line in the output of the spawned task, defaults to `true`.
|
// Whether to show the task line in the output of the spawned task, defaults to `true`.
|
||||||
"show_summary": true,
|
"show_summary": true,
|
||||||
// Whether to show the command line in the output of the spawned task, defaults to `true`.
|
// Whether to show the command line in the output of the spawned task, defaults to `true`.
|
||||||
"show_command": true
|
"show_command": true,
|
||||||
// Represents the tags for inline runnable indicators, or spawning multiple tasks at once.
|
// Represents the tags for inline runnable indicators, or spawning multiple tasks at once.
|
||||||
// "tags": []
|
// "tags": []
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -12,6 +12,6 @@
|
|||||||
"theme": {
|
"theme": {
|
||||||
"mode": "system",
|
"mode": "system",
|
||||||
"light": "One Light",
|
"light": "One Light",
|
||||||
"dark": "One Dark"
|
"dark": "One Dark",
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,33 +71,33 @@
|
|||||||
"editor.document_highlight.read_background": "#83a5981a",
|
"editor.document_highlight.read_background": "#83a5981a",
|
||||||
"editor.document_highlight.write_background": "#92847466",
|
"editor.document_highlight.write_background": "#92847466",
|
||||||
"terminal.background": "#282828ff",
|
"terminal.background": "#282828ff",
|
||||||
"terminal.foreground": "#fbf1c7ff",
|
"terminal.foreground": "#ebdbb2ff",
|
||||||
"terminal.bright_foreground": "#fbf1c7ff",
|
"terminal.bright_foreground": "#fbf1c7ff",
|
||||||
"terminal.dim_foreground": "#282828ff",
|
"terminal.dim_foreground": "#766b5dff",
|
||||||
"terminal.ansi.black": "#282828ff",
|
"terminal.ansi.black": "#282828ff",
|
||||||
"terminal.ansi.bright_black": "#73675eff",
|
"terminal.ansi.bright_black": "#928374ff",
|
||||||
"terminal.ansi.dim_black": "#fbf1c7ff",
|
"terminal.ansi.dim_black": "#fbf1c7ff",
|
||||||
"terminal.ansi.red": "#fb4a35ff",
|
"terminal.ansi.red": "#cc241dff",
|
||||||
"terminal.ansi.bright_red": "#93201dff",
|
"terminal.ansi.bright_red": "#fb4934ff",
|
||||||
"terminal.ansi.dim_red": "#ffaa95ff",
|
"terminal.ansi.dim_red": "#8e1814ff",
|
||||||
"terminal.ansi.green": "#b7bb26ff",
|
"terminal.ansi.green": "#98971aff",
|
||||||
"terminal.ansi.bright_green": "#605c1bff",
|
"terminal.ansi.bright_green": "#b8bb26ff",
|
||||||
"terminal.ansi.dim_green": "#e0dc98ff",
|
"terminal.ansi.dim_green": "#6a6912ff",
|
||||||
"terminal.ansi.yellow": "#f9bd2fff",
|
"terminal.ansi.yellow": "#d79921ff",
|
||||||
"terminal.ansi.bright_yellow": "#91611bff",
|
"terminal.ansi.bright_yellow": "#fabd2fff",
|
||||||
"terminal.ansi.dim_yellow": "#fedc9bff",
|
"terminal.ansi.dim_yellow": "#966a17ff",
|
||||||
"terminal.ansi.blue": "#83a598ff",
|
"terminal.ansi.blue": "#458588ff",
|
||||||
"terminal.ansi.bright_blue": "#414f4aff",
|
"terminal.ansi.bright_blue": "#83a598ff",
|
||||||
"terminal.ansi.dim_blue": "#c0d2cbff",
|
"terminal.ansi.dim_blue": "#305d5fff",
|
||||||
"terminal.ansi.magenta": "#d3869bff",
|
"terminal.ansi.magenta": "#b16286ff",
|
||||||
"terminal.ansi.bright_magenta": "#8e5868ff",
|
"terminal.ansi.bright_magenta": "#d3869bff",
|
||||||
"terminal.ansi.dim_magenta": "#ff9ebbff",
|
"terminal.ansi.dim_magenta": "#7c455eff",
|
||||||
"terminal.ansi.cyan": "#8ec07cff",
|
"terminal.ansi.cyan": "#689d6aff",
|
||||||
"terminal.ansi.bright_cyan": "#45603eff",
|
"terminal.ansi.bright_cyan": "#8ec07cff",
|
||||||
"terminal.ansi.dim_cyan": "#c7dfbdff",
|
"terminal.ansi.dim_cyan": "#496e4aff",
|
||||||
"terminal.ansi.white": "#fbf1c7ff",
|
"terminal.ansi.white": "#a89984ff",
|
||||||
"terminal.ansi.bright_white": "#ffffffff",
|
"terminal.ansi.bright_white": "#fbf1c7ff",
|
||||||
"terminal.ansi.dim_white": "#b0a189ff",
|
"terminal.ansi.dim_white": "#766b5dff",
|
||||||
"link_text.hover": "#83a598ff",
|
"link_text.hover": "#83a598ff",
|
||||||
"version_control.added": "#b7bb26ff",
|
"version_control.added": "#b7bb26ff",
|
||||||
"version_control.modified": "#f9bd2fff",
|
"version_control.modified": "#f9bd2fff",
|
||||||
@@ -478,33 +478,33 @@
|
|||||||
"editor.document_highlight.read_background": "#83a5981a",
|
"editor.document_highlight.read_background": "#83a5981a",
|
||||||
"editor.document_highlight.write_background": "#92847466",
|
"editor.document_highlight.write_background": "#92847466",
|
||||||
"terminal.background": "#1d2021ff",
|
"terminal.background": "#1d2021ff",
|
||||||
"terminal.foreground": "#fbf1c7ff",
|
"terminal.foreground": "#ebdbb2ff",
|
||||||
"terminal.bright_foreground": "#fbf1c7ff",
|
"terminal.bright_foreground": "#fbf1c7ff",
|
||||||
"terminal.dim_foreground": "#1d2021ff",
|
"terminal.dim_foreground": "#766b5dff",
|
||||||
"terminal.ansi.black": "#1d2021ff",
|
"terminal.ansi.black": "#282828ff",
|
||||||
"terminal.ansi.bright_black": "#73675eff",
|
"terminal.ansi.bright_black": "#928374ff",
|
||||||
"terminal.ansi.dim_black": "#fbf1c7ff",
|
"terminal.ansi.dim_black": "#fbf1c7ff",
|
||||||
"terminal.ansi.red": "#fb4a35ff",
|
"terminal.ansi.red": "#cc241dff",
|
||||||
"terminal.ansi.bright_red": "#93201dff",
|
"terminal.ansi.bright_red": "#fb4934ff",
|
||||||
"terminal.ansi.dim_red": "#ffaa95ff",
|
"terminal.ansi.dim_red": "#8e1814ff",
|
||||||
"terminal.ansi.green": "#b7bb26ff",
|
"terminal.ansi.green": "#98971aff",
|
||||||
"terminal.ansi.bright_green": "#605c1bff",
|
"terminal.ansi.bright_green": "#b8bb26ff",
|
||||||
"terminal.ansi.dim_green": "#e0dc98ff",
|
"terminal.ansi.dim_green": "#6a6912ff",
|
||||||
"terminal.ansi.yellow": "#f9bd2fff",
|
"terminal.ansi.yellow": "#d79921ff",
|
||||||
"terminal.ansi.bright_yellow": "#91611bff",
|
"terminal.ansi.bright_yellow": "#fabd2fff",
|
||||||
"terminal.ansi.dim_yellow": "#fedc9bff",
|
"terminal.ansi.dim_yellow": "#966a17ff",
|
||||||
"terminal.ansi.blue": "#83a598ff",
|
"terminal.ansi.blue": "#458588ff",
|
||||||
"terminal.ansi.bright_blue": "#414f4aff",
|
"terminal.ansi.bright_blue": "#83a598ff",
|
||||||
"terminal.ansi.dim_blue": "#c0d2cbff",
|
"terminal.ansi.dim_blue": "#305d5fff",
|
||||||
"terminal.ansi.magenta": "#d3869bff",
|
"terminal.ansi.magenta": "#b16286ff",
|
||||||
"terminal.ansi.bright_magenta": "#8e5868ff",
|
"terminal.ansi.bright_magenta": "#d3869bff",
|
||||||
"terminal.ansi.dim_magenta": "#ff9ebbff",
|
"terminal.ansi.dim_magenta": "#7c455eff",
|
||||||
"terminal.ansi.cyan": "#8ec07cff",
|
"terminal.ansi.cyan": "#689d6aff",
|
||||||
"terminal.ansi.bright_cyan": "#45603eff",
|
"terminal.ansi.bright_cyan": "#8ec07cff",
|
||||||
"terminal.ansi.dim_cyan": "#c7dfbdff",
|
"terminal.ansi.dim_cyan": "#496e4aff",
|
||||||
"terminal.ansi.white": "#fbf1c7ff",
|
"terminal.ansi.white": "#a89984ff",
|
||||||
"terminal.ansi.bright_white": "#ffffffff",
|
"terminal.ansi.bright_white": "#fbf1c7ff",
|
||||||
"terminal.ansi.dim_white": "#b0a189ff",
|
"terminal.ansi.dim_white": "#766b5dff",
|
||||||
"link_text.hover": "#83a598ff",
|
"link_text.hover": "#83a598ff",
|
||||||
"version_control.added": "#b7bb26ff",
|
"version_control.added": "#b7bb26ff",
|
||||||
"version_control.modified": "#f9bd2fff",
|
"version_control.modified": "#f9bd2fff",
|
||||||
@@ -885,33 +885,33 @@
|
|||||||
"editor.document_highlight.read_background": "#83a5981a",
|
"editor.document_highlight.read_background": "#83a5981a",
|
||||||
"editor.document_highlight.write_background": "#92847466",
|
"editor.document_highlight.write_background": "#92847466",
|
||||||
"terminal.background": "#32302fff",
|
"terminal.background": "#32302fff",
|
||||||
"terminal.foreground": "#fbf1c7ff",
|
"terminal.foreground": "#ebdbb2ff",
|
||||||
"terminal.bright_foreground": "#fbf1c7ff",
|
"terminal.bright_foreground": "#fbf1c7ff",
|
||||||
"terminal.dim_foreground": "#32302fff",
|
"terminal.dim_foreground": "#766b5dff",
|
||||||
"terminal.ansi.black": "#32302fff",
|
"terminal.ansi.black": "#282828ff",
|
||||||
"terminal.ansi.bright_black": "#73675eff",
|
"terminal.ansi.bright_black": "#928374ff",
|
||||||
"terminal.ansi.dim_black": "#fbf1c7ff",
|
"terminal.ansi.dim_black": "#fbf1c7ff",
|
||||||
"terminal.ansi.red": "#fb4a35ff",
|
"terminal.ansi.red": "#cc241dff",
|
||||||
"terminal.ansi.bright_red": "#93201dff",
|
"terminal.ansi.bright_red": "#fb4934ff",
|
||||||
"terminal.ansi.dim_red": "#ffaa95ff",
|
"terminal.ansi.dim_red": "#8e1814ff",
|
||||||
"terminal.ansi.green": "#b7bb26ff",
|
"terminal.ansi.green": "#98971aff",
|
||||||
"terminal.ansi.bright_green": "#605c1bff",
|
"terminal.ansi.bright_green": "#b8bb26ff",
|
||||||
"terminal.ansi.dim_green": "#e0dc98ff",
|
"terminal.ansi.dim_green": "#6a6912ff",
|
||||||
"terminal.ansi.yellow": "#f9bd2fff",
|
"terminal.ansi.yellow": "#d79921ff",
|
||||||
"terminal.ansi.bright_yellow": "#91611bff",
|
"terminal.ansi.bright_yellow": "#fabd2fff",
|
||||||
"terminal.ansi.dim_yellow": "#fedc9bff",
|
"terminal.ansi.dim_yellow": "#966a17ff",
|
||||||
"terminal.ansi.blue": "#83a598ff",
|
"terminal.ansi.blue": "#458588ff",
|
||||||
"terminal.ansi.bright_blue": "#414f4aff",
|
"terminal.ansi.bright_blue": "#83a598ff",
|
||||||
"terminal.ansi.dim_blue": "#c0d2cbff",
|
"terminal.ansi.dim_blue": "#305d5fff",
|
||||||
"terminal.ansi.magenta": "#d3869bff",
|
"terminal.ansi.magenta": "#b16286ff",
|
||||||
"terminal.ansi.bright_magenta": "#8e5868ff",
|
"terminal.ansi.bright_magenta": "#d3869bff",
|
||||||
"terminal.ansi.dim_magenta": "#ff9ebbff",
|
"terminal.ansi.dim_magenta": "#7c455eff",
|
||||||
"terminal.ansi.cyan": "#8ec07cff",
|
"terminal.ansi.cyan": "#689d6aff",
|
||||||
"terminal.ansi.bright_cyan": "#45603eff",
|
"terminal.ansi.bright_cyan": "#8ec07cff",
|
||||||
"terminal.ansi.dim_cyan": "#c7dfbdff",
|
"terminal.ansi.dim_cyan": "#496e4aff",
|
||||||
"terminal.ansi.white": "#fbf1c7ff",
|
"terminal.ansi.white": "#a89984ff",
|
||||||
"terminal.ansi.bright_white": "#ffffffff",
|
"terminal.ansi.bright_white": "#fbf1c7ff",
|
||||||
"terminal.ansi.dim_white": "#b0a189ff",
|
"terminal.ansi.dim_white": "#766b5dff",
|
||||||
"link_text.hover": "#83a598ff",
|
"link_text.hover": "#83a598ff",
|
||||||
"version_control.added": "#b7bb26ff",
|
"version_control.added": "#b7bb26ff",
|
||||||
"version_control.modified": "#f9bd2fff",
|
"version_control.modified": "#f9bd2fff",
|
||||||
@@ -1295,30 +1295,30 @@
|
|||||||
"terminal.foreground": "#282828ff",
|
"terminal.foreground": "#282828ff",
|
||||||
"terminal.bright_foreground": "#282828ff",
|
"terminal.bright_foreground": "#282828ff",
|
||||||
"terminal.dim_foreground": "#fbf1c7ff",
|
"terminal.dim_foreground": "#fbf1c7ff",
|
||||||
"terminal.ansi.black": "#282828ff",
|
"terminal.ansi.black": "#fbf1c7ff",
|
||||||
"terminal.ansi.bright_black": "#0b6678ff",
|
"terminal.ansi.bright_black": "#928374ff",
|
||||||
"terminal.ansi.dim_black": "#5f5650ff",
|
"terminal.ansi.dim_black": "#7c6f64ff",
|
||||||
"terminal.ansi.red": "#9d0308ff",
|
"terminal.ansi.red": "#cc241dff",
|
||||||
"terminal.ansi.bright_red": "#db8b7aff",
|
"terminal.ansi.bright_red": "#9d0006ff",
|
||||||
"terminal.ansi.dim_red": "#4e1207ff",
|
"terminal.ansi.dim_red": "#c31c16ff",
|
||||||
"terminal.ansi.green": "#797410ff",
|
"terminal.ansi.green": "#98971aff",
|
||||||
"terminal.ansi.bright_green": "#bfb787ff",
|
"terminal.ansi.bright_green": "#79740eff",
|
||||||
"terminal.ansi.dim_green": "#3e3a11ff",
|
"terminal.ansi.dim_green": "#929015ff",
|
||||||
"terminal.ansi.yellow": "#b57615ff",
|
"terminal.ansi.yellow": "#d79921ff",
|
||||||
"terminal.ansi.bright_yellow": "#e2b88bff",
|
"terminal.ansi.bright_yellow": "#b57614ff",
|
||||||
"terminal.ansi.dim_yellow": "#5c3a12ff",
|
"terminal.ansi.dim_yellow": "#cf8e1aff",
|
||||||
"terminal.ansi.blue": "#0b6678ff",
|
"terminal.ansi.blue": "#458588ff",
|
||||||
"terminal.ansi.bright_blue": "#8fb0baff",
|
"terminal.ansi.bright_blue": "#076678ff",
|
||||||
"terminal.ansi.dim_blue": "#14333bff",
|
"terminal.ansi.dim_blue": "#356f77ff",
|
||||||
"terminal.ansi.magenta": "#8f3e71ff",
|
"terminal.ansi.magenta": "#b16286ff",
|
||||||
"terminal.ansi.bright_magenta": "#c76da0ff",
|
"terminal.ansi.bright_magenta": "#8f3f71ff",
|
||||||
"terminal.ansi.dim_magenta": "#5c2848ff",
|
"terminal.ansi.dim_magenta": "#a85580ff",
|
||||||
"terminal.ansi.cyan": "#437b59ff",
|
"terminal.ansi.cyan": "#689d6aff",
|
||||||
"terminal.ansi.bright_cyan": "#9fbca8ff",
|
"terminal.ansi.bright_cyan": "#427b58ff",
|
||||||
"terminal.ansi.dim_cyan": "#253e2eff",
|
"terminal.ansi.dim_cyan": "#5f9166ff",
|
||||||
"terminal.ansi.white": "#fbf1c7ff",
|
"terminal.ansi.white": "#7c6f64ff",
|
||||||
"terminal.ansi.bright_white": "#ffffffff",
|
"terminal.ansi.bright_white": "#282828ff",
|
||||||
"terminal.ansi.dim_white": "#b0a189ff",
|
"terminal.ansi.dim_white": "#282828ff",
|
||||||
"link_text.hover": "#0b6678ff",
|
"link_text.hover": "#0b6678ff",
|
||||||
"version_control.added": "#797410ff",
|
"version_control.added": "#797410ff",
|
||||||
"version_control.modified": "#b57615ff",
|
"version_control.modified": "#b57615ff",
|
||||||
@@ -1702,30 +1702,30 @@
|
|||||||
"terminal.foreground": "#282828ff",
|
"terminal.foreground": "#282828ff",
|
||||||
"terminal.bright_foreground": "#282828ff",
|
"terminal.bright_foreground": "#282828ff",
|
||||||
"terminal.dim_foreground": "#f9f5d7ff",
|
"terminal.dim_foreground": "#f9f5d7ff",
|
||||||
"terminal.ansi.black": "#282828ff",
|
"terminal.ansi.black": "#fbf1c7ff",
|
||||||
"terminal.ansi.bright_black": "#73675eff",
|
"terminal.ansi.bright_black": "#928374ff",
|
||||||
"terminal.ansi.dim_black": "#f9f5d7ff",
|
"terminal.ansi.dim_black": "#7c6f64ff",
|
||||||
"terminal.ansi.red": "#9d0308ff",
|
"terminal.ansi.red": "#cc241dff",
|
||||||
"terminal.ansi.bright_red": "#db8b7aff",
|
"terminal.ansi.bright_red": "#9d0006ff",
|
||||||
"terminal.ansi.dim_red": "#4e1207ff",
|
"terminal.ansi.dim_red": "#c31c16ff",
|
||||||
"terminal.ansi.green": "#797410ff",
|
"terminal.ansi.green": "#98971aff",
|
||||||
"terminal.ansi.bright_green": "#bfb787ff",
|
"terminal.ansi.bright_green": "#79740eff",
|
||||||
"terminal.ansi.dim_green": "#3e3a11ff",
|
"terminal.ansi.dim_green": "#929015ff",
|
||||||
"terminal.ansi.yellow": "#b57615ff",
|
"terminal.ansi.yellow": "#d79921ff",
|
||||||
"terminal.ansi.bright_yellow": "#e2b88bff",
|
"terminal.ansi.bright_yellow": "#b57614ff",
|
||||||
"terminal.ansi.dim_yellow": "#5c3a12ff",
|
"terminal.ansi.dim_yellow": "#cf8e1aff",
|
||||||
"terminal.ansi.blue": "#0b6678ff",
|
"terminal.ansi.blue": "#458588ff",
|
||||||
"terminal.ansi.bright_blue": "#8fb0baff",
|
"terminal.ansi.bright_blue": "#076678ff",
|
||||||
"terminal.ansi.dim_blue": "#14333bff",
|
"terminal.ansi.dim_blue": "#356f77ff",
|
||||||
"terminal.ansi.magenta": "#8f3e71ff",
|
"terminal.ansi.magenta": "#b16286ff",
|
||||||
"terminal.ansi.bright_magenta": "#c76da0ff",
|
"terminal.ansi.bright_magenta": "#8f3f71ff",
|
||||||
"terminal.ansi.dim_magenta": "#5c2848ff",
|
"terminal.ansi.dim_magenta": "#a85580ff",
|
||||||
"terminal.ansi.cyan": "#437b59ff",
|
"terminal.ansi.cyan": "#689d6aff",
|
||||||
"terminal.ansi.bright_cyan": "#9fbca8ff",
|
"terminal.ansi.bright_cyan": "#427b58ff",
|
||||||
"terminal.ansi.dim_cyan": "#253e2eff",
|
"terminal.ansi.dim_cyan": "#5f9166ff",
|
||||||
"terminal.ansi.white": "#f9f5d7ff",
|
"terminal.ansi.white": "#7c6f64ff",
|
||||||
"terminal.ansi.bright_white": "#ffffffff",
|
"terminal.ansi.bright_white": "#282828ff",
|
||||||
"terminal.ansi.dim_white": "#b0a189ff",
|
"terminal.ansi.dim_white": "#282828ff",
|
||||||
"link_text.hover": "#0b6678ff",
|
"link_text.hover": "#0b6678ff",
|
||||||
"version_control.added": "#797410ff",
|
"version_control.added": "#797410ff",
|
||||||
"version_control.modified": "#b57615ff",
|
"version_control.modified": "#b57615ff",
|
||||||
@@ -2109,30 +2109,30 @@
|
|||||||
"terminal.foreground": "#282828ff",
|
"terminal.foreground": "#282828ff",
|
||||||
"terminal.bright_foreground": "#282828ff",
|
"terminal.bright_foreground": "#282828ff",
|
||||||
"terminal.dim_foreground": "#f2e5bcff",
|
"terminal.dim_foreground": "#f2e5bcff",
|
||||||
"terminal.ansi.black": "#282828ff",
|
"terminal.ansi.black": "#fbf1c7ff",
|
||||||
"terminal.ansi.bright_black": "#73675eff",
|
"terminal.ansi.bright_black": "#928374ff",
|
||||||
"terminal.ansi.dim_black": "#f2e5bcff",
|
"terminal.ansi.dim_black": "#7c6f64ff",
|
||||||
"terminal.ansi.red": "#9d0308ff",
|
"terminal.ansi.red": "#cc241dff",
|
||||||
"terminal.ansi.bright_red": "#db8b7aff",
|
"terminal.ansi.bright_red": "#9d0006ff",
|
||||||
"terminal.ansi.dim_red": "#4e1207ff",
|
"terminal.ansi.dim_red": "#c31c16ff",
|
||||||
"terminal.ansi.green": "#797410ff",
|
"terminal.ansi.green": "#98971aff",
|
||||||
"terminal.ansi.bright_green": "#bfb787ff",
|
"terminal.ansi.bright_green": "#79740eff",
|
||||||
"terminal.ansi.dim_green": "#3e3a11ff",
|
"terminal.ansi.dim_green": "#929015ff",
|
||||||
"terminal.ansi.yellow": "#b57615ff",
|
"terminal.ansi.yellow": "#d79921ff",
|
||||||
"terminal.ansi.bright_yellow": "#e2b88bff",
|
"terminal.ansi.bright_yellow": "#b57614ff",
|
||||||
"terminal.ansi.dim_yellow": "#5c3a12ff",
|
"terminal.ansi.dim_yellow": "#cf8e1aff",
|
||||||
"terminal.ansi.blue": "#0b6678ff",
|
"terminal.ansi.blue": "#458588ff",
|
||||||
"terminal.ansi.bright_blue": "#8fb0baff",
|
"terminal.ansi.bright_blue": "#076678ff",
|
||||||
"terminal.ansi.dim_blue": "#14333bff",
|
"terminal.ansi.dim_blue": "#356f77ff",
|
||||||
"terminal.ansi.magenta": "#8f3e71ff",
|
"terminal.ansi.magenta": "#b16286ff",
|
||||||
"terminal.ansi.bright_magenta": "#c76da0ff",
|
"terminal.ansi.bright_magenta": "#8f3f71ff",
|
||||||
"terminal.ansi.dim_magenta": "#5c2848ff",
|
"terminal.ansi.dim_magenta": "#a85580ff",
|
||||||
"terminal.ansi.cyan": "#437b59ff",
|
"terminal.ansi.cyan": "#689d6aff",
|
||||||
"terminal.ansi.bright_cyan": "#9fbca8ff",
|
"terminal.ansi.bright_cyan": "#427b58ff",
|
||||||
"terminal.ansi.dim_cyan": "#253e2eff",
|
"terminal.ansi.dim_cyan": "#5f9166ff",
|
||||||
"terminal.ansi.white": "#f2e5bcff",
|
"terminal.ansi.white": "#7c6f64ff",
|
||||||
"terminal.ansi.bright_white": "#ffffffff",
|
"terminal.ansi.bright_white": "#282828ff",
|
||||||
"terminal.ansi.dim_white": "#b0a189ff",
|
"terminal.ansi.dim_white": "#282828ff",
|
||||||
"link_text.hover": "#0b6678ff",
|
"link_text.hover": "#0b6678ff",
|
||||||
"version_control.added": "#797410ff",
|
"version_control.added": "#797410ff",
|
||||||
"version_control.modified": "#b57615ff",
|
"version_control.modified": "#b57615ff",
|
||||||
|
|||||||
@@ -68,34 +68,34 @@
|
|||||||
"editor.active_wrap_guide": "#c8ccd41a",
|
"editor.active_wrap_guide": "#c8ccd41a",
|
||||||
"editor.document_highlight.read_background": "#74ade81a",
|
"editor.document_highlight.read_background": "#74ade81a",
|
||||||
"editor.document_highlight.write_background": "#555a6366",
|
"editor.document_highlight.write_background": "#555a6366",
|
||||||
"terminal.background": "#282c33ff",
|
"terminal.background": "#282c34ff",
|
||||||
"terminal.foreground": "#dce0e5ff",
|
"terminal.foreground": "#abb2bfff",
|
||||||
"terminal.bright_foreground": "#dce0e5ff",
|
"terminal.bright_foreground": "#dce0e5ff",
|
||||||
"terminal.dim_foreground": "#282c33ff",
|
"terminal.dim_foreground": "#636d83ff",
|
||||||
"terminal.ansi.black": "#282c33ff",
|
"terminal.ansi.black": "#282c34ff",
|
||||||
"terminal.ansi.bright_black": "#525561ff",
|
"terminal.ansi.bright_black": "#636d83ff",
|
||||||
"terminal.ansi.dim_black": "#dce0e5ff",
|
"terminal.ansi.dim_black": "#3b3f4aff",
|
||||||
"terminal.ansi.red": "#d07277ff",
|
"terminal.ansi.red": "#e06c75ff",
|
||||||
"terminal.ansi.bright_red": "#673a3cff",
|
"terminal.ansi.bright_red": "#EA858Bff",
|
||||||
"terminal.ansi.dim_red": "#eab7b9ff",
|
"terminal.ansi.dim_red": "#a7545aff",
|
||||||
"terminal.ansi.green": "#a1c181ff",
|
"terminal.ansi.green": "#98c379ff",
|
||||||
"terminal.ansi.bright_green": "#4d6140ff",
|
"terminal.ansi.bright_green": "#AAD581ff",
|
||||||
"terminal.ansi.dim_green": "#d1e0bfff",
|
"terminal.ansi.dim_green": "#6d8f59ff",
|
||||||
"terminal.ansi.yellow": "#dec184ff",
|
"terminal.ansi.yellow": "#e5c07bff",
|
||||||
"terminal.ansi.bright_yellow": "#e5c07bff",
|
"terminal.ansi.bright_yellow": "#FFD885ff",
|
||||||
"terminal.ansi.dim_yellow": "#f1dfc1ff",
|
"terminal.ansi.dim_yellow": "#b8985bff",
|
||||||
"terminal.ansi.blue": "#74ade8ff",
|
"terminal.ansi.blue": "#61afefff",
|
||||||
"terminal.ansi.bright_blue": "#385378ff",
|
"terminal.ansi.bright_blue": "#85C1FFff",
|
||||||
"terminal.ansi.dim_blue": "#bed5f4ff",
|
"terminal.ansi.dim_blue": "#457cadff",
|
||||||
"terminal.ansi.magenta": "#b477cfff",
|
"terminal.ansi.magenta": "#c678ddff",
|
||||||
"terminal.ansi.bright_magenta": "#d6b4e4ff",
|
"terminal.ansi.bright_magenta": "#D398EBff",
|
||||||
"terminal.ansi.dim_magenta": "#612a79ff",
|
"terminal.ansi.dim_magenta": "#8d54a0ff",
|
||||||
"terminal.ansi.cyan": "#6eb4bfff",
|
"terminal.ansi.cyan": "#56b6c2ff",
|
||||||
"terminal.ansi.bright_cyan": "#3a565bff",
|
"terminal.ansi.bright_cyan": "#6ED5DEff",
|
||||||
"terminal.ansi.dim_cyan": "#b9d9dfff",
|
"terminal.ansi.dim_cyan": "#3c818aff",
|
||||||
"terminal.ansi.white": "#dce0e5ff",
|
"terminal.ansi.white": "#abb2bfff",
|
||||||
"terminal.ansi.bright_white": "#fafafaff",
|
"terminal.ansi.bright_white": "#fafafaff",
|
||||||
"terminal.ansi.dim_white": "#575d65ff",
|
"terminal.ansi.dim_white": "#8f969bff",
|
||||||
"link_text.hover": "#74ade8ff",
|
"link_text.hover": "#74ade8ff",
|
||||||
"version_control.added": "#27a657ff",
|
"version_control.added": "#27a657ff",
|
||||||
"version_control.modified": "#d3b020ff",
|
"version_control.modified": "#d3b020ff",
|
||||||
@@ -473,33 +473,33 @@
|
|||||||
"editor.document_highlight.read_background": "#5c78e225",
|
"editor.document_highlight.read_background": "#5c78e225",
|
||||||
"editor.document_highlight.write_background": "#a3a3a466",
|
"editor.document_highlight.write_background": "#a3a3a466",
|
||||||
"terminal.background": "#fafafaff",
|
"terminal.background": "#fafafaff",
|
||||||
"terminal.foreground": "#242529ff",
|
"terminal.foreground": "#2a2c33ff",
|
||||||
"terminal.bright_foreground": "#242529ff",
|
"terminal.bright_foreground": "#2a2c33ff",
|
||||||
"terminal.dim_foreground": "#fafafaff",
|
"terminal.dim_foreground": "#bbbbbbff",
|
||||||
"terminal.ansi.black": "#242529ff",
|
"terminal.ansi.black": "#000000ff",
|
||||||
"terminal.ansi.bright_black": "#747579ff",
|
"terminal.ansi.bright_black": "#000000ff",
|
||||||
"terminal.ansi.dim_black": "#97979aff",
|
"terminal.ansi.dim_black": "#555555ff",
|
||||||
"terminal.ansi.red": "#d36151ff",
|
"terminal.ansi.red": "#de3e35ff",
|
||||||
"terminal.ansi.bright_red": "#f0b0a4ff",
|
"terminal.ansi.bright_red": "#de3e35ff",
|
||||||
"terminal.ansi.dim_red": "#6f312aff",
|
"terminal.ansi.dim_red": "#9c2b26ff",
|
||||||
"terminal.ansi.green": "#669f59ff",
|
"terminal.ansi.green": "#3f953aff",
|
||||||
"terminal.ansi.bright_green": "#b2cfa9ff",
|
"terminal.ansi.bright_green": "#3f953aff",
|
||||||
"terminal.ansi.dim_green": "#354d2eff",
|
"terminal.ansi.dim_green": "#2b6927ff",
|
||||||
"terminal.ansi.yellow": "#dec184ff",
|
"terminal.ansi.yellow": "#d2b67cff",
|
||||||
"terminal.ansi.bright_yellow": "#826221ff",
|
"terminal.ansi.bright_yellow": "#d2b67cff",
|
||||||
"terminal.ansi.dim_yellow": "#786441ff",
|
"terminal.ansi.dim_yellow": "#a48c5aff",
|
||||||
"terminal.ansi.blue": "#5c78e2ff",
|
"terminal.ansi.blue": "#2f5af3ff",
|
||||||
"terminal.ansi.bright_blue": "#b5baf2ff",
|
"terminal.ansi.bright_blue": "#2f5af3ff",
|
||||||
"terminal.ansi.dim_blue": "#2d3d75ff",
|
"terminal.ansi.dim_blue": "#2140abff",
|
||||||
"terminal.ansi.magenta": "#984ea5ff",
|
"terminal.ansi.magenta": "#950095ff",
|
||||||
"terminal.ansi.bright_magenta": "#cea6d3ff",
|
"terminal.ansi.bright_magenta": "#a00095ff",
|
||||||
"terminal.ansi.dim_magenta": "#4b2a50ff",
|
"terminal.ansi.dim_magenta": "#6a006aff",
|
||||||
"terminal.ansi.cyan": "#3a82b7ff",
|
"terminal.ansi.cyan": "#3f953aff",
|
||||||
"terminal.ansi.bright_cyan": "#a3bedaff",
|
"terminal.ansi.bright_cyan": "#3f953aff",
|
||||||
"terminal.ansi.dim_cyan": "#254058ff",
|
"terminal.ansi.dim_cyan": "#2b6927ff",
|
||||||
"terminal.ansi.white": "#fafafaff",
|
"terminal.ansi.white": "#bbbbbbff",
|
||||||
"terminal.ansi.bright_white": "#ffffffff",
|
"terminal.ansi.bright_white": "#ffffffff",
|
||||||
"terminal.ansi.dim_white": "#aaaaaaff",
|
"terminal.ansi.dim_white": "#888888ff",
|
||||||
"link_text.hover": "#5c78e2ff",
|
"link_text.hover": "#5c78e2ff",
|
||||||
"version_control.added": "#27a657ff",
|
"version_control.added": "#27a657ff",
|
||||||
"version_control.modified": "#d3b020ff",
|
"version_control.modified": "#d3b020ff",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ disallowed-methods = [
|
|||||||
{ path = "std::process::Command::stderr", reason = "`smol::process::Command::from()` does not preserve stdio configuration", replacement = "smol::process::Command::stderr" },
|
{ path = "std::process::Command::stderr", reason = "`smol::process::Command::from()` does not preserve stdio configuration", replacement = "smol::process::Command::stderr" },
|
||||||
{ path = "serde_json::from_reader", reason = "Parsing from a buffer is much slower than first reading the buffer into a Vec/String, see https://github.com/serde-rs/json/issues/160#issuecomment-253446892. Use `serde_json::from_slice` instead." },
|
{ path = "serde_json::from_reader", reason = "Parsing from a buffer is much slower than first reading the buffer into a Vec/String, see https://github.com/serde-rs/json/issues/160#issuecomment-253446892. Use `serde_json::from_slice` instead." },
|
||||||
{ path = "serde_json_lenient::from_reader", reason = "Parsing from a buffer is much slower than first reading the buffer into a Vec/String, see https://github.com/serde-rs/json/issues/160#issuecomment-253446892, Use `serde_json_lenient::from_slice` instead." },
|
{ path = "serde_json_lenient::from_reader", reason = "Parsing from a buffer is much slower than first reading the buffer into a Vec/String, see https://github.com/serde-rs/json/issues/160#issuecomment-253446892, Use `serde_json_lenient::from_slice` instead." },
|
||||||
|
{ path = "cocoa::foundation::NSString::alloc", reason = "NSString must be autoreleased to avoid memory leaks. Use `ns_string()` helper instead." },
|
||||||
]
|
]
|
||||||
disallowed-types = [
|
disallowed-types = [
|
||||||
# { path = "std::collections::HashMap", replacement = "collections::HashMap" },
|
# { path = "std::collections::HashMap", replacement = "collections::HashMap" },
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ url.workspace = true
|
|||||||
util.workspace = true
|
util.workspace = true
|
||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
watch.workspace = true
|
watch.workspace = true
|
||||||
|
urlencoding.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
env_logger.workspace = true
|
env_logger.workspace = true
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ pub struct UserMessage {
|
|||||||
pub content: ContentBlock,
|
pub content: ContentBlock,
|
||||||
pub chunks: Vec<acp::ContentBlock>,
|
pub chunks: Vec<acp::ContentBlock>,
|
||||||
pub checkpoint: Option<Checkpoint>,
|
pub checkpoint: Option<Checkpoint>,
|
||||||
|
pub indented: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -73,6 +74,7 @@ impl UserMessage {
|
|||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub struct AssistantMessage {
|
pub struct AssistantMessage {
|
||||||
pub chunks: Vec<AssistantMessageChunk>,
|
pub chunks: Vec<AssistantMessageChunk>,
|
||||||
|
pub indented: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AssistantMessage {
|
impl AssistantMessage {
|
||||||
@@ -123,6 +125,14 @@ pub enum AgentThreadEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AgentThreadEntry {
|
impl AgentThreadEntry {
|
||||||
|
pub fn is_indented(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::UserMessage(message) => message.indented,
|
||||||
|
Self::AssistantMessage(message) => message.indented,
|
||||||
|
Self::ToolCall(_) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn to_markdown(&self, cx: &App) -> String {
|
pub fn to_markdown(&self, cx: &App) -> String {
|
||||||
match self {
|
match self {
|
||||||
Self::UserMessage(message) => message.to_markdown(cx),
|
Self::UserMessage(message) => message.to_markdown(cx),
|
||||||
@@ -1184,6 +1194,16 @@ impl AcpThread {
|
|||||||
message_id: Option<UserMessageId>,
|
message_id: Option<UserMessageId>,
|
||||||
chunk: acp::ContentBlock,
|
chunk: acp::ContentBlock,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.push_user_content_block_with_indent(message_id, chunk, false, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push_user_content_block_with_indent(
|
||||||
|
&mut self,
|
||||||
|
message_id: Option<UserMessageId>,
|
||||||
|
chunk: acp::ContentBlock,
|
||||||
|
indented: bool,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let language_registry = self.project.read(cx).languages().clone();
|
let language_registry = self.project.read(cx).languages().clone();
|
||||||
let path_style = self.project.read(cx).path_style(cx);
|
let path_style = self.project.read(cx).path_style(cx);
|
||||||
@@ -1194,8 +1214,10 @@ impl AcpThread {
|
|||||||
id,
|
id,
|
||||||
content,
|
content,
|
||||||
chunks,
|
chunks,
|
||||||
|
indented: existing_indented,
|
||||||
..
|
..
|
||||||
}) = last_entry
|
}) = last_entry
|
||||||
|
&& *existing_indented == indented
|
||||||
{
|
{
|
||||||
*id = message_id.or(id.take());
|
*id = message_id.or(id.take());
|
||||||
content.append(chunk.clone(), &language_registry, path_style, cx);
|
content.append(chunk.clone(), &language_registry, path_style, cx);
|
||||||
@@ -1210,6 +1232,7 @@ impl AcpThread {
|
|||||||
content,
|
content,
|
||||||
chunks: vec![chunk],
|
chunks: vec![chunk],
|
||||||
checkpoint: None,
|
checkpoint: None,
|
||||||
|
indented,
|
||||||
}),
|
}),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
@@ -1221,12 +1244,26 @@ impl AcpThread {
|
|||||||
chunk: acp::ContentBlock,
|
chunk: acp::ContentBlock,
|
||||||
is_thought: bool,
|
is_thought: bool,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.push_assistant_content_block_with_indent(chunk, is_thought, false, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push_assistant_content_block_with_indent(
|
||||||
|
&mut self,
|
||||||
|
chunk: acp::ContentBlock,
|
||||||
|
is_thought: bool,
|
||||||
|
indented: bool,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let language_registry = self.project.read(cx).languages().clone();
|
let language_registry = self.project.read(cx).languages().clone();
|
||||||
let path_style = self.project.read(cx).path_style(cx);
|
let path_style = self.project.read(cx).path_style(cx);
|
||||||
let entries_len = self.entries.len();
|
let entries_len = self.entries.len();
|
||||||
if let Some(last_entry) = self.entries.last_mut()
|
if let Some(last_entry) = self.entries.last_mut()
|
||||||
&& let AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) = last_entry
|
&& let AgentThreadEntry::AssistantMessage(AssistantMessage {
|
||||||
|
chunks,
|
||||||
|
indented: existing_indented,
|
||||||
|
}) = last_entry
|
||||||
|
&& *existing_indented == indented
|
||||||
{
|
{
|
||||||
let idx = entries_len - 1;
|
let idx = entries_len - 1;
|
||||||
cx.emit(AcpThreadEvent::EntryUpdated(idx));
|
cx.emit(AcpThreadEvent::EntryUpdated(idx));
|
||||||
@@ -1255,6 +1292,7 @@ impl AcpThread {
|
|||||||
self.push_entry(
|
self.push_entry(
|
||||||
AgentThreadEntry::AssistantMessage(AssistantMessage {
|
AgentThreadEntry::AssistantMessage(AssistantMessage {
|
||||||
chunks: vec![chunk],
|
chunks: vec![chunk],
|
||||||
|
indented,
|
||||||
}),
|
}),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
@@ -1372,7 +1410,7 @@ impl AcpThread {
|
|||||||
let path_style = self.project.read(cx).path_style(cx);
|
let path_style = self.project.read(cx).path_style(cx);
|
||||||
let id = update.tool_call_id.clone();
|
let id = update.tool_call_id.clone();
|
||||||
|
|
||||||
let agent = self.connection().telemetry_id();
|
let agent_telemetry_id = self.connection().telemetry_id();
|
||||||
let session = self.session_id();
|
let session = self.session_id();
|
||||||
if let ToolCallStatus::Completed | ToolCallStatus::Failed = status {
|
if let ToolCallStatus::Completed | ToolCallStatus::Failed = status {
|
||||||
let status = if matches!(status, ToolCallStatus::Completed) {
|
let status = if matches!(status, ToolCallStatus::Completed) {
|
||||||
@@ -1380,7 +1418,12 @@ impl AcpThread {
|
|||||||
} else {
|
} else {
|
||||||
"failed"
|
"failed"
|
||||||
};
|
};
|
||||||
telemetry::event!("Agent Tool Call Completed", agent, session, status);
|
telemetry::event!(
|
||||||
|
"Agent Tool Call Completed",
|
||||||
|
agent_telemetry_id,
|
||||||
|
session,
|
||||||
|
status
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ix) = self.index_for_tool_call(&id) {
|
if let Some(ix) = self.index_for_tool_call(&id) {
|
||||||
@@ -1699,6 +1742,7 @@ impl AcpThread {
|
|||||||
content: block,
|
content: block,
|
||||||
chunks: message,
|
chunks: message,
|
||||||
checkpoint: None,
|
checkpoint: None,
|
||||||
|
indented: false,
|
||||||
}),
|
}),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
@@ -3556,8 +3600,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AgentConnection for FakeAgentConnection {
|
impl AgentConnection for FakeAgentConnection {
|
||||||
fn telemetry_id(&self) -> &'static str {
|
fn telemetry_id(&self) -> SharedString {
|
||||||
"fake"
|
"fake".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn auth_methods(&self) -> &[acp::AuthMethod] {
|
fn auth_methods(&self) -> &[acp::AuthMethod] {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ impl UserMessageId {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub trait AgentConnection {
|
pub trait AgentConnection {
|
||||||
fn telemetry_id(&self) -> &'static str;
|
fn telemetry_id(&self) -> SharedString;
|
||||||
|
|
||||||
fn new_thread(
|
fn new_thread(
|
||||||
self: Rc<Self>,
|
self: Rc<Self>,
|
||||||
@@ -322,8 +322,8 @@ mod test_support {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AgentConnection for StubAgentConnection {
|
impl AgentConnection for StubAgentConnection {
|
||||||
fn telemetry_id(&self) -> &'static str {
|
fn telemetry_id(&self) -> SharedString {
|
||||||
"stub"
|
"stub".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn auth_methods(&self) -> &[acp::AuthMethod] {
|
fn auth_methods(&self) -> &[acp::AuthMethod] {
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ impl Diff {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn has_revealed_range(&self, cx: &App) -> bool {
|
pub fn has_revealed_range(&self, cx: &App) -> bool {
|
||||||
self.multibuffer().read(cx).excerpt_paths().next().is_some()
|
self.multibuffer().read(cx).paths().next().is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn needs_update(&self, old_text: &str, new_text: &str, cx: &App) -> bool {
|
pub fn needs_update(&self, old_text: &str, new_text: &str, cx: &App) -> bool {
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ use file_icons::FileIcons;
|
|||||||
use prompt_store::{PromptId, UserPromptId};
|
use prompt_store::{PromptId, UserPromptId};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
fmt,
|
fmt,
|
||||||
ops::RangeInclusive,
|
ops::RangeInclusive,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
use ui::{App, IconName, SharedString};
|
use ui::{App, IconName, SharedString};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
use urlencoding::decode;
|
||||||
use util::paths::PathStyle;
|
use util::paths::PathStyle;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Hash)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Hash)]
|
||||||
@@ -74,11 +76,13 @@ impl MentionUri {
|
|||||||
let path = url.path();
|
let path = url.path();
|
||||||
match url.scheme() {
|
match url.scheme() {
|
||||||
"file" => {
|
"file" => {
|
||||||
let path = if path_style.is_windows() {
|
let normalized = if path_style.is_windows() {
|
||||||
path.trim_start_matches("/")
|
path.trim_start_matches("/")
|
||||||
} else {
|
} else {
|
||||||
path
|
path
|
||||||
};
|
};
|
||||||
|
let decoded = decode(normalized).unwrap_or(Cow::Borrowed(normalized));
|
||||||
|
let path = decoded.as_ref();
|
||||||
|
|
||||||
if let Some(fragment) = url.fragment() {
|
if let Some(fragment) = url.fragment() {
|
||||||
let line_range = parse_line_range(fragment)?;
|
let line_range = parse_line_range(fragment)?;
|
||||||
@@ -406,6 +410,19 @@ mod tests {
|
|||||||
assert_eq!(parsed.to_uri().to_string(), selection_uri);
|
assert_eq!(parsed.to_uri().to_string(), selection_uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_file_uri_with_non_ascii() {
|
||||||
|
let file_uri = uri!("file:///path/to/%E6%97%A5%E6%9C%AC%E8%AA%9E.txt");
|
||||||
|
let parsed = MentionUri::parse(file_uri, PathStyle::local()).unwrap();
|
||||||
|
match &parsed {
|
||||||
|
MentionUri::File { abs_path } => {
|
||||||
|
assert_eq!(abs_path, Path::new(path!("/path/to/日本語.txt")));
|
||||||
|
}
|
||||||
|
_ => panic!("Expected File variant"),
|
||||||
|
}
|
||||||
|
assert_eq!(parsed.to_uri().to_string(), file_uri);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_untitled_selection_uri() {
|
fn test_parse_untitled_selection_uri() {
|
||||||
let selection_uri = uri!("zed:///agent/untitled-buffer#L1:10");
|
let selection_uri = uri!("zed:///agent/untitled-buffer#L1:10");
|
||||||
|
|||||||
@@ -187,8 +187,10 @@ pub async fn create_terminal_entity(
|
|||||||
Default::default()
|
Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Disables paging for `git` and hopefully other commands
|
// Disable pagers so agent/terminal commands don't hang behind interactive UIs
|
||||||
env.insert("PAGER".into(), "".into());
|
env.insert("PAGER".into(), "".into());
|
||||||
|
// Override user core.pager (e.g. delta) which Git prefers over PAGER
|
||||||
|
env.insert("GIT_PAGER".into(), "cat".into());
|
||||||
env.extend(env_vars);
|
env.extend(env_vars);
|
||||||
|
|
||||||
// Use remote shell or default system shell, as appropriate
|
// Use remote shell or default system shell, as appropriate
|
||||||
|
|||||||
@@ -371,13 +371,13 @@ impl AcpTools {
|
|||||||
syntax: cx.theme().syntax().clone(),
|
syntax: cx.theme().syntax().clone(),
|
||||||
code_block_overflow_x_scroll: true,
|
code_block_overflow_x_scroll: true,
|
||||||
code_block: StyleRefinement {
|
code_block: StyleRefinement {
|
||||||
text: Some(TextStyleRefinement {
|
text: TextStyleRefinement {
|
||||||
font_family: Some(
|
font_family: Some(
|
||||||
theme_settings.buffer_font.family.clone(),
|
theme_settings.buffer_font.family.clone(),
|
||||||
),
|
),
|
||||||
font_size: Some((base_size * 0.8).into()),
|
font_size: Some((base_size * 0.8).into()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|||||||
@@ -777,7 +777,7 @@ impl ActionLog {
|
|||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ActionLogTelemetry {
|
pub struct ActionLogTelemetry {
|
||||||
pub agent_telemetry_id: &'static str,
|
pub agent_telemetry_id: SharedString,
|
||||||
pub session_id: Arc<str>,
|
pub session_id: Arc<str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ mod legacy_thread;
|
|||||||
mod native_agent_server;
|
mod native_agent_server;
|
||||||
pub mod outline;
|
pub mod outline;
|
||||||
mod templates;
|
mod templates;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
mod thread;
|
mod thread;
|
||||||
mod tools;
|
mod tools;
|
||||||
|
|
||||||
#[cfg(test)]
|
use context_server::ContextServerId;
|
||||||
mod tests;
|
|
||||||
|
|
||||||
pub use db::*;
|
pub use db::*;
|
||||||
pub use history_store::*;
|
pub use history_store::*;
|
||||||
pub use native_agent_server::NativeAgentServer;
|
pub use native_agent_server::NativeAgentServer;
|
||||||
@@ -18,11 +18,11 @@ pub use templates::*;
|
|||||||
pub use thread::*;
|
pub use thread::*;
|
||||||
pub use tools::*;
|
pub use tools::*;
|
||||||
|
|
||||||
use acp_thread::{AcpThread, AgentModelSelector};
|
use acp_thread::{AcpThread, AgentModelSelector, UserMessageId};
|
||||||
use agent_client_protocol as acp;
|
use agent_client_protocol as acp;
|
||||||
use anyhow::{Context as _, Result, anyhow};
|
use anyhow::{Context as _, Result, anyhow};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use collections::{HashSet, IndexMap};
|
use collections::{HashMap, HashSet, IndexMap};
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use futures::channel::{mpsc, oneshot};
|
use futures::channel::{mpsc, oneshot};
|
||||||
use futures::future::Shared;
|
use futures::future::Shared;
|
||||||
@@ -33,12 +33,12 @@ use gpui::{
|
|||||||
use language_model::{LanguageModel, LanguageModelProvider, LanguageModelRegistry};
|
use language_model::{LanguageModel, LanguageModelProvider, LanguageModelRegistry};
|
||||||
use project::{Project, ProjectItem, ProjectPath, Worktree};
|
use project::{Project, ProjectItem, ProjectPath, Worktree};
|
||||||
use prompt_store::{
|
use prompt_store::{
|
||||||
ProjectContext, PromptStore, RulesFileContext, UserRulesContext, WorktreeContext,
|
ProjectContext, PromptStore, RULES_FILE_NAMES, RulesFileContext, UserRulesContext,
|
||||||
|
WorktreeContext,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::{LanguageModelSelection, update_settings_file};
|
use settings::{LanguageModelSelection, update_settings_file};
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -51,18 +51,6 @@ pub struct ProjectSnapshot {
|
|||||||
pub timestamp: DateTime<Utc>,
|
pub timestamp: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
const RULES_FILE_NAMES: [&str; 9] = [
|
|
||||||
".rules",
|
|
||||||
".cursorrules",
|
|
||||||
".windsurfrules",
|
|
||||||
".clinerules",
|
|
||||||
".github/copilot-instructions.md",
|
|
||||||
"CLAUDE.md",
|
|
||||||
"AGENT.md",
|
|
||||||
"AGENTS.md",
|
|
||||||
"GEMINI.md",
|
|
||||||
];
|
|
||||||
|
|
||||||
pub struct RulesLoadingError {
|
pub struct RulesLoadingError {
|
||||||
pub message: SharedString,
|
pub message: SharedString,
|
||||||
}
|
}
|
||||||
@@ -263,12 +251,24 @@ impl NativeAgent {
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
cx.new(|cx| {
|
cx.new(|cx| {
|
||||||
|
let context_server_store = project.read(cx).context_server_store();
|
||||||
|
let context_server_registry =
|
||||||
|
cx.new(|cx| ContextServerRegistry::new(context_server_store.clone(), cx));
|
||||||
|
|
||||||
let mut subscriptions = vec![
|
let mut subscriptions = vec![
|
||||||
cx.subscribe(&project, Self::handle_project_event),
|
cx.subscribe(&project, Self::handle_project_event),
|
||||||
cx.subscribe(
|
cx.subscribe(
|
||||||
&LanguageModelRegistry::global(cx),
|
&LanguageModelRegistry::global(cx),
|
||||||
Self::handle_models_updated_event,
|
Self::handle_models_updated_event,
|
||||||
),
|
),
|
||||||
|
cx.subscribe(
|
||||||
|
&context_server_store,
|
||||||
|
Self::handle_context_server_store_updated,
|
||||||
|
),
|
||||||
|
cx.subscribe(
|
||||||
|
&context_server_registry,
|
||||||
|
Self::handle_context_server_registry_event,
|
||||||
|
),
|
||||||
];
|
];
|
||||||
if let Some(prompt_store) = prompt_store.as_ref() {
|
if let Some(prompt_store) = prompt_store.as_ref() {
|
||||||
subscriptions.push(cx.subscribe(prompt_store, Self::handle_prompts_updated_event))
|
subscriptions.push(cx.subscribe(prompt_store, Self::handle_prompts_updated_event))
|
||||||
@@ -277,16 +277,14 @@ impl NativeAgent {
|
|||||||
let (project_context_needs_refresh_tx, project_context_needs_refresh_rx) =
|
let (project_context_needs_refresh_tx, project_context_needs_refresh_rx) =
|
||||||
watch::channel(());
|
watch::channel(());
|
||||||
Self {
|
Self {
|
||||||
sessions: HashMap::new(),
|
sessions: HashMap::default(),
|
||||||
history,
|
history,
|
||||||
project_context: cx.new(|_| project_context),
|
project_context: cx.new(|_| project_context),
|
||||||
project_context_needs_refresh: project_context_needs_refresh_tx,
|
project_context_needs_refresh: project_context_needs_refresh_tx,
|
||||||
_maintain_project_context: cx.spawn(async move |this, cx| {
|
_maintain_project_context: cx.spawn(async move |this, cx| {
|
||||||
Self::maintain_project_context(this, project_context_needs_refresh_rx, cx).await
|
Self::maintain_project_context(this, project_context_needs_refresh_rx, cx).await
|
||||||
}),
|
}),
|
||||||
context_server_registry: cx.new(|cx| {
|
context_server_registry,
|
||||||
ContextServerRegistry::new(project.read(cx).context_server_store(), cx)
|
|
||||||
}),
|
|
||||||
templates,
|
templates,
|
||||||
models: LanguageModels::new(cx),
|
models: LanguageModels::new(cx),
|
||||||
project,
|
project,
|
||||||
@@ -355,6 +353,9 @@ impl NativeAgent {
|
|||||||
pending_save: Task::ready(()),
|
pending_save: Task::ready(()),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
self.update_available_commands(cx);
|
||||||
|
|
||||||
acp_thread
|
acp_thread
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,10 +426,7 @@ impl NativeAgent {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.flat_map(|(contents, prompt_metadata)| match contents {
|
.flat_map(|(contents, prompt_metadata)| match contents {
|
||||||
Ok(contents) => Some(UserRulesContext {
|
Ok(contents) => Some(UserRulesContext {
|
||||||
uuid: match prompt_metadata.id {
|
uuid: prompt_metadata.id.user_id()?,
|
||||||
prompt_store::PromptId::User { uuid } => uuid,
|
|
||||||
prompt_store::PromptId::EditWorkflow => return None,
|
|
||||||
},
|
|
||||||
title: prompt_metadata.title.map(|title| title.to_string()),
|
title: prompt_metadata.title.map(|title| title.to_string()),
|
||||||
contents,
|
contents,
|
||||||
}),
|
}),
|
||||||
@@ -622,6 +620,99 @@ impl NativeAgent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_context_server_store_updated(
|
||||||
|
&mut self,
|
||||||
|
_store: Entity<project::context_server_store::ContextServerStore>,
|
||||||
|
_event: &project::context_server_store::Event,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.update_available_commands(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_context_server_registry_event(
|
||||||
|
&mut self,
|
||||||
|
_registry: Entity<ContextServerRegistry>,
|
||||||
|
event: &ContextServerRegistryEvent,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
match event {
|
||||||
|
ContextServerRegistryEvent::ToolsChanged => {}
|
||||||
|
ContextServerRegistryEvent::PromptsChanged => {
|
||||||
|
self.update_available_commands(cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_available_commands(&self, cx: &mut Context<Self>) {
|
||||||
|
let available_commands = self.build_available_commands(cx);
|
||||||
|
for session in self.sessions.values() {
|
||||||
|
if let Some(acp_thread) = session.acp_thread.upgrade() {
|
||||||
|
acp_thread.update(cx, |thread, cx| {
|
||||||
|
thread
|
||||||
|
.handle_session_update(
|
||||||
|
acp::SessionUpdate::AvailableCommandsUpdate(
|
||||||
|
acp::AvailableCommandsUpdate::new(available_commands.clone()),
|
||||||
|
),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.log_err();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_available_commands(&self, cx: &App) -> Vec<acp::AvailableCommand> {
|
||||||
|
let registry = self.context_server_registry.read(cx);
|
||||||
|
|
||||||
|
let mut prompt_name_counts: HashMap<&str, usize> = HashMap::default();
|
||||||
|
for context_server_prompt in registry.prompts() {
|
||||||
|
*prompt_name_counts
|
||||||
|
.entry(context_server_prompt.prompt.name.as_str())
|
||||||
|
.or_insert(0) += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
registry
|
||||||
|
.prompts()
|
||||||
|
.flat_map(|context_server_prompt| {
|
||||||
|
let prompt = &context_server_prompt.prompt;
|
||||||
|
|
||||||
|
let should_prefix = prompt_name_counts
|
||||||
|
.get(prompt.name.as_str())
|
||||||
|
.copied()
|
||||||
|
.unwrap_or(0)
|
||||||
|
> 1;
|
||||||
|
|
||||||
|
let name = if should_prefix {
|
||||||
|
format!("{}.{}", context_server_prompt.server_id, prompt.name)
|
||||||
|
} else {
|
||||||
|
prompt.name.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut command = acp::AvailableCommand::new(
|
||||||
|
name,
|
||||||
|
prompt.description.clone().unwrap_or_default(),
|
||||||
|
);
|
||||||
|
|
||||||
|
match prompt.arguments.as_deref() {
|
||||||
|
Some([arg]) => {
|
||||||
|
let hint = format!("<{}>", arg.name);
|
||||||
|
|
||||||
|
command = command.input(acp::AvailableCommandInput::Unstructured(
|
||||||
|
acp::UnstructuredCommandInput::new(hint),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Some([]) | None => {}
|
||||||
|
Some(_) => {
|
||||||
|
// skip >1 argument commands since we don't support them yet
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(command)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn load_thread(
|
pub fn load_thread(
|
||||||
&mut self,
|
&mut self,
|
||||||
id: acp::SessionId,
|
id: acp::SessionId,
|
||||||
@@ -720,6 +811,102 @@ impl NativeAgent {
|
|||||||
history.update(cx, |history, cx| history.reload(cx)).ok();
|
history.update(cx, |history, cx| history.reload(cx)).ok();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn send_mcp_prompt(
|
||||||
|
&self,
|
||||||
|
message_id: UserMessageId,
|
||||||
|
session_id: agent_client_protocol::SessionId,
|
||||||
|
prompt_name: String,
|
||||||
|
server_id: ContextServerId,
|
||||||
|
arguments: HashMap<String, String>,
|
||||||
|
original_content: Vec<acp::ContentBlock>,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Task<Result<acp::PromptResponse>> {
|
||||||
|
let server_store = self.context_server_registry.read(cx).server_store().clone();
|
||||||
|
let path_style = self.project.read(cx).path_style(cx);
|
||||||
|
|
||||||
|
cx.spawn(async move |this, cx| {
|
||||||
|
let prompt =
|
||||||
|
crate::get_prompt(&server_store, &server_id, &prompt_name, arguments, cx).await?;
|
||||||
|
|
||||||
|
let (acp_thread, thread) = this.update(cx, |this, _cx| {
|
||||||
|
let session = this
|
||||||
|
.sessions
|
||||||
|
.get(&session_id)
|
||||||
|
.context("Failed to get session")?;
|
||||||
|
anyhow::Ok((session.acp_thread.clone(), session.thread.clone()))
|
||||||
|
})??;
|
||||||
|
|
||||||
|
let mut last_is_user = true;
|
||||||
|
|
||||||
|
thread.update(cx, |thread, cx| {
|
||||||
|
thread.push_acp_user_block(
|
||||||
|
message_id,
|
||||||
|
original_content.into_iter().skip(1),
|
||||||
|
path_style,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
})?;
|
||||||
|
|
||||||
|
for message in prompt.messages {
|
||||||
|
let context_server::types::PromptMessage { role, content } = message;
|
||||||
|
let block = mcp_message_content_to_acp_content_block(content);
|
||||||
|
|
||||||
|
match role {
|
||||||
|
context_server::types::Role::User => {
|
||||||
|
let id = acp_thread::UserMessageId::new();
|
||||||
|
|
||||||
|
acp_thread.update(cx, |acp_thread, cx| {
|
||||||
|
acp_thread.push_user_content_block_with_indent(
|
||||||
|
Some(id.clone()),
|
||||||
|
block.clone(),
|
||||||
|
true,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
anyhow::Ok(())
|
||||||
|
})??;
|
||||||
|
|
||||||
|
thread.update(cx, |thread, cx| {
|
||||||
|
thread.push_acp_user_block(id, [block], path_style, cx);
|
||||||
|
anyhow::Ok(())
|
||||||
|
})??;
|
||||||
|
}
|
||||||
|
context_server::types::Role::Assistant => {
|
||||||
|
acp_thread.update(cx, |acp_thread, cx| {
|
||||||
|
acp_thread.push_assistant_content_block_with_indent(
|
||||||
|
block.clone(),
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
anyhow::Ok(())
|
||||||
|
})??;
|
||||||
|
|
||||||
|
thread.update(cx, |thread, cx| {
|
||||||
|
thread.push_acp_agent_block(block, cx);
|
||||||
|
anyhow::Ok(())
|
||||||
|
})??;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
last_is_user = role == context_server::types::Role::User;
|
||||||
|
}
|
||||||
|
|
||||||
|
let response_stream = thread.update(cx, |thread, cx| {
|
||||||
|
if last_is_user {
|
||||||
|
thread.send_existing(cx)
|
||||||
|
} else {
|
||||||
|
// Resume if MCP prompt did not end with a user message
|
||||||
|
thread.resume(cx)
|
||||||
|
}
|
||||||
|
})??;
|
||||||
|
|
||||||
|
cx.update(|cx| {
|
||||||
|
NativeAgentConnection::handle_thread_events(response_stream, acp_thread, cx)
|
||||||
|
})?
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wrapper struct that implements the AgentConnection trait
|
/// Wrapper struct that implements the AgentConnection trait
|
||||||
@@ -854,6 +1041,39 @@ impl NativeAgentConnection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct Command<'a> {
|
||||||
|
prompt_name: &'a str,
|
||||||
|
arg_value: &'a str,
|
||||||
|
explicit_server_id: Option<&'a str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Command<'a> {
|
||||||
|
fn parse(prompt: &'a [acp::ContentBlock]) -> Option<Self> {
|
||||||
|
let acp::ContentBlock::Text(text_content) = prompt.first()? else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let text = text_content.text.trim();
|
||||||
|
let command = text.strip_prefix('/')?;
|
||||||
|
let (command, arg_value) = command
|
||||||
|
.split_once(char::is_whitespace)
|
||||||
|
.unwrap_or((command, ""));
|
||||||
|
|
||||||
|
if let Some((server_id, prompt_name)) = command.split_once('.') {
|
||||||
|
Some(Self {
|
||||||
|
prompt_name,
|
||||||
|
arg_value,
|
||||||
|
explicit_server_id: Some(server_id),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Some(Self {
|
||||||
|
prompt_name: command,
|
||||||
|
arg_value,
|
||||||
|
explicit_server_id: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct NativeAgentModelSelector {
|
struct NativeAgentModelSelector {
|
||||||
session_id: acp::SessionId,
|
session_id: acp::SessionId,
|
||||||
connection: NativeAgentConnection,
|
connection: NativeAgentConnection,
|
||||||
@@ -947,8 +1167,8 @@ impl acp_thread::AgentModelSelector for NativeAgentModelSelector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl acp_thread::AgentConnection for NativeAgentConnection {
|
impl acp_thread::AgentConnection for NativeAgentConnection {
|
||||||
fn telemetry_id(&self) -> &'static str {
|
fn telemetry_id(&self) -> SharedString {
|
||||||
"zed"
|
"zed".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_thread(
|
fn new_thread(
|
||||||
@@ -1019,6 +1239,47 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
|||||||
let session_id = params.session_id.clone();
|
let session_id = params.session_id.clone();
|
||||||
log::info!("Received prompt request for session: {}", session_id);
|
log::info!("Received prompt request for session: {}", session_id);
|
||||||
log::debug!("Prompt blocks count: {}", params.prompt.len());
|
log::debug!("Prompt blocks count: {}", params.prompt.len());
|
||||||
|
|
||||||
|
if let Some(parsed_command) = Command::parse(¶ms.prompt) {
|
||||||
|
let registry = self.0.read(cx).context_server_registry.read(cx);
|
||||||
|
|
||||||
|
let explicit_server_id = parsed_command
|
||||||
|
.explicit_server_id
|
||||||
|
.map(|server_id| ContextServerId(server_id.into()));
|
||||||
|
|
||||||
|
if let Some(prompt) =
|
||||||
|
registry.find_prompt(explicit_server_id.as_ref(), parsed_command.prompt_name)
|
||||||
|
{
|
||||||
|
let arguments = if !parsed_command.arg_value.is_empty()
|
||||||
|
&& let Some(arg_name) = prompt
|
||||||
|
.prompt
|
||||||
|
.arguments
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|args| args.first())
|
||||||
|
.map(|arg| arg.name.clone())
|
||||||
|
{
|
||||||
|
HashMap::from_iter([(arg_name, parsed_command.arg_value.to_string())])
|
||||||
|
} else {
|
||||||
|
Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let prompt_name = prompt.prompt.name.clone();
|
||||||
|
let server_id = prompt.server_id.clone();
|
||||||
|
|
||||||
|
return self.0.update(cx, |agent, cx| {
|
||||||
|
agent.send_mcp_prompt(
|
||||||
|
id,
|
||||||
|
session_id.clone(),
|
||||||
|
prompt_name,
|
||||||
|
server_id,
|
||||||
|
arguments,
|
||||||
|
params.prompt,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
let path_style = self.0.read(cx).project.read(cx).path_style(cx);
|
let path_style = self.0.read(cx).project.read(cx).path_style(cx);
|
||||||
|
|
||||||
self.run_turn(session_id, cx, move |thread, cx| {
|
self.run_turn(session_id, cx, move |thread, cx| {
|
||||||
@@ -1219,6 +1480,15 @@ impl TerminalHandle for AcpTerminalHandle {
|
|||||||
self.terminal
|
self.terminal
|
||||||
.read_with(cx, |term, cx| term.current_output(cx))
|
.read_with(cx, |term, cx| term.current_output(cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn kill(&self, cx: &AsyncApp) -> Result<()> {
|
||||||
|
cx.update(|cx| {
|
||||||
|
self.terminal.update(cx, |terminal, cx| {
|
||||||
|
terminal.kill(cx);
|
||||||
|
});
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -1606,3 +1876,35 @@ mod internal_tests {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn mcp_message_content_to_acp_content_block(
|
||||||
|
content: context_server::types::MessageContent,
|
||||||
|
) -> acp::ContentBlock {
|
||||||
|
match content {
|
||||||
|
context_server::types::MessageContent::Text {
|
||||||
|
text,
|
||||||
|
annotations: _,
|
||||||
|
} => text.into(),
|
||||||
|
context_server::types::MessageContent::Image {
|
||||||
|
data,
|
||||||
|
mime_type,
|
||||||
|
annotations: _,
|
||||||
|
} => acp::ContentBlock::Image(acp::ImageContent::new(data, mime_type)),
|
||||||
|
context_server::types::MessageContent::Audio {
|
||||||
|
data,
|
||||||
|
mime_type,
|
||||||
|
annotations: _,
|
||||||
|
} => acp::ContentBlock::Audio(acp::AudioContent::new(data, mime_type)),
|
||||||
|
context_server::types::MessageContent::Resource {
|
||||||
|
resource,
|
||||||
|
annotations: _,
|
||||||
|
} => {
|
||||||
|
let mut link =
|
||||||
|
acp::ResourceLink::new(resource.uri.to_string(), resource.uri.to_string());
|
||||||
|
if let Some(mime_type) = resource.mime_type {
|
||||||
|
link = link.mime_type(mime_type);
|
||||||
|
}
|
||||||
|
acp::ContentBlock::ResourceLink(link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1343,6 +1343,7 @@ fn run_eval(eval: EvalInput) -> eval_utils::EvalOutput<EditEvalMetadata> {
|
|||||||
let test = EditAgentTest::new(&mut cx).await;
|
let test = EditAgentTest::new(&mut cx).await;
|
||||||
test.eval(eval, &mut cx).await
|
test.eval(eval, &mut cx).await
|
||||||
});
|
});
|
||||||
|
cx.quit();
|
||||||
match result {
|
match result {
|
||||||
Ok(output) => eval_utils::EvalOutput {
|
Ok(output) => eval_utils::EvalOutput {
|
||||||
data: output.to_string(),
|
data: output.to_string(),
|
||||||
|
|||||||
@@ -21,10 +21,6 @@ impl NativeAgentServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AgentServer for NativeAgentServer {
|
impl AgentServer for NativeAgentServer {
|
||||||
fn telemetry_id(&self) -> &'static str {
|
|
||||||
"zed"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn name(&self) -> SharedString {
|
fn name(&self) -> SharedString {
|
||||||
"Zed Agent".into()
|
"Zed Agent".into()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ You are a highly skilled software engineer with extensive knowledge in many prog
|
|||||||
3. DO NOT use tools to access items that are already available in the context section.
|
3. DO NOT use tools to access items that are already available in the context section.
|
||||||
4. Use only the tools that are currently available.
|
4. Use only the tools that are currently available.
|
||||||
5. DO NOT use a tool that is not available just because it appears in the conversation. This means the user turned it off.
|
5. DO NOT use a tool that is not available just because it appears in the conversation. This means the user turned it off.
|
||||||
6. NEVER run commands that don't terminate on their own such as web servers (like `npm run start`, `npm run dev`, `python -m http.server`, etc) or file watchers.
|
6. When running commands that may run indefinitely or for a long time (such as build scripts, tests, servers, or file watchers), specify `timeout_ms` to bound runtime. If the command times out, the user can always ask you to run it again with a longer timeout or no timeout if they're willing to wait or cancel manually.
|
||||||
7. Avoid HTML entity escaping - use plain characters instead.
|
7. Avoid HTML entity escaping - use plain characters instead.
|
||||||
|
|
||||||
## Searching and Reading
|
## Searching and Reading
|
||||||
|
|||||||
@@ -9,14 +9,16 @@ use collections::IndexMap;
|
|||||||
use context_server::{ContextServer, ContextServerCommand, ContextServerId};
|
use context_server::{ContextServer, ContextServerCommand, ContextServerId};
|
||||||
use fs::{FakeFs, Fs};
|
use fs::{FakeFs, Fs};
|
||||||
use futures::{
|
use futures::{
|
||||||
StreamExt,
|
FutureExt as _, StreamExt,
|
||||||
channel::{
|
channel::{
|
||||||
mpsc::{self, UnboundedReceiver},
|
mpsc::{self, UnboundedReceiver},
|
||||||
oneshot,
|
oneshot,
|
||||||
},
|
},
|
||||||
|
future::{Fuse, Shared},
|
||||||
};
|
};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, AppContext, Entity, Task, TestAppContext, UpdateGlobal, http_client::FakeHttpClient,
|
App, AppContext, AsyncApp, Entity, Task, TestAppContext, UpdateGlobal,
|
||||||
|
http_client::FakeHttpClient,
|
||||||
};
|
};
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
use language_model::{
|
use language_model::{
|
||||||
@@ -35,12 +37,109 @@ use schemars::JsonSchema;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use settings::{Settings, SettingsStore};
|
use settings::{Settings, SettingsStore};
|
||||||
use std::{path::Path, rc::Rc, sync::Arc, time::Duration};
|
use std::{
|
||||||
|
path::Path,
|
||||||
|
pin::Pin,
|
||||||
|
rc::Rc,
|
||||||
|
sync::{
|
||||||
|
Arc,
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
use util::path;
|
use util::path;
|
||||||
|
|
||||||
mod test_tools;
|
mod test_tools;
|
||||||
use test_tools::*;
|
use test_tools::*;
|
||||||
|
|
||||||
|
fn init_test(cx: &mut TestAppContext) {
|
||||||
|
cx.update(|cx| {
|
||||||
|
let settings_store = SettingsStore::test(cx);
|
||||||
|
cx.set_global(settings_store);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FakeTerminalHandle {
|
||||||
|
killed: Arc<AtomicBool>,
|
||||||
|
wait_for_exit: Shared<Task<acp::TerminalExitStatus>>,
|
||||||
|
output: acp::TerminalOutputResponse,
|
||||||
|
id: acp::TerminalId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FakeTerminalHandle {
|
||||||
|
fn new_never_exits(cx: &mut App) -> Self {
|
||||||
|
let killed = Arc::new(AtomicBool::new(false));
|
||||||
|
|
||||||
|
let killed_for_task = killed.clone();
|
||||||
|
let wait_for_exit = cx
|
||||||
|
.spawn(async move |cx| {
|
||||||
|
loop {
|
||||||
|
if killed_for_task.load(Ordering::SeqCst) {
|
||||||
|
return acp::TerminalExitStatus::new();
|
||||||
|
}
|
||||||
|
cx.background_executor()
|
||||||
|
.timer(Duration::from_millis(1))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.shared();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
killed,
|
||||||
|
wait_for_exit,
|
||||||
|
output: acp::TerminalOutputResponse::new("partial output".to_string(), false),
|
||||||
|
id: acp::TerminalId::new("fake_terminal".to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn was_killed(&self) -> bool {
|
||||||
|
self.killed.load(Ordering::SeqCst)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::TerminalHandle for FakeTerminalHandle {
|
||||||
|
fn id(&self, _cx: &AsyncApp) -> Result<acp::TerminalId> {
|
||||||
|
Ok(self.id.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_output(&self, _cx: &AsyncApp) -> Result<acp::TerminalOutputResponse> {
|
||||||
|
Ok(self.output.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wait_for_exit(&self, _cx: &AsyncApp) -> Result<Shared<Task<acp::TerminalExitStatus>>> {
|
||||||
|
Ok(self.wait_for_exit.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kill(&self, _cx: &AsyncApp) -> Result<()> {
|
||||||
|
self.killed.store(true, Ordering::SeqCst);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FakeThreadEnvironment {
|
||||||
|
handle: Rc<FakeTerminalHandle>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::ThreadEnvironment for FakeThreadEnvironment {
|
||||||
|
fn create_terminal(
|
||||||
|
&self,
|
||||||
|
_command: String,
|
||||||
|
_cwd: Option<std::path::PathBuf>,
|
||||||
|
_output_byte_limit: Option<u64>,
|
||||||
|
_cx: &mut AsyncApp,
|
||||||
|
) -> Task<Result<Rc<dyn crate::TerminalHandle>>> {
|
||||||
|
Task::ready(Ok(self.handle.clone() as Rc<dyn crate::TerminalHandle>))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn always_allow_tools(cx: &mut TestAppContext) {
|
||||||
|
cx.update(|cx| {
|
||||||
|
let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
|
||||||
|
settings.always_allow_tool_actions = true;
|
||||||
|
agent_settings::AgentSettings::override_global(settings, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_echo(cx: &mut TestAppContext) {
|
async fn test_echo(cx: &mut TestAppContext) {
|
||||||
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
||||||
@@ -71,6 +170,120 @@ async fn test_echo(cx: &mut TestAppContext) {
|
|||||||
assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]);
|
assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_terminal_tool_timeout_kills_handle(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
always_allow_tools(cx);
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
let project = Project::test(fs, [], cx).await;
|
||||||
|
|
||||||
|
let handle = Rc::new(cx.update(|cx| FakeTerminalHandle::new_never_exits(cx)));
|
||||||
|
let environment = Rc::new(FakeThreadEnvironment {
|
||||||
|
handle: handle.clone(),
|
||||||
|
});
|
||||||
|
|
||||||
|
#[allow(clippy::arc_with_non_send_sync)]
|
||||||
|
let tool = Arc::new(crate::TerminalTool::new(project, environment));
|
||||||
|
let (event_stream, mut rx) = crate::ToolCallEventStream::test();
|
||||||
|
|
||||||
|
let task = cx.update(|cx| {
|
||||||
|
tool.run(
|
||||||
|
crate::TerminalToolInput {
|
||||||
|
command: "sleep 1000".to_string(),
|
||||||
|
cd: ".".to_string(),
|
||||||
|
timeout_ms: Some(5),
|
||||||
|
},
|
||||||
|
event_stream,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let update = rx.expect_update_fields().await;
|
||||||
|
assert!(
|
||||||
|
update.content.iter().any(|blocks| {
|
||||||
|
blocks
|
||||||
|
.iter()
|
||||||
|
.any(|c| matches!(c, acp::ToolCallContent::Terminal(_)))
|
||||||
|
}),
|
||||||
|
"expected tool call update to include terminal content"
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut task_future: Pin<Box<Fuse<Task<Result<String>>>>> = Box::pin(task.fuse());
|
||||||
|
|
||||||
|
let deadline = std::time::Instant::now() + Duration::from_millis(500);
|
||||||
|
loop {
|
||||||
|
if let Some(result) = task_future.as_mut().now_or_never() {
|
||||||
|
let result = result.expect("terminal tool task should complete");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
handle.was_killed(),
|
||||||
|
"expected terminal handle to be killed on timeout"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
result.contains("partial output"),
|
||||||
|
"expected result to include terminal output, got: {result}"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if std::time::Instant::now() >= deadline {
|
||||||
|
panic!("timed out waiting for terminal tool task to complete");
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.run_until_parked();
|
||||||
|
cx.background_executor.timer(Duration::from_millis(1)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
#[ignore]
|
||||||
|
async fn test_terminal_tool_without_timeout_does_not_kill_handle(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
always_allow_tools(cx);
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
let project = Project::test(fs, [], cx).await;
|
||||||
|
|
||||||
|
let handle = Rc::new(cx.update(|cx| FakeTerminalHandle::new_never_exits(cx)));
|
||||||
|
let environment = Rc::new(FakeThreadEnvironment {
|
||||||
|
handle: handle.clone(),
|
||||||
|
});
|
||||||
|
|
||||||
|
#[allow(clippy::arc_with_non_send_sync)]
|
||||||
|
let tool = Arc::new(crate::TerminalTool::new(project, environment));
|
||||||
|
let (event_stream, mut rx) = crate::ToolCallEventStream::test();
|
||||||
|
|
||||||
|
let _task = cx.update(|cx| {
|
||||||
|
tool.run(
|
||||||
|
crate::TerminalToolInput {
|
||||||
|
command: "sleep 1000".to_string(),
|
||||||
|
cd: ".".to_string(),
|
||||||
|
timeout_ms: None,
|
||||||
|
},
|
||||||
|
event_stream,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let update = rx.expect_update_fields().await;
|
||||||
|
assert!(
|
||||||
|
update.content.iter().any(|blocks| {
|
||||||
|
blocks
|
||||||
|
.iter()
|
||||||
|
.any(|c| matches!(c, acp::ToolCallContent::Terminal(_)))
|
||||||
|
}),
|
||||||
|
"expected tool call update to include terminal content"
|
||||||
|
);
|
||||||
|
|
||||||
|
smol::Timer::after(Duration::from_millis(25)).await;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!handle.was_killed(),
|
||||||
|
"did not expect terminal handle to be killed without a timeout"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_thinking(cx: &mut TestAppContext) {
|
async fn test_thinking(cx: &mut TestAppContext) {
|
||||||
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ use crate::{
|
|||||||
ContextServerRegistry, CopyPathTool, CreateDirectoryTool, DbLanguageModel, DbThread,
|
ContextServerRegistry, CopyPathTool, CreateDirectoryTool, DbLanguageModel, DbThread,
|
||||||
DeletePathTool, DiagnosticsTool, EditFileTool, FetchTool, FindPathTool, GrepTool,
|
DeletePathTool, DiagnosticsTool, EditFileTool, FetchTool, FindPathTool, GrepTool,
|
||||||
ListDirectoryTool, MovePathTool, NowTool, OpenTool, ProjectSnapshot, ReadFileTool,
|
ListDirectoryTool, MovePathTool, NowTool, OpenTool, ProjectSnapshot, ReadFileTool,
|
||||||
SystemPromptTemplate, Template, Templates, TerminalTool, ThinkingTool, WebSearchTool,
|
RestoreFileFromDiskTool, SaveFileTool, SpawnSubagentTool, SystemPromptTemplate, Template,
|
||||||
|
Templates, TerminalTool, ThinkingTool, WebSearchTool,
|
||||||
};
|
};
|
||||||
use acp_thread::{MentionUri, UserMessageId};
|
use acp_thread::{MentionUri, UserMessageId};
|
||||||
use action_log::ActionLog;
|
use action_log::ActionLog;
|
||||||
@@ -107,7 +108,13 @@ impl Message {
|
|||||||
|
|
||||||
pub fn to_request(&self) -> Vec<LanguageModelRequestMessage> {
|
pub fn to_request(&self) -> Vec<LanguageModelRequestMessage> {
|
||||||
match self {
|
match self {
|
||||||
Message::User(message) => vec![message.to_request()],
|
Message::User(message) => {
|
||||||
|
if message.content.is_empty() {
|
||||||
|
vec![]
|
||||||
|
} else {
|
||||||
|
vec![message.to_request()]
|
||||||
|
}
|
||||||
|
}
|
||||||
Message::Agent(message) => message.to_request(),
|
Message::Agent(message) => message.to_request(),
|
||||||
Message::Resume => vec![LanguageModelRequestMessage {
|
Message::Resume => vec![LanguageModelRequestMessage {
|
||||||
role: Role::User,
|
role: Role::User,
|
||||||
@@ -530,6 +537,7 @@ pub trait TerminalHandle {
|
|||||||
fn id(&self, cx: &AsyncApp) -> Result<acp::TerminalId>;
|
fn id(&self, cx: &AsyncApp) -> Result<acp::TerminalId>;
|
||||||
fn current_output(&self, cx: &AsyncApp) -> Result<acp::TerminalOutputResponse>;
|
fn current_output(&self, cx: &AsyncApp) -> Result<acp::TerminalOutputResponse>;
|
||||||
fn wait_for_exit(&self, cx: &AsyncApp) -> Result<Shared<Task<acp::TerminalExitStatus>>>;
|
fn wait_for_exit(&self, cx: &AsyncApp) -> Result<Shared<Task<acp::TerminalExitStatus>>>;
|
||||||
|
fn kill(&self, cx: &AsyncApp) -> Result<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait ThreadEnvironment {
|
pub trait ThreadEnvironment {
|
||||||
@@ -1001,6 +1009,9 @@ impl Thread {
|
|||||||
self.project.clone(),
|
self.project.clone(),
|
||||||
self.action_log.clone(),
|
self.action_log.clone(),
|
||||||
));
|
));
|
||||||
|
self.add_tool(SaveFileTool::new(self.project.clone()));
|
||||||
|
self.add_tool(RestoreFileFromDiskTool::new(self.project.clone()));
|
||||||
|
self.add_tool(SpawnSubagentTool::new(None));
|
||||||
self.add_tool(TerminalTool::new(self.project.clone(), environment));
|
self.add_tool(TerminalTool::new(self.project.clone(), environment));
|
||||||
self.add_tool(ThinkingTool);
|
self.add_tool(ThinkingTool);
|
||||||
self.add_tool(WebSearchTool);
|
self.add_tool(WebSearchTool);
|
||||||
@@ -1137,11 +1148,6 @@ impl Thread {
|
|||||||
where
|
where
|
||||||
T: Into<UserMessageContent>,
|
T: Into<UserMessageContent>,
|
||||||
{
|
{
|
||||||
let model = self.model().context("No language model configured")?;
|
|
||||||
|
|
||||||
log::info!("Thread::send called with model: {}", model.name().0);
|
|
||||||
self.advance_prompt_id();
|
|
||||||
|
|
||||||
let content = content.into_iter().map(Into::into).collect::<Vec<_>>();
|
let content = content.into_iter().map(Into::into).collect::<Vec<_>>();
|
||||||
log::debug!("Thread::send content: {:?}", content);
|
log::debug!("Thread::send content: {:?}", content);
|
||||||
|
|
||||||
@@ -1149,10 +1155,59 @@ impl Thread {
|
|||||||
.push(Message::User(UserMessage { id, content }));
|
.push(Message::User(UserMessage { id, content }));
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
|
||||||
|
self.send_existing(cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_existing(
|
||||||
|
&mut self,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Result<mpsc::UnboundedReceiver<Result<ThreadEvent>>> {
|
||||||
|
let model = self.model().context("No language model configured")?;
|
||||||
|
|
||||||
|
log::info!("Thread::send called with model: {}", model.name().0);
|
||||||
|
self.advance_prompt_id();
|
||||||
|
|
||||||
log::debug!("Total messages in thread: {}", self.messages.len());
|
log::debug!("Total messages in thread: {}", self.messages.len());
|
||||||
self.run_turn(cx)
|
self.run_turn(cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn push_acp_user_block(
|
||||||
|
&mut self,
|
||||||
|
id: UserMessageId,
|
||||||
|
blocks: impl IntoIterator<Item = acp::ContentBlock>,
|
||||||
|
path_style: PathStyle,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let content = blocks
|
||||||
|
.into_iter()
|
||||||
|
.map(|block| UserMessageContent::from_content_block(block, path_style))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
self.messages
|
||||||
|
.push(Message::User(UserMessage { id, content }));
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push_acp_agent_block(&mut self, block: acp::ContentBlock, cx: &mut Context<Self>) {
|
||||||
|
let text = match block {
|
||||||
|
acp::ContentBlock::Text(text_content) => text_content.text,
|
||||||
|
acp::ContentBlock::Image(_) => "[image]".to_string(),
|
||||||
|
acp::ContentBlock::Audio(_) => "[audio]".to_string(),
|
||||||
|
acp::ContentBlock::ResourceLink(resource_link) => resource_link.uri,
|
||||||
|
acp::ContentBlock::Resource(resource) => match resource.resource {
|
||||||
|
acp::EmbeddedResourceResource::TextResourceContents(resource) => resource.uri,
|
||||||
|
acp::EmbeddedResourceResource::BlobResourceContents(resource) => resource.uri,
|
||||||
|
_ => "[resource]".to_string(),
|
||||||
|
},
|
||||||
|
_ => "[unknown]".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.messages.push(Message::Agent(AgentMessage {
|
||||||
|
content: vec![AgentMessageContent::Text(text)],
|
||||||
|
..Default::default()
|
||||||
|
}));
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "eval")]
|
#[cfg(feature = "eval")]
|
||||||
pub fn proceed(
|
pub fn proceed(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -1965,6 +2020,12 @@ impl Thread {
|
|||||||
self.running_turn.as_ref()?.tools.get(name).cloned()
|
self.running_turn.as_ref()?.tools.get(name).cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn has_tool(&self, name: &str) -> bool {
|
||||||
|
self.running_turn
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|turn| turn.tools.contains_key(name))
|
||||||
|
}
|
||||||
|
|
||||||
fn build_request_messages(
|
fn build_request_messages(
|
||||||
&self,
|
&self,
|
||||||
available_tools: Vec<SharedString>,
|
available_tools: Vec<SharedString>,
|
||||||
@@ -2658,7 +2719,6 @@ impl From<UserMessageContent> for acp::ContentBlock {
|
|||||||
fn convert_image(image_content: acp::ImageContent) -> LanguageModelImage {
|
fn convert_image(image_content: acp::ImageContent) -> LanguageModelImage {
|
||||||
LanguageModelImage {
|
LanguageModelImage {
|
||||||
source: image_content.data.into(),
|
source: image_content.data.into(),
|
||||||
// TODO: make this optional?
|
size: None,
|
||||||
size: gpui::Size::new(0.into(), 0.into()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ mod create_directory_tool;
|
|||||||
mod delete_path_tool;
|
mod delete_path_tool;
|
||||||
mod diagnostics_tool;
|
mod diagnostics_tool;
|
||||||
mod edit_file_tool;
|
mod edit_file_tool;
|
||||||
|
|
||||||
mod fetch_tool;
|
mod fetch_tool;
|
||||||
mod find_path_tool;
|
mod find_path_tool;
|
||||||
mod grep_tool;
|
mod grep_tool;
|
||||||
@@ -13,6 +12,9 @@ mod move_path_tool;
|
|||||||
mod now_tool;
|
mod now_tool;
|
||||||
mod open_tool;
|
mod open_tool;
|
||||||
mod read_file_tool;
|
mod read_file_tool;
|
||||||
|
mod restore_file_from_disk_tool;
|
||||||
|
mod save_file_tool;
|
||||||
|
mod spawn_subagent_tool;
|
||||||
|
|
||||||
mod terminal_tool;
|
mod terminal_tool;
|
||||||
mod thinking_tool;
|
mod thinking_tool;
|
||||||
@@ -27,7 +29,6 @@ pub use create_directory_tool::*;
|
|||||||
pub use delete_path_tool::*;
|
pub use delete_path_tool::*;
|
||||||
pub use diagnostics_tool::*;
|
pub use diagnostics_tool::*;
|
||||||
pub use edit_file_tool::*;
|
pub use edit_file_tool::*;
|
||||||
|
|
||||||
pub use fetch_tool::*;
|
pub use fetch_tool::*;
|
||||||
pub use find_path_tool::*;
|
pub use find_path_tool::*;
|
||||||
pub use grep_tool::*;
|
pub use grep_tool::*;
|
||||||
@@ -36,6 +37,9 @@ pub use move_path_tool::*;
|
|||||||
pub use now_tool::*;
|
pub use now_tool::*;
|
||||||
pub use open_tool::*;
|
pub use open_tool::*;
|
||||||
pub use read_file_tool::*;
|
pub use read_file_tool::*;
|
||||||
|
pub use restore_file_from_disk_tool::*;
|
||||||
|
pub use save_file_tool::*;
|
||||||
|
pub use spawn_subagent_tool::*;
|
||||||
|
|
||||||
pub use terminal_tool::*;
|
pub use terminal_tool::*;
|
||||||
pub use thinking_tool::*;
|
pub use thinking_tool::*;
|
||||||
@@ -92,6 +96,9 @@ tools! {
|
|||||||
NowTool,
|
NowTool,
|
||||||
OpenTool,
|
OpenTool,
|
||||||
ReadFileTool,
|
ReadFileTool,
|
||||||
|
RestoreFileFromDiskTool,
|
||||||
|
SaveFileTool,
|
||||||
|
SpawnSubagentTool,
|
||||||
TerminalTool,
|
TerminalTool,
|
||||||
ThinkingTool,
|
ThinkingTool,
|
||||||
WebSearchTool,
|
WebSearchTool,
|
||||||
|
|||||||
@@ -3,11 +3,23 @@ use agent_client_protocol::ToolKind;
|
|||||||
use anyhow::{Result, anyhow, bail};
|
use anyhow::{Result, anyhow, bail};
|
||||||
use collections::{BTreeMap, HashMap};
|
use collections::{BTreeMap, HashMap};
|
||||||
use context_server::ContextServerId;
|
use context_server::ContextServerId;
|
||||||
use gpui::{App, Context, Entity, SharedString, Task};
|
use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task};
|
||||||
use project::context_server_store::{ContextServerStatus, ContextServerStore};
|
use project::context_server_store::{ContextServerStatus, ContextServerStore};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
|
||||||
|
pub struct ContextServerPrompt {
|
||||||
|
pub server_id: ContextServerId,
|
||||||
|
pub prompt: context_server::types::Prompt,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum ContextServerRegistryEvent {
|
||||||
|
ToolsChanged,
|
||||||
|
PromptsChanged,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<ContextServerRegistryEvent> for ContextServerRegistry {}
|
||||||
|
|
||||||
pub struct ContextServerRegistry {
|
pub struct ContextServerRegistry {
|
||||||
server_store: Entity<ContextServerStore>,
|
server_store: Entity<ContextServerStore>,
|
||||||
registered_servers: HashMap<ContextServerId, RegisteredContextServer>,
|
registered_servers: HashMap<ContextServerId, RegisteredContextServer>,
|
||||||
@@ -16,7 +28,20 @@ pub struct ContextServerRegistry {
|
|||||||
|
|
||||||
struct RegisteredContextServer {
|
struct RegisteredContextServer {
|
||||||
tools: BTreeMap<SharedString, Arc<dyn AnyAgentTool>>,
|
tools: BTreeMap<SharedString, Arc<dyn AnyAgentTool>>,
|
||||||
|
prompts: BTreeMap<SharedString, ContextServerPrompt>,
|
||||||
load_tools: Task<Result<()>>,
|
load_tools: Task<Result<()>>,
|
||||||
|
load_prompts: Task<Result<()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RegisteredContextServer {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
tools: BTreeMap::default(),
|
||||||
|
prompts: BTreeMap::default(),
|
||||||
|
load_tools: Task::ready(Ok(())),
|
||||||
|
load_prompts: Task::ready(Ok(())),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ContextServerRegistry {
|
impl ContextServerRegistry {
|
||||||
@@ -28,6 +53,7 @@ impl ContextServerRegistry {
|
|||||||
};
|
};
|
||||||
for server in server_store.read(cx).running_servers() {
|
for server in server_store.read(cx).running_servers() {
|
||||||
this.reload_tools_for_server(server.id(), cx);
|
this.reload_tools_for_server(server.id(), cx);
|
||||||
|
this.reload_prompts_for_server(server.id(), cx);
|
||||||
}
|
}
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
@@ -56,6 +82,41 @@ impl ContextServerRegistry {
|
|||||||
.map(|(id, server)| (id, &server.tools))
|
.map(|(id, server)| (id, &server.tools))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn prompts(&self) -> impl Iterator<Item = &ContextServerPrompt> {
|
||||||
|
self.registered_servers
|
||||||
|
.values()
|
||||||
|
.flat_map(|server| server.prompts.values())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_prompt(
|
||||||
|
&self,
|
||||||
|
server_id: Option<&ContextServerId>,
|
||||||
|
name: &str,
|
||||||
|
) -> Option<&ContextServerPrompt> {
|
||||||
|
if let Some(server_id) = server_id {
|
||||||
|
self.registered_servers
|
||||||
|
.get(server_id)
|
||||||
|
.and_then(|server| server.prompts.get(name))
|
||||||
|
} else {
|
||||||
|
self.registered_servers
|
||||||
|
.values()
|
||||||
|
.find_map(|server| server.prompts.get(name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn server_store(&self) -> &Entity<ContextServerStore> {
|
||||||
|
&self.server_store
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_or_register_server(
|
||||||
|
&mut self,
|
||||||
|
server_id: &ContextServerId,
|
||||||
|
) -> &mut RegisteredContextServer {
|
||||||
|
self.registered_servers
|
||||||
|
.entry(server_id.clone())
|
||||||
|
.or_insert_with(RegisteredContextServer::new)
|
||||||
|
}
|
||||||
|
|
||||||
fn reload_tools_for_server(&mut self, server_id: ContextServerId, cx: &mut Context<Self>) {
|
fn reload_tools_for_server(&mut self, server_id: ContextServerId, cx: &mut Context<Self>) {
|
||||||
let Some(server) = self.server_store.read(cx).get_running_server(&server_id) else {
|
let Some(server) = self.server_store.read(cx).get_running_server(&server_id) else {
|
||||||
return;
|
return;
|
||||||
@@ -67,13 +128,7 @@ impl ContextServerRegistry {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let registered_server =
|
let registered_server = self.get_or_register_server(&server_id);
|
||||||
self.registered_servers
|
|
||||||
.entry(server_id.clone())
|
|
||||||
.or_insert(RegisteredContextServer {
|
|
||||||
tools: BTreeMap::default(),
|
|
||||||
load_tools: Task::ready(Ok(())),
|
|
||||||
});
|
|
||||||
registered_server.load_tools = cx.spawn(async move |this, cx| {
|
registered_server.load_tools = cx.spawn(async move |this, cx| {
|
||||||
let response = client
|
let response = client
|
||||||
.request::<context_server::types::requests::ListTools>(())
|
.request::<context_server::types::requests::ListTools>(())
|
||||||
@@ -94,6 +149,49 @@ impl ContextServerRegistry {
|
|||||||
));
|
));
|
||||||
registered_server.tools.insert(tool.name(), tool);
|
registered_server.tools.insert(tool.name(), tool);
|
||||||
}
|
}
|
||||||
|
cx.emit(ContextServerRegistryEvent::ToolsChanged);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reload_prompts_for_server(&mut self, server_id: ContextServerId, cx: &mut Context<Self>) {
|
||||||
|
let Some(server) = self.server_store.read(cx).get_running_server(&server_id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(client) = server.client() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if !client.capable(context_server::protocol::ServerCapability::Prompts) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let registered_server = self.get_or_register_server(&server_id);
|
||||||
|
|
||||||
|
registered_server.load_prompts = cx.spawn(async move |this, cx| {
|
||||||
|
let response = client
|
||||||
|
.request::<context_server::types::requests::PromptsList>(())
|
||||||
|
.await;
|
||||||
|
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
let Some(registered_server) = this.registered_servers.get_mut(&server_id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
registered_server.prompts.clear();
|
||||||
|
if let Some(response) = response.log_err() {
|
||||||
|
for prompt in response.prompts {
|
||||||
|
let name: SharedString = prompt.name.clone().into();
|
||||||
|
registered_server.prompts.insert(
|
||||||
|
name,
|
||||||
|
ContextServerPrompt {
|
||||||
|
server_id: server_id.clone(),
|
||||||
|
prompt,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
cx.emit(ContextServerRegistryEvent::PromptsChanged);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -112,9 +210,17 @@ impl ContextServerRegistry {
|
|||||||
ContextServerStatus::Starting => {}
|
ContextServerStatus::Starting => {}
|
||||||
ContextServerStatus::Running => {
|
ContextServerStatus::Running => {
|
||||||
self.reload_tools_for_server(server_id.clone(), cx);
|
self.reload_tools_for_server(server_id.clone(), cx);
|
||||||
|
self.reload_prompts_for_server(server_id.clone(), cx);
|
||||||
}
|
}
|
||||||
ContextServerStatus::Stopped | ContextServerStatus::Error(_) => {
|
ContextServerStatus::Stopped | ContextServerStatus::Error(_) => {
|
||||||
self.registered_servers.remove(server_id);
|
if let Some(registered_server) = self.registered_servers.remove(server_id) {
|
||||||
|
if !registered_server.tools.is_empty() {
|
||||||
|
cx.emit(ContextServerRegistryEvent::ToolsChanged);
|
||||||
|
}
|
||||||
|
if !registered_server.prompts.is_empty() {
|
||||||
|
cx.emit(ContextServerRegistryEvent::PromptsChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -251,3 +357,39 @@ impl AnyAgentTool for ContextServerTool {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_prompt(
|
||||||
|
server_store: &Entity<ContextServerStore>,
|
||||||
|
server_id: &ContextServerId,
|
||||||
|
prompt_name: &str,
|
||||||
|
arguments: HashMap<String, String>,
|
||||||
|
cx: &mut AsyncApp,
|
||||||
|
) -> Task<Result<context_server::types::PromptsGetResponse>> {
|
||||||
|
let server = match cx.update(|cx| server_store.read(cx).get_running_server(server_id)) {
|
||||||
|
Ok(server) => server,
|
||||||
|
Err(error) => return Task::ready(Err(error)),
|
||||||
|
};
|
||||||
|
let Some(server) = server else {
|
||||||
|
return Task::ready(Err(anyhow::anyhow!("Context server not found")));
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(protocol) = server.client() else {
|
||||||
|
return Task::ready(Err(anyhow::anyhow!("Context server not initialized")));
|
||||||
|
};
|
||||||
|
|
||||||
|
let prompt_name = prompt_name.to_string();
|
||||||
|
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
let response = protocol
|
||||||
|
.request::<context_server::types::requests::PromptsGet>(
|
||||||
|
context_server::types::PromptsGetParams {
|
||||||
|
name: prompt_name,
|
||||||
|
arguments: (!arguments.is_empty()).then(|| arguments),
|
||||||
|
meta: None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -306,20 +306,39 @@ impl AgentTool for EditFileTool {
|
|||||||
|
|
||||||
// Check if the file has been modified since the agent last read it
|
// Check if the file has been modified since the agent last read it
|
||||||
if let Some(abs_path) = abs_path.as_ref() {
|
if let Some(abs_path) = abs_path.as_ref() {
|
||||||
let (last_read_mtime, current_mtime, is_dirty) = self.thread.update(cx, |thread, cx| {
|
let (last_read_mtime, current_mtime, is_dirty, has_save_tool, has_restore_tool) = self.thread.update(cx, |thread, cx| {
|
||||||
let last_read = thread.file_read_times.get(abs_path).copied();
|
let last_read = thread.file_read_times.get(abs_path).copied();
|
||||||
let current = buffer.read(cx).file().and_then(|file| file.disk_state().mtime());
|
let current = buffer.read(cx).file().and_then(|file| file.disk_state().mtime());
|
||||||
let dirty = buffer.read(cx).is_dirty();
|
let dirty = buffer.read(cx).is_dirty();
|
||||||
(last_read, current, dirty)
|
let has_save = thread.has_tool("save_file");
|
||||||
|
let has_restore = thread.has_tool("restore_file_from_disk");
|
||||||
|
(last_read, current, dirty, has_save, has_restore)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Check for unsaved changes first - these indicate modifications we don't know about
|
// Check for unsaved changes first - these indicate modifications we don't know about
|
||||||
if is_dirty {
|
if is_dirty {
|
||||||
anyhow::bail!(
|
let message = match (has_save_tool, has_restore_tool) {
|
||||||
"This file cannot be written to because it has unsaved changes. \
|
(true, true) => {
|
||||||
Please end the current conversation immediately by telling the user you want to write to this file (mention its path explicitly) but you can't write to it because it has unsaved changes. \
|
"This file has unsaved changes. Ask the user whether they want to keep or discard those changes. \
|
||||||
Ask the user to save that buffer's changes and to inform you when it's ok to proceed."
|
If they want to keep them, ask for confirmation then use the save_file tool to save the file, then retry this edit. \
|
||||||
);
|
If they want to discard them, ask for confirmation then use the restore_file_from_disk tool to restore the on-disk contents, then retry this edit."
|
||||||
|
}
|
||||||
|
(true, false) => {
|
||||||
|
"This file has unsaved changes. Ask the user whether they want to keep or discard those changes. \
|
||||||
|
If they want to keep them, ask for confirmation then use the save_file tool to save the file, then retry this edit. \
|
||||||
|
If they want to discard them, ask the user to manually revert the file, then inform you when it's ok to proceed."
|
||||||
|
}
|
||||||
|
(false, true) => {
|
||||||
|
"This file has unsaved changes. Ask the user whether they want to keep or discard those changes. \
|
||||||
|
If they want to keep them, ask the user to manually save the file, then inform you when it's ok to proceed. \
|
||||||
|
If they want to discard them, ask for confirmation then use the restore_file_from_disk tool to restore the on-disk contents, then retry this edit."
|
||||||
|
}
|
||||||
|
(false, false) => {
|
||||||
|
"This file has unsaved changes. Ask the user whether they want to keep or discard those changes, \
|
||||||
|
then ask them to save or revert the file manually and inform you when it's ok to proceed."
|
||||||
|
}
|
||||||
|
};
|
||||||
|
anyhow::bail!("{}", message);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the file was modified on disk since we last read it
|
// Check if the file was modified on disk since we last read it
|
||||||
@@ -2202,9 +2221,21 @@ mod tests {
|
|||||||
assert!(result.is_err(), "Edit should fail when buffer is dirty");
|
assert!(result.is_err(), "Edit should fail when buffer is dirty");
|
||||||
let error_msg = result.unwrap_err().to_string();
|
let error_msg = result.unwrap_err().to_string();
|
||||||
assert!(
|
assert!(
|
||||||
error_msg.contains("cannot be written to because it has unsaved changes"),
|
error_msg.contains("This file has unsaved changes."),
|
||||||
"Error should mention unsaved changes, got: {}",
|
"Error should mention unsaved changes, got: {}",
|
||||||
error_msg
|
error_msg
|
||||||
);
|
);
|
||||||
|
assert!(
|
||||||
|
error_msg.contains("keep or discard"),
|
||||||
|
"Error should ask whether to keep or discard changes, got: {}",
|
||||||
|
error_msg
|
||||||
|
);
|
||||||
|
// Since save_file and restore_file_from_disk tools aren't added to the thread,
|
||||||
|
// the error message should ask the user to manually save or revert
|
||||||
|
assert!(
|
||||||
|
error_msg.contains("save or revert the file manually"),
|
||||||
|
"Error should ask user to manually save or revert when tools aren't available, got: {}",
|
||||||
|
error_msg
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
352
crates/agent/src/tools/restore_file_from_disk_tool.rs
Normal file
352
crates/agent/src/tools/restore_file_from_disk_tool.rs
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
use agent_client_protocol as acp;
|
||||||
|
use anyhow::Result;
|
||||||
|
use collections::FxHashSet;
|
||||||
|
use gpui::{App, Entity, SharedString, Task};
|
||||||
|
use language::Buffer;
|
||||||
|
use project::Project;
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::{AgentTool, ToolCallEventStream};
|
||||||
|
|
||||||
|
/// Discards unsaved changes in open buffers by reloading file contents from disk.
|
||||||
|
///
|
||||||
|
/// Use this tool when:
|
||||||
|
/// - You attempted to edit files but they have unsaved changes the user does not want to keep.
|
||||||
|
/// - You want to reset files to the on-disk state before retrying an edit.
|
||||||
|
///
|
||||||
|
/// Only use this tool after asking the user for permission, because it will discard unsaved changes.
|
||||||
|
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||||
|
pub struct RestoreFileFromDiskToolInput {
|
||||||
|
/// The paths of the files to restore from disk.
|
||||||
|
pub paths: Vec<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RestoreFileFromDiskTool {
|
||||||
|
project: Entity<Project>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RestoreFileFromDiskTool {
|
||||||
|
pub fn new(project: Entity<Project>) -> Self {
|
||||||
|
Self { project }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AgentTool for RestoreFileFromDiskTool {
|
||||||
|
type Input = RestoreFileFromDiskToolInput;
|
||||||
|
type Output = String;
|
||||||
|
|
||||||
|
fn name() -> &'static str {
|
||||||
|
"restore_file_from_disk"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kind() -> acp::ToolKind {
|
||||||
|
acp::ToolKind::Other
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initial_title(
|
||||||
|
&self,
|
||||||
|
input: Result<Self::Input, serde_json::Value>,
|
||||||
|
_cx: &mut App,
|
||||||
|
) -> SharedString {
|
||||||
|
match input {
|
||||||
|
Ok(input) if input.paths.len() == 1 => "Restore file from disk".into(),
|
||||||
|
Ok(input) => format!("Restore {} files from disk", input.paths.len()).into(),
|
||||||
|
Err(_) => "Restore files from disk".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(
|
||||||
|
self: Arc<Self>,
|
||||||
|
input: Self::Input,
|
||||||
|
_event_stream: ToolCallEventStream,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Task<Result<String>> {
|
||||||
|
let project = self.project.clone();
|
||||||
|
let input_paths = input.paths;
|
||||||
|
|
||||||
|
cx.spawn(async move |cx| {
|
||||||
|
let mut buffers_to_reload: FxHashSet<Entity<Buffer>> = FxHashSet::default();
|
||||||
|
|
||||||
|
let mut restored_paths: Vec<PathBuf> = Vec::new();
|
||||||
|
let mut clean_paths: Vec<PathBuf> = Vec::new();
|
||||||
|
let mut not_found_paths: Vec<PathBuf> = Vec::new();
|
||||||
|
let mut open_errors: Vec<(PathBuf, String)> = Vec::new();
|
||||||
|
let mut dirty_check_errors: Vec<(PathBuf, String)> = Vec::new();
|
||||||
|
let mut reload_errors: Vec<String> = Vec::new();
|
||||||
|
|
||||||
|
for path in input_paths {
|
||||||
|
let project_path =
|
||||||
|
project.read_with(cx, |project, cx| project.find_project_path(&path, cx));
|
||||||
|
|
||||||
|
let project_path = match project_path {
|
||||||
|
Ok(Some(project_path)) => project_path,
|
||||||
|
Ok(None) => {
|
||||||
|
not_found_paths.push(path);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
open_errors.push((path, error.to_string()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let open_buffer_task =
|
||||||
|
project.update(cx, |project, cx| project.open_buffer(project_path, cx));
|
||||||
|
|
||||||
|
let buffer = match open_buffer_task {
|
||||||
|
Ok(task) => match task.await {
|
||||||
|
Ok(buffer) => buffer,
|
||||||
|
Err(error) => {
|
||||||
|
open_errors.push((path, error.to_string()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(error) => {
|
||||||
|
open_errors.push((path, error.to_string()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let is_dirty = match buffer.read_with(cx, |buffer, _| buffer.is_dirty()) {
|
||||||
|
Ok(is_dirty) => is_dirty,
|
||||||
|
Err(error) => {
|
||||||
|
dirty_check_errors.push((path, error.to_string()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if is_dirty {
|
||||||
|
buffers_to_reload.insert(buffer);
|
||||||
|
restored_paths.push(path);
|
||||||
|
} else {
|
||||||
|
clean_paths.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !buffers_to_reload.is_empty() {
|
||||||
|
let reload_task = project.update(cx, |project, cx| {
|
||||||
|
project.reload_buffers(buffers_to_reload, true, cx)
|
||||||
|
});
|
||||||
|
|
||||||
|
match reload_task {
|
||||||
|
Ok(task) => {
|
||||||
|
if let Err(error) = task.await {
|
||||||
|
reload_errors.push(error.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
reload_errors.push(error.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut lines: Vec<String> = Vec::new();
|
||||||
|
|
||||||
|
if !restored_paths.is_empty() {
|
||||||
|
lines.push(format!("Restored {} file(s).", restored_paths.len()));
|
||||||
|
}
|
||||||
|
if !clean_paths.is_empty() {
|
||||||
|
lines.push(format!("{} clean.", clean_paths.len()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !not_found_paths.is_empty() {
|
||||||
|
lines.push(format!("Not found ({}):", not_found_paths.len()));
|
||||||
|
for path in ¬_found_paths {
|
||||||
|
lines.push(format!("- {}", path.display()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !open_errors.is_empty() {
|
||||||
|
lines.push(format!("Open failed ({}):", open_errors.len()));
|
||||||
|
for (path, error) in &open_errors {
|
||||||
|
lines.push(format!("- {}: {}", path.display(), error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !dirty_check_errors.is_empty() {
|
||||||
|
lines.push(format!(
|
||||||
|
"Dirty check failed ({}):",
|
||||||
|
dirty_check_errors.len()
|
||||||
|
));
|
||||||
|
for (path, error) in &dirty_check_errors {
|
||||||
|
lines.push(format!("- {}: {}", path.display(), error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !reload_errors.is_empty() {
|
||||||
|
lines.push(format!("Reload failed ({}):", reload_errors.len()));
|
||||||
|
for error in &reload_errors {
|
||||||
|
lines.push(format!("- {}", error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if lines.is_empty() {
|
||||||
|
Ok("No paths provided.".to_string())
|
||||||
|
} else {
|
||||||
|
Ok(lines.join("\n"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use fs::Fs;
|
||||||
|
use gpui::TestAppContext;
|
||||||
|
use language::LineEnding;
|
||||||
|
use project::FakeFs;
|
||||||
|
use serde_json::json;
|
||||||
|
use settings::SettingsStore;
|
||||||
|
use util::path;
|
||||||
|
|
||||||
|
fn init_test(cx: &mut TestAppContext) {
|
||||||
|
cx.update(|cx| {
|
||||||
|
let settings_store = SettingsStore::test(cx);
|
||||||
|
cx.set_global(settings_store);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_restore_file_from_disk_output_and_effects(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
fs.insert_tree(
|
||||||
|
"/root",
|
||||||
|
json!({
|
||||||
|
"dirty.txt": "on disk: dirty\n",
|
||||||
|
"clean.txt": "on disk: clean\n",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||||
|
let tool = Arc::new(RestoreFileFromDiskTool::new(project.clone()));
|
||||||
|
|
||||||
|
// Make dirty.txt dirty in-memory by saving different content into the buffer without saving to disk.
|
||||||
|
let dirty_project_path = project.read_with(cx, |project, cx| {
|
||||||
|
project
|
||||||
|
.find_project_path("root/dirty.txt", cx)
|
||||||
|
.expect("dirty.txt should exist in project")
|
||||||
|
});
|
||||||
|
|
||||||
|
let dirty_buffer = project
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
project.open_buffer(dirty_project_path, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
dirty_buffer.update(cx, |buffer, cx| {
|
||||||
|
buffer.edit([(0..buffer.len(), "in memory: dirty\n")], None, cx);
|
||||||
|
});
|
||||||
|
assert!(
|
||||||
|
dirty_buffer.read_with(cx, |buffer, _| buffer.is_dirty()),
|
||||||
|
"dirty.txt buffer should be dirty before restore"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ensure clean.txt is opened but remains clean.
|
||||||
|
let clean_project_path = project.read_with(cx, |project, cx| {
|
||||||
|
project
|
||||||
|
.find_project_path("root/clean.txt", cx)
|
||||||
|
.expect("clean.txt should exist in project")
|
||||||
|
});
|
||||||
|
|
||||||
|
let clean_buffer = project
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
project.open_buffer(clean_project_path, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
!clean_buffer.read_with(cx, |buffer, _| buffer.is_dirty()),
|
||||||
|
"clean.txt buffer should start clean"
|
||||||
|
);
|
||||||
|
|
||||||
|
let output = cx
|
||||||
|
.update(|cx| {
|
||||||
|
tool.clone().run(
|
||||||
|
RestoreFileFromDiskToolInput {
|
||||||
|
paths: vec![
|
||||||
|
PathBuf::from("root/dirty.txt"),
|
||||||
|
PathBuf::from("root/clean.txt"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
ToolCallEventStream::test().0,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Output should mention restored + clean.
|
||||||
|
assert!(
|
||||||
|
output.contains("Restored 1 file(s)."),
|
||||||
|
"expected restored count line, got:\n{output}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
output.contains("1 clean."),
|
||||||
|
"expected clean count line, got:\n{output}"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Effect: dirty buffer should be restored back to disk content and become clean.
|
||||||
|
let dirty_text = dirty_buffer.read_with(cx, |buffer, _| buffer.text());
|
||||||
|
assert_eq!(
|
||||||
|
dirty_text, "on disk: dirty\n",
|
||||||
|
"dirty.txt buffer should be restored to disk contents"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!dirty_buffer.read_with(cx, |buffer, _| buffer.is_dirty()),
|
||||||
|
"dirty.txt buffer should not be dirty after restore"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Disk contents should be unchanged (restore-from-disk should not write).
|
||||||
|
let disk_dirty = fs.load(path!("/root/dirty.txt").as_ref()).await.unwrap();
|
||||||
|
assert_eq!(disk_dirty, "on disk: dirty\n");
|
||||||
|
|
||||||
|
// Sanity: clean buffer should remain clean and unchanged.
|
||||||
|
let clean_text = clean_buffer.read_with(cx, |buffer, _| buffer.text());
|
||||||
|
assert_eq!(clean_text, "on disk: clean\n");
|
||||||
|
assert!(
|
||||||
|
!clean_buffer.read_with(cx, |buffer, _| buffer.is_dirty()),
|
||||||
|
"clean.txt buffer should remain clean"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test empty paths case.
|
||||||
|
let output = cx
|
||||||
|
.update(|cx| {
|
||||||
|
tool.clone().run(
|
||||||
|
RestoreFileFromDiskToolInput { paths: vec![] },
|
||||||
|
ToolCallEventStream::test().0,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(output, "No paths provided.");
|
||||||
|
|
||||||
|
// Test not-found path case (path outside the project root).
|
||||||
|
let output = cx
|
||||||
|
.update(|cx| {
|
||||||
|
tool.clone().run(
|
||||||
|
RestoreFileFromDiskToolInput {
|
||||||
|
paths: vec![PathBuf::from("nonexistent/path.txt")],
|
||||||
|
},
|
||||||
|
ToolCallEventStream::test().0,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
output.contains("Not found (1):"),
|
||||||
|
"expected not-found header line, got:\n{output}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
output.contains("- nonexistent/path.txt"),
|
||||||
|
"expected not-found path bullet, got:\n{output}"
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = LineEnding::Unix; // keep import used if the buffer edit API changes
|
||||||
|
}
|
||||||
|
}
|
||||||
351
crates/agent/src/tools/save_file_tool.rs
Normal file
351
crates/agent/src/tools/save_file_tool.rs
Normal file
@@ -0,0 +1,351 @@
|
|||||||
|
use agent_client_protocol as acp;
|
||||||
|
use anyhow::Result;
|
||||||
|
use collections::FxHashSet;
|
||||||
|
use gpui::{App, Entity, SharedString, Task};
|
||||||
|
use language::Buffer;
|
||||||
|
use project::Project;
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::{AgentTool, ToolCallEventStream};
|
||||||
|
|
||||||
|
/// Saves files that have unsaved changes.
|
||||||
|
///
|
||||||
|
/// Use this tool when you need to edit files but they have unsaved changes that must be saved first.
|
||||||
|
/// Only use this tool after asking the user for permission to save their unsaved changes.
|
||||||
|
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||||
|
pub struct SaveFileToolInput {
|
||||||
|
/// The paths of the files to save.
|
||||||
|
pub paths: Vec<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SaveFileTool {
|
||||||
|
project: Entity<Project>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SaveFileTool {
|
||||||
|
pub fn new(project: Entity<Project>) -> Self {
|
||||||
|
Self { project }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AgentTool for SaveFileTool {
|
||||||
|
type Input = SaveFileToolInput;
|
||||||
|
type Output = String;
|
||||||
|
|
||||||
|
fn name() -> &'static str {
|
||||||
|
"save_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kind() -> acp::ToolKind {
|
||||||
|
acp::ToolKind::Other
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initial_title(
|
||||||
|
&self,
|
||||||
|
input: Result<Self::Input, serde_json::Value>,
|
||||||
|
_cx: &mut App,
|
||||||
|
) -> SharedString {
|
||||||
|
match input {
|
||||||
|
Ok(input) if input.paths.len() == 1 => "Save file".into(),
|
||||||
|
Ok(input) => format!("Save {} files", input.paths.len()).into(),
|
||||||
|
Err(_) => "Save files".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(
|
||||||
|
self: Arc<Self>,
|
||||||
|
input: Self::Input,
|
||||||
|
_event_stream: ToolCallEventStream,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Task<Result<String>> {
|
||||||
|
let project = self.project.clone();
|
||||||
|
let input_paths = input.paths;
|
||||||
|
|
||||||
|
cx.spawn(async move |cx| {
|
||||||
|
let mut buffers_to_save: FxHashSet<Entity<Buffer>> = FxHashSet::default();
|
||||||
|
|
||||||
|
let mut saved_paths: Vec<PathBuf> = Vec::new();
|
||||||
|
let mut clean_paths: Vec<PathBuf> = Vec::new();
|
||||||
|
let mut not_found_paths: Vec<PathBuf> = Vec::new();
|
||||||
|
let mut open_errors: Vec<(PathBuf, String)> = Vec::new();
|
||||||
|
let mut dirty_check_errors: Vec<(PathBuf, String)> = Vec::new();
|
||||||
|
let mut save_errors: Vec<(String, String)> = Vec::new();
|
||||||
|
|
||||||
|
for path in input_paths {
|
||||||
|
let project_path =
|
||||||
|
project.read_with(cx, |project, cx| project.find_project_path(&path, cx));
|
||||||
|
|
||||||
|
let project_path = match project_path {
|
||||||
|
Ok(Some(project_path)) => project_path,
|
||||||
|
Ok(None) => {
|
||||||
|
not_found_paths.push(path);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
open_errors.push((path, error.to_string()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let open_buffer_task =
|
||||||
|
project.update(cx, |project, cx| project.open_buffer(project_path, cx));
|
||||||
|
|
||||||
|
let buffer = match open_buffer_task {
|
||||||
|
Ok(task) => match task.await {
|
||||||
|
Ok(buffer) => buffer,
|
||||||
|
Err(error) => {
|
||||||
|
open_errors.push((path, error.to_string()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(error) => {
|
||||||
|
open_errors.push((path, error.to_string()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let is_dirty = match buffer.read_with(cx, |buffer, _| buffer.is_dirty()) {
|
||||||
|
Ok(is_dirty) => is_dirty,
|
||||||
|
Err(error) => {
|
||||||
|
dirty_check_errors.push((path, error.to_string()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if is_dirty {
|
||||||
|
buffers_to_save.insert(buffer);
|
||||||
|
saved_paths.push(path);
|
||||||
|
} else {
|
||||||
|
clean_paths.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save each buffer individually since there's no batch save API.
|
||||||
|
for buffer in buffers_to_save {
|
||||||
|
let path_for_buffer = match buffer.read_with(cx, |buffer, _| {
|
||||||
|
buffer
|
||||||
|
.file()
|
||||||
|
.map(|file| file.path().to_rel_path_buf())
|
||||||
|
.map(|path| path.as_rel_path().as_unix_str().to_owned())
|
||||||
|
}) {
|
||||||
|
Ok(path) => path.unwrap_or_else(|| "<unknown>".to_string()),
|
||||||
|
Err(error) => {
|
||||||
|
save_errors.push(("<unknown>".to_string(), error.to_string()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let save_task = project.update(cx, |project, cx| project.save_buffer(buffer, cx));
|
||||||
|
|
||||||
|
match save_task {
|
||||||
|
Ok(task) => {
|
||||||
|
if let Err(error) = task.await {
|
||||||
|
save_errors.push((path_for_buffer, error.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
save_errors.push((path_for_buffer, error.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut lines: Vec<String> = Vec::new();
|
||||||
|
|
||||||
|
if !saved_paths.is_empty() {
|
||||||
|
lines.push(format!("Saved {} file(s).", saved_paths.len()));
|
||||||
|
}
|
||||||
|
if !clean_paths.is_empty() {
|
||||||
|
lines.push(format!("{} clean.", clean_paths.len()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !not_found_paths.is_empty() {
|
||||||
|
lines.push(format!("Not found ({}):", not_found_paths.len()));
|
||||||
|
for path in ¬_found_paths {
|
||||||
|
lines.push(format!("- {}", path.display()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !open_errors.is_empty() {
|
||||||
|
lines.push(format!("Open failed ({}):", open_errors.len()));
|
||||||
|
for (path, error) in &open_errors {
|
||||||
|
lines.push(format!("- {}: {}", path.display(), error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !dirty_check_errors.is_empty() {
|
||||||
|
lines.push(format!(
|
||||||
|
"Dirty check failed ({}):",
|
||||||
|
dirty_check_errors.len()
|
||||||
|
));
|
||||||
|
for (path, error) in &dirty_check_errors {
|
||||||
|
lines.push(format!("- {}: {}", path.display(), error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !save_errors.is_empty() {
|
||||||
|
lines.push(format!("Save failed ({}):", save_errors.len()));
|
||||||
|
for (path, error) in &save_errors {
|
||||||
|
lines.push(format!("- {}: {}", path, error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if lines.is_empty() {
|
||||||
|
Ok("No paths provided.".to_string())
|
||||||
|
} else {
|
||||||
|
Ok(lines.join("\n"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use fs::Fs;
|
||||||
|
use gpui::TestAppContext;
|
||||||
|
use project::FakeFs;
|
||||||
|
use serde_json::json;
|
||||||
|
use settings::SettingsStore;
|
||||||
|
use util::path;
|
||||||
|
|
||||||
|
fn init_test(cx: &mut TestAppContext) {
|
||||||
|
cx.update(|cx| {
|
||||||
|
let settings_store = SettingsStore::test(cx);
|
||||||
|
cx.set_global(settings_store);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_save_file_output_and_effects(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
fs.insert_tree(
|
||||||
|
"/root",
|
||||||
|
json!({
|
||||||
|
"dirty.txt": "on disk: dirty\n",
|
||||||
|
"clean.txt": "on disk: clean\n",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||||
|
let tool = Arc::new(SaveFileTool::new(project.clone()));
|
||||||
|
|
||||||
|
// Make dirty.txt dirty in-memory.
|
||||||
|
let dirty_project_path = project.read_with(cx, |project, cx| {
|
||||||
|
project
|
||||||
|
.find_project_path("root/dirty.txt", cx)
|
||||||
|
.expect("dirty.txt should exist in project")
|
||||||
|
});
|
||||||
|
|
||||||
|
let dirty_buffer = project
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
project.open_buffer(dirty_project_path, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
dirty_buffer.update(cx, |buffer, cx| {
|
||||||
|
buffer.edit([(0..buffer.len(), "in memory: dirty\n")], None, cx);
|
||||||
|
});
|
||||||
|
assert!(
|
||||||
|
dirty_buffer.read_with(cx, |buffer, _| buffer.is_dirty()),
|
||||||
|
"dirty.txt buffer should be dirty before save"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ensure clean.txt is opened but remains clean.
|
||||||
|
let clean_project_path = project.read_with(cx, |project, cx| {
|
||||||
|
project
|
||||||
|
.find_project_path("root/clean.txt", cx)
|
||||||
|
.expect("clean.txt should exist in project")
|
||||||
|
});
|
||||||
|
|
||||||
|
let clean_buffer = project
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
project.open_buffer(clean_project_path, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
!clean_buffer.read_with(cx, |buffer, _| buffer.is_dirty()),
|
||||||
|
"clean.txt buffer should start clean"
|
||||||
|
);
|
||||||
|
|
||||||
|
let output = cx
|
||||||
|
.update(|cx| {
|
||||||
|
tool.clone().run(
|
||||||
|
SaveFileToolInput {
|
||||||
|
paths: vec![
|
||||||
|
PathBuf::from("root/dirty.txt"),
|
||||||
|
PathBuf::from("root/clean.txt"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
ToolCallEventStream::test().0,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Output should mention saved + clean.
|
||||||
|
assert!(
|
||||||
|
output.contains("Saved 1 file(s)."),
|
||||||
|
"expected saved count line, got:\n{output}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
output.contains("1 clean."),
|
||||||
|
"expected clean count line, got:\n{output}"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Effect: dirty buffer should now be clean and disk should have new content.
|
||||||
|
assert!(
|
||||||
|
!dirty_buffer.read_with(cx, |buffer, _| buffer.is_dirty()),
|
||||||
|
"dirty.txt buffer should not be dirty after save"
|
||||||
|
);
|
||||||
|
|
||||||
|
let disk_dirty = fs.load(path!("/root/dirty.txt").as_ref()).await.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
disk_dirty, "in memory: dirty\n",
|
||||||
|
"dirty.txt disk content should be updated"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Sanity: clean buffer should remain clean and disk unchanged.
|
||||||
|
let disk_clean = fs.load(path!("/root/clean.txt").as_ref()).await.unwrap();
|
||||||
|
assert_eq!(disk_clean, "on disk: clean\n");
|
||||||
|
|
||||||
|
// Test empty paths case.
|
||||||
|
let output = cx
|
||||||
|
.update(|cx| {
|
||||||
|
tool.clone().run(
|
||||||
|
SaveFileToolInput { paths: vec![] },
|
||||||
|
ToolCallEventStream::test().0,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(output, "No paths provided.");
|
||||||
|
|
||||||
|
// Test not-found path case.
|
||||||
|
let output = cx
|
||||||
|
.update(|cx| {
|
||||||
|
tool.clone().run(
|
||||||
|
SaveFileToolInput {
|
||||||
|
paths: vec![PathBuf::from("nonexistent/path.txt")],
|
||||||
|
},
|
||||||
|
ToolCallEventStream::test().0,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
output.contains("Not found (1):"),
|
||||||
|
"expected not-found header line, got:\n{output}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
output.contains("- nonexistent/path.txt"),
|
||||||
|
"expected not-found path bullet, got:\n{output}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
221
crates/agent/src/tools/spawn_subagent_tool.rs
Normal file
221
crates/agent/src/tools/spawn_subagent_tool.rs
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
use crate::{AgentTool, ToolCallEventStream};
|
||||||
|
use agent_client_protocol as acp;
|
||||||
|
use anyhow::{Result, anyhow};
|
||||||
|
use gpui::{App, AsyncApp, SharedString, Task};
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// Spawn a subagent (child thread) that can be visited while it runs, and returns a value to the parent.
|
||||||
|
///
|
||||||
|
/// Note: This file intentionally defines only the tool surface and streaming updates. The actual
|
||||||
|
/// spawning/navigation plumbing requires a host capability (session manager + UI) that is not yet
|
||||||
|
/// present in the native agent tool environment. Until that capability is wired in, this tool will
|
||||||
|
/// fail with a clear error.
|
||||||
|
///
|
||||||
|
/// Expected design (to be implemented in the host):
|
||||||
|
/// - The tool is constructed with a `SubagentHost` implementation that can:
|
||||||
|
/// - create a child session/thread
|
||||||
|
/// - stream child progress updates
|
||||||
|
/// - complete with a final return value
|
||||||
|
/// - provide a navigable URI for the UI (e.g. `zed://agent/thread/<session_id>`)
|
||||||
|
///
|
||||||
|
/// The tool then:
|
||||||
|
/// - emits a `ResourceLink` pointing at the child thread so users can open it
|
||||||
|
/// - streams progress into the tool call card as markdown
|
||||||
|
/// - resolves with the child's final return value (string)
|
||||||
|
#[derive(JsonSchema, Serialize, Deserialize)]
|
||||||
|
pub struct SpawnSubagentToolInput {
|
||||||
|
/// A short label/title for the subagent.
|
||||||
|
pub title: String,
|
||||||
|
|
||||||
|
/// The instructions to run in the subagent.
|
||||||
|
pub prompt: String,
|
||||||
|
|
||||||
|
/// Optional: profile id to use for the subagent.
|
||||||
|
#[serde(default)]
|
||||||
|
pub profile_id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The final return value from the subagent.
|
||||||
|
pub type SpawnSubagentToolOutput = String;
|
||||||
|
|
||||||
|
/// Host interface required to implement spawning + streaming + returning.
|
||||||
|
///
|
||||||
|
/// This is intentionally minimal and object-safe to allow injecting a host backed by `NativeAgent`.
|
||||||
|
pub trait SubagentHost: Send + Sync + 'static {
|
||||||
|
/// Start a child subagent session and return a handle containing a navigable URI plus a stream
|
||||||
|
/// of progress updates and a final result.
|
||||||
|
///
|
||||||
|
/// The returned `SubagentRun` must:
|
||||||
|
/// - yield `Progress` updates in-order
|
||||||
|
/// - eventually yield exactly one `Final` or `Error`
|
||||||
|
fn spawn_subagent(
|
||||||
|
&self,
|
||||||
|
title: String,
|
||||||
|
prompt: String,
|
||||||
|
profile_id: Option<String>,
|
||||||
|
cx: &mut AsyncApp,
|
||||||
|
) -> Task<Result<SubagentRun>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A handle for a running subagent.
|
||||||
|
pub struct SubagentRun {
|
||||||
|
/// URI that the UI can open to navigate to the child thread.
|
||||||
|
pub thread_uri: String,
|
||||||
|
|
||||||
|
/// A human-friendly label for the link.
|
||||||
|
pub thread_label: String,
|
||||||
|
|
||||||
|
/// Progress stream for tool UI updates.
|
||||||
|
pub updates: futures::channel::mpsc::UnboundedReceiver<SubagentUpdate>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum SubagentUpdate {
|
||||||
|
/// A streaming progress chunk (e.g. "thinking…", partial summary, etc).
|
||||||
|
Progress(String),
|
||||||
|
|
||||||
|
/// The final return value for the parent.
|
||||||
|
Final(String),
|
||||||
|
|
||||||
|
/// Terminal error.
|
||||||
|
Error(anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SpawnSubagentTool {
|
||||||
|
host: Option<Arc<dyn SubagentHost>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SpawnSubagentTool {
|
||||||
|
pub fn new(host: Option<Arc<dyn SubagentHost>>) -> Self {
|
||||||
|
Self { host }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AgentTool for SpawnSubagentTool {
|
||||||
|
type Input = SpawnSubagentToolInput;
|
||||||
|
type Output = SpawnSubagentToolOutput;
|
||||||
|
|
||||||
|
fn name() -> &'static str {
|
||||||
|
"spawn_subagent"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kind() -> acp::ToolKind {
|
||||||
|
acp::ToolKind::Other
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description() -> SharedString {
|
||||||
|
"Spawns a child Zed Agent thread (subagent), streams its progress, and returns its final value to the parent."
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initial_title(
|
||||||
|
&self,
|
||||||
|
input: Result<Self::Input, serde_json::Value>,
|
||||||
|
_cx: &mut App,
|
||||||
|
) -> SharedString {
|
||||||
|
if let Ok(input) = input {
|
||||||
|
format!("Spawn subagent: {}", input.title).into()
|
||||||
|
} else {
|
||||||
|
"Spawn subagent".into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(
|
||||||
|
self: Arc<Self>,
|
||||||
|
input: Self::Input,
|
||||||
|
event_stream: ToolCallEventStream,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Task<Result<Self::Output>> {
|
||||||
|
let Some(host) = self.host.clone() else {
|
||||||
|
return Task::ready(Err(anyhow!(
|
||||||
|
"spawn_subagent is not available: native agent host capability is not wired into tools yet"
|
||||||
|
)));
|
||||||
|
};
|
||||||
|
|
||||||
|
let title = input.title;
|
||||||
|
let prompt = input.prompt;
|
||||||
|
let profile_id = input.profile_id;
|
||||||
|
|
||||||
|
cx.spawn(async move |cx| {
|
||||||
|
// Start the child run via host.
|
||||||
|
let mut run = host
|
||||||
|
.spawn_subagent(title.clone(), prompt, profile_id, cx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Emit a link to the child thread so the user can open/visit it.
|
||||||
|
event_stream.update_fields(
|
||||||
|
acp::ToolCallUpdateFields::new().content(vec![acp::ToolCallContent::Content(
|
||||||
|
acp::Content::new(acp::ContentBlock::ResourceLink(
|
||||||
|
acp::ResourceLink::new(run.thread_label.clone(), run.thread_uri.clone())
|
||||||
|
.title(run.thread_label.clone()),
|
||||||
|
)),
|
||||||
|
)]),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Stream progress as markdown appended below the link.
|
||||||
|
let mut accumulated_progress = String::new();
|
||||||
|
while let Some(update) = run.updates.next().await {
|
||||||
|
match update {
|
||||||
|
SubagentUpdate::Progress(chunk) => {
|
||||||
|
if !accumulated_progress.is_empty() {
|
||||||
|
accumulated_progress.push('\n');
|
||||||
|
}
|
||||||
|
accumulated_progress.push_str(&chunk);
|
||||||
|
|
||||||
|
event_stream.update_fields(
|
||||||
|
acp::ToolCallUpdateFields::new().content(vec![
|
||||||
|
acp::ToolCallContent::Content(acp::Content::new(
|
||||||
|
acp::ContentBlock::ResourceLink(
|
||||||
|
acp::ResourceLink::new(
|
||||||
|
run.thread_label.clone(),
|
||||||
|
run.thread_uri.clone(),
|
||||||
|
)
|
||||||
|
.title(run.thread_label.clone()),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
acp::ToolCallContent::Content(acp::Content::new(
|
||||||
|
acp::ContentBlock::Text(acp::TextContent::new(
|
||||||
|
format!("### Subagent progress\n\n{}", accumulated_progress),
|
||||||
|
)),
|
||||||
|
)),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
SubagentUpdate::Final(value) => {
|
||||||
|
// Final update for UI (optional).
|
||||||
|
event_stream.update_fields(
|
||||||
|
acp::ToolCallUpdateFields::new().content(vec![
|
||||||
|
acp::ToolCallContent::Content(acp::Content::new(
|
||||||
|
acp::ContentBlock::ResourceLink(
|
||||||
|
acp::ResourceLink::new(
|
||||||
|
run.thread_label.clone(),
|
||||||
|
run.thread_uri.clone(),
|
||||||
|
)
|
||||||
|
.title(run.thread_label.clone()),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
acp::ToolCallContent::Content(acp::Content::new(
|
||||||
|
acp::ContentBlock::Text(acp::TextContent::new(format!(
|
||||||
|
"### Subagent returned\n\n{}",
|
||||||
|
value
|
||||||
|
))),
|
||||||
|
)),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Ok(value);
|
||||||
|
}
|
||||||
|
SubagentUpdate::Error(error) => {
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(anyhow!("subagent stream ended without producing a final value"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// futures::StreamExt is only needed in the async run implementation; keep it scoped here.
|
||||||
|
use futures::StreamExt as _;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
use agent_client_protocol as acp;
|
use agent_client_protocol as acp;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use gpui::{App, Entity, SharedString, Task};
|
use futures::FutureExt as _;
|
||||||
|
use gpui::{App, AppContext, Entity, SharedString, Task};
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -8,6 +9,7 @@ use std::{
|
|||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
|
time::Duration,
|
||||||
};
|
};
|
||||||
use util::markdown::MarkdownInlineCode;
|
use util::markdown::MarkdownInlineCode;
|
||||||
|
|
||||||
@@ -25,13 +27,17 @@ const COMMAND_OUTPUT_LIMIT: u64 = 16 * 1024;
|
|||||||
///
|
///
|
||||||
/// Do not use this tool for commands that run indefinitely, such as servers (like `npm run start`, `npm run dev`, `python -m http.server`, etc) or file watchers that don't terminate on their own.
|
/// Do not use this tool for commands that run indefinitely, such as servers (like `npm run start`, `npm run dev`, `python -m http.server`, etc) or file watchers that don't terminate on their own.
|
||||||
///
|
///
|
||||||
|
/// For potentially long-running commands, prefer specifying `timeout_ms` to bound runtime and prevent indefinite hangs.
|
||||||
|
///
|
||||||
/// Remember that each invocation of this tool will spawn a new shell process, so you can't rely on any state from previous invocations.
|
/// Remember that each invocation of this tool will spawn a new shell process, so you can't rely on any state from previous invocations.
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||||
pub struct TerminalToolInput {
|
pub struct TerminalToolInput {
|
||||||
/// The one-liner command to execute.
|
/// The one-liner command to execute.
|
||||||
command: String,
|
pub command: String,
|
||||||
/// Working directory for the command. This must be one of the root directories of the project.
|
/// Working directory for the command. This must be one of the root directories of the project.
|
||||||
cd: String,
|
pub cd: String,
|
||||||
|
/// Optional maximum runtime (in milliseconds). If exceeded, the running terminal task is killed.
|
||||||
|
pub timeout_ms: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TerminalTool {
|
pub struct TerminalTool {
|
||||||
@@ -116,7 +122,26 @@ impl AgentTool for TerminalTool {
|
|||||||
acp::ToolCallContent::Terminal(acp::Terminal::new(terminal_id)),
|
acp::ToolCallContent::Terminal(acp::Terminal::new(terminal_id)),
|
||||||
]));
|
]));
|
||||||
|
|
||||||
let exit_status = terminal.wait_for_exit(cx)?.await;
|
let timeout = input.timeout_ms.map(Duration::from_millis);
|
||||||
|
|
||||||
|
let exit_status = match timeout {
|
||||||
|
Some(timeout) => {
|
||||||
|
let wait_for_exit = terminal.wait_for_exit(cx)?;
|
||||||
|
let timeout_task = cx.background_spawn(async move {
|
||||||
|
smol::Timer::after(timeout).await;
|
||||||
|
});
|
||||||
|
|
||||||
|
futures::select! {
|
||||||
|
status = wait_for_exit.clone().fuse() => status,
|
||||||
|
_ = timeout_task.fuse() => {
|
||||||
|
terminal.kill(cx)?;
|
||||||
|
wait_for_exit.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => terminal.wait_for_exit(cx)?.await,
|
||||||
|
};
|
||||||
|
|
||||||
let output = terminal.current_output(cx)?;
|
let output = terminal.current_output(cx)?;
|
||||||
|
|
||||||
Ok(process_content(output, &input.command, exit_status))
|
Ok(process_content(output, &input.command, exit_status))
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ use futures::io::BufReader;
|
|||||||
use project::Project;
|
use project::Project;
|
||||||
use project::agent_server_store::AgentServerCommand;
|
use project::agent_server_store::AgentServerCommand;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use settings::Settings as _;
|
||||||
|
use task::ShellBuilder;
|
||||||
use util::ResultExt as _;
|
use util::ResultExt as _;
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@@ -21,7 +23,7 @@ use gpui::{App, AppContext as _, AsyncApp, Entity, SharedString, Task, WeakEntit
|
|||||||
|
|
||||||
use acp_thread::{AcpThread, AuthRequired, LoadError, TerminalProviderEvent};
|
use acp_thread::{AcpThread, AuthRequired, LoadError, TerminalProviderEvent};
|
||||||
use terminal::TerminalBuilder;
|
use terminal::TerminalBuilder;
|
||||||
use terminal::terminal_settings::{AlternateScroll, CursorShape};
|
use terminal::terminal_settings::{AlternateScroll, CursorShape, TerminalSettings};
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
#[error("Unsupported version")]
|
#[error("Unsupported version")]
|
||||||
@@ -29,7 +31,7 @@ pub struct UnsupportedVersion;
|
|||||||
|
|
||||||
pub struct AcpConnection {
|
pub struct AcpConnection {
|
||||||
server_name: SharedString,
|
server_name: SharedString,
|
||||||
telemetry_id: &'static str,
|
telemetry_id: SharedString,
|
||||||
connection: Rc<acp::ClientSideConnection>,
|
connection: Rc<acp::ClientSideConnection>,
|
||||||
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
|
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
|
||||||
auth_methods: Vec<acp::AuthMethod>,
|
auth_methods: Vec<acp::AuthMethod>,
|
||||||
@@ -54,7 +56,6 @@ pub struct AcpSession {
|
|||||||
|
|
||||||
pub async fn connect(
|
pub async fn connect(
|
||||||
server_name: SharedString,
|
server_name: SharedString,
|
||||||
telemetry_id: &'static str,
|
|
||||||
command: AgentServerCommand,
|
command: AgentServerCommand,
|
||||||
root_dir: &Path,
|
root_dir: &Path,
|
||||||
default_mode: Option<acp::SessionModeId>,
|
default_mode: Option<acp::SessionModeId>,
|
||||||
@@ -64,7 +65,6 @@ pub async fn connect(
|
|||||||
) -> Result<Rc<dyn AgentConnection>> {
|
) -> Result<Rc<dyn AgentConnection>> {
|
||||||
let conn = AcpConnection::stdio(
|
let conn = AcpConnection::stdio(
|
||||||
server_name,
|
server_name,
|
||||||
telemetry_id,
|
|
||||||
command.clone(),
|
command.clone(),
|
||||||
root_dir,
|
root_dir,
|
||||||
default_mode,
|
default_mode,
|
||||||
@@ -81,7 +81,6 @@ const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::ProtocolVersion::V1
|
|||||||
impl AcpConnection {
|
impl AcpConnection {
|
||||||
pub async fn stdio(
|
pub async fn stdio(
|
||||||
server_name: SharedString,
|
server_name: SharedString,
|
||||||
telemetry_id: &'static str,
|
|
||||||
command: AgentServerCommand,
|
command: AgentServerCommand,
|
||||||
root_dir: &Path,
|
root_dir: &Path,
|
||||||
default_mode: Option<acp::SessionModeId>,
|
default_mode: Option<acp::SessionModeId>,
|
||||||
@@ -89,9 +88,11 @@ impl AcpConnection {
|
|||||||
is_remote: bool,
|
is_remote: bool,
|
||||||
cx: &mut AsyncApp,
|
cx: &mut AsyncApp,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let mut child = util::command::new_smol_command(&command.path);
|
let shell = cx.update(|cx| TerminalSettings::get(None, cx).shell.clone())?;
|
||||||
|
let builder = ShellBuilder::new(&shell, cfg!(windows)).non_interactive();
|
||||||
|
let mut child =
|
||||||
|
builder.build_command(Some(command.path.display().to_string()), &command.args);
|
||||||
child
|
child
|
||||||
.args(command.args.iter().map(|arg| arg.as_str()))
|
|
||||||
.envs(command.env.iter().flatten())
|
.envs(command.env.iter().flatten())
|
||||||
.stdin(std::process::Stdio::piped())
|
.stdin(std::process::Stdio::piped())
|
||||||
.stdout(std::process::Stdio::piped())
|
.stdout(std::process::Stdio::piped())
|
||||||
@@ -199,6 +200,13 @@ impl AcpConnection {
|
|||||||
return Err(UnsupportedVersion.into());
|
return Err(UnsupportedVersion.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let telemetry_id = response
|
||||||
|
.agent_info
|
||||||
|
// Use the one the agent provides if we have one
|
||||||
|
.map(|info| info.name.into())
|
||||||
|
// Otherwise, just use the name
|
||||||
|
.unwrap_or_else(|| server_name.clone());
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
auth_methods: response.auth_methods,
|
auth_methods: response.auth_methods,
|
||||||
root_dir: root_dir.to_owned(),
|
root_dir: root_dir.to_owned(),
|
||||||
@@ -233,8 +241,8 @@ impl Drop for AcpConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AgentConnection for AcpConnection {
|
impl AgentConnection for AcpConnection {
|
||||||
fn telemetry_id(&self) -> &'static str {
|
fn telemetry_id(&self) -> SharedString {
|
||||||
self.telemetry_id
|
self.telemetry_id.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_thread(
|
fn new_thread(
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ impl AgentServerDelegate {
|
|||||||
pub trait AgentServer: Send {
|
pub trait AgentServer: Send {
|
||||||
fn logo(&self) -> ui::IconName;
|
fn logo(&self) -> ui::IconName;
|
||||||
fn name(&self) -> SharedString;
|
fn name(&self) -> SharedString;
|
||||||
fn telemetry_id(&self) -> &'static str;
|
|
||||||
fn default_mode(&self, _cx: &mut App) -> Option<agent_client_protocol::SessionModeId> {
|
fn default_mode(&self, _cx: &mut App) -> Option<agent_client_protocol::SessionModeId> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,10 +22,6 @@ pub struct AgentServerLoginCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AgentServer for ClaudeCode {
|
impl AgentServer for ClaudeCode {
|
||||||
fn telemetry_id(&self) -> &'static str {
|
|
||||||
"claude-code"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn name(&self) -> SharedString {
|
fn name(&self) -> SharedString {
|
||||||
"Claude Code".into()
|
"Claude Code".into()
|
||||||
}
|
}
|
||||||
@@ -83,7 +79,6 @@ impl AgentServer for ClaudeCode {
|
|||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
|
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
|
||||||
let name = self.name();
|
let name = self.name();
|
||||||
let telemetry_id = self.telemetry_id();
|
|
||||||
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned());
|
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned());
|
||||||
let is_remote = delegate.project.read(cx).is_via_remote_server();
|
let is_remote = delegate.project.read(cx).is_via_remote_server();
|
||||||
let store = delegate.store.downgrade();
|
let store = delegate.store.downgrade();
|
||||||
@@ -108,7 +103,6 @@ impl AgentServer for ClaudeCode {
|
|||||||
.await?;
|
.await?;
|
||||||
let connection = crate::acp::connect(
|
let connection = crate::acp::connect(
|
||||||
name,
|
name,
|
||||||
telemetry_id,
|
|
||||||
command,
|
command,
|
||||||
root_dir.as_ref(),
|
root_dir.as_ref(),
|
||||||
default_mode,
|
default_mode,
|
||||||
|
|||||||
@@ -23,10 +23,6 @@ pub(crate) mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AgentServer for Codex {
|
impl AgentServer for Codex {
|
||||||
fn telemetry_id(&self) -> &'static str {
|
|
||||||
"codex"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn name(&self) -> SharedString {
|
fn name(&self) -> SharedString {
|
||||||
"Codex".into()
|
"Codex".into()
|
||||||
}
|
}
|
||||||
@@ -84,7 +80,6 @@ impl AgentServer for Codex {
|
|||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
|
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
|
||||||
let name = self.name();
|
let name = self.name();
|
||||||
let telemetry_id = self.telemetry_id();
|
|
||||||
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned());
|
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned());
|
||||||
let is_remote = delegate.project.read(cx).is_via_remote_server();
|
let is_remote = delegate.project.read(cx).is_via_remote_server();
|
||||||
let store = delegate.store.downgrade();
|
let store = delegate.store.downgrade();
|
||||||
@@ -110,7 +105,6 @@ impl AgentServer for Codex {
|
|||||||
|
|
||||||
let connection = crate::acp::connect(
|
let connection = crate::acp::connect(
|
||||||
name,
|
name,
|
||||||
telemetry_id,
|
|
||||||
command,
|
command,
|
||||||
root_dir.as_ref(),
|
root_dir.as_ref(),
|
||||||
default_mode,
|
default_mode,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::{AgentServerDelegate, load_proxy_env};
|
use crate::{AgentServer, AgentServerDelegate, load_proxy_env};
|
||||||
use acp_thread::AgentConnection;
|
use acp_thread::AgentConnection;
|
||||||
use agent_client_protocol as acp;
|
use agent_client_protocol as acp;
|
||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result};
|
||||||
@@ -20,11 +20,7 @@ impl CustomAgentServer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl crate::AgentServer for CustomAgentServer {
|
impl AgentServer for CustomAgentServer {
|
||||||
fn telemetry_id(&self) -> &'static str {
|
|
||||||
"custom"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn name(&self) -> SharedString {
|
fn name(&self) -> SharedString {
|
||||||
self.name.clone()
|
self.name.clone()
|
||||||
}
|
}
|
||||||
@@ -112,14 +108,12 @@ impl crate::AgentServer for CustomAgentServer {
|
|||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
|
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
|
||||||
let name = self.name();
|
let name = self.name();
|
||||||
let telemetry_id = self.telemetry_id();
|
|
||||||
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned());
|
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned());
|
||||||
let is_remote = delegate.project.read(cx).is_via_remote_server();
|
let is_remote = delegate.project.read(cx).is_via_remote_server();
|
||||||
let default_mode = self.default_mode(cx);
|
let default_mode = self.default_mode(cx);
|
||||||
let default_model = self.default_model(cx);
|
let default_model = self.default_model(cx);
|
||||||
let store = delegate.store.downgrade();
|
let store = delegate.store.downgrade();
|
||||||
let extra_env = load_proxy_env(cx);
|
let extra_env = load_proxy_env(cx);
|
||||||
|
|
||||||
cx.spawn(async move |cx| {
|
cx.spawn(async move |cx| {
|
||||||
let (command, root_dir, login) = store
|
let (command, root_dir, login) = store
|
||||||
.update(cx, |store, cx| {
|
.update(cx, |store, cx| {
|
||||||
@@ -139,7 +133,6 @@ impl crate::AgentServer for CustomAgentServer {
|
|||||||
.await?;
|
.await?;
|
||||||
let connection = crate::acp::connect(
|
let connection = crate::acp::connect(
|
||||||
name,
|
name,
|
||||||
telemetry_id,
|
|
||||||
command,
|
command,
|
||||||
root_dir.as_ref(),
|
root_dir.as_ref(),
|
||||||
default_mode,
|
default_mode,
|
||||||
|
|||||||
@@ -12,10 +12,6 @@ use project::agent_server_store::GEMINI_NAME;
|
|||||||
pub struct Gemini;
|
pub struct Gemini;
|
||||||
|
|
||||||
impl AgentServer for Gemini {
|
impl AgentServer for Gemini {
|
||||||
fn telemetry_id(&self) -> &'static str {
|
|
||||||
"gemini-cli"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn name(&self) -> SharedString {
|
fn name(&self) -> SharedString {
|
||||||
"Gemini CLI".into()
|
"Gemini CLI".into()
|
||||||
}
|
}
|
||||||
@@ -31,7 +27,6 @@ impl AgentServer for Gemini {
|
|||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
|
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
|
||||||
let name = self.name();
|
let name = self.name();
|
||||||
let telemetry_id = self.telemetry_id();
|
|
||||||
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned());
|
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned());
|
||||||
let is_remote = delegate.project.read(cx).is_via_remote_server();
|
let is_remote = delegate.project.read(cx).is_via_remote_server();
|
||||||
let store = delegate.store.downgrade();
|
let store = delegate.store.downgrade();
|
||||||
@@ -66,7 +61,6 @@ impl AgentServer for Gemini {
|
|||||||
|
|
||||||
let connection = crate::acp::connect(
|
let connection = crate::acp::connect(
|
||||||
name,
|
name,
|
||||||
telemetry_id,
|
|
||||||
command,
|
command,
|
||||||
root_dir.as_ref(),
|
root_dir.as_ref(),
|
||||||
default_mode,
|
default_mode,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use project::DisableAiSettings;
|
|||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::{
|
use settings::{
|
||||||
DefaultAgentView, DockPosition, LanguageModelParameters, LanguageModelSelection,
|
DefaultAgentView, DockPosition, DockSide, LanguageModelParameters, LanguageModelSelection,
|
||||||
NotifyWhenAgentWaiting, RegisterSetting, Settings,
|
NotifyWhenAgentWaiting, RegisterSetting, Settings,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -24,10 +24,12 @@ pub struct AgentSettings {
|
|||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
pub button: bool,
|
pub button: bool,
|
||||||
pub dock: DockPosition,
|
pub dock: DockPosition,
|
||||||
|
pub agents_panel_dock: DockSide,
|
||||||
pub default_width: Pixels,
|
pub default_width: Pixels,
|
||||||
pub default_height: Pixels,
|
pub default_height: Pixels,
|
||||||
pub default_model: Option<LanguageModelSelection>,
|
pub default_model: Option<LanguageModelSelection>,
|
||||||
pub inline_assistant_model: Option<LanguageModelSelection>,
|
pub inline_assistant_model: Option<LanguageModelSelection>,
|
||||||
|
pub inline_assistant_use_streaming_tools: bool,
|
||||||
pub commit_message_model: Option<LanguageModelSelection>,
|
pub commit_message_model: Option<LanguageModelSelection>,
|
||||||
pub thread_summary_model: Option<LanguageModelSelection>,
|
pub thread_summary_model: Option<LanguageModelSelection>,
|
||||||
pub inline_alternatives: Vec<LanguageModelSelection>,
|
pub inline_alternatives: Vec<LanguageModelSelection>,
|
||||||
@@ -151,10 +153,14 @@ impl Settings for AgentSettings {
|
|||||||
enabled: agent.enabled.unwrap(),
|
enabled: agent.enabled.unwrap(),
|
||||||
button: agent.button.unwrap(),
|
button: agent.button.unwrap(),
|
||||||
dock: agent.dock.unwrap(),
|
dock: agent.dock.unwrap(),
|
||||||
|
agents_panel_dock: agent.agents_panel_dock.unwrap(),
|
||||||
default_width: px(agent.default_width.unwrap()),
|
default_width: px(agent.default_width.unwrap()),
|
||||||
default_height: px(agent.default_height.unwrap()),
|
default_height: px(agent.default_height.unwrap()),
|
||||||
default_model: Some(agent.default_model.unwrap()),
|
default_model: Some(agent.default_model.unwrap()),
|
||||||
inline_assistant_model: agent.inline_assistant_model,
|
inline_assistant_model: agent.inline_assistant_model,
|
||||||
|
inline_assistant_use_streaming_tools: agent
|
||||||
|
.inline_assistant_use_streaming_tools
|
||||||
|
.unwrap_or(true),
|
||||||
commit_message_model: agent.commit_message_model,
|
commit_message_model: agent.commit_message_model,
|
||||||
thread_summary_model: agent.thread_summary_model,
|
thread_summary_model: agent.thread_summary_model,
|
||||||
inline_alternatives: agent.inline_alternatives.unwrap_or_default(),
|
inline_alternatives: agent.inline_alternatives.unwrap_or_default(),
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ path = "src/agent_ui.rs"
|
|||||||
doctest = false
|
doctest = false
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
test-support = ["gpui/test-support", "language/test-support", "reqwest_client"]
|
test-support = ["assistant_text_thread/test-support", "eval_utils", "gpui/test-support", "language/test-support", "reqwest_client", "workspace/test-support"]
|
||||||
unit-eval = []
|
unit-eval = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
@@ -40,6 +40,7 @@ component.workspace = true
|
|||||||
context_server.workspace = true
|
context_server.workspace = true
|
||||||
db.workspace = true
|
db.workspace = true
|
||||||
editor.workspace = true
|
editor.workspace = true
|
||||||
|
eval_utils = { workspace = true, optional = true }
|
||||||
extension.workspace = true
|
extension.workspace = true
|
||||||
extension_host.workspace = true
|
extension_host.workspace = true
|
||||||
feature_flags.workspace = true
|
feature_flags.workspace = true
|
||||||
@@ -71,6 +72,7 @@ postage.workspace = true
|
|||||||
project.workspace = true
|
project.workspace = true
|
||||||
prompt_store.workspace = true
|
prompt_store.workspace = true
|
||||||
proto.workspace = true
|
proto.workspace = true
|
||||||
|
rand.workspace = true
|
||||||
release_channel.workspace = true
|
release_channel.workspace = true
|
||||||
rope.workspace = true
|
rope.workspace = true
|
||||||
rules_library.workspace = true
|
rules_library.workspace = true
|
||||||
@@ -84,7 +86,6 @@ smol.workspace = true
|
|||||||
streaming_diff.workspace = true
|
streaming_diff.workspace = true
|
||||||
task.workspace = true
|
task.workspace = true
|
||||||
telemetry.workspace = true
|
telemetry.workspace = true
|
||||||
telemetry_events.workspace = true
|
|
||||||
terminal.workspace = true
|
terminal.workspace = true
|
||||||
terminal_view.workspace = true
|
terminal_view.workspace = true
|
||||||
text.workspace = true
|
text.workspace = true
|
||||||
@@ -95,6 +96,7 @@ ui.workspace = true
|
|||||||
ui_input.workspace = true
|
ui_input.workspace = true
|
||||||
url.workspace = true
|
url.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
|
uuid.workspace = true
|
||||||
watch.workspace = true
|
watch.workspace = true
|
||||||
workspace.workspace = true
|
workspace.workspace = true
|
||||||
zed_actions.workspace = true
|
zed_actions.workspace = true
|
||||||
@@ -119,7 +121,6 @@ language_model = { workspace = true, "features" = ["test-support"] }
|
|||||||
pretty_assertions.workspace = true
|
pretty_assertions.workspace = true
|
||||||
project = { workspace = true, features = ["test-support"] }
|
project = { workspace = true, features = ["test-support"] }
|
||||||
semver.workspace = true
|
semver.workspace = true
|
||||||
rand.workspace = true
|
|
||||||
reqwest_client.workspace = true
|
reqwest_client.workspace = true
|
||||||
tree-sitter-md.workspace = true
|
tree-sitter-md.workspace = true
|
||||||
unindent.workspace = true
|
unindent.workspace = true
|
||||||
|
|||||||
@@ -565,7 +565,33 @@ impl MessageEditor {
|
|||||||
if let Some((workspace, selections)) =
|
if let Some((workspace, selections)) =
|
||||||
self.workspace.upgrade().zip(editor_clipboard_selections)
|
self.workspace.upgrade().zip(editor_clipboard_selections)
|
||||||
{
|
{
|
||||||
|
let Some(first_selection) = selections.first() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if let Some(file_path) = &first_selection.file_path {
|
||||||
|
// In case someone pastes selections from another window
|
||||||
|
// with a different project, we don't want to insert the
|
||||||
|
// crease (containing the absolute path) since the agent
|
||||||
|
// cannot access files outside the project.
|
||||||
|
let is_in_project = workspace
|
||||||
|
.read(cx)
|
||||||
|
.project()
|
||||||
|
.read(cx)
|
||||||
|
.project_path_for_absolute_path(file_path, cx)
|
||||||
|
.is_some();
|
||||||
|
if !is_in_project {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cx.stop_propagation();
|
cx.stop_propagation();
|
||||||
|
let insertion_target = self
|
||||||
|
.editor
|
||||||
|
.read(cx)
|
||||||
|
.selections
|
||||||
|
.newest_anchor()
|
||||||
|
.start
|
||||||
|
.text_anchor;
|
||||||
|
|
||||||
let project = workspace.read(cx).project().clone();
|
let project = workspace.read(cx).project().clone();
|
||||||
for selection in selections {
|
for selection in selections {
|
||||||
@@ -587,8 +613,7 @@ impl MessageEditor {
|
|||||||
let snapshot = buffer.snapshot(cx);
|
let snapshot = buffer.snapshot(cx);
|
||||||
let (excerpt_id, _, buffer_snapshot) =
|
let (excerpt_id, _, buffer_snapshot) =
|
||||||
snapshot.as_singleton().unwrap();
|
snapshot.as_singleton().unwrap();
|
||||||
let start_offset = buffer_snapshot.len();
|
let text_anchor = insertion_target.bias_left(&buffer_snapshot);
|
||||||
let text_anchor = buffer_snapshot.anchor_before(start_offset);
|
|
||||||
|
|
||||||
editor.insert(&mention_text, window, cx);
|
editor.insert(&mention_text, window, cx);
|
||||||
editor.insert(" ", window, cx);
|
editor.insert(" ", window, cx);
|
||||||
|
|||||||
@@ -12,14 +12,11 @@ use gpui::{
|
|||||||
};
|
};
|
||||||
use ordered_float::OrderedFloat;
|
use ordered_float::OrderedFloat;
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
use ui::{
|
use ui::{DocumentationAside, DocumentationEdge, DocumentationSide, prelude::*};
|
||||||
DocumentationAside, DocumentationEdge, DocumentationSide, IntoElement, KeyBinding, ListItem,
|
|
||||||
ListItemSpacing, prelude::*,
|
|
||||||
};
|
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use zed_actions::agent::OpenSettings;
|
use zed_actions::agent::OpenSettings;
|
||||||
|
|
||||||
use crate::ui::HoldForDefault;
|
use crate::ui::{HoldForDefault, ModelSelectorFooter, ModelSelectorHeader, ModelSelectorListItem};
|
||||||
|
|
||||||
pub type AcpModelSelector = Picker<AcpModelPickerDelegate>;
|
pub type AcpModelSelector = Picker<AcpModelPickerDelegate>;
|
||||||
|
|
||||||
@@ -236,39 +233,19 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
|||||||
fn render_match(
|
fn render_match(
|
||||||
&self,
|
&self,
|
||||||
ix: usize,
|
ix: usize,
|
||||||
selected: bool,
|
is_focused: bool,
|
||||||
_: &mut Window,
|
_: &mut Window,
|
||||||
cx: &mut Context<Picker<Self>>,
|
cx: &mut Context<Picker<Self>>,
|
||||||
) -> Option<Self::ListItem> {
|
) -> Option<Self::ListItem> {
|
||||||
match self.filtered_entries.get(ix)? {
|
match self.filtered_entries.get(ix)? {
|
||||||
AcpModelPickerEntry::Separator(title) => Some(
|
AcpModelPickerEntry::Separator(title) => {
|
||||||
div()
|
Some(ModelSelectorHeader::new(title, ix > 1).into_any_element())
|
||||||
.px_2()
|
}
|
||||||
.pb_1()
|
|
||||||
.when(ix > 1, |this| {
|
|
||||||
this.mt_1()
|
|
||||||
.pt_2()
|
|
||||||
.border_t_1()
|
|
||||||
.border_color(cx.theme().colors().border_variant)
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
Label::new(title)
|
|
||||||
.size(LabelSize::XSmall)
|
|
||||||
.color(Color::Muted),
|
|
||||||
)
|
|
||||||
.into_any_element(),
|
|
||||||
),
|
|
||||||
AcpModelPickerEntry::Model(model_info) => {
|
AcpModelPickerEntry::Model(model_info) => {
|
||||||
let is_selected = Some(model_info) == self.selected_model.as_ref();
|
let is_selected = Some(model_info) == self.selected_model.as_ref();
|
||||||
let default_model = self.agent_server.default_model(cx);
|
let default_model = self.agent_server.default_model(cx);
|
||||||
let is_default = default_model.as_ref() == Some(&model_info.id);
|
let is_default = default_model.as_ref() == Some(&model_info.id);
|
||||||
|
|
||||||
let model_icon_color = if is_selected {
|
|
||||||
Color::Accent
|
|
||||||
} else {
|
|
||||||
Color::Muted
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(
|
Some(
|
||||||
div()
|
div()
|
||||||
.id(("model-picker-menu-child", ix))
|
.id(("model-picker-menu-child", ix))
|
||||||
@@ -284,30 +261,10 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
|||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
ListItem::new(ix)
|
ModelSelectorListItem::new(ix, model_info.name.clone())
|
||||||
.inset(true)
|
.is_focused(is_focused)
|
||||||
.spacing(ListItemSpacing::Sparse)
|
.is_selected(is_selected)
|
||||||
.toggle_state(selected)
|
.when_some(model_info.icon, |this, icon| this.icon(icon)),
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.w_full()
|
|
||||||
.gap_1p5()
|
|
||||||
.when_some(model_info.icon, |this, icon| {
|
|
||||||
this.child(
|
|
||||||
Icon::new(icon)
|
|
||||||
.color(model_icon_color)
|
|
||||||
.size(IconSize::Small)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.child(Label::new(model_info.name.clone()).truncate()),
|
|
||||||
)
|
|
||||||
.end_slot(div().pr_3().when(is_selected, |this| {
|
|
||||||
this.child(
|
|
||||||
Icon::new(IconName::Check)
|
|
||||||
.color(Color::Accent)
|
|
||||||
.size(IconSize::Small),
|
|
||||||
)
|
|
||||||
})),
|
|
||||||
)
|
)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
)
|
)
|
||||||
@@ -343,7 +300,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
|||||||
fn render_footer(
|
fn render_footer(
|
||||||
&self,
|
&self,
|
||||||
_window: &mut Window,
|
_window: &mut Window,
|
||||||
cx: &mut Context<Picker<Self>>,
|
_cx: &mut Context<Picker<Self>>,
|
||||||
) -> Option<AnyElement> {
|
) -> Option<AnyElement> {
|
||||||
let focus_handle = self.focus_handle.clone();
|
let focus_handle = self.focus_handle.clone();
|
||||||
|
|
||||||
@@ -351,26 +308,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(
|
Some(ModelSelectorFooter::new(OpenSettings.boxed_clone(), focus_handle).into_any_element())
|
||||||
h_flex()
|
|
||||||
.w_full()
|
|
||||||
.p_1p5()
|
|
||||||
.border_t_1()
|
|
||||||
.border_color(cx.theme().colors().border_variant)
|
|
||||||
.child(
|
|
||||||
Button::new("configure", "Configure")
|
|
||||||
.full_width()
|
|
||||||
.style(ButtonStyle::Outlined)
|
|
||||||
.key_binding(
|
|
||||||
KeyBinding::for_action_in(&OpenSettings, &focus_handle, cx)
|
|
||||||
.map(|kb| kb.size(rems_from_px(12.))),
|
|
||||||
)
|
|
||||||
.on_click(|_, window, cx| {
|
|
||||||
window.dispatch_action(OpenSettings.boxed_clone(), cx);
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.into_any(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,9 +341,7 @@ async fn fuzzy_search(
|
|||||||
let candidates = model_list
|
let candidates = model_list
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(ix, model)| {
|
.map(|(ix, model)| StringMatchCandidate::new(ix, model.name.as_ref()))
|
||||||
StringMatchCandidate::new(ix, &format!("{}/{}", model.id, model.name))
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let mut matches = match_strings(
|
let mut matches = match_strings(
|
||||||
&candidates,
|
&candidates,
|
||||||
|
|||||||
@@ -63,10 +63,7 @@ use crate::acp::message_editor::{MessageEditor, MessageEditorEvent};
|
|||||||
use crate::agent_diff::AgentDiff;
|
use crate::agent_diff::AgentDiff;
|
||||||
use crate::profile_selector::{ProfileProvider, ProfileSelector};
|
use crate::profile_selector::{ProfileProvider, ProfileSelector};
|
||||||
|
|
||||||
use crate::ui::{
|
use crate::ui::{AgentNotification, AgentNotificationEvent, BurnModeTooltip, UsageCallout};
|
||||||
AgentNotification, AgentNotificationEvent, BurnModeTooltip, UnavailableEditingTooltip,
|
|
||||||
UsageCallout,
|
|
||||||
};
|
|
||||||
use crate::{
|
use crate::{
|
||||||
AgentDiffPane, AgentPanel, AllowAlways, AllowOnce, ContinueThread, ContinueWithBurnMode,
|
AgentDiffPane, AgentPanel, AllowAlways, AllowOnce, ContinueThread, ContinueWithBurnMode,
|
||||||
CycleModeSelector, ExpandMessageEditor, Follow, KeepAll, NewThread, OpenAgentDiff, OpenHistory,
|
CycleModeSelector, ExpandMessageEditor, Follow, KeepAll, NewThread, OpenAgentDiff, OpenHistory,
|
||||||
@@ -170,7 +167,7 @@ impl ThreadFeedbackState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let session_id = thread.read(cx).session_id().clone();
|
let session_id = thread.read(cx).session_id().clone();
|
||||||
let agent = thread.read(cx).connection().telemetry_id();
|
let agent_telemetry_id = thread.read(cx).connection().telemetry_id();
|
||||||
let task = telemetry.thread_data(&session_id, cx);
|
let task = telemetry.thread_data(&session_id, cx);
|
||||||
let rating = match feedback {
|
let rating = match feedback {
|
||||||
ThreadFeedback::Positive => "positive",
|
ThreadFeedback::Positive => "positive",
|
||||||
@@ -180,7 +177,7 @@ impl ThreadFeedbackState {
|
|||||||
let thread = task.await?;
|
let thread = task.await?;
|
||||||
telemetry::event!(
|
telemetry::event!(
|
||||||
"Agent Thread Rated",
|
"Agent Thread Rated",
|
||||||
agent = agent,
|
agent = agent_telemetry_id,
|
||||||
session_id = session_id,
|
session_id = session_id,
|
||||||
rating = rating,
|
rating = rating,
|
||||||
thread = thread
|
thread = thread
|
||||||
@@ -207,13 +204,13 @@ impl ThreadFeedbackState {
|
|||||||
self.comments_editor.take();
|
self.comments_editor.take();
|
||||||
|
|
||||||
let session_id = thread.read(cx).session_id().clone();
|
let session_id = thread.read(cx).session_id().clone();
|
||||||
let agent = thread.read(cx).connection().telemetry_id();
|
let agent_telemetry_id = thread.read(cx).connection().telemetry_id();
|
||||||
let task = telemetry.thread_data(&session_id, cx);
|
let task = telemetry.thread_data(&session_id, cx);
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
let thread = task.await?;
|
let thread = task.await?;
|
||||||
telemetry::event!(
|
telemetry::event!(
|
||||||
"Agent Thread Feedback Comments",
|
"Agent Thread Feedback Comments",
|
||||||
agent = agent,
|
agent = agent_telemetry_id,
|
||||||
session_id = session_id,
|
session_id = session_id,
|
||||||
comments = comments,
|
comments = comments,
|
||||||
thread = thread
|
thread = thread
|
||||||
@@ -333,6 +330,7 @@ impl AcpThreadView {
|
|||||||
project: Entity<Project>,
|
project: Entity<Project>,
|
||||||
history_store: Entity<HistoryStore>,
|
history_store: Entity<HistoryStore>,
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
prompt_store: Option<Entity<PromptStore>>,
|
||||||
|
track_load_event: bool,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
@@ -391,8 +389,20 @@ impl AcpThreadView {
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
let show_codex_windows_warning = crate::ExternalAgent::parse_built_in(agent.as_ref())
|
cx.on_release(|this, cx| {
|
||||||
== Some(crate::ExternalAgent::Codex);
|
for window in this.notifications.drain(..) {
|
||||||
|
window
|
||||||
|
.update(cx, |_, window, _| {
|
||||||
|
window.remove_window();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
let show_codex_windows_warning = cfg!(windows)
|
||||||
|
&& project.read(cx).is_local()
|
||||||
|
&& agent.clone().downcast::<agent_servers::Codex>().is_some();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
agent: agent.clone(),
|
agent: agent.clone(),
|
||||||
@@ -404,6 +414,7 @@ impl AcpThreadView {
|
|||||||
resume_thread.clone(),
|
resume_thread.clone(),
|
||||||
workspace.clone(),
|
workspace.clone(),
|
||||||
project.clone(),
|
project.clone(),
|
||||||
|
track_load_event,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
),
|
),
|
||||||
@@ -448,6 +459,7 @@ impl AcpThreadView {
|
|||||||
self.resume_thread_metadata.clone(),
|
self.resume_thread_metadata.clone(),
|
||||||
self.workspace.clone(),
|
self.workspace.clone(),
|
||||||
self.project.clone(),
|
self.project.clone(),
|
||||||
|
true,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
@@ -461,6 +473,7 @@ impl AcpThreadView {
|
|||||||
resume_thread: Option<DbThreadMetadata>,
|
resume_thread: Option<DbThreadMetadata>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
project: Entity<Project>,
|
project: Entity<Project>,
|
||||||
|
track_load_event: bool,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> ThreadState {
|
) -> ThreadState {
|
||||||
@@ -519,6 +532,10 @@ impl AcpThreadView {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if track_load_event {
|
||||||
|
telemetry::event!("Agent Thread Started", agent = connection.telemetry_id());
|
||||||
|
}
|
||||||
|
|
||||||
let result = if let Some(native_agent) = connection
|
let result = if let Some(native_agent) = connection
|
||||||
.clone()
|
.clone()
|
||||||
.downcast::<agent::NativeAgentConnection>()
|
.downcast::<agent::NativeAgentConnection>()
|
||||||
@@ -684,7 +701,7 @@ impl AcpThreadView {
|
|||||||
this.new_server_version_available = Some(new_version.into());
|
this.new_server_version_available = Some(new_version.into());
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})
|
})
|
||||||
.log_err();
|
.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -1133,8 +1150,8 @@ impl AcpThreadView {
|
|||||||
let Some(thread) = self.thread() else {
|
let Some(thread) = self.thread() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let agent_telemetry_id = self.agent.telemetry_id();
|
|
||||||
let session_id = thread.read(cx).session_id().clone();
|
let session_id = thread.read(cx).session_id().clone();
|
||||||
|
let agent_telemetry_id = thread.read(cx).connection().telemetry_id();
|
||||||
let thread = thread.downgrade();
|
let thread = thread.downgrade();
|
||||||
if self.should_be_following {
|
if self.should_be_following {
|
||||||
self.workspace
|
self.workspace
|
||||||
@@ -1309,7 +1326,7 @@ impl AcpThreadView {
|
|||||||
})?;
|
})?;
|
||||||
anyhow::Ok(())
|
anyhow::Ok(())
|
||||||
})
|
})
|
||||||
.detach();
|
.detach_and_log_err(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open_edited_buffer(
|
fn open_edited_buffer(
|
||||||
@@ -1512,6 +1529,7 @@ impl AcpThreadView {
|
|||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
let agent_telemetry_id = connection.telemetry_id();
|
||||||
|
|
||||||
// Check for the experimental "terminal-auth" _meta field
|
// Check for the experimental "terminal-auth" _meta field
|
||||||
let auth_method = connection.auth_methods().iter().find(|m| m.id == method);
|
let auth_method = connection.auth_methods().iter().find(|m| m.id == method);
|
||||||
@@ -1579,19 +1597,18 @@ impl AcpThreadView {
|
|||||||
);
|
);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
self.auth_task = Some(cx.spawn_in(window, {
|
self.auth_task = Some(cx.spawn_in(window, {
|
||||||
let agent = self.agent.clone();
|
|
||||||
async move |this, cx| {
|
async move |this, cx| {
|
||||||
let result = authenticate.await;
|
let result = authenticate.await;
|
||||||
|
|
||||||
match &result {
|
match &result {
|
||||||
Ok(_) => telemetry::event!(
|
Ok(_) => telemetry::event!(
|
||||||
"Authenticate Agent Succeeded",
|
"Authenticate Agent Succeeded",
|
||||||
agent = agent.telemetry_id()
|
agent = agent_telemetry_id
|
||||||
),
|
),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
telemetry::event!(
|
telemetry::event!(
|
||||||
"Authenticate Agent Failed",
|
"Authenticate Agent Failed",
|
||||||
agent = agent.telemetry_id(),
|
agent = agent_telemetry_id,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1675,6 +1692,7 @@ impl AcpThreadView {
|
|||||||
None,
|
None,
|
||||||
this.workspace.clone(),
|
this.workspace.clone(),
|
||||||
this.project.clone(),
|
this.project.clone(),
|
||||||
|
true,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
@@ -1730,43 +1748,38 @@ impl AcpThreadView {
|
|||||||
connection.authenticate(method, cx)
|
connection.authenticate(method, cx)
|
||||||
};
|
};
|
||||||
cx.notify();
|
cx.notify();
|
||||||
self.auth_task =
|
self.auth_task = Some(cx.spawn_in(window, {
|
||||||
Some(cx.spawn_in(window, {
|
async move |this, cx| {
|
||||||
let agent = self.agent.clone();
|
let result = authenticate.await;
|
||||||
async move |this, cx| {
|
|
||||||
let result = authenticate.await;
|
|
||||||
|
|
||||||
match &result {
|
match &result {
|
||||||
Ok(_) => telemetry::event!(
|
Ok(_) => telemetry::event!(
|
||||||
"Authenticate Agent Succeeded",
|
"Authenticate Agent Succeeded",
|
||||||
agent = agent.telemetry_id()
|
agent = agent_telemetry_id
|
||||||
),
|
),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
telemetry::event!(
|
telemetry::event!("Authenticate Agent Failed", agent = agent_telemetry_id,)
|
||||||
"Authenticate Agent Failed",
|
|
||||||
agent = agent.telemetry_id(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
|
||||||
if let Err(err) = result {
|
|
||||||
if let ThreadState::Unauthenticated {
|
|
||||||
pending_auth_method,
|
|
||||||
..
|
|
||||||
} = &mut this.thread_state
|
|
||||||
{
|
|
||||||
pending_auth_method.take();
|
|
||||||
}
|
|
||||||
this.handle_thread_error(err, cx);
|
|
||||||
} else {
|
|
||||||
this.reset(window, cx);
|
|
||||||
}
|
|
||||||
this.auth_task.take()
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
}
|
||||||
}));
|
|
||||||
|
this.update_in(cx, |this, window, cx| {
|
||||||
|
if let Err(err) = result {
|
||||||
|
if let ThreadState::Unauthenticated {
|
||||||
|
pending_auth_method,
|
||||||
|
..
|
||||||
|
} = &mut this.thread_state
|
||||||
|
{
|
||||||
|
pending_auth_method.take();
|
||||||
|
}
|
||||||
|
this.handle_thread_error(err, cx);
|
||||||
|
} else {
|
||||||
|
this.reset(window, cx);
|
||||||
|
}
|
||||||
|
this.auth_task.take()
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spawn_external_agent_login(
|
fn spawn_external_agent_login(
|
||||||
@@ -1896,10 +1909,11 @@ impl AcpThreadView {
|
|||||||
let Some(thread) = self.thread() else {
|
let Some(thread) = self.thread() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
let agent_telemetry_id = thread.read(cx).connection().telemetry_id();
|
||||||
|
|
||||||
telemetry::event!(
|
telemetry::event!(
|
||||||
"Agent Tool Call Authorized",
|
"Agent Tool Call Authorized",
|
||||||
agent = self.agent.telemetry_id(),
|
agent = agent_telemetry_id,
|
||||||
session = thread.read(cx).session_id(),
|
session = thread.read(cx).session_id(),
|
||||||
option = option_kind
|
option = option_kind
|
||||||
);
|
);
|
||||||
@@ -1937,6 +1951,16 @@ impl AcpThreadView {
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &Context<Self>,
|
cx: &Context<Self>,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
|
let is_indented = entry.is_indented();
|
||||||
|
let is_first_indented = is_indented
|
||||||
|
&& self.thread().is_some_and(|thread| {
|
||||||
|
thread
|
||||||
|
.read(cx)
|
||||||
|
.entries()
|
||||||
|
.get(entry_ix.saturating_sub(1))
|
||||||
|
.is_none_or(|entry| !entry.is_indented())
|
||||||
|
});
|
||||||
|
|
||||||
let primary = match &entry {
|
let primary = match &entry {
|
||||||
AgentThreadEntry::UserMessage(message) => {
|
AgentThreadEntry::UserMessage(message) => {
|
||||||
let Some(editor) = self
|
let Some(editor) = self
|
||||||
@@ -1969,7 +1993,9 @@ impl AcpThreadView {
|
|||||||
v_flex()
|
v_flex()
|
||||||
.id(("user_message", entry_ix))
|
.id(("user_message", entry_ix))
|
||||||
.map(|this| {
|
.map(|this| {
|
||||||
if entry_ix == 0 && !has_checkpoint_button && rules_item.is_none() {
|
if is_first_indented {
|
||||||
|
this.pt_0p5()
|
||||||
|
} else if entry_ix == 0 && !has_checkpoint_button && rules_item.is_none() {
|
||||||
this.pt(rems_from_px(18.))
|
this.pt(rems_from_px(18.))
|
||||||
} else if rules_item.is_some() {
|
} else if rules_item.is_some() {
|
||||||
this.pt_3()
|
this.pt_3()
|
||||||
@@ -2015,6 +2041,9 @@ impl AcpThreadView {
|
|||||||
.shadow_md()
|
.shadow_md()
|
||||||
.bg(cx.theme().colors().editor_background)
|
.bg(cx.theme().colors().editor_background)
|
||||||
.border_1()
|
.border_1()
|
||||||
|
.when(is_indented, |this| {
|
||||||
|
this.py_2().px_2().shadow_sm()
|
||||||
|
})
|
||||||
.when(editing && !editor_focus, |this| this.border_dashed())
|
.when(editing && !editor_focus, |this| this.border_dashed())
|
||||||
.border_color(cx.theme().colors().border)
|
.border_color(cx.theme().colors().border)
|
||||||
.map(|this|{
|
.map(|this|{
|
||||||
@@ -2085,10 +2114,23 @@ impl AcpThreadView {
|
|||||||
.icon_size(IconSize::Small)
|
.icon_size(IconSize::Small)
|
||||||
.icon_color(Color::Muted)
|
.icon_color(Color::Muted)
|
||||||
.style(ButtonStyle::Transparent)
|
.style(ButtonStyle::Transparent)
|
||||||
.tooltip(move |_window, cx| {
|
.tooltip(Tooltip::element({
|
||||||
cx.new(|_| UnavailableEditingTooltip::new(agent_name.clone()))
|
move |_, _| {
|
||||||
.into()
|
v_flex()
|
||||||
})
|
.gap_1()
|
||||||
|
.child(Label::new("Unavailable Editing")).child(
|
||||||
|
div().max_w_64().child(
|
||||||
|
Label::new(format!(
|
||||||
|
"Editing previous messages is not available for {} yet.",
|
||||||
|
agent_name.clone()
|
||||||
|
))
|
||||||
|
.size(LabelSize::Small)
|
||||||
|
.color(Color::Muted),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
}))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -2096,7 +2138,10 @@ impl AcpThreadView {
|
|||||||
)
|
)
|
||||||
.into_any()
|
.into_any()
|
||||||
}
|
}
|
||||||
AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => {
|
AgentThreadEntry::AssistantMessage(AssistantMessage {
|
||||||
|
chunks,
|
||||||
|
indented: _,
|
||||||
|
}) => {
|
||||||
let is_last = entry_ix + 1 == total_entries;
|
let is_last = entry_ix + 1 == total_entries;
|
||||||
|
|
||||||
let style = default_markdown_style(false, false, window, cx);
|
let style = default_markdown_style(false, false, window, cx);
|
||||||
@@ -2130,6 +2175,7 @@ impl AcpThreadView {
|
|||||||
v_flex()
|
v_flex()
|
||||||
.px_5()
|
.px_5()
|
||||||
.py_1p5()
|
.py_1p5()
|
||||||
|
.when(is_first_indented, |this| this.pt_0p5())
|
||||||
.when(is_last, |this| this.pb_4())
|
.when(is_last, |this| this.pb_4())
|
||||||
.w_full()
|
.w_full()
|
||||||
.text_ui(cx)
|
.text_ui(cx)
|
||||||
@@ -2139,19 +2185,48 @@ impl AcpThreadView {
|
|||||||
AgentThreadEntry::ToolCall(tool_call) => {
|
AgentThreadEntry::ToolCall(tool_call) => {
|
||||||
let has_terminals = tool_call.terminals().next().is_some();
|
let has_terminals = tool_call.terminals().next().is_some();
|
||||||
|
|
||||||
div().w_full().map(|this| {
|
div()
|
||||||
if has_terminals {
|
.w_full()
|
||||||
this.children(tool_call.terminals().map(|terminal| {
|
.map(|this| {
|
||||||
self.render_terminal_tool_call(
|
if has_terminals {
|
||||||
entry_ix, terminal, tool_call, window, cx,
|
this.children(tool_call.terminals().map(|terminal| {
|
||||||
)
|
self.render_terminal_tool_call(
|
||||||
}))
|
entry_ix, terminal, tool_call, window, cx,
|
||||||
} else {
|
)
|
||||||
this.child(self.render_tool_call(entry_ix, tool_call, window, cx))
|
}))
|
||||||
}
|
} else {
|
||||||
})
|
this.child(self.render_tool_call(entry_ix, tool_call, window, cx))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.into_any()
|
||||||
}
|
}
|
||||||
.into_any(),
|
};
|
||||||
|
|
||||||
|
let primary = if is_indented {
|
||||||
|
let line_top = if is_first_indented {
|
||||||
|
rems_from_px(-12.0)
|
||||||
|
} else {
|
||||||
|
rems_from_px(0.0)
|
||||||
|
};
|
||||||
|
|
||||||
|
div()
|
||||||
|
.relative()
|
||||||
|
.w_full()
|
||||||
|
.pl(rems_from_px(20.0))
|
||||||
|
.bg(cx.theme().colors().panel_background.opacity(0.2))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.absolute()
|
||||||
|
.left(rems_from_px(18.0))
|
||||||
|
.top(line_top)
|
||||||
|
.bottom_0()
|
||||||
|
.w_px()
|
||||||
|
.bg(cx.theme().colors().border.opacity(0.6)),
|
||||||
|
)
|
||||||
|
.child(primary)
|
||||||
|
.into_any_element()
|
||||||
|
} else {
|
||||||
|
primary
|
||||||
};
|
};
|
||||||
|
|
||||||
let needs_confirmation = if let AgentThreadEntry::ToolCall(tool_call) = entry {
|
let needs_confirmation = if let AgentThreadEntry::ToolCall(tool_call) = entry {
|
||||||
@@ -3509,7 +3584,9 @@ impl AcpThreadView {
|
|||||||
(method.id.0.clone(), method.name.clone())
|
(method.id.0.clone(), method.name.clone())
|
||||||
};
|
};
|
||||||
|
|
||||||
Button::new(SharedString::from(method_id.clone()), name)
|
let agent_telemetry_id = connection.telemetry_id();
|
||||||
|
|
||||||
|
Button::new(method_id.clone(), name)
|
||||||
.label_size(LabelSize::Small)
|
.label_size(LabelSize::Small)
|
||||||
.map(|this| {
|
.map(|this| {
|
||||||
if ix == 0 {
|
if ix == 0 {
|
||||||
@@ -3528,7 +3605,7 @@ impl AcpThreadView {
|
|||||||
cx.listener(move |this, _, window, cx| {
|
cx.listener(move |this, _, window, cx| {
|
||||||
telemetry::event!(
|
telemetry::event!(
|
||||||
"Authenticate Agent Started",
|
"Authenticate Agent Started",
|
||||||
agent = this.agent.telemetry_id(),
|
agent = agent_telemetry_id,
|
||||||
method = method_id
|
method = method_id
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -4200,7 +4277,11 @@ impl AcpThreadView {
|
|||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
.on_action(cx.listener(|this, _: &CycleModeSelector, window, cx| {
|
.on_action(cx.listener(|this, _: &CycleModeSelector, window, cx| {
|
||||||
if let Some(mode_selector) = this.mode_selector() {
|
if let Some(profile_selector) = this.profile_selector.as_ref() {
|
||||||
|
profile_selector.update(cx, |profile_selector, cx| {
|
||||||
|
profile_selector.cycle_profile(cx);
|
||||||
|
});
|
||||||
|
} else if let Some(mode_selector) = this.mode_selector() {
|
||||||
mode_selector.update(cx, |mode_selector, cx| {
|
mode_selector.update(cx, |mode_selector, cx| {
|
||||||
mode_selector.cycle_mode(window, cx);
|
mode_selector.cycle_mode(window, cx);
|
||||||
});
|
});
|
||||||
@@ -4851,6 +4932,32 @@ impl AcpThreadView {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn scroll_to_most_recent_user_prompt(&mut self, cx: &mut Context<Self>) {
|
||||||
|
let Some(thread) = self.thread() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let entries = thread.read(cx).entries();
|
||||||
|
if entries.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the most recent user message and scroll it to the top of the viewport.
|
||||||
|
// (Fallback: if no user message exists, scroll to the bottom.)
|
||||||
|
if let Some(ix) = entries
|
||||||
|
.iter()
|
||||||
|
.rposition(|entry| matches!(entry, AgentThreadEntry::UserMessage(_)))
|
||||||
|
{
|
||||||
|
self.list_state.scroll_to(ListOffset {
|
||||||
|
item_ix: ix,
|
||||||
|
offset_in_item: px(0.0),
|
||||||
|
});
|
||||||
|
cx.notify();
|
||||||
|
} else {
|
||||||
|
self.scroll_to_bottom(cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn scroll_to_bottom(&mut self, cx: &mut Context<Self>) {
|
pub fn scroll_to_bottom(&mut self, cx: &mut Context<Self>) {
|
||||||
if let Some(thread) = self.thread() {
|
if let Some(thread) = self.thread() {
|
||||||
let entry_count = thread.read(cx).entries().len();
|
let entry_count = thread.read(cx).entries().len();
|
||||||
@@ -4946,8 +5053,8 @@ impl AcpThreadView {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if let Some(screen_window) = cx
|
if let Some(screen_window) = cx
|
||||||
.open_window(options, |_, cx| {
|
.open_window(options, |_window, cx| {
|
||||||
cx.new(|_| {
|
cx.new(|_cx| {
|
||||||
AgentNotification::new(title.clone(), caption.clone(), icon, project_name)
|
AgentNotification::new(title.clone(), caption.clone(), icon, project_name)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -5069,6 +5176,16 @@ impl AcpThreadView {
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
let scroll_to_recent_user_prompt =
|
||||||
|
IconButton::new("scroll_to_recent_user_prompt", IconName::ForwardArrow)
|
||||||
|
.shape(ui::IconButtonShape::Square)
|
||||||
|
.icon_size(IconSize::Small)
|
||||||
|
.icon_color(Color::Ignored)
|
||||||
|
.tooltip(Tooltip::text("Scroll To Most Recent User Prompt"))
|
||||||
|
.on_click(cx.listener(move |this, _, _, cx| {
|
||||||
|
this.scroll_to_most_recent_user_prompt(cx);
|
||||||
|
}));
|
||||||
|
|
||||||
let scroll_to_top = IconButton::new("scroll_to_top", IconName::ArrowUp)
|
let scroll_to_top = IconButton::new("scroll_to_top", IconName::ArrowUp)
|
||||||
.shape(ui::IconButtonShape::Square)
|
.shape(ui::IconButtonShape::Square)
|
||||||
.icon_size(IconSize::Small)
|
.icon_size(IconSize::Small)
|
||||||
@@ -5145,6 +5262,7 @@ impl AcpThreadView {
|
|||||||
|
|
||||||
container
|
container
|
||||||
.child(open_as_markdown)
|
.child(open_as_markdown)
|
||||||
|
.child(scroll_to_recent_user_prompt)
|
||||||
.child(scroll_to_top)
|
.child(scroll_to_top)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
@@ -5376,47 +5494,39 @@ impl AcpThreadView {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_codex_windows_warning(&self, cx: &mut Context<Self>) -> Option<Callout> {
|
fn render_codex_windows_warning(&self, cx: &mut Context<Self>) -> Callout {
|
||||||
if self.show_codex_windows_warning {
|
Callout::new()
|
||||||
Some(
|
.icon(IconName::Warning)
|
||||||
Callout::new()
|
.severity(Severity::Warning)
|
||||||
.icon(IconName::Warning)
|
.title("Codex on Windows")
|
||||||
.severity(Severity::Warning)
|
.description("For best performance, run Codex in Windows Subsystem for Linux (WSL2)")
|
||||||
.title("Codex on Windows")
|
.actions_slot(
|
||||||
.description(
|
Button::new("open-wsl-modal", "Open in WSL")
|
||||||
"For best performance, run Codex in Windows Subsystem for Linux (WSL2)",
|
.icon_size(IconSize::Small)
|
||||||
)
|
.icon_color(Color::Muted)
|
||||||
.actions_slot(
|
.on_click(cx.listener({
|
||||||
Button::new("open-wsl-modal", "Open in WSL")
|
move |_, _, _window, cx| {
|
||||||
.icon_size(IconSize::Small)
|
#[cfg(windows)]
|
||||||
.icon_color(Color::Muted)
|
_window.dispatch_action(
|
||||||
.on_click(cx.listener({
|
zed_actions::wsl_actions::OpenWsl::default().boxed_clone(),
|
||||||
move |_, _, _window, cx| {
|
cx,
|
||||||
#[cfg(windows)]
|
);
|
||||||
_window.dispatch_action(
|
cx.notify();
|
||||||
zed_actions::wsl_actions::OpenWsl::default().boxed_clone(),
|
}
|
||||||
cx,
|
})),
|
||||||
);
|
)
|
||||||
cx.notify();
|
.dismiss_action(
|
||||||
}
|
IconButton::new("dismiss", IconName::Close)
|
||||||
})),
|
.icon_size(IconSize::Small)
|
||||||
)
|
.icon_color(Color::Muted)
|
||||||
.dismiss_action(
|
.tooltip(Tooltip::text("Dismiss Warning"))
|
||||||
IconButton::new("dismiss", IconName::Close)
|
.on_click(cx.listener({
|
||||||
.icon_size(IconSize::Small)
|
move |this, _, _, cx| {
|
||||||
.icon_color(Color::Muted)
|
this.show_codex_windows_warning = false;
|
||||||
.tooltip(Tooltip::text("Dismiss Warning"))
|
cx.notify();
|
||||||
.on_click(cx.listener({
|
}
|
||||||
move |this, _, _, cx| {
|
})),
|
||||||
this.show_codex_windows_warning = false;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_thread_error(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
|
fn render_thread_error(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
|
||||||
@@ -5936,12 +6046,8 @@ impl Render for AcpThreadView {
|
|||||||
_ => this,
|
_ => this,
|
||||||
})
|
})
|
||||||
.children(self.render_thread_retry_status_callout(window, cx))
|
.children(self.render_thread_retry_status_callout(window, cx))
|
||||||
.children({
|
.when(self.show_codex_windows_warning, |this| {
|
||||||
if cfg!(windows) && self.project.read(cx).is_local() {
|
this.child(self.render_codex_windows_warning(cx))
|
||||||
self.render_codex_windows_warning(cx)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.children(self.render_thread_error(window, cx))
|
.children(self.render_thread_error(window, cx))
|
||||||
.when_some(
|
.when_some(
|
||||||
@@ -6057,13 +6163,13 @@ fn default_markdown_style(
|
|||||||
},
|
},
|
||||||
border_color: Some(colors.border_variant),
|
border_color: Some(colors.border_variant),
|
||||||
background: Some(colors.editor_background.into()),
|
background: Some(colors.editor_background.into()),
|
||||||
text: Some(TextStyleRefinement {
|
text: TextStyleRefinement {
|
||||||
font_family: Some(theme_settings.buffer_font.family.clone()),
|
font_family: Some(theme_settings.buffer_font.family.clone()),
|
||||||
font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
|
font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
|
||||||
font_features: Some(theme_settings.buffer_font.features.clone()),
|
font_features: Some(theme_settings.buffer_font.features.clone()),
|
||||||
font_size: Some(buffer_font_size.into()),
|
font_size: Some(buffer_font_size.into()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
inline_code: TextStyleRefinement {
|
inline_code: TextStyleRefinement {
|
||||||
@@ -6374,6 +6480,57 @@ pub(crate) mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_notification_closed_when_thread_view_dropped(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
|
||||||
|
let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
|
||||||
|
|
||||||
|
let weak_view = thread_view.downgrade();
|
||||||
|
|
||||||
|
let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
|
||||||
|
message_editor.update_in(cx, |editor, window, cx| {
|
||||||
|
editor.set_text("Hello", window, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.deactivate_window();
|
||||||
|
|
||||||
|
thread_view.update_in(cx, |thread_view, window, cx| {
|
||||||
|
thread_view.send(window, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
// Verify notification is shown
|
||||||
|
assert!(
|
||||||
|
cx.windows()
|
||||||
|
.iter()
|
||||||
|
.any(|window| window.downcast::<AgentNotification>().is_some()),
|
||||||
|
"Expected notification to be shown"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Drop the thread view (simulating navigation to a new thread)
|
||||||
|
drop(thread_view);
|
||||||
|
drop(message_editor);
|
||||||
|
// Trigger an update to flush effects, which will call release_dropped_entities
|
||||||
|
cx.update(|_window, _cx| {});
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
// Verify the entity was actually released
|
||||||
|
assert!(
|
||||||
|
!weak_view.is_upgradable(),
|
||||||
|
"Thread view entity should be released after dropping"
|
||||||
|
);
|
||||||
|
|
||||||
|
// The notification should be automatically closed via on_release
|
||||||
|
assert!(
|
||||||
|
!cx.windows()
|
||||||
|
.iter()
|
||||||
|
.any(|window| window.downcast::<AgentNotification>().is_some()),
|
||||||
|
"Notification should be closed when thread view is dropped"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async fn setup_thread_view(
|
async fn setup_thread_view(
|
||||||
agent: impl AgentServer + 'static,
|
agent: impl AgentServer + 'static,
|
||||||
cx: &mut TestAppContext,
|
cx: &mut TestAppContext,
|
||||||
@@ -6398,6 +6555,7 @@ pub(crate) mod tests {
|
|||||||
project,
|
project,
|
||||||
history_store,
|
history_store,
|
||||||
None,
|
None,
|
||||||
|
false,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
@@ -6475,10 +6633,6 @@ pub(crate) mod tests {
|
|||||||
where
|
where
|
||||||
C: 'static + AgentConnection + Send + Clone,
|
C: 'static + AgentConnection + Send + Clone,
|
||||||
{
|
{
|
||||||
fn telemetry_id(&self) -> &'static str {
|
|
||||||
"test"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn logo(&self) -> ui::IconName {
|
fn logo(&self) -> ui::IconName {
|
||||||
ui::IconName::Ai
|
ui::IconName::Ai
|
||||||
}
|
}
|
||||||
@@ -6505,8 +6659,8 @@ pub(crate) mod tests {
|
|||||||
struct SaboteurAgentConnection;
|
struct SaboteurAgentConnection;
|
||||||
|
|
||||||
impl AgentConnection for SaboteurAgentConnection {
|
impl AgentConnection for SaboteurAgentConnection {
|
||||||
fn telemetry_id(&self) -> &'static str {
|
fn telemetry_id(&self) -> SharedString {
|
||||||
"saboteur"
|
"saboteur".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_thread(
|
fn new_thread(
|
||||||
@@ -6569,8 +6723,8 @@ pub(crate) mod tests {
|
|||||||
struct RefusalAgentConnection;
|
struct RefusalAgentConnection;
|
||||||
|
|
||||||
impl AgentConnection for RefusalAgentConnection {
|
impl AgentConnection for RefusalAgentConnection {
|
||||||
fn telemetry_id(&self) -> &'static str {
|
fn telemetry_id(&self) -> SharedString {
|
||||||
"refusal"
|
"refusal".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_thread(
|
fn new_thread(
|
||||||
@@ -6671,6 +6825,7 @@ pub(crate) mod tests {
|
|||||||
project.clone(),
|
project.clone(),
|
||||||
history_store.clone(),
|
history_store.clone(),
|
||||||
None,
|
None,
|
||||||
|
false,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
@@ -6791,6 +6946,70 @@ pub(crate) mod tests {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_scroll_to_most_recent_user_prompt(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
|
||||||
|
let connection = StubAgentConnection::new();
|
||||||
|
|
||||||
|
// Each user prompt will result in a user message entry plus an agent message entry.
|
||||||
|
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
|
||||||
|
acp::ContentChunk::new("Response 1".into()),
|
||||||
|
)]);
|
||||||
|
|
||||||
|
let (thread_view, cx) =
|
||||||
|
setup_thread_view(StubAgentServer::new(connection.clone()), cx).await;
|
||||||
|
|
||||||
|
let thread = thread_view
|
||||||
|
.read_with(cx, |view, _| view.thread().cloned())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
thread
|
||||||
|
.update(cx, |thread, cx| thread.send_raw("Prompt 1", cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
|
||||||
|
acp::ContentChunk::new("Response 2".into()),
|
||||||
|
)]);
|
||||||
|
|
||||||
|
thread
|
||||||
|
.update(cx, |thread, cx| thread.send_raw("Prompt 2", cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
// Move somewhere else first so we're not trivially already on the last user prompt.
|
||||||
|
thread_view.update(cx, |view, cx| {
|
||||||
|
view.scroll_to_top(cx);
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
thread_view.update(cx, |view, cx| {
|
||||||
|
view.scroll_to_most_recent_user_prompt(cx);
|
||||||
|
let scroll_top = view.list_state.logical_scroll_top();
|
||||||
|
// Entries layout is: [User1, Assistant1, User2, Assistant2]
|
||||||
|
assert_eq!(scroll_top.item_ix, 2);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_scroll_to_most_recent_user_prompt_falls_back_to_bottom_without_user_messages(
|
||||||
|
cx: &mut TestAppContext,
|
||||||
|
) {
|
||||||
|
init_test(cx);
|
||||||
|
|
||||||
|
let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
|
||||||
|
|
||||||
|
// With no entries, scrolling should be a no-op and must not panic.
|
||||||
|
thread_view.update(cx, |view, cx| {
|
||||||
|
view.scroll_to_most_recent_user_prompt(cx);
|
||||||
|
let scroll_top = view.list_state.logical_scroll_top();
|
||||||
|
assert_eq!(scroll_top.item_ix, 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_message_editing_cancel(cx: &mut TestAppContext) {
|
async fn test_message_editing_cancel(cx: &mut TestAppContext) {
|
||||||
init_test(cx);
|
init_test(cx);
|
||||||
|
|||||||
@@ -34,9 +34,9 @@ use project::{
|
|||||||
};
|
};
|
||||||
use settings::{Settings, SettingsStore, update_settings_file};
|
use settings::{Settings, SettingsStore, update_settings_file};
|
||||||
use ui::{
|
use ui::{
|
||||||
Button, ButtonStyle, Chip, CommonAnimationExt, ContextMenu, ContextMenuEntry, Disclosure,
|
ButtonStyle, Chip, CommonAnimationExt, ContextMenu, ContextMenuEntry, Disclosure, Divider,
|
||||||
Divider, DividerColor, ElevationIndex, IconName, IconPosition, IconSize, Indicator, LabelSize,
|
DividerColor, ElevationIndex, Indicator, LabelSize, PopoverMenu, Switch, Tooltip,
|
||||||
PopoverMenu, Switch, Tooltip, WithScrollbar, prelude::*,
|
WithScrollbar, prelude::*,
|
||||||
};
|
};
|
||||||
use util::ResultExt as _;
|
use util::ResultExt as _;
|
||||||
use workspace::{Workspace, create_and_open_local_file};
|
use workspace::{Workspace, create_and_open_local_file};
|
||||||
@@ -838,7 +838,7 @@ impl AgentConfiguration {
|
|||||||
.min_w_0()
|
.min_w_0()
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.id(SharedString::from(format!("tooltip-{}", item_id)))
|
.id(format!("tooltip-{}", item_id))
|
||||||
.h_full()
|
.h_full()
|
||||||
.w_3()
|
.w_3()
|
||||||
.mr_2()
|
.mr_2()
|
||||||
@@ -975,9 +975,12 @@ impl AgentConfiguration {
|
|||||||
let icon = if let Some(icon_path) = agent_server_store.agent_icon(&name) {
|
let icon = if let Some(icon_path) = agent_server_store.agent_icon(&name) {
|
||||||
AgentIcon::Path(icon_path)
|
AgentIcon::Path(icon_path)
|
||||||
} else {
|
} else {
|
||||||
AgentIcon::Name(IconName::Ai)
|
AgentIcon::Name(IconName::Sparkle)
|
||||||
};
|
};
|
||||||
(name, icon)
|
let display_name = agent_server_store
|
||||||
|
.agent_display_name(&name)
|
||||||
|
.unwrap_or_else(|| name.0.clone());
|
||||||
|
(name, icon, display_name)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@@ -1084,6 +1087,7 @@ impl AgentConfiguration {
|
|||||||
.child(self.render_agent_server(
|
.child(self.render_agent_server(
|
||||||
AgentIcon::Name(IconName::AiClaude),
|
AgentIcon::Name(IconName::AiClaude),
|
||||||
"Claude Code",
|
"Claude Code",
|
||||||
|
"Claude Code",
|
||||||
false,
|
false,
|
||||||
cx,
|
cx,
|
||||||
))
|
))
|
||||||
@@ -1091,6 +1095,7 @@ impl AgentConfiguration {
|
|||||||
.child(self.render_agent_server(
|
.child(self.render_agent_server(
|
||||||
AgentIcon::Name(IconName::AiOpenAi),
|
AgentIcon::Name(IconName::AiOpenAi),
|
||||||
"Codex CLI",
|
"Codex CLI",
|
||||||
|
"Codex CLI",
|
||||||
false,
|
false,
|
||||||
cx,
|
cx,
|
||||||
))
|
))
|
||||||
@@ -1098,16 +1103,23 @@ impl AgentConfiguration {
|
|||||||
.child(self.render_agent_server(
|
.child(self.render_agent_server(
|
||||||
AgentIcon::Name(IconName::AiGemini),
|
AgentIcon::Name(IconName::AiGemini),
|
||||||
"Gemini CLI",
|
"Gemini CLI",
|
||||||
|
"Gemini CLI",
|
||||||
false,
|
false,
|
||||||
cx,
|
cx,
|
||||||
))
|
))
|
||||||
.map(|mut parent| {
|
.map(|mut parent| {
|
||||||
for (name, icon) in user_defined_agents {
|
for (name, icon, display_name) in user_defined_agents {
|
||||||
parent = parent
|
parent = parent
|
||||||
.child(
|
.child(
|
||||||
Divider::horizontal().color(DividerColor::BorderFaded),
|
Divider::horizontal().color(DividerColor::BorderFaded),
|
||||||
)
|
)
|
||||||
.child(self.render_agent_server(icon, name, true, cx));
|
.child(self.render_agent_server(
|
||||||
|
icon,
|
||||||
|
name,
|
||||||
|
display_name,
|
||||||
|
true,
|
||||||
|
cx,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
parent
|
parent
|
||||||
}),
|
}),
|
||||||
@@ -1118,11 +1130,14 @@ impl AgentConfiguration {
|
|||||||
fn render_agent_server(
|
fn render_agent_server(
|
||||||
&self,
|
&self,
|
||||||
icon: AgentIcon,
|
icon: AgentIcon,
|
||||||
name: impl Into<SharedString>,
|
id: impl Into<SharedString>,
|
||||||
|
display_name: impl Into<SharedString>,
|
||||||
external: bool,
|
external: bool,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> impl IntoElement {
|
) -> impl IntoElement {
|
||||||
let name = name.into();
|
let id = id.into();
|
||||||
|
let display_name = display_name.into();
|
||||||
|
|
||||||
let icon = match icon {
|
let icon = match icon {
|
||||||
AgentIcon::Name(icon_name) => Icon::new(icon_name)
|
AgentIcon::Name(icon_name) => Icon::new(icon_name)
|
||||||
.size(IconSize::Small)
|
.size(IconSize::Small)
|
||||||
@@ -1132,12 +1147,15 @@ impl AgentConfiguration {
|
|||||||
.color(Color::Muted),
|
.color(Color::Muted),
|
||||||
};
|
};
|
||||||
|
|
||||||
let tooltip_id = SharedString::new(format!("agent-source-{}", name));
|
let tooltip_id = SharedString::new(format!("agent-source-{}", id));
|
||||||
let tooltip_message = format!("The {} agent was installed from an extension.", name);
|
let tooltip_message = format!(
|
||||||
|
"The {} agent was installed from an extension.",
|
||||||
|
display_name
|
||||||
|
);
|
||||||
|
|
||||||
let agent_server_name = ExternalAgentServerName(name.clone());
|
let agent_server_name = ExternalAgentServerName(id.clone());
|
||||||
|
|
||||||
let uninstall_btn_id = SharedString::from(format!("uninstall-{}", name));
|
let uninstall_btn_id = SharedString::from(format!("uninstall-{}", id));
|
||||||
let uninstall_button = IconButton::new(uninstall_btn_id, IconName::Trash)
|
let uninstall_button = IconButton::new(uninstall_btn_id, IconName::Trash)
|
||||||
.icon_color(Color::Muted)
|
.icon_color(Color::Muted)
|
||||||
.icon_size(IconSize::Small)
|
.icon_size(IconSize::Small)
|
||||||
@@ -1161,7 +1179,7 @@ impl AgentConfiguration {
|
|||||||
h_flex()
|
h_flex()
|
||||||
.gap_1p5()
|
.gap_1p5()
|
||||||
.child(icon)
|
.child(icon)
|
||||||
.child(Label::new(name))
|
.child(Label::new(display_name))
|
||||||
.when(external, |this| {
|
.when(external, |this| {
|
||||||
this.child(
|
this.child(
|
||||||
div()
|
div()
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ impl ConfigureContextServerToolsModal {
|
|||||||
v_flex()
|
v_flex()
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.id(SharedString::from(format!("tool-header-{}", index)))
|
.id(format!("tool-header-{}", index))
|
||||||
.py_1()
|
.py_1()
|
||||||
.pl_1()
|
.pl_1()
|
||||||
.pr_2()
|
.pr_2()
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use editor::Editor;
|
|||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription, prelude::*};
|
use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription, prelude::*};
|
||||||
use language_model::{LanguageModel, LanguageModelRegistry};
|
use language_model::{LanguageModel, LanguageModelRegistry};
|
||||||
|
use settings::SettingsStore;
|
||||||
use settings::{
|
use settings::{
|
||||||
LanguageModelProviderSetting, LanguageModelSelection, Settings as _, update_settings_file,
|
LanguageModelProviderSetting, LanguageModelSelection, Settings as _, update_settings_file,
|
||||||
};
|
};
|
||||||
@@ -94,6 +95,7 @@ pub struct ViewProfileMode {
|
|||||||
configure_default_model: NavigableEntry,
|
configure_default_model: NavigableEntry,
|
||||||
configure_tools: NavigableEntry,
|
configure_tools: NavigableEntry,
|
||||||
configure_mcps: NavigableEntry,
|
configure_mcps: NavigableEntry,
|
||||||
|
delete_profile: NavigableEntry,
|
||||||
cancel_item: NavigableEntry,
|
cancel_item: NavigableEntry,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,6 +111,7 @@ pub struct ManageProfilesModal {
|
|||||||
active_model: Option<Arc<dyn LanguageModel>>,
|
active_model: Option<Arc<dyn LanguageModel>>,
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
|
_settings_subscription: Subscription,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ManageProfilesModal {
|
impl ManageProfilesModal {
|
||||||
@@ -148,12 +151,23 @@ impl ManageProfilesModal {
|
|||||||
) -> Self {
|
) -> Self {
|
||||||
let focus_handle = cx.focus_handle();
|
let focus_handle = cx.focus_handle();
|
||||||
|
|
||||||
|
// Keep this modal in sync with settings changes (including profile deletion).
|
||||||
|
let settings_subscription =
|
||||||
|
cx.observe_global_in::<SettingsStore>(window, |this, window, cx| {
|
||||||
|
if matches!(this.mode, Mode::ChooseProfile(_)) {
|
||||||
|
this.mode = Mode::choose_profile(window, cx);
|
||||||
|
this.focus_handle(cx).focus(window);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
fs,
|
fs,
|
||||||
active_model,
|
active_model,
|
||||||
context_server_registry,
|
context_server_registry,
|
||||||
focus_handle,
|
focus_handle,
|
||||||
mode: Mode::choose_profile(window, cx),
|
mode: Mode::choose_profile(window, cx),
|
||||||
|
_settings_subscription: settings_subscription,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,6 +206,7 @@ impl ManageProfilesModal {
|
|||||||
configure_default_model: NavigableEntry::focusable(cx),
|
configure_default_model: NavigableEntry::focusable(cx),
|
||||||
configure_tools: NavigableEntry::focusable(cx),
|
configure_tools: NavigableEntry::focusable(cx),
|
||||||
configure_mcps: NavigableEntry::focusable(cx),
|
configure_mcps: NavigableEntry::focusable(cx),
|
||||||
|
delete_profile: NavigableEntry::focusable(cx),
|
||||||
cancel_item: NavigableEntry::focusable(cx),
|
cancel_item: NavigableEntry::focusable(cx),
|
||||||
});
|
});
|
||||||
self.focus_handle(cx).focus(window);
|
self.focus_handle(cx).focus(window);
|
||||||
@@ -369,6 +384,42 @@ impl ManageProfilesModal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn delete_profile(
|
||||||
|
&mut self,
|
||||||
|
profile_id: AgentProfileId,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
if builtin_profiles::is_builtin(&profile_id) {
|
||||||
|
self.view_profile(profile_id, window, cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let fs = self.fs.clone();
|
||||||
|
|
||||||
|
update_settings_file(fs, cx, move |settings, _cx| {
|
||||||
|
let Some(agent_settings) = settings.agent.as_mut() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(profiles) = agent_settings.profiles.as_mut() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
profiles.shift_remove(profile_id.0.as_ref());
|
||||||
|
|
||||||
|
if agent_settings
|
||||||
|
.default_profile
|
||||||
|
.as_deref()
|
||||||
|
.is_some_and(|default_profile| default_profile == profile_id.0.as_ref())
|
||||||
|
{
|
||||||
|
agent_settings.default_profile = Some(AgentProfileId::default().0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.choose_profile(window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
fn cancel(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn cancel(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
match &self.mode {
|
match &self.mode {
|
||||||
Mode::ChooseProfile { .. } => {
|
Mode::ChooseProfile { .. } => {
|
||||||
@@ -422,7 +473,7 @@ impl ManageProfilesModal {
|
|||||||
let is_focused = profile.navigation.focus_handle.contains_focused(window, cx);
|
let is_focused = profile.navigation.focus_handle.contains_focused(window, cx);
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.id(SharedString::from(format!("profile-{}", profile.id)))
|
.id(format!("profile-{}", profile.id))
|
||||||
.track_focus(&profile.navigation.focus_handle)
|
.track_focus(&profile.navigation.focus_handle)
|
||||||
.on_action({
|
.on_action({
|
||||||
let profile_id = profile.id.clone();
|
let profile_id = profile.id.clone();
|
||||||
@@ -431,7 +482,7 @@ impl ManageProfilesModal {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
ListItem::new(SharedString::from(format!("profile-{}", profile.id)))
|
ListItem::new(format!("profile-{}", profile.id))
|
||||||
.toggle_state(is_focused)
|
.toggle_state(is_focused)
|
||||||
.inset(true)
|
.inset(true)
|
||||||
.spacing(ListItemSpacing::Sparse)
|
.spacing(ListItemSpacing::Sparse)
|
||||||
@@ -756,6 +807,40 @@ impl ManageProfilesModal {
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.id("delete-profile")
|
||||||
|
.track_focus(&mode.delete_profile.focus_handle)
|
||||||
|
.on_action({
|
||||||
|
let profile_id = mode.profile_id.clone();
|
||||||
|
cx.listener(move |this, _: &menu::Confirm, window, cx| {
|
||||||
|
this.delete_profile(profile_id.clone(), window, cx);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.child(
|
||||||
|
ListItem::new("delete-profile")
|
||||||
|
.toggle_state(
|
||||||
|
mode.delete_profile
|
||||||
|
.focus_handle
|
||||||
|
.contains_focused(window, cx),
|
||||||
|
)
|
||||||
|
.inset(true)
|
||||||
|
.spacing(ListItemSpacing::Sparse)
|
||||||
|
.start_slot(
|
||||||
|
Icon::new(IconName::Trash)
|
||||||
|
.size(IconSize::Small)
|
||||||
|
.color(Color::Error),
|
||||||
|
)
|
||||||
|
.child(Label::new("Delete Profile").color(Color::Error))
|
||||||
|
.disabled(builtin_profiles::is_builtin(&mode.profile_id))
|
||||||
|
.on_click({
|
||||||
|
let profile_id = mode.profile_id.clone();
|
||||||
|
cx.listener(move |this, _, window, cx| {
|
||||||
|
this.delete_profile(profile_id.clone(), window, cx);
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
.child(ListSeparator)
|
.child(ListSeparator)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
@@ -805,6 +890,7 @@ impl ManageProfilesModal {
|
|||||||
.entry(mode.configure_default_model)
|
.entry(mode.configure_default_model)
|
||||||
.entry(mode.configure_tools)
|
.entry(mode.configure_tools)
|
||||||
.entry(mode.configure_mcps)
|
.entry(mode.configure_mcps)
|
||||||
|
.entry(mode.delete_profile)
|
||||||
.entry(mode.cancel_item)
|
.entry(mode.cancel_item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,7 +130,12 @@ impl AgentDiffPane {
|
|||||||
.action_log()
|
.action_log()
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.changed_buffers(cx);
|
.changed_buffers(cx);
|
||||||
let mut paths_to_delete = self.multibuffer.read(cx).paths().collect::<HashSet<_>>();
|
let mut paths_to_delete = self
|
||||||
|
.multibuffer
|
||||||
|
.read(cx)
|
||||||
|
.paths()
|
||||||
|
.cloned()
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
|
||||||
for (buffer, diff_handle) in changed_buffers {
|
for (buffer, diff_handle) in changed_buffers {
|
||||||
if buffer.read(cx).file().is_none() {
|
if buffer.read(cx).file().is_none() {
|
||||||
|
|||||||
@@ -63,6 +63,10 @@ impl AgentModelSelector {
|
|||||||
pub fn toggle(&self, window: &mut Window, cx: &mut Context<Self>) {
|
pub fn toggle(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.menu_handle.toggle(window, cx);
|
self.menu_handle.toggle(window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn active_model(&self, cx: &App) -> Option<language_model::ConfiguredModel> {
|
||||||
|
self.selector.read(cx).delegate.active_model(cx)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for AgentModelSelector {
|
impl Render for AgentModelSelector {
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ use std::{ops::Range, path::Path, rc::Rc, sync::Arc, time::Duration};
|
|||||||
|
|
||||||
use acp_thread::AcpThread;
|
use acp_thread::AcpThread;
|
||||||
use agent::{ContextServerRegistry, DbThreadMetadata, HistoryEntry, HistoryStore};
|
use agent::{ContextServerRegistry, DbThreadMetadata, HistoryEntry, HistoryStore};
|
||||||
|
use agent_servers::AgentServer;
|
||||||
use db::kvp::{Dismissable, KEY_VALUE_STORE};
|
use db::kvp::{Dismissable, KEY_VALUE_STORE};
|
||||||
use project::{
|
use project::{
|
||||||
ExternalAgentServerName,
|
ExternalAgentServerName,
|
||||||
agent_server_store::{CLAUDE_CODE_NAME, CODEX_NAME, GEMINI_NAME},
|
agent_server_store::{CLAUDE_CODE_NAME, CODEX_NAME, GEMINI_NAME},
|
||||||
|
trusted_worktrees::{RemoteHostLocation, TrustedWorktrees, wait_for_workspace_trust},
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::{
|
use settings::{
|
||||||
@@ -259,7 +261,18 @@ impl AgentType {
|
|||||||
Self::Gemini => Some(IconName::AiGemini),
|
Self::Gemini => Some(IconName::AiGemini),
|
||||||
Self::ClaudeCode => Some(IconName::AiClaude),
|
Self::ClaudeCode => Some(IconName::AiClaude),
|
||||||
Self::Codex => Some(IconName::AiOpenAi),
|
Self::Codex => Some(IconName::AiOpenAi),
|
||||||
Self::Custom { .. } => Some(IconName::Terminal),
|
Self::Custom { .. } => Some(IconName::Sparkle),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_mcp(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::NativeAgent => false,
|
||||||
|
Self::TextThread => false,
|
||||||
|
Self::Custom { .. } => false,
|
||||||
|
Self::Gemini => true,
|
||||||
|
Self::ClaudeCode => true,
|
||||||
|
Self::Codex => true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -287,7 +300,7 @@ impl ActiveView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn native_agent(
|
fn native_agent(
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
prompt_store: Option<Entity<PromptStore>>,
|
||||||
history_store: Entity<agent::HistoryStore>,
|
history_store: Entity<agent::HistoryStore>,
|
||||||
@@ -305,6 +318,7 @@ impl ActiveView {
|
|||||||
project,
|
project,
|
||||||
history_store,
|
history_store,
|
||||||
prompt_store,
|
prompt_store,
|
||||||
|
false,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
@@ -441,6 +455,9 @@ pub struct AgentPanel {
|
|||||||
pending_serialization: Option<Task<Result<()>>>,
|
pending_serialization: Option<Task<Result<()>>>,
|
||||||
onboarding: Entity<AgentPanelOnboarding>,
|
onboarding: Entity<AgentPanelOnboarding>,
|
||||||
selected_agent: AgentType,
|
selected_agent: AgentType,
|
||||||
|
new_agent_thread_task: Task<()>,
|
||||||
|
show_trust_workspace_message: bool,
|
||||||
|
_worktree_trust_subscription: Option<Subscription>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AgentPanel {
|
impl AgentPanel {
|
||||||
@@ -664,6 +681,48 @@ impl AgentPanel {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut show_trust_workspace_message = false;
|
||||||
|
let worktree_trust_subscription =
|
||||||
|
TrustedWorktrees::try_get_global(cx).and_then(|trusted_worktrees| {
|
||||||
|
let has_global_trust = trusted_worktrees.update(cx, |trusted_worktrees, cx| {
|
||||||
|
trusted_worktrees.can_trust_workspace(
|
||||||
|
project
|
||||||
|
.read(cx)
|
||||||
|
.remote_connection_options(cx)
|
||||||
|
.map(RemoteHostLocation::from),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
if has_global_trust {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
show_trust_workspace_message = true;
|
||||||
|
let project = project.clone();
|
||||||
|
Some(cx.subscribe(
|
||||||
|
&trusted_worktrees,
|
||||||
|
move |agent_panel, trusted_worktrees, _, cx| {
|
||||||
|
let new_show_trust_workspace_message =
|
||||||
|
!trusted_worktrees.update(cx, |trusted_worktrees, cx| {
|
||||||
|
trusted_worktrees.can_trust_workspace(
|
||||||
|
project
|
||||||
|
.read(cx)
|
||||||
|
.remote_connection_options(cx)
|
||||||
|
.map(RemoteHostLocation::from),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
if new_show_trust_workspace_message
|
||||||
|
!= agent_panel.show_trust_workspace_message
|
||||||
|
{
|
||||||
|
agent_panel.show_trust_workspace_message =
|
||||||
|
new_show_trust_workspace_message;
|
||||||
|
cx.notify();
|
||||||
|
};
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let mut panel = Self {
|
let mut panel = Self {
|
||||||
active_view,
|
active_view,
|
||||||
workspace,
|
workspace,
|
||||||
@@ -686,11 +745,14 @@ impl AgentPanel {
|
|||||||
height: None,
|
height: None,
|
||||||
zoomed: false,
|
zoomed: false,
|
||||||
pending_serialization: None,
|
pending_serialization: None,
|
||||||
|
new_agent_thread_task: Task::ready(()),
|
||||||
onboarding,
|
onboarding,
|
||||||
acp_history,
|
acp_history,
|
||||||
history_store,
|
history_store,
|
||||||
selected_agent: AgentType::default(),
|
selected_agent: AgentType::default(),
|
||||||
loading: false,
|
loading: false,
|
||||||
|
show_trust_workspace_message,
|
||||||
|
_worktree_trust_subscription: worktree_trust_subscription,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initial sync of agent servers from extensions
|
// Initial sync of agent servers from extensions
|
||||||
@@ -883,40 +945,63 @@ impl AgentPanel {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let server = ext_agent.server(fs, history);
|
if ext_agent.is_mcp() {
|
||||||
|
let wait_task = this.update(cx, |agent_panel, cx| {
|
||||||
if !loading {
|
agent_panel.project.update(cx, |project, cx| {
|
||||||
telemetry::event!("Agent Thread Started", agent = server.telemetry_id());
|
wait_for_workspace_trust(
|
||||||
|
project.remote_connection_options(cx),
|
||||||
|
"context servers",
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
if let Some(wait_task) = wait_task {
|
||||||
|
this.update_in(cx, |agent_panel, window, cx| {
|
||||||
|
agent_panel.show_trust_workspace_message = true;
|
||||||
|
cx.notify();
|
||||||
|
agent_panel.new_agent_thread_task =
|
||||||
|
cx.spawn_in(window, async move |agent_panel, cx| {
|
||||||
|
wait_task.await;
|
||||||
|
let server = ext_agent.server(fs, history);
|
||||||
|
agent_panel
|
||||||
|
.update_in(cx, |agent_panel, window, cx| {
|
||||||
|
agent_panel.show_trust_workspace_message = false;
|
||||||
|
cx.notify();
|
||||||
|
agent_panel._external_thread(
|
||||||
|
server,
|
||||||
|
resume_thread,
|
||||||
|
summarize_thread,
|
||||||
|
workspace,
|
||||||
|
project,
|
||||||
|
loading,
|
||||||
|
ext_agent,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
});
|
||||||
|
})?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
let server = ext_agent.server(fs, history);
|
||||||
let selected_agent = ext_agent.into();
|
this.update_in(cx, |agent_panel, window, cx| {
|
||||||
if this.selected_agent != selected_agent {
|
agent_panel._external_thread(
|
||||||
this.selected_agent = selected_agent;
|
server,
|
||||||
this.serialize(cx);
|
resume_thread,
|
||||||
}
|
summarize_thread,
|
||||||
|
workspace,
|
||||||
let thread_view = cx.new(|cx| {
|
project,
|
||||||
crate::acp::AcpThreadView::new(
|
loading,
|
||||||
server,
|
ext_agent,
|
||||||
resume_thread,
|
|
||||||
summarize_thread,
|
|
||||||
workspace.clone(),
|
|
||||||
project,
|
|
||||||
this.history_store.clone(),
|
|
||||||
this.prompt_store.clone(),
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
this.set_active_view(
|
|
||||||
ActiveView::ExternalAgentThread { thread_view },
|
|
||||||
!loading,
|
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
})
|
})?;
|
||||||
|
|
||||||
|
anyhow::Ok(())
|
||||||
})
|
})
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
}
|
}
|
||||||
@@ -1425,6 +1510,36 @@ impl AgentPanel {
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
|
let wait_task = if agent.is_mcp() {
|
||||||
|
self.project.update(cx, |project, cx| {
|
||||||
|
wait_for_workspace_trust(
|
||||||
|
project.remote_connection_options(cx),
|
||||||
|
"context servers",
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
if let Some(wait_task) = wait_task {
|
||||||
|
self.show_trust_workspace_message = true;
|
||||||
|
cx.notify();
|
||||||
|
self.new_agent_thread_task = cx.spawn_in(window, async move |agent_panel, cx| {
|
||||||
|
wait_task.await;
|
||||||
|
agent_panel
|
||||||
|
.update_in(cx, |agent_panel, window, cx| {
|
||||||
|
agent_panel.show_trust_workspace_message = false;
|
||||||
|
cx.notify();
|
||||||
|
agent_panel._new_agent_thread(agent, window, cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
self._new_agent_thread(agent, window, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _new_agent_thread(&mut self, agent: AgentType, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
match agent {
|
match agent {
|
||||||
AgentType::TextThread => {
|
AgentType::TextThread => {
|
||||||
window.dispatch_action(NewTextThread.boxed_clone(), cx);
|
window.dispatch_action(NewTextThread.boxed_clone(), cx);
|
||||||
@@ -1479,6 +1594,47 @@ impl AgentPanel {
|
|||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn _external_thread(
|
||||||
|
&mut self,
|
||||||
|
server: Rc<dyn AgentServer>,
|
||||||
|
resume_thread: Option<DbThreadMetadata>,
|
||||||
|
summarize_thread: Option<DbThreadMetadata>,
|
||||||
|
workspace: WeakEntity<Workspace>,
|
||||||
|
project: Entity<Project>,
|
||||||
|
loading: bool,
|
||||||
|
ext_agent: ExternalAgent,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let selected_agent = AgentType::from(ext_agent);
|
||||||
|
if self.selected_agent != selected_agent {
|
||||||
|
self.selected_agent = selected_agent;
|
||||||
|
self.serialize(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
let thread_view = cx.new(|cx| {
|
||||||
|
crate::acp::AcpThreadView::new(
|
||||||
|
server,
|
||||||
|
resume_thread,
|
||||||
|
summarize_thread,
|
||||||
|
workspace.clone(),
|
||||||
|
project,
|
||||||
|
self.history_store.clone(),
|
||||||
|
self.prompt_store.clone(),
|
||||||
|
!loading,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
self.set_active_view(
|
||||||
|
ActiveView::ExternalAgentThread { thread_view },
|
||||||
|
!loading,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Focusable for AgentPanel {
|
impl Focusable for AgentPanel {
|
||||||
@@ -1853,14 +2009,17 @@ impl AgentPanel {
|
|||||||
let agent_server_store = self.project.read(cx).agent_server_store().clone();
|
let agent_server_store = self.project.read(cx).agent_server_store().clone();
|
||||||
let focus_handle = self.focus_handle(cx);
|
let focus_handle = self.focus_handle(cx);
|
||||||
|
|
||||||
// Get custom icon path for selected agent before building menu (to avoid borrow issues)
|
let (selected_agent_custom_icon, selected_agent_label) =
|
||||||
let selected_agent_custom_icon =
|
|
||||||
if let AgentType::Custom { name, .. } = &self.selected_agent {
|
if let AgentType::Custom { name, .. } = &self.selected_agent {
|
||||||
agent_server_store
|
let store = agent_server_store.read(cx);
|
||||||
.read(cx)
|
let icon = store.agent_icon(&ExternalAgentServerName(name.clone()));
|
||||||
.agent_icon(&ExternalAgentServerName(name.clone()))
|
|
||||||
|
let label = store
|
||||||
|
.agent_display_name(&ExternalAgentServerName(name.clone()))
|
||||||
|
.unwrap_or_else(|| self.selected_agent.label());
|
||||||
|
(icon, label)
|
||||||
} else {
|
} else {
|
||||||
None
|
(None, self.selected_agent.label())
|
||||||
};
|
};
|
||||||
|
|
||||||
let active_thread = match &self.active_view {
|
let active_thread = match &self.active_view {
|
||||||
@@ -2083,13 +2242,16 @@ impl AgentPanel {
|
|||||||
|
|
||||||
for agent_name in agent_names {
|
for agent_name in agent_names {
|
||||||
let icon_path = agent_server_store.agent_icon(&agent_name);
|
let icon_path = agent_server_store.agent_icon(&agent_name);
|
||||||
|
let display_name = agent_server_store
|
||||||
|
.agent_display_name(&agent_name)
|
||||||
|
.unwrap_or_else(|| agent_name.0.clone());
|
||||||
|
|
||||||
let mut entry = ContextMenuEntry::new(agent_name.clone());
|
let mut entry = ContextMenuEntry::new(display_name);
|
||||||
|
|
||||||
if let Some(icon_path) = icon_path {
|
if let Some(icon_path) = icon_path {
|
||||||
entry = entry.custom_icon_svg(icon_path);
|
entry = entry.custom_icon_svg(icon_path);
|
||||||
} else {
|
} else {
|
||||||
entry = entry.icon(IconName::Terminal);
|
entry = entry.icon(IconName::Sparkle);
|
||||||
}
|
}
|
||||||
entry = entry
|
entry = entry
|
||||||
.when(
|
.when(
|
||||||
@@ -2153,8 +2315,6 @@ impl AgentPanel {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let selected_agent_label = self.selected_agent.label();
|
|
||||||
|
|
||||||
let is_thread_loading = self
|
let is_thread_loading = self
|
||||||
.active_thread_view()
|
.active_thread_view()
|
||||||
.map(|thread| thread.read(cx).is_loading())
|
.map(|thread| thread.read(cx).is_loading())
|
||||||
@@ -2555,6 +2715,38 @@ impl AgentPanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_workspace_trust_message(&self, cx: &Context<Self>) -> Option<impl IntoElement> {
|
||||||
|
if !self.show_trust_workspace_message {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let description = "To protect your system, third-party code—like MCP servers—won't run until you mark this workspace as safe.";
|
||||||
|
|
||||||
|
Some(
|
||||||
|
Callout::new()
|
||||||
|
.icon(IconName::Warning)
|
||||||
|
.severity(Severity::Warning)
|
||||||
|
.border_position(ui::BorderPosition::Bottom)
|
||||||
|
.title("You're in Restricted Mode")
|
||||||
|
.description(description)
|
||||||
|
.actions_slot(
|
||||||
|
Button::new("open-trust-modal", "Configure Project Trust")
|
||||||
|
.label_size(LabelSize::Small)
|
||||||
|
.style(ButtonStyle::Outlined)
|
||||||
|
.on_click({
|
||||||
|
cx.listener(move |this, _, window, cx| {
|
||||||
|
this.workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
workspace
|
||||||
|
.show_worktree_trust_security_modal(true, window, cx)
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn key_context(&self) -> KeyContext {
|
fn key_context(&self) -> KeyContext {
|
||||||
let mut key_context = KeyContext::new_with_defaults();
|
let mut key_context = KeyContext::new_with_defaults();
|
||||||
key_context.add("AgentPanel");
|
key_context.add("AgentPanel");
|
||||||
@@ -2607,6 +2799,7 @@ impl Render for AgentPanel {
|
|||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
.child(self.render_toolbar(window, cx))
|
.child(self.render_toolbar(window, cx))
|
||||||
|
.children(self.render_workspace_trust_message(cx))
|
||||||
.children(self.render_onboarding(window, cx))
|
.children(self.render_onboarding(window, cx))
|
||||||
.map(|parent| match &self.active_view {
|
.map(|parent| match &self.active_view {
|
||||||
ActiveView::ExternalAgentThread { thread_view, .. } => parent
|
ActiveView::ExternalAgentThread { thread_view, .. } => parent
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
mod acp;
|
pub mod acp;
|
||||||
mod agent_configuration;
|
mod agent_configuration;
|
||||||
mod agent_diff;
|
mod agent_diff;
|
||||||
mod agent_model_selector;
|
mod agent_model_selector;
|
||||||
@@ -7,8 +7,6 @@ mod buffer_codegen;
|
|||||||
mod completion_provider;
|
mod completion_provider;
|
||||||
mod context;
|
mod context;
|
||||||
mod context_server_configuration;
|
mod context_server_configuration;
|
||||||
#[cfg(test)]
|
|
||||||
mod evals;
|
|
||||||
mod inline_assistant;
|
mod inline_assistant;
|
||||||
mod inline_prompt_editor;
|
mod inline_prompt_editor;
|
||||||
mod language_model_selector;
|
mod language_model_selector;
|
||||||
@@ -28,7 +26,7 @@ use agent_settings::{AgentProfileId, AgentSettings};
|
|||||||
use assistant_slash_command::SlashCommandRegistry;
|
use assistant_slash_command::SlashCommandRegistry;
|
||||||
use client::Client;
|
use client::Client;
|
||||||
use command_palette_hooks::CommandPaletteFilter;
|
use command_palette_hooks::CommandPaletteFilter;
|
||||||
use feature_flags::FeatureFlagAppExt as _;
|
use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt as _};
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use gpui::{Action, App, Entity, SharedString, actions};
|
use gpui::{Action, App, Entity, SharedString, actions};
|
||||||
use language::{
|
use language::{
|
||||||
@@ -160,16 +158,6 @@ pub enum ExternalAgent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ExternalAgent {
|
impl ExternalAgent {
|
||||||
pub fn parse_built_in(server: &dyn agent_servers::AgentServer) -> Option<Self> {
|
|
||||||
match server.telemetry_id() {
|
|
||||||
"gemini-cli" => Some(Self::Gemini),
|
|
||||||
"claude-code" => Some(Self::ClaudeCode),
|
|
||||||
"codex" => Some(Self::Codex),
|
|
||||||
"zed" => Some(Self::NativeAgent),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn server(
|
pub fn server(
|
||||||
&self,
|
&self,
|
||||||
fs: Arc<dyn fs::Fs>,
|
fs: Arc<dyn fs::Fs>,
|
||||||
@@ -183,6 +171,16 @@ impl ExternalAgent {
|
|||||||
Self::Custom { name } => Rc::new(agent_servers::CustomAgentServer::new(name.clone())),
|
Self::Custom { name } => Rc::new(agent_servers::CustomAgentServer::new(name.clone())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_mcp(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Gemini => true,
|
||||||
|
Self::ClaudeCode => true,
|
||||||
|
Self::Codex => true,
|
||||||
|
Self::NativeAgent => false,
|
||||||
|
Self::Custom { .. } => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Opens the profile management interface for configuring agent tools and settings.
|
/// Opens the profile management interface for configuring agent tools and settings.
|
||||||
@@ -226,7 +224,7 @@ pub fn init(
|
|||||||
is_eval: bool,
|
is_eval: bool,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) {
|
) {
|
||||||
assistant_text_thread::init(client.clone(), cx);
|
assistant_text_thread::init(client, cx);
|
||||||
rules_library::init(cx);
|
rules_library::init(cx);
|
||||||
if !is_eval {
|
if !is_eval {
|
||||||
// Initializing the language model from the user settings messes with the eval, so we only initialize them when
|
// Initializing the language model from the user settings messes with the eval, so we only initialize them when
|
||||||
@@ -239,13 +237,8 @@ pub fn init(
|
|||||||
TextThreadEditor::init(cx);
|
TextThreadEditor::init(cx);
|
||||||
|
|
||||||
register_slash_commands(cx);
|
register_slash_commands(cx);
|
||||||
inline_assistant::init(
|
inline_assistant::init(fs.clone(), prompt_builder.clone(), cx);
|
||||||
fs.clone(),
|
terminal_inline_assistant::init(fs.clone(), prompt_builder, cx);
|
||||||
prompt_builder.clone(),
|
|
||||||
client.telemetry().clone(),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
terminal_inline_assistant::init(fs.clone(), prompt_builder, client.telemetry().clone(), cx);
|
|
||||||
cx.observe_new(move |workspace, window, cx| {
|
cx.observe_new(move |workspace, window, cx| {
|
||||||
ConfigureContextServerModal::register(workspace, language_registry.clone(), window, cx)
|
ConfigureContextServerModal::register(workspace, language_registry.clone(), window, cx)
|
||||||
})
|
})
|
||||||
@@ -261,23 +254,31 @@ pub fn init(
|
|||||||
update_command_palette_filter(app_cx);
|
update_command_palette_filter(app_cx);
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
|
cx.on_flags_ready(|_, cx| {
|
||||||
|
update_command_palette_filter(cx);
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_command_palette_filter(cx: &mut App) {
|
fn update_command_palette_filter(cx: &mut App) {
|
||||||
let disable_ai = DisableAiSettings::get_global(cx).disable_ai;
|
let disable_ai = DisableAiSettings::get_global(cx).disable_ai;
|
||||||
let agent_enabled = AgentSettings::get_global(cx).enabled;
|
let agent_enabled = AgentSettings::get_global(cx).enabled;
|
||||||
|
let agent_v2_enabled = cx.has_flag::<AgentV2FeatureFlag>();
|
||||||
let edit_prediction_provider = AllLanguageSettings::get_global(cx)
|
let edit_prediction_provider = AllLanguageSettings::get_global(cx)
|
||||||
.edit_predictions
|
.edit_predictions
|
||||||
.provider;
|
.provider;
|
||||||
|
|
||||||
CommandPaletteFilter::update_global(cx, |filter, _| {
|
CommandPaletteFilter::update_global(cx, |filter, _| {
|
||||||
use editor::actions::{
|
use editor::actions::{
|
||||||
AcceptEditPrediction, AcceptPartialEditPrediction, NextEditPrediction,
|
AcceptEditPrediction, AcceptNextLineEditPrediction, AcceptNextWordEditPrediction,
|
||||||
PreviousEditPrediction, ShowEditPrediction, ToggleEditPrediction,
|
NextEditPrediction, PreviousEditPrediction, ShowEditPrediction, ToggleEditPrediction,
|
||||||
};
|
};
|
||||||
let edit_prediction_actions = [
|
let edit_prediction_actions = [
|
||||||
TypeId::of::<AcceptEditPrediction>(),
|
TypeId::of::<AcceptEditPrediction>(),
|
||||||
TypeId::of::<AcceptPartialEditPrediction>(),
|
TypeId::of::<AcceptNextWordEditPrediction>(),
|
||||||
|
TypeId::of::<AcceptNextLineEditPrediction>(),
|
||||||
|
TypeId::of::<AcceptEditPrediction>(),
|
||||||
TypeId::of::<ShowEditPrediction>(),
|
TypeId::of::<ShowEditPrediction>(),
|
||||||
TypeId::of::<NextEditPrediction>(),
|
TypeId::of::<NextEditPrediction>(),
|
||||||
TypeId::of::<PreviousEditPrediction>(),
|
TypeId::of::<PreviousEditPrediction>(),
|
||||||
@@ -286,6 +287,7 @@ fn update_command_palette_filter(cx: &mut App) {
|
|||||||
|
|
||||||
if disable_ai {
|
if disable_ai {
|
||||||
filter.hide_namespace("agent");
|
filter.hide_namespace("agent");
|
||||||
|
filter.hide_namespace("agents");
|
||||||
filter.hide_namespace("assistant");
|
filter.hide_namespace("assistant");
|
||||||
filter.hide_namespace("copilot");
|
filter.hide_namespace("copilot");
|
||||||
filter.hide_namespace("supermaven");
|
filter.hide_namespace("supermaven");
|
||||||
@@ -297,8 +299,10 @@ fn update_command_palette_filter(cx: &mut App) {
|
|||||||
} else {
|
} else {
|
||||||
if agent_enabled {
|
if agent_enabled {
|
||||||
filter.show_namespace("agent");
|
filter.show_namespace("agent");
|
||||||
|
filter.show_namespace("agents");
|
||||||
} else {
|
} else {
|
||||||
filter.hide_namespace("agent");
|
filter.hide_namespace("agent");
|
||||||
|
filter.hide_namespace("agents");
|
||||||
}
|
}
|
||||||
|
|
||||||
filter.show_namespace("assistant");
|
filter.show_namespace("assistant");
|
||||||
@@ -334,6 +338,9 @@ fn update_command_palette_filter(cx: &mut App) {
|
|||||||
|
|
||||||
filter.show_namespace("zed_predict_onboarding");
|
filter.show_namespace("zed_predict_onboarding");
|
||||||
filter.show_action_types(&[TypeId::of::<zed_actions::OpenZedPredictOnboarding>()]);
|
filter.show_action_types(&[TypeId::of::<zed_actions::OpenZedPredictOnboarding>()]);
|
||||||
|
if !agent_v2_enabled {
|
||||||
|
filter.hide_action_types(&[TypeId::of::<zed_actions::agent::ToggleAgentPane>()]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -432,7 +439,7 @@ mod tests {
|
|||||||
use gpui::{BorrowAppContext, TestAppContext, px};
|
use gpui::{BorrowAppContext, TestAppContext, px};
|
||||||
use project::DisableAiSettings;
|
use project::DisableAiSettings;
|
||||||
use settings::{
|
use settings::{
|
||||||
DefaultAgentView, DockPosition, NotifyWhenAgentWaiting, Settings, SettingsStore,
|
DefaultAgentView, DockPosition, DockSide, NotifyWhenAgentWaiting, Settings, SettingsStore,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
@@ -451,10 +458,12 @@ mod tests {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
button: true,
|
button: true,
|
||||||
dock: DockPosition::Right,
|
dock: DockPosition::Right,
|
||||||
|
agents_panel_dock: DockSide::Left,
|
||||||
default_width: px(300.),
|
default_width: px(300.),
|
||||||
default_height: px(600.),
|
default_height: px(600.),
|
||||||
default_model: None,
|
default_model: None,
|
||||||
inline_assistant_model: None,
|
inline_assistant_model: None,
|
||||||
|
inline_assistant_use_streaming_tools: false,
|
||||||
commit_message_model: None,
|
commit_message_model: None,
|
||||||
thread_summary_model: None,
|
thread_summary_model: None,
|
||||||
inline_alternatives: vec![],
|
inline_alternatives: vec![],
|
||||||
|
|||||||
@@ -1,23 +1,26 @@
|
|||||||
use crate::{context::LoadedContext, inline_prompt_editor::CodegenStatus};
|
use crate::{context::LoadedContext, inline_prompt_editor::CodegenStatus};
|
||||||
use agent_settings::AgentSettings;
|
use agent_settings::AgentSettings;
|
||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result};
|
||||||
use client::telemetry::Telemetry;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use cloud_llm_client::CompletionIntent;
|
use cloud_llm_client::CompletionIntent;
|
||||||
use collections::HashSet;
|
use collections::HashSet;
|
||||||
use editor::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint};
|
use editor::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint};
|
||||||
use feature_flags::{FeatureFlagAppExt as _, InlineAssistantV2FeatureFlag};
|
use feature_flags::{FeatureFlagAppExt as _, InlineAssistantUseToolFeatureFlag};
|
||||||
use futures::{
|
use futures::{
|
||||||
SinkExt, Stream, StreamExt, TryStreamExt as _,
|
SinkExt, Stream, StreamExt, TryStreamExt as _,
|
||||||
channel::mpsc,
|
channel::mpsc,
|
||||||
future::{LocalBoxFuture, Shared},
|
future::{LocalBoxFuture, Shared},
|
||||||
join,
|
join,
|
||||||
|
stream::BoxStream,
|
||||||
};
|
};
|
||||||
use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Subscription, Task};
|
use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Subscription, Task};
|
||||||
use language::{Buffer, IndentKind, Point, TransactionId, line_diff};
|
use language::{Buffer, IndentKind, LanguageName, Point, TransactionId, line_diff};
|
||||||
use language_model::{
|
use language_model::{
|
||||||
LanguageModel, LanguageModelCompletionError, LanguageModelRegistry, LanguageModelRequest,
|
LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
|
||||||
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelTextStream, Role,
|
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||||
report_assistant_event,
|
LanguageModelRequestTool, LanguageModelTextStream, LanguageModelToolChoice,
|
||||||
|
LanguageModelToolUse, Role, TokenUsage,
|
||||||
};
|
};
|
||||||
use multi_buffer::MultiBufferRow;
|
use multi_buffer::MultiBufferRow;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
@@ -25,6 +28,7 @@ use prompt_store::PromptBuilder;
|
|||||||
use rope::Rope;
|
use rope::Rope;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use settings::Settings as _;
|
||||||
use smol::future::FutureExt;
|
use smol::future::FutureExt;
|
||||||
use std::{
|
use std::{
|
||||||
cmp,
|
cmp,
|
||||||
@@ -37,28 +41,24 @@ use std::{
|
|||||||
time::Instant,
|
time::Instant,
|
||||||
};
|
};
|
||||||
use streaming_diff::{CharOperation, LineDiff, LineOperation, StreamingDiff};
|
use streaming_diff::{CharOperation, LineDiff, LineOperation, StreamingDiff};
|
||||||
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
|
|
||||||
use ui::SharedString;
|
|
||||||
|
|
||||||
/// Use this tool to provide a message to the user when you're unable to complete a task.
|
/// Use this tool when you cannot or should not make a rewrite. This includes:
|
||||||
|
/// - The user's request is unclear, ambiguous, or nonsensical
|
||||||
|
/// - The requested change cannot be made by only editing the <rewrite_this> section
|
||||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||||
pub struct FailureMessageInput {
|
pub struct FailureMessageInput {
|
||||||
/// A brief message to the user explaining why you're unable to fulfill the request or to ask a question about the request.
|
/// A brief message to the user explaining why you're unable to fulfill the request or to ask a question about the request.
|
||||||
///
|
#[serde(default)]
|
||||||
/// The message may use markdown formatting if you wish.
|
|
||||||
pub message: String,
|
pub message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Replaces text in <rewrite_this></rewrite_this> tags with your replacement_text.
|
/// Replaces text in <rewrite_this></rewrite_this> tags with your replacement_text.
|
||||||
|
/// Only use this tool when you are confident you understand the user's request and can fulfill it
|
||||||
|
/// by editing the marked section.
|
||||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||||
pub struct RewriteSectionInput {
|
pub struct RewriteSectionInput {
|
||||||
/// A brief description of the edit you have made.
|
|
||||||
///
|
|
||||||
/// The description may use markdown formatting if you wish.
|
|
||||||
/// This is optional - if the edit is simple or obvious, you should leave it empty.
|
|
||||||
pub description: String,
|
|
||||||
|
|
||||||
/// The text to replace the section with.
|
/// The text to replace the section with.
|
||||||
|
#[serde(default)]
|
||||||
pub replacement_text: String,
|
pub replacement_text: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,9 +70,9 @@ pub struct BufferCodegen {
|
|||||||
buffer: Entity<MultiBuffer>,
|
buffer: Entity<MultiBuffer>,
|
||||||
range: Range<Anchor>,
|
range: Range<Anchor>,
|
||||||
initial_transaction_id: Option<TransactionId>,
|
initial_transaction_id: Option<TransactionId>,
|
||||||
telemetry: Arc<Telemetry>,
|
|
||||||
builder: Arc<PromptBuilder>,
|
builder: Arc<PromptBuilder>,
|
||||||
pub is_insertion: bool,
|
pub is_insertion: bool,
|
||||||
|
session_id: Uuid,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BufferCodegen {
|
impl BufferCodegen {
|
||||||
@@ -80,7 +80,7 @@ impl BufferCodegen {
|
|||||||
buffer: Entity<MultiBuffer>,
|
buffer: Entity<MultiBuffer>,
|
||||||
range: Range<Anchor>,
|
range: Range<Anchor>,
|
||||||
initial_transaction_id: Option<TransactionId>,
|
initial_transaction_id: Option<TransactionId>,
|
||||||
telemetry: Arc<Telemetry>,
|
session_id: Uuid,
|
||||||
builder: Arc<PromptBuilder>,
|
builder: Arc<PromptBuilder>,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
@@ -89,8 +89,8 @@ impl BufferCodegen {
|
|||||||
buffer.clone(),
|
buffer.clone(),
|
||||||
range.clone(),
|
range.clone(),
|
||||||
false,
|
false,
|
||||||
Some(telemetry.clone()),
|
|
||||||
builder.clone(),
|
builder.clone(),
|
||||||
|
session_id,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
@@ -103,8 +103,8 @@ impl BufferCodegen {
|
|||||||
buffer,
|
buffer,
|
||||||
range,
|
range,
|
||||||
initial_transaction_id,
|
initial_transaction_id,
|
||||||
telemetry,
|
|
||||||
builder,
|
builder,
|
||||||
|
session_id,
|
||||||
};
|
};
|
||||||
this.activate(0, cx);
|
this.activate(0, cx);
|
||||||
this
|
this
|
||||||
@@ -119,10 +119,18 @@ impl BufferCodegen {
|
|||||||
.push(cx.subscribe(&codegen, |_, _, event, cx| cx.emit(*event)));
|
.push(cx.subscribe(&codegen, |_, _, event, cx| cx.emit(*event)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn active_completion(&self, cx: &App) -> Option<String> {
|
||||||
|
self.active_alternative().read(cx).current_completion()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn active_alternative(&self) -> &Entity<CodegenAlternative> {
|
pub fn active_alternative(&self) -> &Entity<CodegenAlternative> {
|
||||||
&self.alternatives[self.active_alternative]
|
&self.alternatives[self.active_alternative]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn language_name(&self, cx: &App) -> Option<LanguageName> {
|
||||||
|
self.active_alternative().read(cx).language_name(cx)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn status<'a>(&self, cx: &'a App) -> &'a CodegenStatus {
|
pub fn status<'a>(&self, cx: &'a App) -> &'a CodegenStatus {
|
||||||
&self.active_alternative().read(cx).status
|
&self.active_alternative().read(cx).status
|
||||||
}
|
}
|
||||||
@@ -181,8 +189,8 @@ impl BufferCodegen {
|
|||||||
self.buffer.clone(),
|
self.buffer.clone(),
|
||||||
self.range.clone(),
|
self.range.clone(),
|
||||||
false,
|
false,
|
||||||
Some(self.telemetry.clone()),
|
|
||||||
self.builder.clone(),
|
self.builder.clone(),
|
||||||
|
self.session_id,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
}));
|
}));
|
||||||
@@ -241,6 +249,14 @@ impl BufferCodegen {
|
|||||||
pub fn last_equal_ranges<'a>(&self, cx: &'a App) -> &'a [Range<Anchor>] {
|
pub fn last_equal_ranges<'a>(&self, cx: &'a App) -> &'a [Range<Anchor>] {
|
||||||
self.active_alternative().read(cx).last_equal_ranges()
|
self.active_alternative().read(cx).last_equal_ranges()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn selected_text<'a>(&self, cx: &'a App) -> Option<&'a str> {
|
||||||
|
self.active_alternative().read(cx).selected_text()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn session_id(&self) -> Uuid {
|
||||||
|
self.session_id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<CodegenEvent> for BufferCodegen {}
|
impl EventEmitter<CodegenEvent> for BufferCodegen {}
|
||||||
@@ -256,7 +272,6 @@ pub struct CodegenAlternative {
|
|||||||
status: CodegenStatus,
|
status: CodegenStatus,
|
||||||
generation: Task<()>,
|
generation: Task<()>,
|
||||||
diff: Diff,
|
diff: Diff,
|
||||||
telemetry: Option<Arc<Telemetry>>,
|
|
||||||
_subscription: gpui::Subscription,
|
_subscription: gpui::Subscription,
|
||||||
builder: Arc<PromptBuilder>,
|
builder: Arc<PromptBuilder>,
|
||||||
active: bool,
|
active: bool,
|
||||||
@@ -264,8 +279,11 @@ pub struct CodegenAlternative {
|
|||||||
line_operations: Vec<LineOperation>,
|
line_operations: Vec<LineOperation>,
|
||||||
elapsed_time: Option<f64>,
|
elapsed_time: Option<f64>,
|
||||||
completion: Option<String>,
|
completion: Option<String>,
|
||||||
|
selected_text: Option<String>,
|
||||||
pub message_id: Option<String>,
|
pub message_id: Option<String>,
|
||||||
pub model_explanation: Option<SharedString>,
|
session_id: Uuid,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub failure: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<CodegenEvent> for CodegenAlternative {}
|
impl EventEmitter<CodegenEvent> for CodegenAlternative {}
|
||||||
@@ -275,8 +293,8 @@ impl CodegenAlternative {
|
|||||||
buffer: Entity<MultiBuffer>,
|
buffer: Entity<MultiBuffer>,
|
||||||
range: Range<Anchor>,
|
range: Range<Anchor>,
|
||||||
active: bool,
|
active: bool,
|
||||||
telemetry: Option<Arc<Telemetry>>,
|
|
||||||
builder: Arc<PromptBuilder>,
|
builder: Arc<PromptBuilder>,
|
||||||
|
session_id: Uuid,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let snapshot = buffer.read(cx).snapshot(cx);
|
let snapshot = buffer.read(cx).snapshot(cx);
|
||||||
@@ -315,7 +333,6 @@ impl CodegenAlternative {
|
|||||||
status: CodegenStatus::Idle,
|
status: CodegenStatus::Idle,
|
||||||
generation: Task::ready(()),
|
generation: Task::ready(()),
|
||||||
diff: Diff::default(),
|
diff: Diff::default(),
|
||||||
telemetry,
|
|
||||||
builder,
|
builder,
|
||||||
active: active,
|
active: active,
|
||||||
edits: Vec::new(),
|
edits: Vec::new(),
|
||||||
@@ -323,11 +340,21 @@ impl CodegenAlternative {
|
|||||||
range,
|
range,
|
||||||
elapsed_time: None,
|
elapsed_time: None,
|
||||||
completion: None,
|
completion: None,
|
||||||
model_explanation: None,
|
selected_text: None,
|
||||||
|
session_id,
|
||||||
|
description: None,
|
||||||
|
failure: None,
|
||||||
_subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
|
_subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn language_name(&self, cx: &App) -> Option<LanguageName> {
|
||||||
|
self.old_buffer
|
||||||
|
.read(cx)
|
||||||
|
.language()
|
||||||
|
.map(|language| language.name())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_active(&mut self, active: bool, cx: &mut Context<Self>) {
|
pub fn set_active(&mut self, active: bool, cx: &mut Context<Self>) {
|
||||||
if active != self.active {
|
if active != self.active {
|
||||||
self.active = active;
|
self.active = active;
|
||||||
@@ -369,6 +396,12 @@ impl CodegenAlternative {
|
|||||||
&self.last_equal_ranges
|
&self.last_equal_ranges
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn use_streaming_tools(model: &dyn LanguageModel, cx: &App) -> bool {
|
||||||
|
model.supports_streaming_tools()
|
||||||
|
&& cx.has_flag::<InlineAssistantUseToolFeatureFlag>()
|
||||||
|
&& AgentSettings::get_global(cx).inline_assistant_use_streaming_tools
|
||||||
|
}
|
||||||
|
|
||||||
pub fn start(
|
pub fn start(
|
||||||
&mut self,
|
&mut self,
|
||||||
user_prompt: String,
|
user_prompt: String,
|
||||||
@@ -376,6 +409,9 @@ impl CodegenAlternative {
|
|||||||
model: Arc<dyn LanguageModel>,
|
model: Arc<dyn LanguageModel>,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
// Clear the model explanation since the user has started a new generation.
|
||||||
|
self.description = None;
|
||||||
|
|
||||||
if let Some(transformation_transaction_id) = self.transformation_transaction_id.take() {
|
if let Some(transformation_transaction_id) = self.transformation_transaction_id.take() {
|
||||||
self.buffer.update(cx, |buffer, cx| {
|
self.buffer.update(cx, |buffer, cx| {
|
||||||
buffer.undo_transaction(transformation_transaction_id, cx);
|
buffer.undo_transaction(transformation_transaction_id, cx);
|
||||||
@@ -384,33 +420,34 @@ impl CodegenAlternative {
|
|||||||
|
|
||||||
self.edit_position = Some(self.range.start.bias_right(&self.snapshot));
|
self.edit_position = Some(self.range.start.bias_right(&self.snapshot));
|
||||||
|
|
||||||
let api_key = model.api_key(cx);
|
if Self::use_streaming_tools(model.as_ref(), cx) {
|
||||||
let telemetry_id = model.telemetry_id();
|
|
||||||
let provider_id = model.provider_id();
|
|
||||||
|
|
||||||
if cx.has_flag::<InlineAssistantV2FeatureFlag>() {
|
|
||||||
let request = self.build_request(&model, user_prompt, context_task, cx)?;
|
let request = self.build_request(&model, user_prompt, context_task, cx)?;
|
||||||
let tool_use =
|
let completion_events = cx.spawn({
|
||||||
cx.spawn(async move |_, cx| model.stream_completion_tool(request.await, cx).await);
|
let model = model.clone();
|
||||||
self.handle_tool_use(telemetry_id, provider_id.to_string(), api_key, tool_use, cx);
|
async move |_, cx| model.stream_completion(request.await, cx).await
|
||||||
|
});
|
||||||
|
self.generation = self.handle_completion(model, completion_events, cx);
|
||||||
} else {
|
} else {
|
||||||
let stream: LocalBoxFuture<Result<LanguageModelTextStream>> =
|
let stream: LocalBoxFuture<Result<LanguageModelTextStream>> =
|
||||||
if user_prompt.trim().to_lowercase() == "delete" {
|
if user_prompt.trim().to_lowercase() == "delete" {
|
||||||
async { Ok(LanguageModelTextStream::default()) }.boxed_local()
|
async { Ok(LanguageModelTextStream::default()) }.boxed_local()
|
||||||
} else {
|
} else {
|
||||||
let request = self.build_request(&model, user_prompt, context_task, cx)?;
|
let request = self.build_request(&model, user_prompt, context_task, cx)?;
|
||||||
cx.spawn(async move |_, cx| {
|
cx.spawn({
|
||||||
Ok(model.stream_completion_text(request.await, cx).await?)
|
let model = model.clone();
|
||||||
|
async move |_, cx| {
|
||||||
|
Ok(model.stream_completion_text(request.await, cx).await?)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.boxed_local()
|
.boxed_local()
|
||||||
};
|
};
|
||||||
self.handle_stream(telemetry_id, provider_id.to_string(), api_key, stream, cx);
|
self.generation = self.handle_stream(model, stream, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_request_v2(
|
fn build_request_tools(
|
||||||
&self,
|
&self,
|
||||||
model: &Arc<dyn LanguageModel>,
|
model: &Arc<dyn LanguageModel>,
|
||||||
user_prompt: String,
|
user_prompt: String,
|
||||||
@@ -446,7 +483,7 @@ impl CodegenAlternative {
|
|||||||
|
|
||||||
let system_prompt = self
|
let system_prompt = self
|
||||||
.builder
|
.builder
|
||||||
.generate_inline_transformation_prompt_v2(
|
.generate_inline_transformation_prompt_tools(
|
||||||
language_name,
|
language_name,
|
||||||
buffer,
|
buffer,
|
||||||
range.start.0..range.end.0,
|
range.start.0..range.end.0,
|
||||||
@@ -456,6 +493,9 @@ impl CodegenAlternative {
|
|||||||
let temperature = AgentSettings::temperature_for_model(model, cx);
|
let temperature = AgentSettings::temperature_for_model(model, cx);
|
||||||
|
|
||||||
let tool_input_format = model.tool_input_format();
|
let tool_input_format = model.tool_input_format();
|
||||||
|
let tool_choice = model
|
||||||
|
.supports_tool_choice(LanguageModelToolChoice::Any)
|
||||||
|
.then_some(LanguageModelToolChoice::Any);
|
||||||
|
|
||||||
Ok(cx.spawn(async move |_cx| {
|
Ok(cx.spawn(async move |_cx| {
|
||||||
let mut messages = vec![LanguageModelRequestMessage {
|
let mut messages = vec![LanguageModelRequestMessage {
|
||||||
@@ -498,7 +538,7 @@ impl CodegenAlternative {
|
|||||||
intent: Some(CompletionIntent::InlineAssist),
|
intent: Some(CompletionIntent::InlineAssist),
|
||||||
mode: None,
|
mode: None,
|
||||||
tools,
|
tools,
|
||||||
tool_choice: None,
|
tool_choice,
|
||||||
stop: Vec::new(),
|
stop: Vec::new(),
|
||||||
temperature,
|
temperature,
|
||||||
messages,
|
messages,
|
||||||
@@ -514,8 +554,8 @@ impl CodegenAlternative {
|
|||||||
context_task: Shared<Task<Option<LoadedContext>>>,
|
context_task: Shared<Task<Option<LoadedContext>>>,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Result<Task<LanguageModelRequest>> {
|
) -> Result<Task<LanguageModelRequest>> {
|
||||||
if cx.has_flag::<InlineAssistantV2FeatureFlag>() {
|
if Self::use_streaming_tools(model.as_ref(), cx) {
|
||||||
return self.build_request_v2(model, user_prompt, context_task, cx);
|
return self.build_request_tools(model, user_prompt, context_task, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
let buffer = self.buffer.read(cx).snapshot(cx);
|
let buffer = self.buffer.read(cx).snapshot(cx);
|
||||||
@@ -588,12 +628,14 @@ impl CodegenAlternative {
|
|||||||
|
|
||||||
pub fn handle_stream(
|
pub fn handle_stream(
|
||||||
&mut self,
|
&mut self,
|
||||||
model_telemetry_id: String,
|
model: Arc<dyn LanguageModel>,
|
||||||
model_provider_id: String,
|
|
||||||
model_api_key: Option<String>,
|
|
||||||
stream: impl 'static + Future<Output = Result<LanguageModelTextStream>>,
|
stream: impl 'static + Future<Output = Result<LanguageModelTextStream>>,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) -> Task<()> {
|
||||||
|
let anthropic_reporter = language_model::AnthropicEventReporter::new(&model, cx);
|
||||||
|
let session_id = self.session_id;
|
||||||
|
let model_telemetry_id = model.telemetry_id();
|
||||||
|
let model_provider_id = model.provider_id().to_string();
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
|
|
||||||
// Make a new snapshot and re-resolve anchor in case the document was modified.
|
// Make a new snapshot and re-resolve anchor in case the document was modified.
|
||||||
@@ -608,6 +650,8 @@ impl CodegenAlternative {
|
|||||||
.text_for_range(self.range.start..self.range.end)
|
.text_for_range(self.range.start..self.range.end)
|
||||||
.collect::<Rope>();
|
.collect::<Rope>();
|
||||||
|
|
||||||
|
self.selected_text = Some(selected_text.to_string());
|
||||||
|
|
||||||
let selection_start = self.range.start.to_point(&snapshot);
|
let selection_start = self.range.start.to_point(&snapshot);
|
||||||
|
|
||||||
// Start with the indentation of the first line in the selection
|
// Start with the indentation of the first line in the selection
|
||||||
@@ -629,8 +673,6 @@ impl CodegenAlternative {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let http_client = cx.http_client();
|
|
||||||
let telemetry = self.telemetry.clone();
|
|
||||||
let language_name = {
|
let language_name = {
|
||||||
let multibuffer = self.buffer.read(cx);
|
let multibuffer = self.buffer.read(cx);
|
||||||
let snapshot = multibuffer.snapshot(cx);
|
let snapshot = multibuffer.snapshot(cx);
|
||||||
@@ -647,7 +689,8 @@ impl CodegenAlternative {
|
|||||||
let completion = Arc::new(Mutex::new(String::new()));
|
let completion = Arc::new(Mutex::new(String::new()));
|
||||||
let completion_clone = completion.clone();
|
let completion_clone = completion.clone();
|
||||||
|
|
||||||
self.generation = cx.spawn(async move |codegen, cx| {
|
cx.notify();
|
||||||
|
cx.spawn(async move |codegen, cx| {
|
||||||
let stream = stream.await;
|
let stream = stream.await;
|
||||||
|
|
||||||
let token_usage = stream
|
let token_usage = stream
|
||||||
@@ -662,10 +705,11 @@ impl CodegenAlternative {
|
|||||||
let model_telemetry_id = model_telemetry_id.clone();
|
let model_telemetry_id = model_telemetry_id.clone();
|
||||||
let model_provider_id = model_provider_id.clone();
|
let model_provider_id = model_provider_id.clone();
|
||||||
let (mut diff_tx, mut diff_rx) = mpsc::channel(1);
|
let (mut diff_tx, mut diff_rx) = mpsc::channel(1);
|
||||||
let executor = cx.background_executor().clone();
|
|
||||||
let message_id = message_id.clone();
|
let message_id = message_id.clone();
|
||||||
let line_based_stream_diff: Task<anyhow::Result<()>> =
|
let line_based_stream_diff: Task<anyhow::Result<()>> = cx.background_spawn({
|
||||||
cx.background_spawn(async move {
|
let anthropic_reporter = anthropic_reporter.clone();
|
||||||
|
let language_name = language_name.clone();
|
||||||
|
async move {
|
||||||
let mut response_latency = None;
|
let mut response_latency = None;
|
||||||
let request_start = Instant::now();
|
let request_start = Instant::now();
|
||||||
let diff = async {
|
let diff = async {
|
||||||
@@ -673,6 +717,7 @@ impl CodegenAlternative {
|
|||||||
stream?.stream.map_err(|error| error.into()),
|
stream?.stream.map_err(|error| error.into()),
|
||||||
);
|
);
|
||||||
futures::pin_mut!(chunks);
|
futures::pin_mut!(chunks);
|
||||||
|
|
||||||
let mut diff = StreamingDiff::new(selected_text.to_string());
|
let mut diff = StreamingDiff::new(selected_text.to_string());
|
||||||
let mut line_diff = LineDiff::default();
|
let mut line_diff = LineDiff::default();
|
||||||
|
|
||||||
@@ -761,27 +806,30 @@ impl CodegenAlternative {
|
|||||||
let result = diff.await;
|
let result = diff.await;
|
||||||
|
|
||||||
let error_message = result.as_ref().err().map(|error| error.to_string());
|
let error_message = result.as_ref().err().map(|error| error.to_string());
|
||||||
report_assistant_event(
|
telemetry::event!(
|
||||||
AssistantEventData {
|
"Assistant Responded",
|
||||||
conversation_id: None,
|
kind = "inline",
|
||||||
message_id,
|
phase = "response",
|
||||||
kind: AssistantKind::Inline,
|
session_id = session_id.to_string(),
|
||||||
phase: AssistantPhase::Response,
|
model = model_telemetry_id,
|
||||||
model: model_telemetry_id,
|
model_provider = model_provider_id,
|
||||||
model_provider: model_provider_id,
|
language_name = language_name.as_ref().map(|n| n.to_string()),
|
||||||
response_latency,
|
message_id = message_id.as_deref(),
|
||||||
error_message,
|
response_latency = response_latency,
|
||||||
language_name: language_name.map(|name| name.to_proto()),
|
error_message = error_message.as_deref(),
|
||||||
},
|
|
||||||
telemetry,
|
|
||||||
http_client,
|
|
||||||
model_api_key,
|
|
||||||
&executor,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
anthropic_reporter.report(language_model::AnthropicEventData {
|
||||||
|
completion_type: language_model::AnthropicCompletionType::Editor,
|
||||||
|
event: language_model::AnthropicEventType::Response,
|
||||||
|
language_name: language_name.map(|n| n.to_string()),
|
||||||
|
message_id,
|
||||||
|
});
|
||||||
|
|
||||||
result?;
|
result?;
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
|
||||||
while let Some((char_ops, line_ops)) = diff_rx.next().await {
|
while let Some((char_ops, line_ops)) = diff_rx.next().await {
|
||||||
codegen.update(cx, |codegen, cx| {
|
codegen.update(cx, |codegen, cx| {
|
||||||
@@ -864,8 +912,25 @@ impl CodegenAlternative {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
});
|
})
|
||||||
cx.notify();
|
}
|
||||||
|
|
||||||
|
pub fn current_completion(&self) -> Option<String> {
|
||||||
|
self.completion.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub fn current_description(&self) -> Option<String> {
|
||||||
|
self.description.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub fn current_failure(&self) -> Option<String> {
|
||||||
|
self.failure.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn selected_text(&self) -> Option<&str> {
|
||||||
|
self.selected_text.as_deref()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stop(&mut self, cx: &mut Context<Self>) {
|
pub fn stop(&mut self, cx: &mut Context<Self>) {
|
||||||
@@ -1040,21 +1105,27 @@ impl CodegenAlternative {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_tool_use(
|
fn handle_completion(
|
||||||
&mut self,
|
&mut self,
|
||||||
_telemetry_id: String,
|
model: Arc<dyn LanguageModel>,
|
||||||
_provider_id: String,
|
completion_stream: Task<
|
||||||
_api_key: Option<String>,
|
Result<
|
||||||
tool_use: impl 'static
|
BoxStream<
|
||||||
+ Future<
|
'static,
|
||||||
Output = Result<language_model::LanguageModelToolUse, LanguageModelCompletionError>,
|
Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
|
||||||
|
>,
|
||||||
|
LanguageModelCompletionError,
|
||||||
|
>,
|
||||||
>,
|
>,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) -> Task<()> {
|
||||||
self.diff = Diff::default();
|
self.diff = Diff::default();
|
||||||
self.status = CodegenStatus::Pending;
|
self.status = CodegenStatus::Pending;
|
||||||
|
|
||||||
self.generation = cx.spawn(async move |codegen, cx| {
|
cx.notify();
|
||||||
|
// Leaving this in generation so that STOP equivalent events are respected even
|
||||||
|
// while we're still pre-processing the completion event
|
||||||
|
cx.spawn(async move |codegen, cx| {
|
||||||
let finish_with_status = |status: CodegenStatus, cx: &mut AsyncApp| {
|
let finish_with_status = |status: CodegenStatus, cx: &mut AsyncApp| {
|
||||||
let _ = codegen.update(cx, |this, cx| {
|
let _ = codegen.update(cx, |this, cx| {
|
||||||
this.status = status;
|
this.status = status;
|
||||||
@@ -1063,76 +1134,188 @@ impl CodegenAlternative {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
let tool_use = tool_use.await;
|
let mut completion_events = match completion_stream.await {
|
||||||
|
Ok(events) => events,
|
||||||
|
Err(err) => {
|
||||||
|
finish_with_status(CodegenStatus::Error(err.into()), cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
match tool_use {
|
enum ToolUseOutput {
|
||||||
Ok(tool_use) if tool_use.name.as_ref() == "rewrite_section" => {
|
Rewrite {
|
||||||
// Parse the input JSON into RewriteSectionInput
|
text: String,
|
||||||
match serde_json::from_value::<RewriteSectionInput>(tool_use.input) {
|
description: Option<String>,
|
||||||
Ok(input) => {
|
},
|
||||||
// Store the description if non-empty
|
Failure(String),
|
||||||
let description = if !input.description.trim().is_empty() {
|
}
|
||||||
Some(input.description.clone())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
// Apply the replacement text to the buffer and compute diff
|
enum ModelUpdate {
|
||||||
let batch_diff_task = codegen
|
Description(String),
|
||||||
.update(cx, |this, cx| {
|
Failure(String),
|
||||||
this.model_explanation = description.map(Into::into);
|
}
|
||||||
let range = this.range.clone();
|
|
||||||
this.apply_edits(
|
|
||||||
std::iter::once((range, input.replacement_text)),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
this.reapply_batch_diff(cx)
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
// Wait for the diff computation to complete
|
let chars_read_so_far = Arc::new(Mutex::new(0usize));
|
||||||
if let Some(diff_task) = batch_diff_task {
|
let process_tool_use = move |tool_use: LanguageModelToolUse| -> Option<ToolUseOutput> {
|
||||||
diff_task.await;
|
let mut chars_read_so_far = chars_read_so_far.lock();
|
||||||
|
match tool_use.name.as_ref() {
|
||||||
|
"rewrite_section" => {
|
||||||
|
let Ok(input) =
|
||||||
|
serde_json::from_value::<RewriteSectionInput>(tool_use.input)
|
||||||
|
else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let text = input.replacement_text[*chars_read_so_far..].to_string();
|
||||||
|
*chars_read_so_far = input.replacement_text.len();
|
||||||
|
Some(ToolUseOutput::Rewrite {
|
||||||
|
text,
|
||||||
|
description: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"failure_message" => {
|
||||||
|
let Ok(mut input) =
|
||||||
|
serde_json::from_value::<FailureMessageInput>(tool_use.input)
|
||||||
|
else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
Some(ToolUseOutput::Failure(std::mem::take(&mut input.message)))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let (message_tx, mut message_rx) = futures::channel::mpsc::unbounded::<ModelUpdate>();
|
||||||
|
|
||||||
|
cx.spawn({
|
||||||
|
let codegen = codegen.clone();
|
||||||
|
async move |cx| {
|
||||||
|
while let Some(update) = message_rx.next().await {
|
||||||
|
let _ = codegen.update(cx, |this, _cx| match update {
|
||||||
|
ModelUpdate::Description(d) => this.description = Some(d),
|
||||||
|
ModelUpdate::Failure(f) => this.failure = Some(f),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
let mut message_id = None;
|
||||||
|
let mut first_text = None;
|
||||||
|
let last_token_usage = Arc::new(Mutex::new(TokenUsage::default()));
|
||||||
|
let total_text = Arc::new(Mutex::new(String::new()));
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if let Some(first_event) = completion_events.next().await {
|
||||||
|
match first_event {
|
||||||
|
Ok(LanguageModelCompletionEvent::StartMessage { message_id: id }) => {
|
||||||
|
message_id = Some(id);
|
||||||
|
}
|
||||||
|
Ok(LanguageModelCompletionEvent::ToolUse(tool_use)) => {
|
||||||
|
if let Some(output) = process_tool_use(tool_use) {
|
||||||
|
let (text, update) = match output {
|
||||||
|
ToolUseOutput::Rewrite { text, description } => {
|
||||||
|
(Some(text), description.map(ModelUpdate::Description))
|
||||||
|
}
|
||||||
|
ToolUseOutput::Failure(message) => {
|
||||||
|
(None, Some(ModelUpdate::Failure(message)))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Some(update) = update {
|
||||||
|
let _ = message_tx.unbounded_send(update);
|
||||||
|
}
|
||||||
|
first_text = text;
|
||||||
|
if first_text.is_some() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
finish_with_status(CodegenStatus::Done, cx);
|
Ok(LanguageModelCompletionEvent::UsageUpdate(token_usage)) => {
|
||||||
return;
|
*last_token_usage.lock() = token_usage;
|
||||||
|
}
|
||||||
|
Ok(LanguageModelCompletionEvent::Text(text)) => {
|
||||||
|
let mut lock = total_text.lock();
|
||||||
|
lock.push_str(&text);
|
||||||
|
}
|
||||||
|
Ok(e) => {
|
||||||
|
log::warn!("Unexpected event: {:?}", e);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
finish_with_status(CodegenStatus::Error(e.into()), cx);
|
finish_with_status(CodegenStatus::Error(e.into()), cx);
|
||||||
return;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(tool_use) if tool_use.name.as_ref() == "failure_message" => {
|
|
||||||
// Handle failure message tool use
|
|
||||||
match serde_json::from_value::<FailureMessageInput>(tool_use.input) {
|
|
||||||
Ok(input) => {
|
|
||||||
let _ = codegen.update(cx, |this, _cx| {
|
|
||||||
// Store the failure message as the tool description
|
|
||||||
this.model_explanation = Some(input.message.into());
|
|
||||||
});
|
|
||||||
finish_with_status(CodegenStatus::Done, cx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
finish_with_status(CodegenStatus::Error(e.into()), cx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(_tool_use) => {
|
|
||||||
// Unexpected tool.
|
|
||||||
finish_with_status(CodegenStatus::Done, cx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
finish_with_status(CodegenStatus::Error(e.into()), cx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
cx.notify();
|
let Some(first_text) = first_text else {
|
||||||
|
finish_with_status(CodegenStatus::Done, cx);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let move_last_token_usage = last_token_usage.clone();
|
||||||
|
|
||||||
|
let text_stream = Box::pin(futures::stream::once(async { Ok(first_text) }).chain(
|
||||||
|
completion_events.filter_map(move |e| {
|
||||||
|
let process_tool_use = process_tool_use.clone();
|
||||||
|
let last_token_usage = move_last_token_usage.clone();
|
||||||
|
let total_text = total_text.clone();
|
||||||
|
let mut message_tx = message_tx.clone();
|
||||||
|
async move {
|
||||||
|
match e {
|
||||||
|
Ok(LanguageModelCompletionEvent::ToolUse(tool_use)) => {
|
||||||
|
let Some(output) = process_tool_use(tool_use) else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let (text, update) = match output {
|
||||||
|
ToolUseOutput::Rewrite { text, description } => {
|
||||||
|
(Some(text), description.map(ModelUpdate::Description))
|
||||||
|
}
|
||||||
|
ToolUseOutput::Failure(message) => {
|
||||||
|
(None, Some(ModelUpdate::Failure(message)))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Some(update) = update {
|
||||||
|
let _ = message_tx.send(update).await;
|
||||||
|
}
|
||||||
|
text.map(Ok)
|
||||||
|
}
|
||||||
|
Ok(LanguageModelCompletionEvent::UsageUpdate(token_usage)) => {
|
||||||
|
*last_token_usage.lock() = token_usage;
|
||||||
|
None
|
||||||
|
}
|
||||||
|
Ok(LanguageModelCompletionEvent::Text(text)) => {
|
||||||
|
let mut lock = total_text.lock();
|
||||||
|
lock.push_str(&text);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
Ok(LanguageModelCompletionEvent::Stop(_reason)) => None,
|
||||||
|
e => {
|
||||||
|
log::error!("UNEXPECTED EVENT {:?}", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
|
||||||
|
let language_model_text_stream = LanguageModelTextStream {
|
||||||
|
message_id: message_id,
|
||||||
|
stream: text_stream,
|
||||||
|
last_token_usage,
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(task) = codegen
|
||||||
|
.update(cx, move |codegen, cx| {
|
||||||
|
codegen.handle_stream(model, async { Ok(language_model_text_stream) }, cx)
|
||||||
|
})
|
||||||
|
.ok()
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
task.await;
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1296,6 +1479,7 @@ mod tests {
|
|||||||
use gpui::TestAppContext;
|
use gpui::TestAppContext;
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
use language::{Buffer, Point};
|
use language::{Buffer, Point};
|
||||||
|
use language_model::fake_provider::FakeLanguageModel;
|
||||||
use language_model::{LanguageModelRegistry, TokenUsage};
|
use language_model::{LanguageModelRegistry, TokenUsage};
|
||||||
use languages::rust_lang;
|
use languages::rust_lang;
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
@@ -1326,8 +1510,8 @@ mod tests {
|
|||||||
buffer.clone(),
|
buffer.clone(),
|
||||||
range.clone(),
|
range.clone(),
|
||||||
true,
|
true,
|
||||||
None,
|
|
||||||
prompt_builder,
|
prompt_builder,
|
||||||
|
Uuid::new_v4(),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
@@ -1388,8 +1572,8 @@ mod tests {
|
|||||||
buffer.clone(),
|
buffer.clone(),
|
||||||
range.clone(),
|
range.clone(),
|
||||||
true,
|
true,
|
||||||
None,
|
|
||||||
prompt_builder,
|
prompt_builder,
|
||||||
|
Uuid::new_v4(),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
@@ -1452,8 +1636,8 @@ mod tests {
|
|||||||
buffer.clone(),
|
buffer.clone(),
|
||||||
range.clone(),
|
range.clone(),
|
||||||
true,
|
true,
|
||||||
None,
|
|
||||||
prompt_builder,
|
prompt_builder,
|
||||||
|
Uuid::new_v4(),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
@@ -1516,8 +1700,8 @@ mod tests {
|
|||||||
buffer.clone(),
|
buffer.clone(),
|
||||||
range.clone(),
|
range.clone(),
|
||||||
true,
|
true,
|
||||||
None,
|
|
||||||
prompt_builder,
|
prompt_builder,
|
||||||
|
Uuid::new_v4(),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
@@ -1568,8 +1752,8 @@ mod tests {
|
|||||||
buffer.clone(),
|
buffer.clone(),
|
||||||
range.clone(),
|
range.clone(),
|
||||||
false,
|
false,
|
||||||
None,
|
|
||||||
prompt_builder,
|
prompt_builder,
|
||||||
|
Uuid::new_v4(),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
@@ -1658,11 +1842,10 @@ mod tests {
|
|||||||
cx: &mut TestAppContext,
|
cx: &mut TestAppContext,
|
||||||
) -> mpsc::UnboundedSender<String> {
|
) -> mpsc::UnboundedSender<String> {
|
||||||
let (chunks_tx, chunks_rx) = mpsc::unbounded();
|
let (chunks_tx, chunks_rx) = mpsc::unbounded();
|
||||||
|
let model = Arc::new(FakeLanguageModel::default());
|
||||||
codegen.update(cx, |codegen, cx| {
|
codegen.update(cx, |codegen, cx| {
|
||||||
codegen.handle_stream(
|
codegen.generation = codegen.handle_stream(
|
||||||
String::new(),
|
model,
|
||||||
String::new(),
|
|
||||||
None,
|
|
||||||
future::ready(Ok(LanguageModelTextStream {
|
future::ready(Ok(LanguageModelTextStream {
|
||||||
message_id: None,
|
message_id: None,
|
||||||
stream: chunks_rx.map(Ok).boxed(),
|
stream: chunks_rx.map(Ok).boxed(),
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ use project::{
|
|||||||
Completion, CompletionDisplayOptions, CompletionIntent, CompletionResponse,
|
Completion, CompletionDisplayOptions, CompletionIntent, CompletionResponse,
|
||||||
PathMatchCandidateSet, Project, ProjectPath, Symbol, WorktreeId,
|
PathMatchCandidateSet, Project, ProjectPath, Symbol, WorktreeId,
|
||||||
};
|
};
|
||||||
use prompt_store::{PromptId, PromptStore, UserPromptId};
|
use prompt_store::{PromptStore, UserPromptId};
|
||||||
use rope::Point;
|
use rope::Point;
|
||||||
use text::{Anchor, ToPoint as _};
|
use text::{Anchor, ToPoint as _};
|
||||||
use ui::prelude::*;
|
use ui::prelude::*;
|
||||||
@@ -1585,13 +1585,10 @@ pub(crate) fn search_rules(
|
|||||||
if metadata.default {
|
if metadata.default {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
match metadata.id {
|
Some(RulesContextEntry {
|
||||||
PromptId::EditWorkflow => None,
|
prompt_id: metadata.id.user_id()?,
|
||||||
PromptId::User { uuid } => Some(RulesContextEntry {
|
title: metadata.title?,
|
||||||
prompt_id: uuid,
|
})
|
||||||
title: metadata.title?,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use crate::inline_assistant::test::run_inline_assistant_test;
|
|
||||||
|
|
||||||
use eval_utils::{EvalOutput, NoProcessor};
|
|
||||||
use gpui::TestAppContext;
|
|
||||||
use language_model::{LanguageModelRegistry, SelectedModel};
|
|
||||||
use rand::{SeedableRng as _, rngs::StdRng};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[cfg_attr(not(feature = "unit-eval"), ignore)]
|
|
||||||
fn eval_single_cursor_edit() {
|
|
||||||
eval_utils::eval(20, 1.0, NoProcessor, move || {
|
|
||||||
run_eval(
|
|
||||||
&EvalInput {
|
|
||||||
prompt: "Rename this variable to buffer_text".to_string(),
|
|
||||||
buffer: indoc::indoc! {"
|
|
||||||
struct EvalExampleStruct {
|
|
||||||
text: Strˇing,
|
|
||||||
prompt: String,
|
|
||||||
}
|
|
||||||
"}
|
|
||||||
.to_string(),
|
|
||||||
},
|
|
||||||
&|_, output| {
|
|
||||||
let expected = indoc::indoc! {"
|
|
||||||
struct EvalExampleStruct {
|
|
||||||
buffer_text: String,
|
|
||||||
prompt: String,
|
|
||||||
}
|
|
||||||
"};
|
|
||||||
if output == expected {
|
|
||||||
EvalOutput {
|
|
||||||
outcome: eval_utils::OutcomeKind::Passed,
|
|
||||||
data: "Passed!".to_string(),
|
|
||||||
metadata: (),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
EvalOutput {
|
|
||||||
outcome: eval_utils::OutcomeKind::Failed,
|
|
||||||
data: format!("Failed to rename variable, output: {}", output),
|
|
||||||
metadata: (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
struct EvalInput {
|
|
||||||
buffer: String,
|
|
||||||
prompt: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_eval(
|
|
||||||
input: &EvalInput,
|
|
||||||
judge: &dyn Fn(&EvalInput, &str) -> eval_utils::EvalOutput<()>,
|
|
||||||
) -> eval_utils::EvalOutput<()> {
|
|
||||||
let dispatcher = gpui::TestDispatcher::new(StdRng::from_os_rng());
|
|
||||||
let mut cx = TestAppContext::build(dispatcher, None);
|
|
||||||
cx.skip_drawing();
|
|
||||||
|
|
||||||
let buffer_text = run_inline_assistant_test(
|
|
||||||
input.buffer.clone(),
|
|
||||||
input.prompt.clone(),
|
|
||||||
|cx| {
|
|
||||||
// Reconfigure to use a real model instead of the fake one
|
|
||||||
let model_name = std::env::var("ZED_AGENT_MODEL")
|
|
||||||
.unwrap_or("anthropic/claude-sonnet-4-latest".into());
|
|
||||||
|
|
||||||
let selected_model = SelectedModel::from_str(&model_name)
|
|
||||||
.expect("Invalid model format. Use 'provider/model-id'");
|
|
||||||
|
|
||||||
log::info!("Selected model: {selected_model:?}");
|
|
||||||
|
|
||||||
cx.update(|_, cx| {
|
|
||||||
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
|
|
||||||
registry.select_inline_assistant_model(Some(&selected_model), cx);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|_cx| {
|
|
||||||
log::info!("Waiting for actual response from the LLM...");
|
|
||||||
},
|
|
||||||
&mut cx,
|
|
||||||
);
|
|
||||||
|
|
||||||
judge(input, &buffer_text)
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
|
use language_model::AnthropicEventData;
|
||||||
|
use language_model::report_anthropic_event;
|
||||||
use std::cmp;
|
use std::cmp;
|
||||||
use std::mem;
|
use std::mem;
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::context::load_context;
|
use crate::context::load_context;
|
||||||
use crate::mention_set::MentionSet;
|
use crate::mention_set::MentionSet;
|
||||||
@@ -15,7 +18,6 @@ use crate::{
|
|||||||
use agent::HistoryStore;
|
use agent::HistoryStore;
|
||||||
use agent_settings::AgentSettings;
|
use agent_settings::AgentSettings;
|
||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result};
|
||||||
use client::telemetry::Telemetry;
|
|
||||||
use collections::{HashMap, HashSet, VecDeque, hash_map};
|
use collections::{HashMap, HashSet, VecDeque, hash_map};
|
||||||
use editor::EditorSnapshot;
|
use editor::EditorSnapshot;
|
||||||
use editor::MultiBufferOffset;
|
use editor::MultiBufferOffset;
|
||||||
@@ -38,15 +40,13 @@ use gpui::{
|
|||||||
WeakEntity, Window, point,
|
WeakEntity, Window, point,
|
||||||
};
|
};
|
||||||
use language::{Buffer, Point, Selection, TransactionId};
|
use language::{Buffer, Point, Selection, TransactionId};
|
||||||
use language_model::{
|
use language_model::{ConfigurationError, ConfiguredModel, LanguageModelRegistry};
|
||||||
ConfigurationError, ConfiguredModel, LanguageModelRegistry, report_assistant_event,
|
|
||||||
};
|
|
||||||
use multi_buffer::MultiBufferRow;
|
use multi_buffer::MultiBufferRow;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use project::{CodeAction, DisableAiSettings, LspAction, Project, ProjectTransaction};
|
use project::{CodeAction, DisableAiSettings, LspAction, Project, ProjectTransaction};
|
||||||
use prompt_store::{PromptBuilder, PromptStore};
|
use prompt_store::{PromptBuilder, PromptStore};
|
||||||
use settings::{Settings, SettingsStore};
|
use settings::{Settings, SettingsStore};
|
||||||
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
|
|
||||||
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
|
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
|
||||||
use text::{OffsetRangeExt, ToPoint as _};
|
use text::{OffsetRangeExt, ToPoint as _};
|
||||||
use ui::prelude::*;
|
use ui::prelude::*;
|
||||||
@@ -54,13 +54,8 @@ use util::{RangeExt, ResultExt, maybe};
|
|||||||
use workspace::{ItemHandle, Toast, Workspace, dock::Panel, notifications::NotificationId};
|
use workspace::{ItemHandle, Toast, Workspace, dock::Panel, notifications::NotificationId};
|
||||||
use zed_actions::agent::OpenSettings;
|
use zed_actions::agent::OpenSettings;
|
||||||
|
|
||||||
pub fn init(
|
pub fn init(fs: Arc<dyn Fs>, prompt_builder: Arc<PromptBuilder>, cx: &mut App) {
|
||||||
fs: Arc<dyn Fs>,
|
cx.set_global(InlineAssistant::new(fs, prompt_builder));
|
||||||
prompt_builder: Arc<PromptBuilder>,
|
|
||||||
telemetry: Arc<Telemetry>,
|
|
||||||
cx: &mut App,
|
|
||||||
) {
|
|
||||||
cx.set_global(InlineAssistant::new(fs, prompt_builder, telemetry));
|
|
||||||
|
|
||||||
cx.observe_global::<SettingsStore>(|cx| {
|
cx.observe_global::<SettingsStore>(|cx| {
|
||||||
if DisableAiSettings::get_global(cx).disable_ai {
|
if DisableAiSettings::get_global(cx).disable_ai {
|
||||||
@@ -100,7 +95,6 @@ pub struct InlineAssistant {
|
|||||||
confirmed_assists: HashMap<InlineAssistId, Entity<CodegenAlternative>>,
|
confirmed_assists: HashMap<InlineAssistId, Entity<CodegenAlternative>>,
|
||||||
prompt_history: VecDeque<String>,
|
prompt_history: VecDeque<String>,
|
||||||
prompt_builder: Arc<PromptBuilder>,
|
prompt_builder: Arc<PromptBuilder>,
|
||||||
telemetry: Arc<Telemetry>,
|
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
_inline_assistant_completions: Option<mpsc::UnboundedSender<anyhow::Result<InlineAssistId>>>,
|
_inline_assistant_completions: Option<mpsc::UnboundedSender<anyhow::Result<InlineAssistId>>>,
|
||||||
}
|
}
|
||||||
@@ -108,11 +102,7 @@ pub struct InlineAssistant {
|
|||||||
impl Global for InlineAssistant {}
|
impl Global for InlineAssistant {}
|
||||||
|
|
||||||
impl InlineAssistant {
|
impl InlineAssistant {
|
||||||
pub fn new(
|
pub fn new(fs: Arc<dyn Fs>, prompt_builder: Arc<PromptBuilder>) -> Self {
|
||||||
fs: Arc<dyn Fs>,
|
|
||||||
prompt_builder: Arc<PromptBuilder>,
|
|
||||||
telemetry: Arc<Telemetry>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
next_assist_id: InlineAssistId::default(),
|
next_assist_id: InlineAssistId::default(),
|
||||||
next_assist_group_id: InlineAssistGroupId::default(),
|
next_assist_group_id: InlineAssistGroupId::default(),
|
||||||
@@ -122,20 +112,11 @@ impl InlineAssistant {
|
|||||||
confirmed_assists: HashMap::default(),
|
confirmed_assists: HashMap::default(),
|
||||||
prompt_history: VecDeque::default(),
|
prompt_history: VecDeque::default(),
|
||||||
prompt_builder,
|
prompt_builder,
|
||||||
telemetry,
|
|
||||||
fs,
|
fs,
|
||||||
_inline_assistant_completions: None,
|
_inline_assistant_completions: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
pub fn set_completion_receiver(
|
|
||||||
&mut self,
|
|
||||||
sender: mpsc::UnboundedSender<anyhow::Result<InlineAssistId>>,
|
|
||||||
) {
|
|
||||||
self._inline_assistant_completions = Some(sender);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn register_workspace(
|
pub fn register_workspace(
|
||||||
&mut self,
|
&mut self,
|
||||||
workspace: &Entity<Workspace>,
|
workspace: &Entity<Workspace>,
|
||||||
@@ -457,17 +438,25 @@ impl InlineAssistant {
|
|||||||
codegen_ranges.push(anchor_range);
|
codegen_ranges.push(anchor_range);
|
||||||
|
|
||||||
if let Some(model) = LanguageModelRegistry::read_global(cx).inline_assistant_model() {
|
if let Some(model) = LanguageModelRegistry::read_global(cx).inline_assistant_model() {
|
||||||
self.telemetry.report_assistant_event(AssistantEventData {
|
telemetry::event!(
|
||||||
conversation_id: None,
|
"Assistant Invoked",
|
||||||
kind: AssistantKind::Inline,
|
kind = "inline",
|
||||||
phase: AssistantPhase::Invoked,
|
phase = "invoked",
|
||||||
message_id: None,
|
model = model.model.telemetry_id(),
|
||||||
model: model.model.telemetry_id(),
|
model_provider = model.provider.id().to_string(),
|
||||||
model_provider: model.provider.id().to_string(),
|
language_name = buffer.language().map(|language| language.name().to_proto())
|
||||||
response_latency: None,
|
);
|
||||||
error_message: None,
|
|
||||||
language_name: buffer.language().map(|language| language.name().to_proto()),
|
report_anthropic_event(
|
||||||
});
|
&model.model,
|
||||||
|
AnthropicEventData {
|
||||||
|
completion_type: language_model::AnthropicCompletionType::Editor,
|
||||||
|
event: language_model::AnthropicEventType::Invoked,
|
||||||
|
language_name: buffer.language().map(|language| language.name().to_proto()),
|
||||||
|
message_id: None,
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -491,6 +480,7 @@ impl InlineAssistant {
|
|||||||
let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
|
let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
|
||||||
|
|
||||||
let assist_group_id = self.next_assist_group_id.post_inc();
|
let assist_group_id = self.next_assist_group_id.post_inc();
|
||||||
|
let session_id = Uuid::new_v4();
|
||||||
let prompt_buffer = cx.new(|cx| {
|
let prompt_buffer = cx.new(|cx| {
|
||||||
MultiBuffer::singleton(
|
MultiBuffer::singleton(
|
||||||
cx.new(|cx| Buffer::local(initial_prompt.unwrap_or_default(), cx)),
|
cx.new(|cx| Buffer::local(initial_prompt.unwrap_or_default(), cx)),
|
||||||
@@ -508,7 +498,7 @@ impl InlineAssistant {
|
|||||||
editor.read(cx).buffer().clone(),
|
editor.read(cx).buffer().clone(),
|
||||||
range.clone(),
|
range.clone(),
|
||||||
initial_transaction_id,
|
initial_transaction_id,
|
||||||
self.telemetry.clone(),
|
session_id,
|
||||||
self.prompt_builder.clone(),
|
self.prompt_builder.clone(),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
@@ -522,6 +512,7 @@ impl InlineAssistant {
|
|||||||
self.prompt_history.clone(),
|
self.prompt_history.clone(),
|
||||||
prompt_buffer.clone(),
|
prompt_buffer.clone(),
|
||||||
codegen.clone(),
|
codegen.clone(),
|
||||||
|
session_id,
|
||||||
self.fs.clone(),
|
self.fs.clone(),
|
||||||
thread_store.clone(),
|
thread_store.clone(),
|
||||||
prompt_store.clone(),
|
prompt_store.clone(),
|
||||||
@@ -1069,8 +1060,6 @@ impl InlineAssistant {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let active_alternative = assist.codegen.read(cx).active_alternative().clone();
|
let active_alternative = assist.codegen.read(cx).active_alternative().clone();
|
||||||
let message_id = active_alternative.read(cx).message_id.clone();
|
|
||||||
|
|
||||||
if let Some(model) = LanguageModelRegistry::read_global(cx).inline_assistant_model() {
|
if let Some(model) = LanguageModelRegistry::read_global(cx).inline_assistant_model() {
|
||||||
let language_name = assist.editor.upgrade().and_then(|editor| {
|
let language_name = assist.editor.upgrade().and_then(|editor| {
|
||||||
let multibuffer = editor.read(cx).buffer().read(cx);
|
let multibuffer = editor.read(cx).buffer().read(cx);
|
||||||
@@ -1079,28 +1068,49 @@ impl InlineAssistant {
|
|||||||
ranges
|
ranges
|
||||||
.first()
|
.first()
|
||||||
.and_then(|(buffer, _, _)| buffer.language())
|
.and_then(|(buffer, _, _)| buffer.language())
|
||||||
.map(|language| language.name())
|
.map(|language| language.name().0.to_string())
|
||||||
});
|
});
|
||||||
report_assistant_event(
|
|
||||||
AssistantEventData {
|
let codegen = assist.codegen.read(cx);
|
||||||
conversation_id: None,
|
let session_id = codegen.session_id();
|
||||||
kind: AssistantKind::Inline,
|
let message_id = active_alternative.read(cx).message_id.clone();
|
||||||
|
let model_telemetry_id = model.model.telemetry_id();
|
||||||
|
let model_provider_id = model.model.provider_id().to_string();
|
||||||
|
|
||||||
|
let (phase, event_type, anthropic_event_type) = if undo {
|
||||||
|
(
|
||||||
|
"rejected",
|
||||||
|
"Assistant Response Rejected",
|
||||||
|
language_model::AnthropicEventType::Reject,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
"accepted",
|
||||||
|
"Assistant Response Accepted",
|
||||||
|
language_model::AnthropicEventType::Accept,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
telemetry::event!(
|
||||||
|
event_type,
|
||||||
|
phase,
|
||||||
|
session_id = session_id.to_string(),
|
||||||
|
kind = "inline",
|
||||||
|
model = model_telemetry_id,
|
||||||
|
model_provider = model_provider_id,
|
||||||
|
language_name = language_name,
|
||||||
|
message_id = message_id.as_deref(),
|
||||||
|
);
|
||||||
|
|
||||||
|
report_anthropic_event(
|
||||||
|
&model.model,
|
||||||
|
language_model::AnthropicEventData {
|
||||||
|
completion_type: language_model::AnthropicCompletionType::Editor,
|
||||||
|
event: anthropic_event_type,
|
||||||
|
language_name,
|
||||||
message_id,
|
message_id,
|
||||||
phase: if undo {
|
|
||||||
AssistantPhase::Rejected
|
|
||||||
} else {
|
|
||||||
AssistantPhase::Accepted
|
|
||||||
},
|
|
||||||
model: model.model.telemetry_id(),
|
|
||||||
model_provider: model.model.provider_id().to_string(),
|
|
||||||
response_latency: None,
|
|
||||||
error_message: None,
|
|
||||||
language_name: language_name.map(|name| name.to_proto()),
|
|
||||||
},
|
},
|
||||||
Some(self.telemetry.clone()),
|
cx,
|
||||||
cx.http_client(),
|
|
||||||
model.model.api_key(cx),
|
|
||||||
cx.background_executor(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1455,60 +1465,8 @@ impl InlineAssistant {
|
|||||||
let old_snapshot = codegen.snapshot(cx);
|
let old_snapshot = codegen.snapshot(cx);
|
||||||
let old_buffer = codegen.old_buffer(cx);
|
let old_buffer = codegen.old_buffer(cx);
|
||||||
let deleted_row_ranges = codegen.diff(cx).deleted_row_ranges.clone();
|
let deleted_row_ranges = codegen.diff(cx).deleted_row_ranges.clone();
|
||||||
// let model_explanation = codegen.model_explanation(cx);
|
|
||||||
|
|
||||||
editor.update(cx, |editor, cx| {
|
editor.update(cx, |editor, cx| {
|
||||||
// Update tool description block
|
|
||||||
// if let Some(description) = model_explanation {
|
|
||||||
// if let Some(block_id) = decorations.model_explanation {
|
|
||||||
// editor.remove_blocks(HashSet::from_iter([block_id]), None, cx);
|
|
||||||
// let new_block_id = editor.insert_blocks(
|
|
||||||
// [BlockProperties {
|
|
||||||
// style: BlockStyle::Flex,
|
|
||||||
// placement: BlockPlacement::Below(assist.range.end),
|
|
||||||
// height: Some(1),
|
|
||||||
// render: Arc::new({
|
|
||||||
// let description = description.clone();
|
|
||||||
// move |cx| {
|
|
||||||
// div()
|
|
||||||
// .w_full()
|
|
||||||
// .py_1()
|
|
||||||
// .px_2()
|
|
||||||
// .bg(cx.theme().colors().editor_background)
|
|
||||||
// .border_y_1()
|
|
||||||
// .border_color(cx.theme().status().info_border)
|
|
||||||
// .child(
|
|
||||||
// Label::new(description.clone())
|
|
||||||
// .color(Color::Muted)
|
|
||||||
// .size(LabelSize::Small),
|
|
||||||
// )
|
|
||||||
// .into_any_element()
|
|
||||||
// }
|
|
||||||
// }),
|
|
||||||
// priority: 0,
|
|
||||||
// }],
|
|
||||||
// None,
|
|
||||||
// cx,
|
|
||||||
// );
|
|
||||||
// decorations.model_explanation = new_block_id.into_iter().next();
|
|
||||||
// }
|
|
||||||
// } else if let Some(block_id) = decorations.model_explanation {
|
|
||||||
// // Hide the block if there's no description
|
|
||||||
// editor.remove_blocks(HashSet::from_iter([block_id]), None, cx);
|
|
||||||
// let new_block_id = editor.insert_blocks(
|
|
||||||
// [BlockProperties {
|
|
||||||
// style: BlockStyle::Flex,
|
|
||||||
// placement: BlockPlacement::Below(assist.range.end),
|
|
||||||
// height: Some(0),
|
|
||||||
// render: Arc::new(|_cx| div().into_any_element()),
|
|
||||||
// priority: 0,
|
|
||||||
// }],
|
|
||||||
// None,
|
|
||||||
// cx,
|
|
||||||
// );
|
|
||||||
// decorations.model_explanation = new_block_id.into_iter().next();
|
|
||||||
// }
|
|
||||||
|
|
||||||
let old_blocks = mem::take(&mut decorations.removed_line_block_ids);
|
let old_blocks = mem::take(&mut decorations.removed_line_block_ids);
|
||||||
editor.remove_blocks(old_blocks, None, cx);
|
editor.remove_blocks(old_blocks, None, cx);
|
||||||
|
|
||||||
@@ -1627,6 +1585,27 @@ impl InlineAssistant {
|
|||||||
.map(InlineAssistTarget::Terminal)
|
.map(InlineAssistTarget::Terminal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub fn set_completion_receiver(
|
||||||
|
&mut self,
|
||||||
|
sender: mpsc::UnboundedSender<anyhow::Result<InlineAssistId>>,
|
||||||
|
) {
|
||||||
|
self._inline_assistant_completions = Some(sender);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub fn get_codegen(
|
||||||
|
&mut self,
|
||||||
|
assist_id: InlineAssistId,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Option<Entity<CodegenAlternative>> {
|
||||||
|
self.assists.get(&assist_id).map(|inline_assist| {
|
||||||
|
inline_assist
|
||||||
|
.codegen
|
||||||
|
.update(cx, |codegen, _cx| codegen.active_alternative().clone())
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct EditorInlineAssists {
|
struct EditorInlineAssists {
|
||||||
@@ -2048,8 +2027,10 @@ fn merge_ranges(ranges: &mut Vec<Range<Anchor>>, buffer: &MultiBufferSnapshot) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "unit-eval"))]
|
||||||
|
#[cfg_attr(not(test), allow(dead_code))]
|
||||||
pub mod test {
|
pub mod test {
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use agent::HistoryStore;
|
use agent::HistoryStore;
|
||||||
@@ -2060,7 +2041,6 @@ pub mod test {
|
|||||||
use futures::channel::mpsc;
|
use futures::channel::mpsc;
|
||||||
use gpui::{AppContext, TestAppContext, UpdateGlobal as _};
|
use gpui::{AppContext, TestAppContext, UpdateGlobal as _};
|
||||||
use language::Buffer;
|
use language::Buffer;
|
||||||
use language_model::LanguageModelRegistry;
|
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use prompt_store::PromptBuilder;
|
use prompt_store::PromptBuilder;
|
||||||
use smol::stream::StreamExt as _;
|
use smol::stream::StreamExt as _;
|
||||||
@@ -2069,13 +2049,32 @@ pub mod test {
|
|||||||
|
|
||||||
use crate::InlineAssistant;
|
use crate::InlineAssistant;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum InlineAssistantOutput {
|
||||||
|
Success {
|
||||||
|
completion: Option<String>,
|
||||||
|
description: Option<String>,
|
||||||
|
full_buffer_text: String,
|
||||||
|
},
|
||||||
|
Failure {
|
||||||
|
failure: String,
|
||||||
|
},
|
||||||
|
// These fields are used for logging
|
||||||
|
#[allow(unused)]
|
||||||
|
Malformed {
|
||||||
|
completion: Option<String>,
|
||||||
|
description: Option<String>,
|
||||||
|
failure: Option<String>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
pub fn run_inline_assistant_test<SetupF, TestF>(
|
pub fn run_inline_assistant_test<SetupF, TestF>(
|
||||||
base_buffer: String,
|
base_buffer: String,
|
||||||
prompt: String,
|
prompt: String,
|
||||||
setup: SetupF,
|
setup: SetupF,
|
||||||
test: TestF,
|
test: TestF,
|
||||||
cx: &mut TestAppContext,
|
cx: &mut TestAppContext,
|
||||||
) -> String
|
) -> InlineAssistantOutput
|
||||||
where
|
where
|
||||||
SetupF: FnOnce(&mut gpui::VisualTestContext),
|
SetupF: FnOnce(&mut gpui::VisualTestContext),
|
||||||
TestF: FnOnce(&mut gpui::VisualTestContext),
|
TestF: FnOnce(&mut gpui::VisualTestContext),
|
||||||
@@ -2088,8 +2087,7 @@ pub mod test {
|
|||||||
cx.set_http_client(http);
|
cx.set_http_client(http);
|
||||||
Client::production(cx)
|
Client::production(cx)
|
||||||
});
|
});
|
||||||
let mut inline_assistant =
|
let mut inline_assistant = InlineAssistant::new(fs.clone(), prompt_builder);
|
||||||
InlineAssistant::new(fs.clone(), prompt_builder, client.telemetry().clone());
|
|
||||||
|
|
||||||
let (tx, mut completion_rx) = mpsc::unbounded();
|
let (tx, mut completion_rx) = mpsc::unbounded();
|
||||||
inline_assistant.set_completion_receiver(tx);
|
inline_assistant.set_completion_receiver(tx);
|
||||||
@@ -2168,39 +2166,217 @@ pub mod test {
|
|||||||
|
|
||||||
test(cx);
|
test(cx);
|
||||||
|
|
||||||
cx.executor()
|
let assist_id = cx
|
||||||
.block_test(async { completion_rx.next().await });
|
.executor()
|
||||||
|
.block_test(async { completion_rx.next().await })
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
buffer.read_with(cx, |buffer, _| buffer.text())
|
let (completion, description, failure) = cx.update(|_, cx| {
|
||||||
}
|
InlineAssistant::update_global(cx, |inline_assistant, cx| {
|
||||||
|
let codegen = inline_assistant.get_codegen(assist_id, cx).unwrap();
|
||||||
|
|
||||||
#[allow(unused)]
|
let completion = codegen.read(cx).current_completion();
|
||||||
pub fn test_inline_assistant(
|
let description = codegen.read(cx).current_description();
|
||||||
base_buffer: &'static str,
|
let failure = codegen.read(cx).current_failure();
|
||||||
llm_output: &'static str,
|
|
||||||
cx: &mut TestAppContext,
|
|
||||||
) -> String {
|
|
||||||
run_inline_assistant_test(
|
|
||||||
base_buffer.to_string(),
|
|
||||||
"Prompt doesn't matter because we're using a fake model".to_string(),
|
|
||||||
|cx| {
|
|
||||||
cx.update(|_, cx| LanguageModelRegistry::test(cx));
|
|
||||||
},
|
|
||||||
|cx| {
|
|
||||||
let fake_model = cx.update(|_, cx| {
|
|
||||||
LanguageModelRegistry::global(cx)
|
|
||||||
.update(cx, |registry, _| registry.fake_model())
|
|
||||||
});
|
|
||||||
let fake = fake_model.as_fake();
|
|
||||||
|
|
||||||
// let fake = fake_model;
|
(completion, description, failure)
|
||||||
fake.send_last_completion_stream_text_chunk(llm_output.to_string());
|
})
|
||||||
fake.end_last_completion_stream();
|
});
|
||||||
|
|
||||||
// Run again to process the model's response
|
if failure.is_some() && (completion.is_some() || description.is_some()) {
|
||||||
cx.run_until_parked();
|
InlineAssistantOutput::Malformed {
|
||||||
},
|
completion,
|
||||||
cx,
|
description,
|
||||||
)
|
failure,
|
||||||
|
}
|
||||||
|
} else if let Some(failure) = failure {
|
||||||
|
InlineAssistantOutput::Failure { failure }
|
||||||
|
} else {
|
||||||
|
InlineAssistantOutput::Success {
|
||||||
|
completion,
|
||||||
|
description,
|
||||||
|
full_buffer_text: buffer.read_with(cx, |buffer, _| buffer.text()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "unit-eval"))]
|
||||||
|
#[cfg_attr(not(test), allow(dead_code))]
|
||||||
|
pub mod evals {
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use eval_utils::{EvalOutput, NoProcessor};
|
||||||
|
use gpui::TestAppContext;
|
||||||
|
use language_model::{LanguageModelRegistry, SelectedModel};
|
||||||
|
use rand::{SeedableRng as _, rngs::StdRng};
|
||||||
|
|
||||||
|
use crate::inline_assistant::test::{InlineAssistantOutput, run_inline_assistant_test};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg_attr(not(feature = "unit-eval"), ignore)]
|
||||||
|
fn eval_single_cursor_edit() {
|
||||||
|
run_eval(
|
||||||
|
20,
|
||||||
|
1.0,
|
||||||
|
"Rename this variable to buffer_text".to_string(),
|
||||||
|
indoc::indoc! {"
|
||||||
|
struct EvalExampleStruct {
|
||||||
|
text: Strˇing,
|
||||||
|
prompt: String,
|
||||||
|
}
|
||||||
|
"}
|
||||||
|
.to_string(),
|
||||||
|
exact_buffer_match(indoc::indoc! {"
|
||||||
|
struct EvalExampleStruct {
|
||||||
|
buffer_text: String,
|
||||||
|
prompt: String,
|
||||||
|
}
|
||||||
|
"}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg_attr(not(feature = "unit-eval"), ignore)]
|
||||||
|
fn eval_cant_do() {
|
||||||
|
run_eval(
|
||||||
|
20,
|
||||||
|
0.95,
|
||||||
|
"Rename the struct to EvalExampleStructNope",
|
||||||
|
indoc::indoc! {"
|
||||||
|
struct EvalExampleStruct {
|
||||||
|
text: Strˇing,
|
||||||
|
prompt: String,
|
||||||
|
}
|
||||||
|
"},
|
||||||
|
uncertain_output,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg_attr(not(feature = "unit-eval"), ignore)]
|
||||||
|
fn eval_unclear() {
|
||||||
|
run_eval(
|
||||||
|
20,
|
||||||
|
0.95,
|
||||||
|
"Make exactly the change I want you to make",
|
||||||
|
indoc::indoc! {"
|
||||||
|
struct EvalExampleStruct {
|
||||||
|
text: Strˇing,
|
||||||
|
prompt: String,
|
||||||
|
}
|
||||||
|
"},
|
||||||
|
uncertain_output,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_eval(
|
||||||
|
iterations: usize,
|
||||||
|
expected_pass_ratio: f32,
|
||||||
|
prompt: impl Into<String>,
|
||||||
|
buffer: impl Into<String>,
|
||||||
|
judge: impl Fn(InlineAssistantOutput) -> eval_utils::EvalOutput<()> + Send + Sync + 'static,
|
||||||
|
) {
|
||||||
|
let buffer = buffer.into();
|
||||||
|
let prompt = prompt.into();
|
||||||
|
|
||||||
|
eval_utils::eval(iterations, expected_pass_ratio, NoProcessor, move || {
|
||||||
|
let dispatcher = gpui::TestDispatcher::new(StdRng::from_os_rng());
|
||||||
|
let mut cx = TestAppContext::build(dispatcher, None);
|
||||||
|
cx.skip_drawing();
|
||||||
|
|
||||||
|
let output = run_inline_assistant_test(
|
||||||
|
buffer.clone(),
|
||||||
|
prompt.clone(),
|
||||||
|
|cx| {
|
||||||
|
// Reconfigure to use a real model instead of the fake one
|
||||||
|
let model_name = std::env::var("ZED_AGENT_MODEL")
|
||||||
|
.unwrap_or("anthropic/claude-sonnet-4-latest".into());
|
||||||
|
|
||||||
|
let selected_model = SelectedModel::from_str(&model_name)
|
||||||
|
.expect("Invalid model format. Use 'provider/model-id'");
|
||||||
|
|
||||||
|
log::info!("Selected model: {selected_model:?}");
|
||||||
|
|
||||||
|
cx.update(|_, cx| {
|
||||||
|
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
|
||||||
|
registry.select_inline_assistant_model(Some(&selected_model), cx);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|_cx| {
|
||||||
|
log::info!("Waiting for actual response from the LLM...");
|
||||||
|
},
|
||||||
|
&mut cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
cx.quit();
|
||||||
|
|
||||||
|
judge(output)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn uncertain_output(output: InlineAssistantOutput) -> EvalOutput<()> {
|
||||||
|
match &output {
|
||||||
|
o @ InlineAssistantOutput::Success {
|
||||||
|
completion,
|
||||||
|
description,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
if description.is_some() && completion.is_none() {
|
||||||
|
EvalOutput::passed(format!(
|
||||||
|
"Assistant produced no completion, but a description:\n{}",
|
||||||
|
description.as_ref().unwrap()
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
EvalOutput::failed(format!("Assistant produced a completion:\n{:?}", o))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
InlineAssistantOutput::Failure {
|
||||||
|
failure: error_message,
|
||||||
|
} => EvalOutput::passed(format!(
|
||||||
|
"Assistant produced a failure message: {}",
|
||||||
|
error_message
|
||||||
|
)),
|
||||||
|
o @ InlineAssistantOutput::Malformed { .. } => {
|
||||||
|
EvalOutput::failed(format!("Assistant produced a malformed response:\n{:?}", o))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exact_buffer_match(
|
||||||
|
correct_output: impl Into<String>,
|
||||||
|
) -> impl Fn(InlineAssistantOutput) -> EvalOutput<()> {
|
||||||
|
let correct_output = correct_output.into();
|
||||||
|
move |output| match output {
|
||||||
|
InlineAssistantOutput::Success {
|
||||||
|
description,
|
||||||
|
full_buffer_text,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
if full_buffer_text == correct_output && description.is_none() {
|
||||||
|
EvalOutput::passed("Assistant output matches")
|
||||||
|
} else if full_buffer_text == correct_output {
|
||||||
|
EvalOutput::failed(format!(
|
||||||
|
"Assistant output produced an unescessary description description:\n{:?}",
|
||||||
|
description
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
EvalOutput::failed(format!(
|
||||||
|
"Assistant output does not match expected output:\n{:?}\ndescription:\n{:?}",
|
||||||
|
full_buffer_text, description
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
o @ InlineAssistantOutput::Failure { .. } => EvalOutput::failed(format!(
|
||||||
|
"Assistant output does not match expected output: {:?}",
|
||||||
|
o
|
||||||
|
)),
|
||||||
|
o @ InlineAssistantOutput::Malformed { .. } => EvalOutput::failed(format!(
|
||||||
|
"Assistant output does not match expected output: {:?}",
|
||||||
|
o
|
||||||
|
)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ use editor::{
|
|||||||
ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
|
ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
|
||||||
actions::{MoveDown, MoveUp},
|
actions::{MoveDown, MoveUp},
|
||||||
};
|
};
|
||||||
|
use feature_flags::{FeatureFlagAppExt, InlineAssistantUseToolFeatureFlag};
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, Subscription,
|
AnyElement, App, ClipboardItem, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||||
TextStyle, TextStyleRefinement, WeakEntity, Window,
|
Subscription, TextStyle, TextStyleRefinement, WeakEntity, Window, actions,
|
||||||
};
|
};
|
||||||
use language_model::{LanguageModel, LanguageModelRegistry};
|
use language_model::{LanguageModel, LanguageModelRegistry};
|
||||||
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
|
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
|
||||||
@@ -26,11 +27,13 @@ use std::sync::Arc;
|
|||||||
use theme::ThemeSettings;
|
use theme::ThemeSettings;
|
||||||
use ui::utils::WithRemSize;
|
use ui::utils::WithRemSize;
|
||||||
use ui::{IconButtonShape, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*};
|
use ui::{IconButtonShape, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*};
|
||||||
use workspace::Workspace;
|
use uuid::Uuid;
|
||||||
|
use workspace::notifications::NotificationId;
|
||||||
|
use workspace::{Toast, Workspace};
|
||||||
use zed_actions::agent::ToggleModelSelector;
|
use zed_actions::agent::ToggleModelSelector;
|
||||||
|
|
||||||
use crate::agent_model_selector::AgentModelSelector;
|
use crate::agent_model_selector::AgentModelSelector;
|
||||||
use crate::buffer_codegen::BufferCodegen;
|
use crate::buffer_codegen::{BufferCodegen, CodegenAlternative};
|
||||||
use crate::completion_provider::{
|
use crate::completion_provider::{
|
||||||
PromptCompletionProvider, PromptCompletionProviderDelegate, PromptContextType,
|
PromptCompletionProvider, PromptCompletionProviderDelegate, PromptContextType,
|
||||||
};
|
};
|
||||||
@@ -39,6 +42,19 @@ use crate::mention_set::{MentionSet, crease_for_mention};
|
|||||||
use crate::terminal_codegen::TerminalCodegen;
|
use crate::terminal_codegen::TerminalCodegen;
|
||||||
use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext};
|
use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext};
|
||||||
|
|
||||||
|
actions!(inline_assistant, [ThumbsUpResult, ThumbsDownResult]);
|
||||||
|
|
||||||
|
enum CompletionState {
|
||||||
|
Pending,
|
||||||
|
Generated { completion_text: Option<String> },
|
||||||
|
Rated,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SessionState {
|
||||||
|
session_id: Uuid,
|
||||||
|
completion: CompletionState,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct PromptEditor<T> {
|
pub struct PromptEditor<T> {
|
||||||
pub editor: Entity<Editor>,
|
pub editor: Entity<Editor>,
|
||||||
mode: PromptEditorMode,
|
mode: PromptEditorMode,
|
||||||
@@ -54,6 +70,7 @@ pub struct PromptEditor<T> {
|
|||||||
_codegen_subscription: Subscription,
|
_codegen_subscription: Subscription,
|
||||||
editor_subscriptions: Vec<Subscription>,
|
editor_subscriptions: Vec<Subscription>,
|
||||||
show_rate_limit_notice: bool,
|
show_rate_limit_notice: bool,
|
||||||
|
session_state: SessionState,
|
||||||
_phantom: std::marker::PhantomData<T>,
|
_phantom: std::marker::PhantomData<T>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,11 +101,11 @@ impl<T: 'static> Render for PromptEditor<T> {
|
|||||||
let left_gutter_width = gutter.full_width() + (gutter.margin / 2.0);
|
let left_gutter_width = gutter.full_width() + (gutter.margin / 2.0);
|
||||||
let right_padding = editor_margins.right + RIGHT_PADDING;
|
let right_padding = editor_margins.right + RIGHT_PADDING;
|
||||||
|
|
||||||
let explanation = codegen
|
let active_alternative = codegen.active_alternative().read(cx);
|
||||||
.active_alternative()
|
let explanation = active_alternative
|
||||||
.read(cx)
|
.description
|
||||||
.model_explanation
|
.clone()
|
||||||
.clone();
|
.or_else(|| active_alternative.failure.clone());
|
||||||
|
|
||||||
(left_gutter_width, right_padding, explanation)
|
(left_gutter_width, right_padding, explanation)
|
||||||
}
|
}
|
||||||
@@ -122,7 +139,7 @@ impl<T: 'static> Render for PromptEditor<T> {
|
|||||||
|
|
||||||
if let Some(explanation) = &explanation {
|
if let Some(explanation) = &explanation {
|
||||||
markdown.update(cx, |markdown, cx| {
|
markdown.update(cx, |markdown, cx| {
|
||||||
markdown.reset(explanation.clone(), cx);
|
markdown.reset(SharedString::from(explanation), cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,6 +170,8 @@ impl<T: 'static> Render for PromptEditor<T> {
|
|||||||
.on_action(cx.listener(Self::cancel))
|
.on_action(cx.listener(Self::cancel))
|
||||||
.on_action(cx.listener(Self::move_up))
|
.on_action(cx.listener(Self::move_up))
|
||||||
.on_action(cx.listener(Self::move_down))
|
.on_action(cx.listener(Self::move_down))
|
||||||
|
.on_action(cx.listener(Self::thumbs_up))
|
||||||
|
.on_action(cx.listener(Self::thumbs_down))
|
||||||
.capture_action(cx.listener(Self::cycle_prev))
|
.capture_action(cx.listener(Self::cycle_prev))
|
||||||
.capture_action(cx.listener(Self::cycle_next))
|
.capture_action(cx.listener(Self::cycle_next))
|
||||||
.child(
|
.child(
|
||||||
@@ -429,6 +448,7 @@ impl<T: 'static> PromptEditor<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.edited_since_done = true;
|
self.edited_since_done = true;
|
||||||
|
self.session_state.completion = CompletionState::Pending;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
EditorEvent::Blurred => {
|
EditorEvent::Blurred => {
|
||||||
@@ -500,22 +520,207 @@ impl<T: 'static> PromptEditor<T> {
|
|||||||
fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
|
fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
match self.codegen_status(cx) {
|
match self.codegen_status(cx) {
|
||||||
CodegenStatus::Idle => {
|
CodegenStatus::Idle => {
|
||||||
|
self.fire_started_telemetry(cx);
|
||||||
cx.emit(PromptEditorEvent::StartRequested);
|
cx.emit(PromptEditorEvent::StartRequested);
|
||||||
}
|
}
|
||||||
CodegenStatus::Pending => {}
|
CodegenStatus::Pending => {}
|
||||||
CodegenStatus::Done => {
|
CodegenStatus::Done => {
|
||||||
if self.edited_since_done {
|
if self.edited_since_done {
|
||||||
|
self.fire_started_telemetry(cx);
|
||||||
cx.emit(PromptEditorEvent::StartRequested);
|
cx.emit(PromptEditorEvent::StartRequested);
|
||||||
} else {
|
} else {
|
||||||
cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
|
cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CodegenStatus::Error(_) => {
|
CodegenStatus::Error(_) => {
|
||||||
|
self.fire_started_telemetry(cx);
|
||||||
cx.emit(PromptEditorEvent::StartRequested);
|
cx.emit(PromptEditorEvent::StartRequested);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn fire_started_telemetry(&self, cx: &Context<Self>) {
|
||||||
|
let Some(model) = LanguageModelRegistry::read_global(cx).inline_assistant_model() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let model_telemetry_id = model.model.telemetry_id();
|
||||||
|
let model_provider_id = model.provider.id().to_string();
|
||||||
|
|
||||||
|
let (kind, language_name) = match &self.mode {
|
||||||
|
PromptEditorMode::Buffer { codegen, .. } => {
|
||||||
|
let codegen = codegen.read(cx);
|
||||||
|
(
|
||||||
|
"inline",
|
||||||
|
codegen.language_name(cx).map(|name| name.to_string()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
PromptEditorMode::Terminal { .. } => ("inline_terminal", None),
|
||||||
|
};
|
||||||
|
|
||||||
|
telemetry::event!(
|
||||||
|
"Assistant Started",
|
||||||
|
session_id = self.session_state.session_id.to_string(),
|
||||||
|
kind = kind,
|
||||||
|
phase = "started",
|
||||||
|
model = model_telemetry_id,
|
||||||
|
model_provider = model_provider_id,
|
||||||
|
language_name = language_name,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn thumbs_up(&mut self, _: &ThumbsUpResult, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
match &self.session_state.completion {
|
||||||
|
CompletionState::Pending => {
|
||||||
|
self.toast("Can't rate, still generating...", None, cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CompletionState::Rated => {
|
||||||
|
self.toast(
|
||||||
|
"Already rated this completion",
|
||||||
|
Some(self.session_state.session_id),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CompletionState::Generated { completion_text } => {
|
||||||
|
let model_info = self.model_selector.read(cx).active_model(cx);
|
||||||
|
let (model_id, use_streaming_tools) = {
|
||||||
|
let Some(configured_model) = model_info else {
|
||||||
|
self.toast("No configured model", None, cx);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
(
|
||||||
|
configured_model.model.telemetry_id(),
|
||||||
|
CodegenAlternative::use_streaming_tools(
|
||||||
|
configured_model.model.as_ref(),
|
||||||
|
cx,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let selected_text = match &self.mode {
|
||||||
|
PromptEditorMode::Buffer { codegen, .. } => {
|
||||||
|
codegen.read(cx).selected_text(cx).map(|s| s.to_string())
|
||||||
|
}
|
||||||
|
PromptEditorMode::Terminal { .. } => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let prompt = self.editor.read(cx).text(cx);
|
||||||
|
|
||||||
|
let kind = match &self.mode {
|
||||||
|
PromptEditorMode::Buffer { .. } => "inline",
|
||||||
|
PromptEditorMode::Terminal { .. } => "inline_terminal",
|
||||||
|
};
|
||||||
|
|
||||||
|
telemetry::event!(
|
||||||
|
"Inline Assistant Rated",
|
||||||
|
rating = "positive",
|
||||||
|
session_id = self.session_state.session_id.to_string(),
|
||||||
|
kind = kind,
|
||||||
|
model = model_id,
|
||||||
|
prompt = prompt,
|
||||||
|
completion = completion_text,
|
||||||
|
selected_text = selected_text,
|
||||||
|
use_streaming_tools
|
||||||
|
);
|
||||||
|
|
||||||
|
self.session_state.completion = CompletionState::Rated;
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn thumbs_down(&mut self, _: &ThumbsDownResult, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
match &self.session_state.completion {
|
||||||
|
CompletionState::Pending => {
|
||||||
|
self.toast("Can't rate, still generating...", None, cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CompletionState::Rated => {
|
||||||
|
self.toast(
|
||||||
|
"Already rated this completion",
|
||||||
|
Some(self.session_state.session_id),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CompletionState::Generated { completion_text } => {
|
||||||
|
let model_info = self.model_selector.read(cx).active_model(cx);
|
||||||
|
let (model_telemetry_id, use_streaming_tools) = {
|
||||||
|
let Some(configured_model) = model_info else {
|
||||||
|
self.toast("No configured model", None, cx);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
(
|
||||||
|
configured_model.model.telemetry_id(),
|
||||||
|
CodegenAlternative::use_streaming_tools(
|
||||||
|
configured_model.model.as_ref(),
|
||||||
|
cx,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let selected_text = match &self.mode {
|
||||||
|
PromptEditorMode::Buffer { codegen, .. } => {
|
||||||
|
codegen.read(cx).selected_text(cx).map(|s| s.to_string())
|
||||||
|
}
|
||||||
|
PromptEditorMode::Terminal { .. } => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let prompt = self.editor.read(cx).text(cx);
|
||||||
|
|
||||||
|
let kind = match &self.mode {
|
||||||
|
PromptEditorMode::Buffer { .. } => "inline",
|
||||||
|
PromptEditorMode::Terminal { .. } => "inline_terminal",
|
||||||
|
};
|
||||||
|
|
||||||
|
telemetry::event!(
|
||||||
|
"Inline Assistant Rated",
|
||||||
|
rating = "negative",
|
||||||
|
session_id = self.session_state.session_id.to_string(),
|
||||||
|
kind = kind,
|
||||||
|
model = model_telemetry_id,
|
||||||
|
prompt = prompt,
|
||||||
|
completion = completion_text,
|
||||||
|
selected_text = selected_text,
|
||||||
|
use_streaming_tools
|
||||||
|
);
|
||||||
|
|
||||||
|
self.session_state.completion = CompletionState::Rated;
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toast(&mut self, msg: &str, uuid: Option<Uuid>, cx: &mut Context<'_, PromptEditor<T>>) {
|
||||||
|
self.workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
enum InlinePromptRating {}
|
||||||
|
workspace.show_toast(
|
||||||
|
{
|
||||||
|
let mut toast = Toast::new(
|
||||||
|
NotificationId::unique::<InlinePromptRating>(),
|
||||||
|
msg.to_string(),
|
||||||
|
)
|
||||||
|
.autohide();
|
||||||
|
|
||||||
|
if let Some(uuid) = uuid {
|
||||||
|
toast = toast.on_click("Click to copy rating ID", move |_, cx| {
|
||||||
|
cx.write_to_clipboard(ClipboardItem::new_string(uuid.to_string()));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
toast
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {
|
fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
if let Some(ix) = self.prompt_history_ix {
|
if let Some(ix) = self.prompt_history_ix {
|
||||||
if ix > 0 {
|
if ix > 0 {
|
||||||
@@ -621,6 +826,9 @@ impl<T: 'static> PromptEditor<T> {
|
|||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
]
|
]
|
||||||
} else {
|
} else {
|
||||||
|
let show_rating_buttons = cx.has_flag::<InlineAssistantUseToolFeatureFlag>();
|
||||||
|
let rated = matches!(self.session_state.completion, CompletionState::Rated);
|
||||||
|
|
||||||
let accept = IconButton::new("accept", IconName::Check)
|
let accept = IconButton::new("accept", IconName::Check)
|
||||||
.icon_color(Color::Info)
|
.icon_color(Color::Info)
|
||||||
.shape(IconButtonShape::Square)
|
.shape(IconButtonShape::Square)
|
||||||
@@ -632,25 +840,59 @@ impl<T: 'static> PromptEditor<T> {
|
|||||||
}))
|
}))
|
||||||
.into_any_element();
|
.into_any_element();
|
||||||
|
|
||||||
match &self.mode {
|
let mut buttons = Vec::new();
|
||||||
PromptEditorMode::Terminal { .. } => vec![
|
|
||||||
accept,
|
if show_rating_buttons {
|
||||||
IconButton::new("confirm", IconName::PlayFilled)
|
buttons.push(
|
||||||
.icon_color(Color::Info)
|
IconButton::new("thumbs-down", IconName::ThumbsDown)
|
||||||
|
.icon_color(if rated { Color::Muted } else { Color::Default })
|
||||||
.shape(IconButtonShape::Square)
|
.shape(IconButtonShape::Square)
|
||||||
.tooltip(|_window, cx| {
|
.disabled(rated)
|
||||||
Tooltip::for_action(
|
.tooltip(Tooltip::text("Bad result"))
|
||||||
"Execute Generated Command",
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
&menu::SecondaryConfirm,
|
this.thumbs_down(&ThumbsDownResult, window, cx);
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.on_click(cx.listener(|_, _, _, cx| {
|
|
||||||
cx.emit(PromptEditorEvent::ConfirmRequested { execute: true });
|
|
||||||
}))
|
}))
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
],
|
);
|
||||||
PromptEditorMode::Buffer { .. } => vec![accept],
|
|
||||||
|
buttons.push(
|
||||||
|
IconButton::new("thumbs-up", IconName::ThumbsUp)
|
||||||
|
.icon_color(if rated { Color::Muted } else { Color::Default })
|
||||||
|
.shape(IconButtonShape::Square)
|
||||||
|
.disabled(rated)
|
||||||
|
.tooltip(Tooltip::text("Good result"))
|
||||||
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
|
this.thumbs_up(&ThumbsUpResult, window, cx);
|
||||||
|
}))
|
||||||
|
.into_any_element(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
buttons.push(accept);
|
||||||
|
|
||||||
|
match &self.mode {
|
||||||
|
PromptEditorMode::Terminal { .. } => {
|
||||||
|
buttons.push(
|
||||||
|
IconButton::new("confirm", IconName::PlayFilled)
|
||||||
|
.icon_color(Color::Info)
|
||||||
|
.shape(IconButtonShape::Square)
|
||||||
|
.tooltip(|_window, cx| {
|
||||||
|
Tooltip::for_action(
|
||||||
|
"Execute Generated Command",
|
||||||
|
&menu::SecondaryConfirm,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.on_click(cx.listener(|_, _, _, cx| {
|
||||||
|
cx.emit(PromptEditorEvent::ConfirmRequested {
|
||||||
|
execute: true,
|
||||||
|
});
|
||||||
|
}))
|
||||||
|
.into_any_element(),
|
||||||
|
);
|
||||||
|
buttons
|
||||||
|
}
|
||||||
|
PromptEditorMode::Buffer { .. } => buttons,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -909,6 +1151,7 @@ impl PromptEditor<BufferCodegen> {
|
|||||||
prompt_history: VecDeque<String>,
|
prompt_history: VecDeque<String>,
|
||||||
prompt_buffer: Entity<MultiBuffer>,
|
prompt_buffer: Entity<MultiBuffer>,
|
||||||
codegen: Entity<BufferCodegen>,
|
codegen: Entity<BufferCodegen>,
|
||||||
|
session_id: Uuid,
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
history_store: Entity<HistoryStore>,
|
history_store: Entity<HistoryStore>,
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
prompt_store: Option<Entity<PromptStore>>,
|
||||||
@@ -979,6 +1222,10 @@ impl PromptEditor<BufferCodegen> {
|
|||||||
editor_subscriptions: Vec::new(),
|
editor_subscriptions: Vec::new(),
|
||||||
show_rate_limit_notice: false,
|
show_rate_limit_notice: false,
|
||||||
mode,
|
mode,
|
||||||
|
session_state: SessionState {
|
||||||
|
session_id,
|
||||||
|
completion: CompletionState::Pending,
|
||||||
|
},
|
||||||
_phantom: Default::default(),
|
_phantom: Default::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -989,7 +1236,7 @@ impl PromptEditor<BufferCodegen> {
|
|||||||
|
|
||||||
fn handle_codegen_changed(
|
fn handle_codegen_changed(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: Entity<BufferCodegen>,
|
codegen: Entity<BufferCodegen>,
|
||||||
cx: &mut Context<PromptEditor<BufferCodegen>>,
|
cx: &mut Context<PromptEditor<BufferCodegen>>,
|
||||||
) {
|
) {
|
||||||
match self.codegen_status(cx) {
|
match self.codegen_status(cx) {
|
||||||
@@ -998,10 +1245,15 @@ impl PromptEditor<BufferCodegen> {
|
|||||||
.update(cx, |editor, _| editor.set_read_only(false));
|
.update(cx, |editor, _| editor.set_read_only(false));
|
||||||
}
|
}
|
||||||
CodegenStatus::Pending => {
|
CodegenStatus::Pending => {
|
||||||
|
self.session_state.completion = CompletionState::Pending;
|
||||||
self.editor
|
self.editor
|
||||||
.update(cx, |editor, _| editor.set_read_only(true));
|
.update(cx, |editor, _| editor.set_read_only(true));
|
||||||
}
|
}
|
||||||
CodegenStatus::Done => {
|
CodegenStatus::Done => {
|
||||||
|
let completion = codegen.read(cx).active_completion(cx);
|
||||||
|
self.session_state.completion = CompletionState::Generated {
|
||||||
|
completion_text: completion,
|
||||||
|
};
|
||||||
self.edited_since_done = false;
|
self.edited_since_done = false;
|
||||||
self.editor
|
self.editor
|
||||||
.update(cx, |editor, _| editor.set_read_only(false));
|
.update(cx, |editor, _| editor.set_read_only(false));
|
||||||
@@ -1057,6 +1309,7 @@ impl PromptEditor<TerminalCodegen> {
|
|||||||
prompt_history: VecDeque<String>,
|
prompt_history: VecDeque<String>,
|
||||||
prompt_buffer: Entity<MultiBuffer>,
|
prompt_buffer: Entity<MultiBuffer>,
|
||||||
codegen: Entity<TerminalCodegen>,
|
codegen: Entity<TerminalCodegen>,
|
||||||
|
session_id: Uuid,
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
history_store: Entity<HistoryStore>,
|
history_store: Entity<HistoryStore>,
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
prompt_store: Option<Entity<PromptStore>>,
|
||||||
@@ -1122,6 +1375,10 @@ impl PromptEditor<TerminalCodegen> {
|
|||||||
editor_subscriptions: Vec::new(),
|
editor_subscriptions: Vec::new(),
|
||||||
mode,
|
mode,
|
||||||
show_rate_limit_notice: false,
|
show_rate_limit_notice: false,
|
||||||
|
session_state: SessionState {
|
||||||
|
session_id,
|
||||||
|
completion: CompletionState::Pending,
|
||||||
|
},
|
||||||
_phantom: Default::default(),
|
_phantom: Default::default(),
|
||||||
};
|
};
|
||||||
this.count_lines(cx);
|
this.count_lines(cx);
|
||||||
@@ -1154,17 +1411,21 @@ impl PromptEditor<TerminalCodegen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_codegen_changed(&mut self, _: Entity<TerminalCodegen>, cx: &mut Context<Self>) {
|
fn handle_codegen_changed(&mut self, codegen: Entity<TerminalCodegen>, cx: &mut Context<Self>) {
|
||||||
match &self.codegen().read(cx).status {
|
match &self.codegen().read(cx).status {
|
||||||
CodegenStatus::Idle => {
|
CodegenStatus::Idle => {
|
||||||
self.editor
|
self.editor
|
||||||
.update(cx, |editor, _| editor.set_read_only(false));
|
.update(cx, |editor, _| editor.set_read_only(false));
|
||||||
}
|
}
|
||||||
CodegenStatus::Pending => {
|
CodegenStatus::Pending => {
|
||||||
|
self.session_state.completion = CompletionState::Pending;
|
||||||
self.editor
|
self.editor
|
||||||
.update(cx, |editor, _| editor.set_read_only(true));
|
.update(cx, |editor, _| editor.set_read_only(true));
|
||||||
}
|
}
|
||||||
CodegenStatus::Done | CodegenStatus::Error(_) => {
|
CodegenStatus::Done | CodegenStatus::Error(_) => {
|
||||||
|
self.session_state.completion = CompletionState::Generated {
|
||||||
|
completion_text: codegen.read(cx).completion(),
|
||||||
|
};
|
||||||
self.edited_since_done = false;
|
self.edited_since_done = false;
|
||||||
self.editor
|
self.editor
|
||||||
.update(cx, |editor, _| editor.set_read_only(false));
|
.update(cx, |editor, _| editor.set_read_only(false));
|
||||||
|
|||||||
@@ -11,9 +11,11 @@ use language_model::{
|
|||||||
};
|
};
|
||||||
use ordered_float::OrderedFloat;
|
use ordered_float::OrderedFloat;
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
use ui::{KeyBinding, ListItem, ListItemSpacing, prelude::*};
|
use ui::prelude::*;
|
||||||
use zed_actions::agent::OpenSettings;
|
use zed_actions::agent::OpenSettings;
|
||||||
|
|
||||||
|
use crate::ui::{ModelSelectorFooter, ModelSelectorHeader, ModelSelectorListItem};
|
||||||
|
|
||||||
type OnModelChanged = Arc<dyn Fn(Arc<dyn LanguageModel>, &mut App) + 'static>;
|
type OnModelChanged = Arc<dyn Fn(Arc<dyn LanguageModel>, &mut App) + 'static>;
|
||||||
type GetActiveModel = Arc<dyn Fn(&App) -> Option<ConfiguredModel> + 'static>;
|
type GetActiveModel = Arc<dyn Fn(&App) -> Option<ConfiguredModel> + 'static>;
|
||||||
|
|
||||||
@@ -459,28 +461,14 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
|||||||
fn render_match(
|
fn render_match(
|
||||||
&self,
|
&self,
|
||||||
ix: usize,
|
ix: usize,
|
||||||
selected: bool,
|
is_focused: bool,
|
||||||
_: &mut Window,
|
_: &mut Window,
|
||||||
cx: &mut Context<Picker<Self>>,
|
cx: &mut Context<Picker<Self>>,
|
||||||
) -> Option<Self::ListItem> {
|
) -> Option<Self::ListItem> {
|
||||||
match self.filtered_entries.get(ix)? {
|
match self.filtered_entries.get(ix)? {
|
||||||
LanguageModelPickerEntry::Separator(title) => Some(
|
LanguageModelPickerEntry::Separator(title) => {
|
||||||
div()
|
Some(ModelSelectorHeader::new(title, ix > 1).into_any_element())
|
||||||
.px_2()
|
}
|
||||||
.pb_1()
|
|
||||||
.when(ix > 1, |this| {
|
|
||||||
this.mt_1()
|
|
||||||
.pt_2()
|
|
||||||
.border_t_1()
|
|
||||||
.border_color(cx.theme().colors().border_variant)
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
Label::new(title)
|
|
||||||
.size(LabelSize::XSmall)
|
|
||||||
.color(Color::Muted),
|
|
||||||
)
|
|
||||||
.into_any_element(),
|
|
||||||
),
|
|
||||||
LanguageModelPickerEntry::Model(model_info) => {
|
LanguageModelPickerEntry::Model(model_info) => {
|
||||||
let active_model = (self.get_active_model)(cx);
|
let active_model = (self.get_active_model)(cx);
|
||||||
let active_provider_id = active_model.as_ref().map(|m| m.provider.id());
|
let active_provider_id = active_model.as_ref().map(|m| m.provider.id());
|
||||||
@@ -489,35 +477,11 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
|||||||
let is_selected = Some(model_info.model.provider_id()) == active_provider_id
|
let is_selected = Some(model_info.model.provider_id()) == active_provider_id
|
||||||
&& Some(model_info.model.id()) == active_model_id;
|
&& Some(model_info.model.id()) == active_model_id;
|
||||||
|
|
||||||
let model_icon_color = if is_selected {
|
|
||||||
Color::Accent
|
|
||||||
} else {
|
|
||||||
Color::Muted
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(
|
Some(
|
||||||
ListItem::new(ix)
|
ModelSelectorListItem::new(ix, model_info.model.name().0)
|
||||||
.inset(true)
|
.is_focused(is_focused)
|
||||||
.spacing(ListItemSpacing::Sparse)
|
.is_selected(is_selected)
|
||||||
.toggle_state(selected)
|
.icon(model_info.icon)
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.w_full()
|
|
||||||
.gap_1p5()
|
|
||||||
.child(
|
|
||||||
Icon::new(model_info.icon)
|
|
||||||
.color(model_icon_color)
|
|
||||||
.size(IconSize::Small),
|
|
||||||
)
|
|
||||||
.child(Label::new(model_info.model.name().0).truncate()),
|
|
||||||
)
|
|
||||||
.end_slot(div().pr_3().when(is_selected, |this| {
|
|
||||||
this.child(
|
|
||||||
Icon::new(IconName::Check)
|
|
||||||
.color(Color::Accent)
|
|
||||||
.size(IconSize::Small),
|
|
||||||
)
|
|
||||||
}))
|
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -527,34 +491,15 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
|||||||
fn render_footer(
|
fn render_footer(
|
||||||
&self,
|
&self,
|
||||||
_window: &mut Window,
|
_window: &mut Window,
|
||||||
cx: &mut Context<Picker<Self>>,
|
_cx: &mut Context<Picker<Self>>,
|
||||||
) -> Option<gpui::AnyElement> {
|
) -> Option<gpui::AnyElement> {
|
||||||
let focus_handle = self.focus_handle.clone();
|
|
||||||
|
|
||||||
if !self.popover_styles {
|
if !self.popover_styles {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(
|
let focus_handle = self.focus_handle.clone();
|
||||||
h_flex()
|
|
||||||
.w_full()
|
Some(ModelSelectorFooter::new(OpenSettings.boxed_clone(), focus_handle).into_any_element())
|
||||||
.p_1p5()
|
|
||||||
.border_t_1()
|
|
||||||
.border_color(cx.theme().colors().border_variant)
|
|
||||||
.child(
|
|
||||||
Button::new("configure", "Configure")
|
|
||||||
.full_width()
|
|
||||||
.style(ButtonStyle::Outlined)
|
|
||||||
.key_binding(
|
|
||||||
KeyBinding::for_action_in(&OpenSettings, &focus_handle, cx)
|
|
||||||
.map(|kb| kb.size(rems_from_px(12.))),
|
|
||||||
)
|
|
||||||
.on_click(|_, window, cx| {
|
|
||||||
window.dispatch_action(OpenSettings.boxed_clone(), cx);
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.into_any(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::{ManageProfiles, ToggleProfileSelector};
|
use crate::{CycleModeSelector, ManageProfiles, ToggleProfileSelector};
|
||||||
use agent_settings::{
|
use agent_settings::{
|
||||||
AgentProfile, AgentProfileId, AgentSettings, AvailableProfiles, builtin_profiles,
|
AgentProfile, AgentProfileId, AgentSettings, AvailableProfiles, builtin_profiles,
|
||||||
};
|
};
|
||||||
@@ -70,6 +70,29 @@ impl ProfileSelector {
|
|||||||
self.picker_handle.clone()
|
self.picker_handle.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn cycle_profile(&mut self, cx: &mut Context<Self>) {
|
||||||
|
if !self.provider.profiles_supported(cx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let profiles = AgentProfile::available_profiles(cx);
|
||||||
|
if profiles.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let current_profile_id = self.provider.profile_id(cx);
|
||||||
|
let current_index = profiles
|
||||||
|
.keys()
|
||||||
|
.position(|id| id == ¤t_profile_id)
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
let next_index = (current_index + 1) % profiles.len();
|
||||||
|
|
||||||
|
if let Some((next_profile_id, _)) = profiles.get_index(next_index) {
|
||||||
|
self.provider.set_profile(next_profile_id.clone(), cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn ensure_picker(
|
fn ensure_picker(
|
||||||
&mut self,
|
&mut self,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
@@ -163,14 +186,29 @@ impl Render for ProfileSelector {
|
|||||||
PickerPopoverMenu::new(
|
PickerPopoverMenu::new(
|
||||||
picker,
|
picker,
|
||||||
trigger_button,
|
trigger_button,
|
||||||
move |_window, cx| {
|
Tooltip::element({
|
||||||
Tooltip::for_action_in(
|
move |_window, cx| {
|
||||||
"Toggle Profile Menu",
|
let container = || h_flex().gap_1().justify_between();
|
||||||
&ToggleProfileSelector,
|
v_flex()
|
||||||
&focus_handle,
|
.gap_1()
|
||||||
cx,
|
.child(
|
||||||
)
|
container()
|
||||||
},
|
.pb_1()
|
||||||
|
.border_b_1()
|
||||||
|
.border_color(cx.theme().colors().border_variant)
|
||||||
|
.child(Label::new("Cycle Through Profiles"))
|
||||||
|
.child(KeyBinding::for_action_in(
|
||||||
|
&CycleModeSelector,
|
||||||
|
&focus_handle,
|
||||||
|
cx,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.child(container().child(Label::new("Toggle Profile Menu")).child(
|
||||||
|
KeyBinding::for_action_in(&ToggleProfileSelector, &focus_handle, cx),
|
||||||
|
))
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
|
}),
|
||||||
gpui::Corner::BottomRight,
|
gpui::Corner::BottomRight,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
@@ -542,7 +580,7 @@ impl PickerDelegate for ProfilePickerDelegate {
|
|||||||
let is_active = active_id == candidate.id;
|
let is_active = active_id == candidate.id;
|
||||||
|
|
||||||
Some(
|
Some(
|
||||||
ListItem::new(SharedString::from(candidate.id.0.clone()))
|
ListItem::new(candidate.id.0.clone())
|
||||||
.inset(true)
|
.inset(true)
|
||||||
.spacing(ListItemSpacing::Sparse)
|
.spacing(ListItemSpacing::Sparse)
|
||||||
.toggle_state(selected)
|
.toggle_state(selected)
|
||||||
|
|||||||
@@ -1,37 +1,38 @@
|
|||||||
use crate::inline_prompt_editor::CodegenStatus;
|
use crate::inline_prompt_editor::CodegenStatus;
|
||||||
use client::telemetry::Telemetry;
|
|
||||||
use futures::{SinkExt, StreamExt, channel::mpsc};
|
use futures::{SinkExt, StreamExt, channel::mpsc};
|
||||||
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Task};
|
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Task};
|
||||||
use language_model::{
|
use language_model::{ConfiguredModel, LanguageModelRegistry, LanguageModelRequest};
|
||||||
ConfiguredModel, LanguageModelRegistry, LanguageModelRequest, report_assistant_event,
|
use std::time::Instant;
|
||||||
};
|
|
||||||
use std::{sync::Arc, time::Instant};
|
|
||||||
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
|
|
||||||
use terminal::Terminal;
|
use terminal::Terminal;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub struct TerminalCodegen {
|
pub struct TerminalCodegen {
|
||||||
pub status: CodegenStatus,
|
pub status: CodegenStatus,
|
||||||
pub telemetry: Option<Arc<Telemetry>>,
|
|
||||||
terminal: Entity<Terminal>,
|
terminal: Entity<Terminal>,
|
||||||
generation: Task<()>,
|
generation: Task<()>,
|
||||||
pub message_id: Option<String>,
|
pub message_id: Option<String>,
|
||||||
transaction: Option<TerminalTransaction>,
|
transaction: Option<TerminalTransaction>,
|
||||||
|
session_id: Uuid,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<CodegenEvent> for TerminalCodegen {}
|
impl EventEmitter<CodegenEvent> for TerminalCodegen {}
|
||||||
|
|
||||||
impl TerminalCodegen {
|
impl TerminalCodegen {
|
||||||
pub fn new(terminal: Entity<Terminal>, telemetry: Option<Arc<Telemetry>>) -> Self {
|
pub fn new(terminal: Entity<Terminal>, session_id: Uuid) -> Self {
|
||||||
Self {
|
Self {
|
||||||
terminal,
|
terminal,
|
||||||
telemetry,
|
|
||||||
status: CodegenStatus::Idle,
|
status: CodegenStatus::Idle,
|
||||||
generation: Task::ready(()),
|
generation: Task::ready(()),
|
||||||
message_id: None,
|
message_id: None,
|
||||||
transaction: None,
|
transaction: None,
|
||||||
|
session_id,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn session_id(&self) -> Uuid {
|
||||||
|
self.session_id
|
||||||
|
}
|
||||||
|
|
||||||
pub fn start(&mut self, prompt_task: Task<LanguageModelRequest>, cx: &mut Context<Self>) {
|
pub fn start(&mut self, prompt_task: Task<LanguageModelRequest>, cx: &mut Context<Self>) {
|
||||||
let Some(ConfiguredModel { model, .. }) =
|
let Some(ConfiguredModel { model, .. }) =
|
||||||
LanguageModelRegistry::read_global(cx).inline_assistant_model()
|
LanguageModelRegistry::read_global(cx).inline_assistant_model()
|
||||||
@@ -39,15 +40,15 @@ impl TerminalCodegen {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let model_api_key = model.api_key(cx);
|
let anthropic_reporter = language_model::AnthropicEventReporter::new(&model, cx);
|
||||||
let http_client = cx.http_client();
|
let session_id = self.session_id;
|
||||||
let telemetry = self.telemetry.clone();
|
let model_telemetry_id = model.telemetry_id();
|
||||||
|
let model_provider_id = model.provider_id().to_string();
|
||||||
|
|
||||||
self.status = CodegenStatus::Pending;
|
self.status = CodegenStatus::Pending;
|
||||||
self.transaction = Some(TerminalTransaction::start(self.terminal.clone()));
|
self.transaction = Some(TerminalTransaction::start(self.terminal.clone()));
|
||||||
self.generation = cx.spawn(async move |this, cx| {
|
self.generation = cx.spawn(async move |this, cx| {
|
||||||
let prompt = prompt_task.await;
|
let prompt = prompt_task.await;
|
||||||
let model_telemetry_id = model.telemetry_id();
|
|
||||||
let model_provider_id = model.provider_id();
|
|
||||||
let response = model.stream_completion_text(prompt, cx).await;
|
let response = model.stream_completion_text(prompt, cx).await;
|
||||||
let generate = async {
|
let generate = async {
|
||||||
let message_id = response
|
let message_id = response
|
||||||
@@ -59,7 +60,7 @@ impl TerminalCodegen {
|
|||||||
|
|
||||||
let task = cx.background_spawn({
|
let task = cx.background_spawn({
|
||||||
let message_id = message_id.clone();
|
let message_id = message_id.clone();
|
||||||
let executor = cx.background_executor().clone();
|
let anthropic_reporter = anthropic_reporter.clone();
|
||||||
async move {
|
async move {
|
||||||
let mut response_latency = None;
|
let mut response_latency = None;
|
||||||
let request_start = Instant::now();
|
let request_start = Instant::now();
|
||||||
@@ -79,24 +80,27 @@ impl TerminalCodegen {
|
|||||||
let result = task.await;
|
let result = task.await;
|
||||||
|
|
||||||
let error_message = result.as_ref().err().map(|error| error.to_string());
|
let error_message = result.as_ref().err().map(|error| error.to_string());
|
||||||
report_assistant_event(
|
|
||||||
AssistantEventData {
|
telemetry::event!(
|
||||||
conversation_id: None,
|
"Assistant Responded",
|
||||||
kind: AssistantKind::InlineTerminal,
|
session_id = session_id.to_string(),
|
||||||
message_id,
|
kind = "inline_terminal",
|
||||||
phase: AssistantPhase::Response,
|
phase = "response",
|
||||||
model: model_telemetry_id,
|
model = model_telemetry_id,
|
||||||
model_provider: model_provider_id.to_string(),
|
model_provider = model_provider_id,
|
||||||
response_latency,
|
language_name = Option::<&str>::None,
|
||||||
error_message,
|
message_id = message_id,
|
||||||
language_name: None,
|
response_latency = response_latency,
|
||||||
},
|
error_message = error_message,
|
||||||
telemetry,
|
|
||||||
http_client,
|
|
||||||
model_api_key,
|
|
||||||
&executor,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
anthropic_reporter.report(language_model::AnthropicEventData {
|
||||||
|
completion_type: language_model::AnthropicCompletionType::Terminal,
|
||||||
|
event: language_model::AnthropicEventType::Response,
|
||||||
|
language_name: None,
|
||||||
|
message_id,
|
||||||
|
});
|
||||||
|
|
||||||
result?;
|
result?;
|
||||||
anyhow::Ok(())
|
anyhow::Ok(())
|
||||||
}
|
}
|
||||||
@@ -135,6 +139,12 @@ impl TerminalCodegen {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn completion(&self) -> Option<String> {
|
||||||
|
self.transaction
|
||||||
|
.as_ref()
|
||||||
|
.map(|transaction| transaction.completion.clone())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn stop(&mut self, cx: &mut Context<Self>) {
|
pub fn stop(&mut self, cx: &mut Context<Self>) {
|
||||||
self.status = CodegenStatus::Done;
|
self.status = CodegenStatus::Done;
|
||||||
self.generation = Task::ready(());
|
self.generation = Task::ready(());
|
||||||
@@ -167,27 +177,32 @@ pub const CLEAR_INPUT: &str = "\x03";
|
|||||||
const CARRIAGE_RETURN: &str = "\x0d";
|
const CARRIAGE_RETURN: &str = "\x0d";
|
||||||
|
|
||||||
struct TerminalTransaction {
|
struct TerminalTransaction {
|
||||||
|
completion: String,
|
||||||
terminal: Entity<Terminal>,
|
terminal: Entity<Terminal>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TerminalTransaction {
|
impl TerminalTransaction {
|
||||||
pub fn start(terminal: Entity<Terminal>) -> Self {
|
pub fn start(terminal: Entity<Terminal>) -> Self {
|
||||||
Self { terminal }
|
Self {
|
||||||
|
completion: String::new(),
|
||||||
|
terminal,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn push(&mut self, hunk: String, cx: &mut App) {
|
pub fn push(&mut self, hunk: String, cx: &mut App) {
|
||||||
// Ensure that the assistant cannot accidentally execute commands that are streamed into the terminal
|
// Ensure that the assistant cannot accidentally execute commands that are streamed into the terminal
|
||||||
let input = Self::sanitize_input(hunk);
|
let input = Self::sanitize_input(hunk);
|
||||||
|
self.completion.push_str(&input);
|
||||||
self.terminal
|
self.terminal
|
||||||
.update(cx, |terminal, _| terminal.input(input.into_bytes()));
|
.update(cx, |terminal, _| terminal.input(input.into_bytes()));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn undo(&self, cx: &mut App) {
|
pub fn undo(self, cx: &mut App) {
|
||||||
self.terminal
|
self.terminal
|
||||||
.update(cx, |terminal, _| terminal.input(CLEAR_INPUT.as_bytes()));
|
.update(cx, |terminal, _| terminal.input(CLEAR_INPUT.as_bytes()));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn complete(&self, cx: &mut App) {
|
pub fn complete(self, cx: &mut App) {
|
||||||
self.terminal
|
self.terminal
|
||||||
.update(cx, |terminal, _| terminal.input(CARRIAGE_RETURN.as_bytes()));
|
.update(cx, |terminal, _| terminal.input(CARRIAGE_RETURN.as_bytes()));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use crate::{
|
|||||||
use agent::HistoryStore;
|
use agent::HistoryStore;
|
||||||
use agent_settings::AgentSettings;
|
use agent_settings::AgentSettings;
|
||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result};
|
||||||
use client::telemetry::Telemetry;
|
|
||||||
use cloud_llm_client::CompletionIntent;
|
use cloud_llm_client::CompletionIntent;
|
||||||
use collections::{HashMap, VecDeque};
|
use collections::{HashMap, VecDeque};
|
||||||
use editor::{MultiBuffer, actions::SelectAll};
|
use editor::{MultiBuffer, actions::SelectAll};
|
||||||
@@ -17,24 +17,19 @@ use gpui::{App, Entity, Focusable, Global, Subscription, Task, UpdateGlobal, Wea
|
|||||||
use language::Buffer;
|
use language::Buffer;
|
||||||
use language_model::{
|
use language_model::{
|
||||||
ConfiguredModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
ConfiguredModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||||
Role, report_assistant_event,
|
Role, report_anthropic_event,
|
||||||
};
|
};
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use prompt_store::{PromptBuilder, PromptStore};
|
use prompt_store::{PromptBuilder, PromptStore};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
|
|
||||||
use terminal_view::TerminalView;
|
use terminal_view::TerminalView;
|
||||||
use ui::prelude::*;
|
use ui::prelude::*;
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
use uuid::Uuid;
|
||||||
use workspace::{Toast, Workspace, notifications::NotificationId};
|
use workspace::{Toast, Workspace, notifications::NotificationId};
|
||||||
|
|
||||||
pub fn init(
|
pub fn init(fs: Arc<dyn Fs>, prompt_builder: Arc<PromptBuilder>, cx: &mut App) {
|
||||||
fs: Arc<dyn Fs>,
|
cx.set_global(TerminalInlineAssistant::new(fs, prompt_builder));
|
||||||
prompt_builder: Arc<PromptBuilder>,
|
|
||||||
telemetry: Arc<Telemetry>,
|
|
||||||
cx: &mut App,
|
|
||||||
) {
|
|
||||||
cx.set_global(TerminalInlineAssistant::new(fs, prompt_builder, telemetry));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_CONTEXT_LINES: usize = 50;
|
const DEFAULT_CONTEXT_LINES: usize = 50;
|
||||||
@@ -44,7 +39,6 @@ pub struct TerminalInlineAssistant {
|
|||||||
next_assist_id: TerminalInlineAssistId,
|
next_assist_id: TerminalInlineAssistId,
|
||||||
assists: HashMap<TerminalInlineAssistId, TerminalInlineAssist>,
|
assists: HashMap<TerminalInlineAssistId, TerminalInlineAssist>,
|
||||||
prompt_history: VecDeque<String>,
|
prompt_history: VecDeque<String>,
|
||||||
telemetry: Option<Arc<Telemetry>>,
|
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
prompt_builder: Arc<PromptBuilder>,
|
prompt_builder: Arc<PromptBuilder>,
|
||||||
}
|
}
|
||||||
@@ -52,16 +46,11 @@ pub struct TerminalInlineAssistant {
|
|||||||
impl Global for TerminalInlineAssistant {}
|
impl Global for TerminalInlineAssistant {}
|
||||||
|
|
||||||
impl TerminalInlineAssistant {
|
impl TerminalInlineAssistant {
|
||||||
pub fn new(
|
pub fn new(fs: Arc<dyn Fs>, prompt_builder: Arc<PromptBuilder>) -> Self {
|
||||||
fs: Arc<dyn Fs>,
|
|
||||||
prompt_builder: Arc<PromptBuilder>,
|
|
||||||
telemetry: Arc<Telemetry>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
next_assist_id: TerminalInlineAssistId::default(),
|
next_assist_id: TerminalInlineAssistId::default(),
|
||||||
assists: HashMap::default(),
|
assists: HashMap::default(),
|
||||||
prompt_history: VecDeque::default(),
|
prompt_history: VecDeque::default(),
|
||||||
telemetry: Some(telemetry),
|
|
||||||
fs,
|
fs,
|
||||||
prompt_builder,
|
prompt_builder,
|
||||||
}
|
}
|
||||||
@@ -80,13 +69,14 @@ impl TerminalInlineAssistant {
|
|||||||
) {
|
) {
|
||||||
let terminal = terminal_view.read(cx).terminal().clone();
|
let terminal = terminal_view.read(cx).terminal().clone();
|
||||||
let assist_id = self.next_assist_id.post_inc();
|
let assist_id = self.next_assist_id.post_inc();
|
||||||
|
let session_id = Uuid::new_v4();
|
||||||
let prompt_buffer = cx.new(|cx| {
|
let prompt_buffer = cx.new(|cx| {
|
||||||
MultiBuffer::singleton(
|
MultiBuffer::singleton(
|
||||||
cx.new(|cx| Buffer::local(initial_prompt.unwrap_or_default(), cx)),
|
cx.new(|cx| Buffer::local(initial_prompt.unwrap_or_default(), cx)),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
let codegen = cx.new(|_| TerminalCodegen::new(terminal, self.telemetry.clone()));
|
let codegen = cx.new(|_| TerminalCodegen::new(terminal, session_id));
|
||||||
|
|
||||||
let prompt_editor = cx.new(|cx| {
|
let prompt_editor = cx.new(|cx| {
|
||||||
PromptEditor::new_terminal(
|
PromptEditor::new_terminal(
|
||||||
@@ -94,6 +84,7 @@ impl TerminalInlineAssistant {
|
|||||||
self.prompt_history.clone(),
|
self.prompt_history.clone(),
|
||||||
prompt_buffer.clone(),
|
prompt_buffer.clone(),
|
||||||
codegen,
|
codegen,
|
||||||
|
session_id,
|
||||||
self.fs.clone(),
|
self.fs.clone(),
|
||||||
thread_store.clone(),
|
thread_store.clone(),
|
||||||
prompt_store.clone(),
|
prompt_store.clone(),
|
||||||
@@ -309,27 +300,45 @@ impl TerminalInlineAssistant {
|
|||||||
LanguageModelRegistry::read_global(cx).inline_assistant_model()
|
LanguageModelRegistry::read_global(cx).inline_assistant_model()
|
||||||
{
|
{
|
||||||
let codegen = assist.codegen.read(cx);
|
let codegen = assist.codegen.read(cx);
|
||||||
let executor = cx.background_executor().clone();
|
let session_id = codegen.session_id();
|
||||||
report_assistant_event(
|
let message_id = codegen.message_id.clone();
|
||||||
AssistantEventData {
|
let model_telemetry_id = model.telemetry_id();
|
||||||
conversation_id: None,
|
let model_provider_id = model.provider_id().to_string();
|
||||||
kind: AssistantKind::InlineTerminal,
|
|
||||||
message_id: codegen.message_id.clone(),
|
let (phase, event_type, anthropic_event_type) = if undo {
|
||||||
phase: if undo {
|
(
|
||||||
AssistantPhase::Rejected
|
"rejected",
|
||||||
} else {
|
"Assistant Response Rejected",
|
||||||
AssistantPhase::Accepted
|
language_model::AnthropicEventType::Reject,
|
||||||
},
|
)
|
||||||
model: model.telemetry_id(),
|
} else {
|
||||||
model_provider: model.provider_id().to_string(),
|
(
|
||||||
response_latency: None,
|
"accepted",
|
||||||
error_message: None,
|
"Assistant Response Accepted",
|
||||||
|
language_model::AnthropicEventType::Accept,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fire Zed telemetry
|
||||||
|
telemetry::event!(
|
||||||
|
event_type,
|
||||||
|
kind = "inline_terminal",
|
||||||
|
phase = phase,
|
||||||
|
model = model_telemetry_id,
|
||||||
|
model_provider = model_provider_id,
|
||||||
|
message_id = message_id,
|
||||||
|
session_id = session_id,
|
||||||
|
);
|
||||||
|
|
||||||
|
report_anthropic_event(
|
||||||
|
&model,
|
||||||
|
language_model::AnthropicEventData {
|
||||||
|
completion_type: language_model::AnthropicCompletionType::Terminal,
|
||||||
|
event: anthropic_event_type,
|
||||||
language_name: None,
|
language_name: None,
|
||||||
|
message_id,
|
||||||
},
|
},
|
||||||
codegen.telemetry.clone(),
|
cx,
|
||||||
cx.http_client(),
|
|
||||||
model.api_key(cx),
|
|
||||||
&executor,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3324,7 +3324,6 @@ mod tests {
|
|||||||
let mut text_thread = TextThread::local(
|
let mut text_thread = TextThread::local(
|
||||||
registry,
|
registry,
|
||||||
None,
|
None,
|
||||||
None,
|
|
||||||
prompt_builder.clone(),
|
prompt_builder.clone(),
|
||||||
Arc::new(SlashCommandWorkingSet::default()),
|
Arc::new(SlashCommandWorkingSet::default()),
|
||||||
cx,
|
cx,
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ mod burn_mode_tooltip;
|
|||||||
mod claude_code_onboarding_modal;
|
mod claude_code_onboarding_modal;
|
||||||
mod end_trial_upsell;
|
mod end_trial_upsell;
|
||||||
mod hold_for_default;
|
mod hold_for_default;
|
||||||
|
mod model_selector_components;
|
||||||
mod onboarding_modal;
|
mod onboarding_modal;
|
||||||
mod unavailable_editing_tooltip;
|
|
||||||
mod usage_callout;
|
mod usage_callout;
|
||||||
|
|
||||||
pub use acp_onboarding_modal::*;
|
pub use acp_onboarding_modal::*;
|
||||||
@@ -14,6 +14,6 @@ pub use burn_mode_tooltip::*;
|
|||||||
pub use claude_code_onboarding_modal::*;
|
pub use claude_code_onboarding_modal::*;
|
||||||
pub use end_trial_upsell::*;
|
pub use end_trial_upsell::*;
|
||||||
pub use hold_for_default::*;
|
pub use hold_for_default::*;
|
||||||
|
pub use model_selector_components::*;
|
||||||
pub use onboarding_modal::*;
|
pub use onboarding_modal::*;
|
||||||
pub use unavailable_editing_tooltip::*;
|
|
||||||
pub use usage_callout::*;
|
pub use usage_callout::*;
|
||||||
|
|||||||
147
crates/agent_ui/src/ui/model_selector_components.rs
Normal file
147
crates/agent_ui/src/ui/model_selector_components.rs
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
use gpui::{Action, FocusHandle, prelude::*};
|
||||||
|
use ui::{KeyBinding, ListItem, ListItemSpacing, prelude::*};
|
||||||
|
|
||||||
|
#[derive(IntoElement)]
|
||||||
|
pub struct ModelSelectorHeader {
|
||||||
|
title: SharedString,
|
||||||
|
has_border: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ModelSelectorHeader {
|
||||||
|
pub fn new(title: impl Into<SharedString>, has_border: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
title: title.into(),
|
||||||
|
has_border,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderOnce for ModelSelectorHeader {
|
||||||
|
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
|
div()
|
||||||
|
.px_2()
|
||||||
|
.pb_1()
|
||||||
|
.when(self.has_border, |this| {
|
||||||
|
this.mt_1()
|
||||||
|
.pt_2()
|
||||||
|
.border_t_1()
|
||||||
|
.border_color(cx.theme().colors().border_variant)
|
||||||
|
})
|
||||||
|
.child(
|
||||||
|
Label::new(self.title)
|
||||||
|
.size(LabelSize::XSmall)
|
||||||
|
.color(Color::Muted),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(IntoElement)]
|
||||||
|
pub struct ModelSelectorListItem {
|
||||||
|
index: usize,
|
||||||
|
title: SharedString,
|
||||||
|
icon: Option<IconName>,
|
||||||
|
is_selected: bool,
|
||||||
|
is_focused: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ModelSelectorListItem {
|
||||||
|
pub fn new(index: usize, title: impl Into<SharedString>) -> Self {
|
||||||
|
Self {
|
||||||
|
index,
|
||||||
|
title: title.into(),
|
||||||
|
icon: None,
|
||||||
|
is_selected: false,
|
||||||
|
is_focused: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn icon(mut self, icon: IconName) -> Self {
|
||||||
|
self.icon = Some(icon);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_selected(mut self, is_selected: bool) -> Self {
|
||||||
|
self.is_selected = is_selected;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_focused(mut self, is_focused: bool) -> Self {
|
||||||
|
self.is_focused = is_focused;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderOnce for ModelSelectorListItem {
|
||||||
|
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||||
|
let model_icon_color = if self.is_selected {
|
||||||
|
Color::Accent
|
||||||
|
} else {
|
||||||
|
Color::Muted
|
||||||
|
};
|
||||||
|
|
||||||
|
ListItem::new(self.index)
|
||||||
|
.inset(true)
|
||||||
|
.spacing(ListItemSpacing::Sparse)
|
||||||
|
.toggle_state(self.is_focused)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.w_full()
|
||||||
|
.gap_1p5()
|
||||||
|
.when_some(self.icon, |this, icon| {
|
||||||
|
this.child(
|
||||||
|
Icon::new(icon)
|
||||||
|
.color(model_icon_color)
|
||||||
|
.size(IconSize::Small),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.child(Label::new(self.title).truncate()),
|
||||||
|
)
|
||||||
|
.end_slot(div().pr_2().when(self.is_selected, |this| {
|
||||||
|
this.child(
|
||||||
|
Icon::new(IconName::Check)
|
||||||
|
.color(Color::Accent)
|
||||||
|
.size(IconSize::Small),
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(IntoElement)]
|
||||||
|
pub struct ModelSelectorFooter {
|
||||||
|
action: Box<dyn Action>,
|
||||||
|
focus_handle: FocusHandle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ModelSelectorFooter {
|
||||||
|
pub fn new(action: Box<dyn Action>, focus_handle: FocusHandle) -> Self {
|
||||||
|
Self {
|
||||||
|
action,
|
||||||
|
focus_handle,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderOnce for ModelSelectorFooter {
|
||||||
|
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
|
let action = self.action;
|
||||||
|
let focus_handle = self.focus_handle;
|
||||||
|
|
||||||
|
h_flex()
|
||||||
|
.w_full()
|
||||||
|
.p_1p5()
|
||||||
|
.border_t_1()
|
||||||
|
.border_color(cx.theme().colors().border_variant)
|
||||||
|
.child(
|
||||||
|
Button::new("configure", "Configure")
|
||||||
|
.full_width()
|
||||||
|
.style(ButtonStyle::Outlined)
|
||||||
|
.key_binding(
|
||||||
|
KeyBinding::for_action_in(action.as_ref(), &focus_handle, cx)
|
||||||
|
.map(|kb| kb.size(rems_from_px(12.))),
|
||||||
|
)
|
||||||
|
.on_click(move |_, window, cx| {
|
||||||
|
window.dispatch_action(action.boxed_clone(), cx);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
use gpui::{Context, IntoElement, Render, Window};
|
|
||||||
use ui::{prelude::*, tooltip_container};
|
|
||||||
|
|
||||||
pub struct UnavailableEditingTooltip {
|
|
||||||
agent_name: SharedString,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UnavailableEditingTooltip {
|
|
||||||
pub fn new(agent_name: SharedString) -> Self {
|
|
||||||
Self { agent_name }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for UnavailableEditingTooltip {
|
|
||||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
tooltip_container(cx, |this, _| {
|
|
||||||
this.child(Label::new("Unavailable Editing")).child(
|
|
||||||
div().max_w_64().child(
|
|
||||||
Label::new(format!(
|
|
||||||
"Editing previous messages is not available for {} yet.",
|
|
||||||
self.agent_name
|
|
||||||
))
|
|
||||||
.size(LabelSize::Small)
|
|
||||||
.color(Color::Muted),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
40
crates/agent_ui_v2/Cargo.toml
Normal file
40
crates/agent_ui_v2/Cargo.toml
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
[package]
|
||||||
|
name = "agent_ui_v2"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
license = "GPL-3.0-or-later"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/agent_ui_v2.rs"
|
||||||
|
doctest = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
agent.workspace = true
|
||||||
|
agent_servers.workspace = true
|
||||||
|
agent_settings.workspace = true
|
||||||
|
agent_ui.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
|
assistant_text_thread.workspace = true
|
||||||
|
chrono.workspace = true
|
||||||
|
db.workspace = true
|
||||||
|
editor.workspace = true
|
||||||
|
feature_flags.workspace = true
|
||||||
|
fs.workspace = true
|
||||||
|
fuzzy.workspace = true
|
||||||
|
gpui.workspace = true
|
||||||
|
menu.workspace = true
|
||||||
|
project.workspace = true
|
||||||
|
prompt_store.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
settings.workspace = true
|
||||||
|
text.workspace = true
|
||||||
|
time.workspace = true
|
||||||
|
time_format.workspace = true
|
||||||
|
ui.workspace = true
|
||||||
|
util.workspace = true
|
||||||
|
workspace.workspace = true
|
||||||
1
crates/agent_ui_v2/LICENSE-GPL
Symbolic link
1
crates/agent_ui_v2/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
LICENSE-GPL
|
||||||
287
crates/agent_ui_v2/src/agent_thread_pane.rs
Normal file
287
crates/agent_ui_v2/src/agent_thread_pane.rs
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
use agent::{HistoryEntry, HistoryEntryId, HistoryStore, NativeAgentServer};
|
||||||
|
use agent_servers::AgentServer;
|
||||||
|
use agent_settings::AgentSettings;
|
||||||
|
use agent_ui::acp::AcpThreadView;
|
||||||
|
use fs::Fs;
|
||||||
|
use gpui::{
|
||||||
|
Entity, EventEmitter, Focusable, Pixels, SharedString, Subscription, WeakEntity, prelude::*,
|
||||||
|
};
|
||||||
|
use project::Project;
|
||||||
|
use prompt_store::PromptStore;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use settings::DockSide;
|
||||||
|
use settings::Settings as _;
|
||||||
|
use std::rc::Rc;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use ui::{Tab, Tooltip, prelude::*};
|
||||||
|
use workspace::{
|
||||||
|
Workspace,
|
||||||
|
dock::{ClosePane, MinimizePane, UtilityPane, UtilityPanePosition},
|
||||||
|
utility_pane::UtilityPaneSlot,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const DEFAULT_UTILITY_PANE_WIDTH: Pixels = gpui::px(400.0);
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub enum SerializedHistoryEntryId {
|
||||||
|
AcpThread(String),
|
||||||
|
TextThread(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<HistoryEntryId> for SerializedHistoryEntryId {
|
||||||
|
fn from(id: HistoryEntryId) -> Self {
|
||||||
|
match id {
|
||||||
|
HistoryEntryId::AcpThread(session_id) => {
|
||||||
|
SerializedHistoryEntryId::AcpThread(session_id.0.to_string())
|
||||||
|
}
|
||||||
|
HistoryEntryId::TextThread(path) => {
|
||||||
|
SerializedHistoryEntryId::TextThread(path.to_string_lossy().to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct SerializedAgentThreadPane {
|
||||||
|
pub expanded: bool,
|
||||||
|
pub width: Option<Pixels>,
|
||||||
|
pub thread_id: Option<SerializedHistoryEntryId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum AgentsUtilityPaneEvent {
|
||||||
|
StateChanged,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<AgentsUtilityPaneEvent> for AgentThreadPane {}
|
||||||
|
impl EventEmitter<MinimizePane> for AgentThreadPane {}
|
||||||
|
impl EventEmitter<ClosePane> for AgentThreadPane {}
|
||||||
|
|
||||||
|
struct ActiveThreadView {
|
||||||
|
view: Entity<AcpThreadView>,
|
||||||
|
thread_id: HistoryEntryId,
|
||||||
|
_notify: Subscription,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AgentThreadPane {
|
||||||
|
focus_handle: gpui::FocusHandle,
|
||||||
|
expanded: bool,
|
||||||
|
width: Option<Pixels>,
|
||||||
|
thread_view: Option<ActiveThreadView>,
|
||||||
|
workspace: WeakEntity<Workspace>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AgentThreadPane {
|
||||||
|
pub fn new(workspace: WeakEntity<Workspace>, cx: &mut ui::Context<Self>) -> Self {
|
||||||
|
let focus_handle = cx.focus_handle();
|
||||||
|
Self {
|
||||||
|
focus_handle,
|
||||||
|
expanded: false,
|
||||||
|
width: None,
|
||||||
|
thread_view: None,
|
||||||
|
workspace,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn thread_id(&self) -> Option<HistoryEntryId> {
|
||||||
|
self.thread_view.as_ref().map(|tv| tv.thread_id.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn serialize(&self) -> SerializedAgentThreadPane {
|
||||||
|
SerializedAgentThreadPane {
|
||||||
|
expanded: self.expanded,
|
||||||
|
width: self.width,
|
||||||
|
thread_id: self.thread_id().map(SerializedHistoryEntryId::from),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open_thread(
|
||||||
|
&mut self,
|
||||||
|
entry: HistoryEntry,
|
||||||
|
fs: Arc<dyn Fs>,
|
||||||
|
workspace: WeakEntity<Workspace>,
|
||||||
|
project: Entity<Project>,
|
||||||
|
history_store: Entity<HistoryStore>,
|
||||||
|
prompt_store: Option<Entity<PromptStore>>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let thread_id = entry.id();
|
||||||
|
|
||||||
|
let resume_thread = match &entry {
|
||||||
|
HistoryEntry::AcpThread(thread) => Some(thread.clone()),
|
||||||
|
HistoryEntry::TextThread(_) => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let agent: Rc<dyn AgentServer> = Rc::new(NativeAgentServer::new(fs, history_store.clone()));
|
||||||
|
|
||||||
|
let thread_view = cx.new(|cx| {
|
||||||
|
AcpThreadView::new(
|
||||||
|
agent,
|
||||||
|
resume_thread,
|
||||||
|
None,
|
||||||
|
workspace,
|
||||||
|
project,
|
||||||
|
history_store,
|
||||||
|
prompt_store,
|
||||||
|
true,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let notify = cx.observe(&thread_view, |_, _, cx| {
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
|
||||||
|
self.thread_view = Some(ActiveThreadView {
|
||||||
|
view: thread_view,
|
||||||
|
thread_id,
|
||||||
|
_notify: notify,
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn title(&self, cx: &App) -> SharedString {
|
||||||
|
if let Some(active_thread_view) = &self.thread_view {
|
||||||
|
let thread_view = active_thread_view.view.read(cx);
|
||||||
|
if let Some(thread) = thread_view.thread() {
|
||||||
|
let title = thread.read(cx).title();
|
||||||
|
if !title.is_empty() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
thread_view.title(cx)
|
||||||
|
} else {
|
||||||
|
"Thread".into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_header(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let position = self.position(window, cx);
|
||||||
|
let slot = match position {
|
||||||
|
UtilityPanePosition::Left => UtilityPaneSlot::Left,
|
||||||
|
UtilityPanePosition::Right => UtilityPaneSlot::Right,
|
||||||
|
};
|
||||||
|
|
||||||
|
let workspace = self.workspace.clone();
|
||||||
|
let toggle_icon = self.toggle_icon(cx);
|
||||||
|
let title = self.title(cx);
|
||||||
|
|
||||||
|
let pane_toggle_button = |workspace: WeakEntity<Workspace>| {
|
||||||
|
IconButton::new("toggle_utility_pane", toggle_icon)
|
||||||
|
.icon_size(IconSize::Small)
|
||||||
|
.tooltip(Tooltip::text("Toggle Agent Pane"))
|
||||||
|
.on_click(move |_, window, cx| {
|
||||||
|
workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
workspace.toggle_utility_pane(slot, window, cx)
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
h_flex()
|
||||||
|
.id("utility-pane-header")
|
||||||
|
.w_full()
|
||||||
|
.h(Tab::container_height(cx))
|
||||||
|
.px_1p5()
|
||||||
|
.gap(DynamicSpacing::Base06.rems(cx))
|
||||||
|
.when(slot == UtilityPaneSlot::Right, |this| {
|
||||||
|
this.flex_row_reverse()
|
||||||
|
})
|
||||||
|
.flex_none()
|
||||||
|
.border_b_1()
|
||||||
|
.border_color(cx.theme().colors().border)
|
||||||
|
.child(pane_toggle_button(workspace))
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.size_full()
|
||||||
|
.min_w_0()
|
||||||
|
.gap_1()
|
||||||
|
.map(|this| {
|
||||||
|
if slot == UtilityPaneSlot::Right {
|
||||||
|
this.flex_row_reverse().justify_start()
|
||||||
|
} else {
|
||||||
|
this.justify_between()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.child(Label::new(title).truncate())
|
||||||
|
.child(
|
||||||
|
IconButton::new("close_btn", IconName::Close)
|
||||||
|
.icon_size(IconSize::Small)
|
||||||
|
.tooltip(Tooltip::text("Close Agent Pane"))
|
||||||
|
.on_click(cx.listener(|this, _: &gpui::ClickEvent, _window, cx| {
|
||||||
|
cx.emit(ClosePane);
|
||||||
|
this.thread_view = None;
|
||||||
|
cx.notify()
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Focusable for AgentThreadPane {
|
||||||
|
fn focus_handle(&self, cx: &ui::App) -> gpui::FocusHandle {
|
||||||
|
if let Some(thread_view) = &self.thread_view {
|
||||||
|
thread_view.view.focus_handle(cx)
|
||||||
|
} else {
|
||||||
|
self.focus_handle.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UtilityPane for AgentThreadPane {
|
||||||
|
fn position(&self, _window: &Window, cx: &App) -> UtilityPanePosition {
|
||||||
|
match AgentSettings::get_global(cx).agents_panel_dock {
|
||||||
|
DockSide::Left => UtilityPanePosition::Left,
|
||||||
|
DockSide::Right => UtilityPanePosition::Right,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle_icon(&self, _cx: &App) -> IconName {
|
||||||
|
IconName::Thread
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expanded(&self, _cx: &App) -> bool {
|
||||||
|
self.expanded
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_expanded(&mut self, expanded: bool, cx: &mut Context<Self>) {
|
||||||
|
self.expanded = expanded;
|
||||||
|
cx.emit(AgentsUtilityPaneEvent::StateChanged);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn width(&self, _cx: &App) -> Pixels {
|
||||||
|
self.width.unwrap_or(DEFAULT_UTILITY_PANE_WIDTH)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_width(&mut self, width: Option<Pixels>, cx: &mut Context<Self>) {
|
||||||
|
self.width = width;
|
||||||
|
cx.emit(AgentsUtilityPaneEvent::StateChanged);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for AgentThreadPane {
|
||||||
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let content = if let Some(thread_view) = &self.thread_view {
|
||||||
|
div().size_full().child(thread_view.view.clone())
|
||||||
|
} else {
|
||||||
|
div()
|
||||||
|
.size_full()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.child(Label::new("Select a thread to view details").size(LabelSize::Default))
|
||||||
|
};
|
||||||
|
|
||||||
|
div()
|
||||||
|
.size_full()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.child(self.render_header(window, cx))
|
||||||
|
.child(content)
|
||||||
|
}
|
||||||
|
}
|
||||||
4
crates/agent_ui_v2/src/agent_ui_v2.rs
Normal file
4
crates/agent_ui_v2/src/agent_ui_v2.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
mod agent_thread_pane;
|
||||||
|
mod thread_history;
|
||||||
|
|
||||||
|
pub mod agents_panel;
|
||||||
437
crates/agent_ui_v2/src/agents_panel.rs
Normal file
437
crates/agent_ui_v2/src/agents_panel.rs
Normal file
@@ -0,0 +1,437 @@
|
|||||||
|
use agent::{HistoryEntry, HistoryEntryId, HistoryStore};
|
||||||
|
use agent_settings::AgentSettings;
|
||||||
|
use anyhow::Result;
|
||||||
|
use assistant_text_thread::TextThreadStore;
|
||||||
|
use db::kvp::KEY_VALUE_STORE;
|
||||||
|
use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt};
|
||||||
|
use fs::Fs;
|
||||||
|
use gpui::{
|
||||||
|
Action, AsyncWindowContext, Entity, EventEmitter, Focusable, Pixels, Subscription, Task,
|
||||||
|
WeakEntity, actions, prelude::*,
|
||||||
|
};
|
||||||
|
use project::Project;
|
||||||
|
use prompt_store::{PromptBuilder, PromptStore};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use settings::{Settings as _, update_settings_file};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use ui::{App, Context, IconName, IntoElement, ParentElement, Render, Styled, Window};
|
||||||
|
use util::ResultExt;
|
||||||
|
use workspace::{
|
||||||
|
Panel, Workspace,
|
||||||
|
dock::{ClosePane, DockPosition, PanelEvent, UtilityPane},
|
||||||
|
utility_pane::{UtilityPaneSlot, utility_slot_for_dock_position},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::agent_thread_pane::{
|
||||||
|
AgentThreadPane, AgentsUtilityPaneEvent, SerializedAgentThreadPane, SerializedHistoryEntryId,
|
||||||
|
};
|
||||||
|
use crate::thread_history::{AcpThreadHistory, ThreadHistoryEvent};
|
||||||
|
|
||||||
|
const AGENTS_PANEL_KEY: &str = "agents_panel";
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
struct SerializedAgentsPanel {
|
||||||
|
width: Option<Pixels>,
|
||||||
|
pane: Option<SerializedAgentThreadPane>,
|
||||||
|
}
|
||||||
|
|
||||||
|
actions!(
|
||||||
|
agents,
|
||||||
|
[
|
||||||
|
/// Toggle the visibility of the agents panel.
|
||||||
|
ToggleAgentsPanel
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
pub fn init(cx: &mut App) {
|
||||||
|
cx.observe_new(|workspace: &mut Workspace, _, _| {
|
||||||
|
workspace.register_action(|workspace, _: &ToggleAgentsPanel, window, cx| {
|
||||||
|
workspace.toggle_panel_focus::<AgentsPanel>(window, cx);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AgentsPanel {
|
||||||
|
focus_handle: gpui::FocusHandle,
|
||||||
|
workspace: WeakEntity<Workspace>,
|
||||||
|
project: Entity<Project>,
|
||||||
|
agent_thread_pane: Option<Entity<AgentThreadPane>>,
|
||||||
|
history: Entity<AcpThreadHistory>,
|
||||||
|
history_store: Entity<HistoryStore>,
|
||||||
|
prompt_store: Option<Entity<PromptStore>>,
|
||||||
|
fs: Arc<dyn Fs>,
|
||||||
|
width: Option<Pixels>,
|
||||||
|
pending_serialization: Task<Option<()>>,
|
||||||
|
_subscriptions: Vec<Subscription>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AgentsPanel {
|
||||||
|
pub fn load(
|
||||||
|
workspace: WeakEntity<Workspace>,
|
||||||
|
cx: AsyncWindowContext,
|
||||||
|
) -> Task<Result<Entity<Self>, anyhow::Error>> {
|
||||||
|
cx.spawn(async move |cx| {
|
||||||
|
let serialized_panel = cx
|
||||||
|
.background_spawn(async move {
|
||||||
|
KEY_VALUE_STORE
|
||||||
|
.read_kvp(AGENTS_PANEL_KEY)
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.and_then(|panel| {
|
||||||
|
serde_json::from_str::<SerializedAgentsPanel>(&panel).ok()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let (fs, project, prompt_builder) = workspace.update(cx, |workspace, cx| {
|
||||||
|
let fs = workspace.app_state().fs.clone();
|
||||||
|
let project = workspace.project().clone();
|
||||||
|
let prompt_builder = PromptBuilder::load(fs.clone(), false, cx);
|
||||||
|
(fs, project, prompt_builder)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let text_thread_store = workspace
|
||||||
|
.update(cx, |_, cx| {
|
||||||
|
TextThreadStore::new(
|
||||||
|
project.clone(),
|
||||||
|
prompt_builder.clone(),
|
||||||
|
Default::default(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let prompt_store = workspace
|
||||||
|
.update(cx, |_, cx| PromptStore::global(cx))?
|
||||||
|
.await
|
||||||
|
.log_err();
|
||||||
|
|
||||||
|
workspace.update_in(cx, |_, window, cx| {
|
||||||
|
cx.new(|cx| {
|
||||||
|
let mut panel = Self::new(
|
||||||
|
workspace.clone(),
|
||||||
|
fs,
|
||||||
|
project,
|
||||||
|
prompt_store,
|
||||||
|
text_thread_store,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
if let Some(serialized_panel) = serialized_panel {
|
||||||
|
panel.width = serialized_panel.width;
|
||||||
|
if let Some(serialized_pane) = serialized_panel.pane {
|
||||||
|
panel.restore_utility_pane(serialized_pane, window, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panel
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new(
|
||||||
|
workspace: WeakEntity<Workspace>,
|
||||||
|
fs: Arc<dyn Fs>,
|
||||||
|
project: Entity<Project>,
|
||||||
|
prompt_store: Option<Entity<PromptStore>>,
|
||||||
|
text_thread_store: Entity<TextThreadStore>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut ui::Context<Self>,
|
||||||
|
) -> Self {
|
||||||
|
let focus_handle = cx.focus_handle();
|
||||||
|
|
||||||
|
let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx));
|
||||||
|
let history = cx.new(|cx| AcpThreadHistory::new(history_store.clone(), window, cx));
|
||||||
|
|
||||||
|
let this = cx.weak_entity();
|
||||||
|
let subscriptions = vec![
|
||||||
|
cx.subscribe_in(&history, window, Self::handle_history_event),
|
||||||
|
cx.on_flags_ready(move |_, cx| {
|
||||||
|
this.update(cx, |_, cx| {
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
Self {
|
||||||
|
focus_handle,
|
||||||
|
workspace,
|
||||||
|
project,
|
||||||
|
agent_thread_pane: None,
|
||||||
|
history,
|
||||||
|
history_store,
|
||||||
|
prompt_store,
|
||||||
|
fs,
|
||||||
|
width: None,
|
||||||
|
pending_serialization: Task::ready(None),
|
||||||
|
_subscriptions: subscriptions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn restore_utility_pane(
|
||||||
|
&mut self,
|
||||||
|
serialized_pane: SerializedAgentThreadPane,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let Some(thread_id) = &serialized_pane.thread_id else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let entry = self
|
||||||
|
.history_store
|
||||||
|
.read(cx)
|
||||||
|
.entries()
|
||||||
|
.find(|e| match (&e.id(), thread_id) {
|
||||||
|
(
|
||||||
|
HistoryEntryId::AcpThread(session_id),
|
||||||
|
SerializedHistoryEntryId::AcpThread(id),
|
||||||
|
) => session_id.to_string() == *id,
|
||||||
|
(HistoryEntryId::TextThread(path), SerializedHistoryEntryId::TextThread(id)) => {
|
||||||
|
path.to_string_lossy() == *id
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(entry) = entry {
|
||||||
|
self.open_thread(
|
||||||
|
entry,
|
||||||
|
serialized_pane.expanded,
|
||||||
|
serialized_pane.width,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_utility_pane_event(
|
||||||
|
&mut self,
|
||||||
|
_utility_pane: Entity<AgentThreadPane>,
|
||||||
|
event: &AgentsUtilityPaneEvent,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
match event {
|
||||||
|
AgentsUtilityPaneEvent::StateChanged => {
|
||||||
|
self.serialize(cx);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_close_pane_event(
|
||||||
|
&mut self,
|
||||||
|
_utility_pane: Entity<AgentThreadPane>,
|
||||||
|
_event: &ClosePane,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.agent_thread_pane = None;
|
||||||
|
self.serialize(cx);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_history_event(
|
||||||
|
&mut self,
|
||||||
|
_history: &Entity<AcpThreadHistory>,
|
||||||
|
event: &ThreadHistoryEvent,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
match event {
|
||||||
|
ThreadHistoryEvent::Open(entry) => {
|
||||||
|
self.open_thread(entry.clone(), true, None, window, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_thread(
|
||||||
|
&mut self,
|
||||||
|
entry: HistoryEntry,
|
||||||
|
expanded: bool,
|
||||||
|
width: Option<Pixels>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let entry_id = entry.id();
|
||||||
|
|
||||||
|
if let Some(existing_pane) = &self.agent_thread_pane {
|
||||||
|
if existing_pane.read(cx).thread_id() == Some(entry_id) {
|
||||||
|
existing_pane.update(cx, |pane, cx| {
|
||||||
|
pane.set_expanded(true, cx);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let fs = self.fs.clone();
|
||||||
|
let workspace = self.workspace.clone();
|
||||||
|
let project = self.project.clone();
|
||||||
|
let history_store = self.history_store.clone();
|
||||||
|
let prompt_store = self.prompt_store.clone();
|
||||||
|
|
||||||
|
let agent_thread_pane = cx.new(|cx| {
|
||||||
|
let mut pane = AgentThreadPane::new(workspace.clone(), cx);
|
||||||
|
pane.open_thread(
|
||||||
|
entry,
|
||||||
|
fs,
|
||||||
|
workspace.clone(),
|
||||||
|
project,
|
||||||
|
history_store,
|
||||||
|
prompt_store,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
if let Some(width) = width {
|
||||||
|
pane.set_width(Some(width), cx);
|
||||||
|
}
|
||||||
|
pane.set_expanded(expanded, cx);
|
||||||
|
pane
|
||||||
|
});
|
||||||
|
|
||||||
|
let state_subscription = cx.subscribe(&agent_thread_pane, Self::handle_utility_pane_event);
|
||||||
|
let close_subscription = cx.subscribe(&agent_thread_pane, Self::handle_close_pane_event);
|
||||||
|
|
||||||
|
self._subscriptions.push(state_subscription);
|
||||||
|
self._subscriptions.push(close_subscription);
|
||||||
|
|
||||||
|
let slot = self.utility_slot(window, cx);
|
||||||
|
let panel_id = cx.entity_id();
|
||||||
|
|
||||||
|
if let Some(workspace) = self.workspace.upgrade() {
|
||||||
|
workspace.update(cx, |workspace, cx| {
|
||||||
|
workspace.register_utility_pane(slot, panel_id, agent_thread_pane.clone(), cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
self.agent_thread_pane = Some(agent_thread_pane);
|
||||||
|
self.serialize(cx);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn utility_slot(&self, window: &Window, cx: &App) -> UtilityPaneSlot {
|
||||||
|
let position = self.position(window, cx);
|
||||||
|
utility_slot_for_dock_position(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn re_register_utility_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if let Some(pane) = &self.agent_thread_pane {
|
||||||
|
let slot = self.utility_slot(window, cx);
|
||||||
|
let panel_id = cx.entity_id();
|
||||||
|
let pane = pane.clone();
|
||||||
|
|
||||||
|
if let Some(workspace) = self.workspace.upgrade() {
|
||||||
|
workspace.update(cx, |workspace, cx| {
|
||||||
|
workspace.register_utility_pane(slot, panel_id, pane, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serialize(&mut self, cx: &mut Context<Self>) {
|
||||||
|
let width = self.width;
|
||||||
|
let pane = self
|
||||||
|
.agent_thread_pane
|
||||||
|
.as_ref()
|
||||||
|
.map(|pane| pane.read(cx).serialize());
|
||||||
|
|
||||||
|
self.pending_serialization = cx.background_spawn(async move {
|
||||||
|
KEY_VALUE_STORE
|
||||||
|
.write_kvp(
|
||||||
|
AGENTS_PANEL_KEY.into(),
|
||||||
|
serde_json::to_string(&SerializedAgentsPanel { width, pane }).unwrap(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.log_err()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<PanelEvent> for AgentsPanel {}
|
||||||
|
|
||||||
|
impl Focusable for AgentsPanel {
|
||||||
|
fn focus_handle(&self, _cx: &ui::App) -> gpui::FocusHandle {
|
||||||
|
self.focus_handle.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Panel for AgentsPanel {
|
||||||
|
fn persistent_name() -> &'static str {
|
||||||
|
"AgentsPanel"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn panel_key() -> &'static str {
|
||||||
|
AGENTS_PANEL_KEY
|
||||||
|
}
|
||||||
|
|
||||||
|
fn position(&self, _window: &Window, cx: &App) -> DockPosition {
|
||||||
|
match AgentSettings::get_global(cx).agents_panel_dock {
|
||||||
|
settings::DockSide::Left => DockPosition::Left,
|
||||||
|
settings::DockSide::Right => DockPosition::Right,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn position_is_valid(&self, position: DockPosition) -> bool {
|
||||||
|
position != DockPosition::Bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_position(
|
||||||
|
&mut self,
|
||||||
|
position: DockPosition,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
update_settings_file(self.fs.clone(), cx, move |settings, _| {
|
||||||
|
settings.agent.get_or_insert_default().agents_panel_dock = Some(match position {
|
||||||
|
DockPosition::Left => settings::DockSide::Left,
|
||||||
|
DockPosition::Right | DockPosition::Bottom => settings::DockSide::Right,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
self.re_register_utility_pane(window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self, window: &Window, cx: &App) -> Pixels {
|
||||||
|
let settings = AgentSettings::get_global(cx);
|
||||||
|
match self.position(window, cx) {
|
||||||
|
DockPosition::Left | DockPosition::Right => {
|
||||||
|
self.width.unwrap_or(settings.default_width)
|
||||||
|
}
|
||||||
|
DockPosition::Bottom => self.width.unwrap_or(settings.default_height),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
match self.position(window, cx) {
|
||||||
|
DockPosition::Left | DockPosition::Right => self.width = size,
|
||||||
|
DockPosition::Bottom => {}
|
||||||
|
}
|
||||||
|
self.serialize(cx);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
|
||||||
|
(self.enabled(cx) && AgentSettings::get_global(cx).button).then_some(IconName::ZedAgentTwo)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
|
||||||
|
Some("Agents Panel")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle_action(&self) -> Box<dyn Action> {
|
||||||
|
Box::new(ToggleAgentsPanel)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn activation_priority(&self) -> u32 {
|
||||||
|
4
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enabled(&self, cx: &App) -> bool {
|
||||||
|
AgentSettings::get_global(cx).enabled(cx) && cx.has_flag::<AgentV2FeatureFlag>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for AgentsPanel {
|
||||||
|
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
gpui::div().size_full().child(self.history.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
735
crates/agent_ui_v2/src/thread_history.rs
Normal file
735
crates/agent_ui_v2/src/thread_history.rs
Normal file
@@ -0,0 +1,735 @@
|
|||||||
|
use agent::{HistoryEntry, HistoryStore};
|
||||||
|
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
|
||||||
|
use editor::{Editor, EditorEvent};
|
||||||
|
use fuzzy::StringMatchCandidate;
|
||||||
|
use gpui::{
|
||||||
|
App, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Task,
|
||||||
|
UniformListScrollHandle, Window, actions, uniform_list,
|
||||||
|
};
|
||||||
|
use std::{fmt::Display, ops::Range};
|
||||||
|
use text::Bias;
|
||||||
|
use time::{OffsetDateTime, UtcOffset};
|
||||||
|
use ui::{
|
||||||
|
HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Tab, Tooltip, WithScrollbar,
|
||||||
|
prelude::*,
|
||||||
|
};
|
||||||
|
|
||||||
|
actions!(
|
||||||
|
agents,
|
||||||
|
[
|
||||||
|
/// Removes all thread history.
|
||||||
|
RemoveHistory,
|
||||||
|
/// Removes the currently selected thread.
|
||||||
|
RemoveSelectedThread,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
pub struct AcpThreadHistory {
|
||||||
|
pub(crate) history_store: Entity<HistoryStore>,
|
||||||
|
scroll_handle: UniformListScrollHandle,
|
||||||
|
selected_index: usize,
|
||||||
|
hovered_index: Option<usize>,
|
||||||
|
search_editor: Entity<Editor>,
|
||||||
|
search_query: SharedString,
|
||||||
|
visible_items: Vec<ListItemType>,
|
||||||
|
local_timezone: UtcOffset,
|
||||||
|
confirming_delete_history: bool,
|
||||||
|
_update_task: Task<()>,
|
||||||
|
_subscriptions: Vec<gpui::Subscription>,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ListItemType {
|
||||||
|
BucketSeparator(TimeBucket),
|
||||||
|
Entry {
|
||||||
|
entry: HistoryEntry,
|
||||||
|
format: EntryTimeFormat,
|
||||||
|
},
|
||||||
|
SearchResult {
|
||||||
|
entry: HistoryEntry,
|
||||||
|
positions: Vec<usize>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ListItemType {
|
||||||
|
fn history_entry(&self) -> Option<&HistoryEntry> {
|
||||||
|
match self {
|
||||||
|
ListItemType::Entry { entry, .. } => Some(entry),
|
||||||
|
ListItemType::SearchResult { entry, .. } => Some(entry),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub enum ThreadHistoryEvent {
|
||||||
|
Open(HistoryEntry),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<ThreadHistoryEvent> for AcpThreadHistory {}
|
||||||
|
|
||||||
|
impl AcpThreadHistory {
|
||||||
|
pub fn new(
|
||||||
|
history_store: Entity<agent::HistoryStore>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Self {
|
||||||
|
let search_editor = cx.new(|cx| {
|
||||||
|
let mut editor = Editor::single_line(window, cx);
|
||||||
|
editor.set_placeholder_text("Search threads...", window, cx);
|
||||||
|
editor
|
||||||
|
});
|
||||||
|
|
||||||
|
let search_editor_subscription =
|
||||||
|
cx.subscribe(&search_editor, |this, search_editor, event, cx| {
|
||||||
|
if let EditorEvent::BufferEdited = event {
|
||||||
|
let query = search_editor.read(cx).text(cx);
|
||||||
|
if this.search_query != query {
|
||||||
|
this.search_query = query.into();
|
||||||
|
this.update_visible_items(false, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let history_store_subscription = cx.observe(&history_store, |this, _, cx| {
|
||||||
|
this.update_visible_items(true, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
let scroll_handle = UniformListScrollHandle::default();
|
||||||
|
|
||||||
|
let mut this = Self {
|
||||||
|
history_store,
|
||||||
|
scroll_handle,
|
||||||
|
selected_index: 0,
|
||||||
|
hovered_index: None,
|
||||||
|
visible_items: Default::default(),
|
||||||
|
search_editor,
|
||||||
|
local_timezone: UtcOffset::from_whole_seconds(
|
||||||
|
chrono::Local::now().offset().local_minus_utc(),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
search_query: SharedString::default(),
|
||||||
|
confirming_delete_history: false,
|
||||||
|
_subscriptions: vec![search_editor_subscription, history_store_subscription],
|
||||||
|
_update_task: Task::ready(()),
|
||||||
|
};
|
||||||
|
this.update_visible_items(false, cx);
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_visible_items(&mut self, preserve_selected_item: bool, cx: &mut Context<Self>) {
|
||||||
|
let entries = self
|
||||||
|
.history_store
|
||||||
|
.update(cx, |store, _| store.entries().collect());
|
||||||
|
let new_list_items = if self.search_query.is_empty() {
|
||||||
|
self.add_list_separators(entries, cx)
|
||||||
|
} else {
|
||||||
|
self.filter_search_results(entries, cx)
|
||||||
|
};
|
||||||
|
let selected_history_entry = if preserve_selected_item {
|
||||||
|
self.selected_history_entry().cloned()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
self._update_task = cx.spawn(async move |this, cx| {
|
||||||
|
let new_visible_items = new_list_items.await;
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
let new_selected_index = if let Some(history_entry) = selected_history_entry {
|
||||||
|
let history_entry_id = history_entry.id();
|
||||||
|
new_visible_items
|
||||||
|
.iter()
|
||||||
|
.position(|visible_entry| {
|
||||||
|
visible_entry
|
||||||
|
.history_entry()
|
||||||
|
.is_some_and(|entry| entry.id() == history_entry_id)
|
||||||
|
})
|
||||||
|
.unwrap_or(0)
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
this.visible_items = new_visible_items;
|
||||||
|
this.set_selected_index(new_selected_index, Bias::Right, cx);
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_list_separators(&self, entries: Vec<HistoryEntry>, cx: &App) -> Task<Vec<ListItemType>> {
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
let mut items = Vec::with_capacity(entries.len() + 1);
|
||||||
|
let mut bucket = None;
|
||||||
|
let today = Local::now().naive_local().date();
|
||||||
|
|
||||||
|
for entry in entries.into_iter() {
|
||||||
|
let entry_date = entry
|
||||||
|
.updated_at()
|
||||||
|
.with_timezone(&Local)
|
||||||
|
.naive_local()
|
||||||
|
.date();
|
||||||
|
let entry_bucket = TimeBucket::from_dates(today, entry_date);
|
||||||
|
|
||||||
|
if Some(entry_bucket) != bucket {
|
||||||
|
bucket = Some(entry_bucket);
|
||||||
|
items.push(ListItemType::BucketSeparator(entry_bucket));
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push(ListItemType::Entry {
|
||||||
|
entry,
|
||||||
|
format: entry_bucket.into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
items
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn filter_search_results(
|
||||||
|
&self,
|
||||||
|
entries: Vec<HistoryEntry>,
|
||||||
|
cx: &App,
|
||||||
|
) -> Task<Vec<ListItemType>> {
|
||||||
|
let query = self.search_query.clone();
|
||||||
|
cx.background_spawn({
|
||||||
|
let executor = cx.background_executor().clone();
|
||||||
|
async move {
|
||||||
|
let mut candidates = Vec::with_capacity(entries.len());
|
||||||
|
|
||||||
|
for (idx, entry) in entries.iter().enumerate() {
|
||||||
|
candidates.push(StringMatchCandidate::new(idx, entry.title()));
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_MATCHES: usize = 100;
|
||||||
|
|
||||||
|
let matches = fuzzy::match_strings(
|
||||||
|
&candidates,
|
||||||
|
&query,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
MAX_MATCHES,
|
||||||
|
&Default::default(),
|
||||||
|
executor,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
matches
|
||||||
|
.into_iter()
|
||||||
|
.map(|search_match| ListItemType::SearchResult {
|
||||||
|
entry: entries[search_match.candidate_id].clone(),
|
||||||
|
positions: search_match.positions,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn search_produced_no_matches(&self) -> bool {
|
||||||
|
self.visible_items.is_empty() && !self.search_query.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected_history_entry(&self) -> Option<&HistoryEntry> {
|
||||||
|
self.get_history_entry(self.selected_index)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_history_entry(&self, visible_items_ix: usize) -> Option<&HistoryEntry> {
|
||||||
|
self.visible_items.get(visible_items_ix)?.history_entry()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_selected_index(&mut self, mut index: usize, bias: Bias, cx: &mut Context<Self>) {
|
||||||
|
if self.visible_items.is_empty() {
|
||||||
|
self.selected_index = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
while matches!(
|
||||||
|
self.visible_items.get(index),
|
||||||
|
None | Some(ListItemType::BucketSeparator(..))
|
||||||
|
) {
|
||||||
|
index = match bias {
|
||||||
|
Bias::Left => {
|
||||||
|
if index == 0 {
|
||||||
|
self.visible_items.len() - 1
|
||||||
|
} else {
|
||||||
|
index - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Bias::Right => {
|
||||||
|
if index >= self.visible_items.len() - 1 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
index + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
self.selected_index = index;
|
||||||
|
self.scroll_handle
|
||||||
|
.scroll_to_item(index, ScrollStrategy::Top);
|
||||||
|
cx.notify()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_previous(
|
||||||
|
&mut self,
|
||||||
|
_: &menu::SelectPrevious,
|
||||||
|
_window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
if self.selected_index == 0 {
|
||||||
|
self.set_selected_index(self.visible_items.len() - 1, Bias::Left, cx);
|
||||||
|
} else {
|
||||||
|
self.set_selected_index(self.selected_index - 1, Bias::Left, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_next(
|
||||||
|
&mut self,
|
||||||
|
_: &menu::SelectNext,
|
||||||
|
_window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
if self.selected_index == self.visible_items.len() - 1 {
|
||||||
|
self.set_selected_index(0, Bias::Right, cx);
|
||||||
|
} else {
|
||||||
|
self.set_selected_index(self.selected_index + 1, Bias::Right, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_first(
|
||||||
|
&mut self,
|
||||||
|
_: &menu::SelectFirst,
|
||||||
|
_window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.set_selected_index(0, Bias::Right, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.set_selected_index(self.visible_items.len() - 1, Bias::Left, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.confirm_entry(self.selected_index, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn confirm_entry(&mut self, ix: usize, cx: &mut Context<Self>) {
|
||||||
|
let Some(entry) = self.get_history_entry(ix) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
cx.emit(ThreadHistoryEvent::Open(entry.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_selected_thread(
|
||||||
|
&mut self,
|
||||||
|
_: &RemoveSelectedThread,
|
||||||
|
_window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.remove_thread(self.selected_index, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_thread(&mut self, visible_item_ix: usize, cx: &mut Context<Self>) {
|
||||||
|
let Some(entry) = self.get_history_entry(visible_item_ix) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let task = match entry {
|
||||||
|
HistoryEntry::AcpThread(thread) => self
|
||||||
|
.history_store
|
||||||
|
.update(cx, |this, cx| this.delete_thread(thread.id.clone(), cx)),
|
||||||
|
HistoryEntry::TextThread(text_thread) => self.history_store.update(cx, |this, cx| {
|
||||||
|
this.delete_text_thread(text_thread.path.clone(), cx)
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
task.detach_and_log_err(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_history(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.history_store.update(cx, |store, cx| {
|
||||||
|
store.delete_threads(cx).detach_and_log_err(cx)
|
||||||
|
});
|
||||||
|
self.confirming_delete_history = false;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt_delete_history(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.confirming_delete_history = true;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cancel_delete_history(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.confirming_delete_history = false;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_list_items(
|
||||||
|
&mut self,
|
||||||
|
range: Range<usize>,
|
||||||
|
_window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Vec<AnyElement> {
|
||||||
|
self.visible_items
|
||||||
|
.get(range.clone())
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(ix, item)| self.render_list_item(item, range.start + ix, cx))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_list_item(&self, item: &ListItemType, ix: usize, cx: &Context<Self>) -> AnyElement {
|
||||||
|
match item {
|
||||||
|
ListItemType::Entry { entry, format } => self
|
||||||
|
.render_history_entry(entry, *format, ix, Vec::default(), cx)
|
||||||
|
.into_any(),
|
||||||
|
ListItemType::SearchResult { entry, positions } => self.render_history_entry(
|
||||||
|
entry,
|
||||||
|
EntryTimeFormat::DateAndTime,
|
||||||
|
ix,
|
||||||
|
positions.clone(),
|
||||||
|
cx,
|
||||||
|
),
|
||||||
|
ListItemType::BucketSeparator(bucket) => div()
|
||||||
|
.px(DynamicSpacing::Base06.rems(cx))
|
||||||
|
.pt_2()
|
||||||
|
.pb_1()
|
||||||
|
.child(
|
||||||
|
Label::new(bucket.to_string())
|
||||||
|
.size(LabelSize::XSmall)
|
||||||
|
.color(Color::Muted),
|
||||||
|
)
|
||||||
|
.into_any_element(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_history_entry(
|
||||||
|
&self,
|
||||||
|
entry: &HistoryEntry,
|
||||||
|
format: EntryTimeFormat,
|
||||||
|
ix: usize,
|
||||||
|
highlight_positions: Vec<usize>,
|
||||||
|
cx: &Context<Self>,
|
||||||
|
) -> AnyElement {
|
||||||
|
let selected = ix == self.selected_index;
|
||||||
|
let hovered = Some(ix) == self.hovered_index;
|
||||||
|
let timestamp = entry.updated_at().timestamp();
|
||||||
|
let thread_timestamp = format.format_timestamp(timestamp, self.local_timezone);
|
||||||
|
|
||||||
|
h_flex()
|
||||||
|
.w_full()
|
||||||
|
.pb_1()
|
||||||
|
.child(
|
||||||
|
ListItem::new(ix)
|
||||||
|
.rounded()
|
||||||
|
.toggle_state(selected)
|
||||||
|
.spacing(ListItemSpacing::Sparse)
|
||||||
|
.start_slot(
|
||||||
|
h_flex()
|
||||||
|
.w_full()
|
||||||
|
.gap_2()
|
||||||
|
.justify_between()
|
||||||
|
.child(
|
||||||
|
HighlightedLabel::new(entry.title(), highlight_positions)
|
||||||
|
.size(LabelSize::Small)
|
||||||
|
.truncate(),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Label::new(thread_timestamp)
|
||||||
|
.color(Color::Muted)
|
||||||
|
.size(LabelSize::XSmall),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.on_hover(cx.listener(move |this, is_hovered, _window, cx| {
|
||||||
|
if *is_hovered {
|
||||||
|
this.hovered_index = Some(ix);
|
||||||
|
} else if this.hovered_index == Some(ix) {
|
||||||
|
this.hovered_index = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
}))
|
||||||
|
.end_slot::<IconButton>(if hovered {
|
||||||
|
Some(
|
||||||
|
IconButton::new("delete", IconName::Trash)
|
||||||
|
.shape(IconButtonShape::Square)
|
||||||
|
.icon_size(IconSize::XSmall)
|
||||||
|
.icon_color(Color::Muted)
|
||||||
|
.tooltip(move |_window, cx| {
|
||||||
|
Tooltip::for_action("Delete", &RemoveSelectedThread, cx)
|
||||||
|
})
|
||||||
|
.on_click(cx.listener(move |this, _, _, cx| {
|
||||||
|
this.remove_thread(ix, cx);
|
||||||
|
cx.stop_propagation()
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
})
|
||||||
|
.on_click(cx.listener(move |this, _, _, cx| this.confirm_entry(ix, cx))),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Focusable for AcpThreadHistory {
|
||||||
|
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||||
|
self.search_editor.focus_handle(cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for AcpThreadHistory {
|
||||||
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let has_no_history = self.history_store.read(cx).is_empty(cx);
|
||||||
|
|
||||||
|
v_flex()
|
||||||
|
.key_context("ThreadHistory")
|
||||||
|
.size_full()
|
||||||
|
.bg(cx.theme().colors().panel_background)
|
||||||
|
.on_action(cx.listener(Self::select_previous))
|
||||||
|
.on_action(cx.listener(Self::select_next))
|
||||||
|
.on_action(cx.listener(Self::select_first))
|
||||||
|
.on_action(cx.listener(Self::select_last))
|
||||||
|
.on_action(cx.listener(Self::confirm))
|
||||||
|
.on_action(cx.listener(Self::remove_selected_thread))
|
||||||
|
.on_action(cx.listener(|this, _: &RemoveHistory, window, cx| {
|
||||||
|
this.remove_history(window, cx);
|
||||||
|
}))
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.h(Tab::container_height(cx))
|
||||||
|
.w_full()
|
||||||
|
.py_1()
|
||||||
|
.px_2()
|
||||||
|
.gap_2()
|
||||||
|
.justify_between()
|
||||||
|
.border_b_1()
|
||||||
|
.border_color(cx.theme().colors().border)
|
||||||
|
.child(
|
||||||
|
Icon::new(IconName::MagnifyingGlass)
|
||||||
|
.color(Color::Muted)
|
||||||
|
.size(IconSize::Small),
|
||||||
|
)
|
||||||
|
.child(self.search_editor.clone()),
|
||||||
|
)
|
||||||
|
.child({
|
||||||
|
let view = v_flex()
|
||||||
|
.id("list-container")
|
||||||
|
.relative()
|
||||||
|
.overflow_hidden()
|
||||||
|
.flex_grow();
|
||||||
|
|
||||||
|
if has_no_history {
|
||||||
|
view.justify_center().items_center().child(
|
||||||
|
Label::new("You don't have any past threads yet.")
|
||||||
|
.size(LabelSize::Small)
|
||||||
|
.color(Color::Muted),
|
||||||
|
)
|
||||||
|
} else if self.search_produced_no_matches() {
|
||||||
|
view.justify_center()
|
||||||
|
.items_center()
|
||||||
|
.child(Label::new("No threads match your search.").size(LabelSize::Small))
|
||||||
|
} else {
|
||||||
|
view.child(
|
||||||
|
uniform_list(
|
||||||
|
"thread-history",
|
||||||
|
self.visible_items.len(),
|
||||||
|
cx.processor(|this, range: Range<usize>, window, cx| {
|
||||||
|
this.render_list_items(range, window, cx)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.p_1()
|
||||||
|
.pr_4()
|
||||||
|
.track_scroll(&self.scroll_handle)
|
||||||
|
.flex_grow(),
|
||||||
|
)
|
||||||
|
.vertical_scrollbar_for(&self.scroll_handle, window, cx)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.when(!has_no_history, |this| {
|
||||||
|
this.child(
|
||||||
|
h_flex()
|
||||||
|
.p_2()
|
||||||
|
.border_t_1()
|
||||||
|
.border_color(cx.theme().colors().border_variant)
|
||||||
|
.when(!self.confirming_delete_history, |this| {
|
||||||
|
this.child(
|
||||||
|
Button::new("delete_history", "Delete All History")
|
||||||
|
.full_width()
|
||||||
|
.style(ButtonStyle::Outlined)
|
||||||
|
.label_size(LabelSize::Small)
|
||||||
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
|
this.prompt_delete_history(window, cx);
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.when(self.confirming_delete_history, |this| {
|
||||||
|
this.w_full()
|
||||||
|
.gap_2()
|
||||||
|
.flex_wrap()
|
||||||
|
.justify_between()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.flex_wrap()
|
||||||
|
.gap_1()
|
||||||
|
.child(
|
||||||
|
Label::new("Delete all threads?")
|
||||||
|
.size(LabelSize::Small),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Label::new("You won't be able to recover them later.")
|
||||||
|
.size(LabelSize::Small)
|
||||||
|
.color(Color::Muted),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.child(
|
||||||
|
Button::new("cancel_delete", "Cancel")
|
||||||
|
.label_size(LabelSize::Small)
|
||||||
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
|
this.cancel_delete_history(window, cx);
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new("confirm_delete", "Delete")
|
||||||
|
.style(ButtonStyle::Tinted(ui::TintColor::Error))
|
||||||
|
.color(Color::Error)
|
||||||
|
.label_size(LabelSize::Small)
|
||||||
|
.on_click(cx.listener(|_, _, window, cx| {
|
||||||
|
window.dispatch_action(
|
||||||
|
Box::new(RemoveHistory),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub enum EntryTimeFormat {
|
||||||
|
DateAndTime,
|
||||||
|
TimeOnly,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EntryTimeFormat {
|
||||||
|
fn format_timestamp(&self, timestamp: i64, timezone: UtcOffset) -> String {
|
||||||
|
let timestamp = OffsetDateTime::from_unix_timestamp(timestamp).unwrap();
|
||||||
|
|
||||||
|
match self {
|
||||||
|
EntryTimeFormat::DateAndTime => time_format::format_localized_timestamp(
|
||||||
|
timestamp,
|
||||||
|
OffsetDateTime::now_utc(),
|
||||||
|
timezone,
|
||||||
|
time_format::TimestampFormat::EnhancedAbsolute,
|
||||||
|
),
|
||||||
|
EntryTimeFormat::TimeOnly => time_format::format_time(timestamp.to_offset(timezone)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TimeBucket> for EntryTimeFormat {
|
||||||
|
fn from(bucket: TimeBucket) -> Self {
|
||||||
|
match bucket {
|
||||||
|
TimeBucket::Today => EntryTimeFormat::TimeOnly,
|
||||||
|
TimeBucket::Yesterday => EntryTimeFormat::TimeOnly,
|
||||||
|
TimeBucket::ThisWeek => EntryTimeFormat::DateAndTime,
|
||||||
|
TimeBucket::PastWeek => EntryTimeFormat::DateAndTime,
|
||||||
|
TimeBucket::All => EntryTimeFormat::DateAndTime,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
|
||||||
|
enum TimeBucket {
|
||||||
|
Today,
|
||||||
|
Yesterday,
|
||||||
|
ThisWeek,
|
||||||
|
PastWeek,
|
||||||
|
All,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TimeBucket {
|
||||||
|
fn from_dates(reference: NaiveDate, date: NaiveDate) -> Self {
|
||||||
|
if date == reference {
|
||||||
|
return TimeBucket::Today;
|
||||||
|
}
|
||||||
|
|
||||||
|
if date == reference - TimeDelta::days(1) {
|
||||||
|
return TimeBucket::Yesterday;
|
||||||
|
}
|
||||||
|
|
||||||
|
let week = date.iso_week();
|
||||||
|
|
||||||
|
if reference.iso_week() == week {
|
||||||
|
return TimeBucket::ThisWeek;
|
||||||
|
}
|
||||||
|
|
||||||
|
let last_week = (reference - TimeDelta::days(7)).iso_week();
|
||||||
|
|
||||||
|
if week == last_week {
|
||||||
|
return TimeBucket::PastWeek;
|
||||||
|
}
|
||||||
|
|
||||||
|
TimeBucket::All
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for TimeBucket {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
TimeBucket::Today => write!(f, "Today"),
|
||||||
|
TimeBucket::Yesterday => write!(f, "Yesterday"),
|
||||||
|
TimeBucket::ThisWeek => write!(f, "This Week"),
|
||||||
|
TimeBucket::PastWeek => write!(f, "Past Week"),
|
||||||
|
TimeBucket::All => write!(f, "All"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use chrono::NaiveDate;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_time_bucket_from_dates() {
|
||||||
|
let today = NaiveDate::from_ymd_opt(2023, 1, 15).unwrap();
|
||||||
|
|
||||||
|
let date = today;
|
||||||
|
assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::Today);
|
||||||
|
|
||||||
|
let date = NaiveDate::from_ymd_opt(2023, 1, 14).unwrap();
|
||||||
|
assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::Yesterday);
|
||||||
|
|
||||||
|
let date = NaiveDate::from_ymd_opt(2023, 1, 13).unwrap();
|
||||||
|
assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::ThisWeek);
|
||||||
|
|
||||||
|
let date = NaiveDate::from_ymd_opt(2023, 1, 11).unwrap();
|
||||||
|
assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::ThisWeek);
|
||||||
|
|
||||||
|
let date = NaiveDate::from_ymd_opt(2023, 1, 8).unwrap();
|
||||||
|
assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::PastWeek);
|
||||||
|
|
||||||
|
let date = NaiveDate::from_ymd_opt(2023, 1, 5).unwrap();
|
||||||
|
assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::PastWeek);
|
||||||
|
|
||||||
|
// All: not in this week or last week
|
||||||
|
let date = NaiveDate::from_ymd_opt(2023, 1, 1).unwrap();
|
||||||
|
assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::All);
|
||||||
|
|
||||||
|
// Test year boundary cases
|
||||||
|
let new_year = NaiveDate::from_ymd_opt(2023, 1, 1).unwrap();
|
||||||
|
|
||||||
|
let date = NaiveDate::from_ymd_opt(2022, 12, 31).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
TimeBucket::from_dates(new_year, date),
|
||||||
|
TimeBucket::Yesterday
|
||||||
|
);
|
||||||
|
|
||||||
|
let date = NaiveDate::from_ymd_opt(2022, 12, 28).unwrap();
|
||||||
|
assert_eq!(TimeBucket::from_dates(new_year, date), TimeBucket::ThisWeek);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -429,10 +429,24 @@ impl Model {
|
|||||||
let mut headers = vec![];
|
let mut headers = vec![];
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
|
Self::ClaudeOpus4
|
||||||
|
| Self::ClaudeOpus4_1
|
||||||
|
| Self::ClaudeOpus4_5
|
||||||
|
| Self::ClaudeSonnet4
|
||||||
|
| Self::ClaudeSonnet4_5
|
||||||
|
| Self::ClaudeOpus4Thinking
|
||||||
|
| Self::ClaudeOpus4_1Thinking
|
||||||
|
| Self::ClaudeOpus4_5Thinking
|
||||||
|
| Self::ClaudeSonnet4Thinking
|
||||||
|
| Self::ClaudeSonnet4_5Thinking => {
|
||||||
|
// Fine-grained tool streaming for newer models
|
||||||
|
headers.push("fine-grained-tool-streaming-2025-05-14".to_string());
|
||||||
|
}
|
||||||
Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => {
|
Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => {
|
||||||
// Try beta token-efficient tool use (supported in Claude 3.7 Sonnet only)
|
// Try beta token-efficient tool use (supported in Claude 3.7 Sonnet only)
|
||||||
// https://docs.anthropic.com/en/docs/build-with-claude/tool-use/token-efficient-tool-use
|
// https://docs.anthropic.com/en/docs/build-with-claude/tool-use/token-efficient-tool-use
|
||||||
headers.push("token-efficient-tools-2025-02-19".to_string());
|
headers.push("token-efficient-tools-2025-02-19".to_string());
|
||||||
|
headers.push("fine-grained-tool-streaming-2025-05-14".to_string());
|
||||||
}
|
}
|
||||||
Self::Custom {
|
Self::Custom {
|
||||||
extra_beta_headers, ..
|
extra_beta_headers, ..
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ feature_flags.workspace = true
|
|||||||
fs.workspace = true
|
fs.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
fuzzy.workspace = true
|
fuzzy.workspace = true
|
||||||
globset.workspace = true
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
html_to_markdown.workspace = true
|
html_to_markdown.workspace = true
|
||||||
http_client.workspace = true
|
http_client.workspace = true
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user