Compare commits

..

1 Commits

Author SHA1 Message Date
Conrad Irwin
4f4d2423c2 regression? 2025-11-04 22:01:24 -07:00
694 changed files with 17126 additions and 37055 deletions

View File

@@ -39,21 +39,3 @@ body:
Output of "zed: copy system specs into clipboard" Output of "zed: copy system specs into clipboard"
validations: validations:
required: true required: true
- type: textarea
attributes:
label: If applicable, attach your `Zed.log` file to this issue.
description: |
From the command palette, run `zed: open log` to see the last 1000 lines.
Or run `zed: reveal log in file manager` to reveal the log file itself.
value: |
<details><summary>Zed.log</summary>
<!-- Paste your log inside the code block. -->
```log
```
</details>
validations:
required: false

View File

@@ -33,21 +33,3 @@ body:
Output of "zed: copy system specs into clipboard" Output of "zed: copy system specs into clipboard"
validations: validations:
required: true required: true
- type: textarea
attributes:
label: If applicable, attach your `Zed.log` file to this issue.
description: |
From the command palette, run `zed: open log` to see the last 1000 lines.
Or run `zed: reveal log in file manager` to reveal the log file itself.
value: |
<details><summary>Zed.log</summary>
<!-- Paste your log inside the code block. -->
```log
```
</details>
validations:
required: false

View File

@@ -33,21 +33,3 @@ body:
Output of "zed: copy system specs into clipboard" Output of "zed: copy system specs into clipboard"
validations: validations:
required: true required: true
- type: textarea
attributes:
label: If applicable, attach your `Zed.log` file to this issue.
description: |
From the command palette, run `zed: open log` to see the last 1000 lines.
Or run `zed: reveal log in file manager` to reveal the log file itself.
value: |
<details><summary>Zed.log</summary>
<!-- Paste your log inside the code block. -->
```log
```
</details>
validations:
required: false

View File

@@ -33,21 +33,3 @@ body:
Output of "zed: copy system specs into clipboard" Output of "zed: copy system specs into clipboard"
validations: validations:
required: true required: true
- type: textarea
attributes:
label: If applicable, attach your `Zed.log` file to this issue.
description: |
From the command palette, run `zed: open log` to see the last 1000 lines.
Or run `zed: reveal log in file manager` to reveal the log file itself.
value: |
<details><summary>Zed.log</summary>
<!-- Paste your log inside the code block. -->
```log
```
</details>
validations:
required: false

View File

@@ -56,20 +56,3 @@ body:
Output of "zed: copy system specs into clipboard" Output of "zed: copy system specs into clipboard"
validations: validations:
required: true required: true
- type: textarea
attributes:
label: If applicable, attach your `Zed.log` file to this issue.
description: |
From the command palette, run `zed: open log` to see the last 1000 lines.
Or run `zed: reveal log in file manager` to reveal the log file itself.
value: |
<details><summary>Zed.log</summary>
<!-- Paste your log inside the code block. -->
```log
```
</details>
validations:
required: false

View File

@@ -1,4 +1,4 @@
# yaml-language-server: $schema=https://www.schemastore.org/github-issue-config.json # yaml-language-server: $schema=https://json.schemastore.org/github-issue-config.json
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: Feature Request - name: Feature Request

View File

@@ -4,8 +4,10 @@ description: "Runs the tests"
runs: runs:
using: "composite" using: "composite"
steps: steps:
- name: Install nextest - name: Install Rust
uses: taiki-e/install-action@nextest shell: bash -euxo pipefail {0}
run: |
cargo install cargo-nextest --locked
- name: Install Node - name: Install Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4

View File

@@ -11,8 +11,9 @@ runs:
using: "composite" using: "composite"
steps: steps:
- name: Install test runner - name: Install test runner
shell: powershell
working-directory: ${{ inputs.working-directory }} working-directory: ${{ inputs.working-directory }}
uses: taiki-e/install-action@nextest run: cargo install cargo-nextest --locked
- name: Install Node - name: Install Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4

View File

@@ -1,104 +0,0 @@
# Generated from xtask::workflows::after_release
# Rebuild with `cargo xtask workflows`.
name: after_release
on:
release:
types:
- published
jobs:
rebuild_releases_page:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- 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 }}
shell: bash -euxo pipefail {0}
- 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
shell: bash -euxo pipefail {0}
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
post_to_discord:
needs:
- rebuild_releases_page
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- id: get-release-url
name: after_release::post_to_discord::get_release_url
run: |
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
URL="https://zed.dev/releases/preview"
else
URL="https://zed.dev/releases/stable"
fi
echo "URL=$URL" >> "$GITHUB_OUTPUT"
shell: bash -euxo pipefail {0}
- id: get-content
name: after_release::post_to_discord::get_content
uses: 2428392/gh-truncate-string-action@b3ff790d21cf42af3ca7579146eedb93c8fb0757
with:
stringToTruncate: |
📣 Zed [${{ github.event.release.tag_name }}](<${{ steps.get-release-url.outputs.URL }}>) was just released!
${{ github.event.release.body }}
maxLength: 2000
truncationSymbol: '...'
- name: after_release::post_to_discord::discord_webhook_action
uses: tsickert/discord-webhook@c840d45a03a323fbc3f7507ac7769dbd91bfb164
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_RELEASE_NOTES }}
content: ${{ steps.get-content.outputs.string }}
publish_winget:
runs-on: self-32vcpu-windows-2022
steps:
- id: set-package-name
name: after_release::publish_winget::set_package_name
run: |
if ("${{ github.event.release.prerelease }}" -eq "true") {
$PACKAGE_NAME = "ZedIndustries.Zed.Preview"
} else {
$PACKAGE_NAME = "ZedIndustries.Zed"
}
echo "PACKAGE_NAME=$PACKAGE_NAME" >> $env:GITHUB_OUTPUT
shell: pwsh
- name: after_release::publish_winget::winget_releaser
uses: vedantmgoyal9/winget-releaser@19e706d4c9121098010096f9c495a70a7518b30f
with:
identifier: ${{ steps.set-package-name.outputs.PACKAGE_NAME }}
max-versions-to-keep: 5
token: ${{ secrets.WINGET_TOKEN }}
create_sentry_release:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: release::create_sentry_release
uses: getsentry/action-release@526942b68292201ac6bbb99b9a0747d4abee354c
with:
environment: production
env:
SENTRY_ORG: zed-dev
SENTRY_PROJECT: zed
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
notify_on_failure:
needs:
- rebuild_releases_page
- post_to_discord
- publish_winget
- create_sentry_release
if: failure()
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: release::notify_on_failure::notify_slack
run: |-
curl -X POST -H 'Content-type: application/json'\
--data '{"text":"${{ github.workflow }} failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}' "$SLACK_WEBHOOK"
shell: bash -euxo pipefail {0}
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_WORKFLOW_FAILURES }}

View File

@@ -42,7 +42,7 @@ jobs:
exit 1 exit 1
;; ;;
esac esac
which cargo-set-version > /dev/null || cargo install cargo-edit -f --no-default-features --features "set-version" which cargo-set-version > /dev/null || cargo install cargo-edit
output="$(cargo set-version -p zed --bump patch 2>&1 | sed 's/.* //')" output="$(cargo set-version -p zed --bump patch 2>&1 | sed 's/.* //')"
export GIT_COMMITTER_NAME="Zed Bot" export GIT_COMMITTER_NAME="Zed Bot"
export GIT_COMMITTER_EMAIL="hi@zed.dev" export GIT_COMMITTER_EMAIL="hi@zed.dev"

View File

@@ -1,7 +1,6 @@
# Generated from xtask::workflows::cherry_pick # Generated from xtask::workflows::cherry_pick
# Rebuild with `cargo xtask workflows`. # Rebuild with `cargo xtask workflows`.
name: cherry_pick name: cherry_pick
run-name: 'cherry_pick to ${{ inputs.channel }} #${{ inputs.pr_number }}'
on: on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
@@ -17,10 +16,6 @@ on:
description: channel description: channel
required: true required: true
type: string type: string
pr_number:
description: pr_number
required: true
type: string
jobs: jobs:
run_cherry_pick: run_cherry_pick:
runs-on: namespace-profile-2x4-ubuntu-2404 runs-on: namespace-profile-2x4-ubuntu-2404

View File

@@ -1,7 +1,7 @@
name: "Close Stale Issues" name: "Close Stale Issues"
on: on:
schedule: schedule:
- cron: "0 8 31 DEC *" - cron: "0 7,9,11 * * 3"
workflow_dispatch: workflow_dispatch:
jobs: jobs:
@@ -15,15 +15,14 @@ jobs:
stale-issue-message: > stale-issue-message: >
Hi there! 👋 Hi there! 👋
We're working to clean up our issue tracker by closing older bugs that might not be relevant anymore. If you are able to reproduce this issue in the latest version of Zed, please let us know by commenting on this issue, and it will be kept open. If you can't reproduce it, feel free to close the issue yourself. Otherwise, it will close automatically in 14 days. We're working to clean up our issue tracker by closing older issues that might not be relevant anymore. If you are able to reproduce this issue in the latest version of Zed, please let us know by commenting on this issue, and we will keep it open. If you can't reproduce it, feel free to close the issue yourself. Otherwise, we'll close it in 7 days.
Thanks for your help! Thanks for your help!
close-issue-message: "This issue was closed due to inactivity. If you're still experiencing this problem, please open a new issue with a link to this issue." close-issue-message: "This issue was closed due to inactivity. If you're still experiencing this problem, please open a new issue with a link to this issue."
days-before-stale: 60 days-before-stale: 120
days-before-close: 14 days-before-close: 7
only-issue-types: "Bug,Crash" any-of-issue-labels: "bug,panic / crash"
operations-per-run: 1000 operations-per-run: 1000
ascending: true ascending: true
enable-statistics: true enable-statistics: true
stale-issue-label: "stale" stale-issue-label: "stale"
exempt-issue-labels: "never stale"

View File

@@ -0,0 +1,93 @@
# IF YOU UPDATE THE NAME OF ANY GITHUB SECRET, YOU MUST CHERRY PICK THE COMMIT
# TO BOTH STABLE AND PREVIEW CHANNELS
name: Release Actions
on:
release:
types: [published]
jobs:
discord_release:
if: github.repository_owner == 'zed-industries'
runs-on: ubuntu-latest
steps:
- name: Get release URL
id: get-release-url
run: |
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
URL="https://zed.dev/releases/preview"
else
URL="https://zed.dev/releases/stable"
fi
echo "URL=$URL" >> "$GITHUB_OUTPUT"
- name: Get content
uses: 2428392/gh-truncate-string-action@b3ff790d21cf42af3ca7579146eedb93c8fb0757 # v1.4.1
id: get-content
with:
stringToTruncate: |
📣 Zed [${{ github.event.release.tag_name }}](<${{ steps.get-release-url.outputs.URL }}>) was just released!
${{ github.event.release.body }}
maxLength: 2000
truncationSymbol: "..."
- name: Discord Webhook Action
uses: tsickert/discord-webhook@c840d45a03a323fbc3f7507ac7769dbd91bfb164 # v5.3.0
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_RELEASE_NOTES }}
content: ${{ steps.get-content.outputs.string }}
publish-winget:
runs-on:
- ubuntu-latest
steps:
- name: Set Package Name
id: set-package-name
run: |
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
PACKAGE_NAME=ZedIndustries.Zed.Preview
else
PACKAGE_NAME=ZedIndustries.Zed
fi
echo "PACKAGE_NAME=$PACKAGE_NAME" >> "$GITHUB_OUTPUT"
- uses: vedantmgoyal9/winget-releaser@19e706d4c9121098010096f9c495a70a7518b30f # v2
with:
identifier: ${{ steps.set-package-name.outputs.PACKAGE_NAME }}
max-versions-to-keep: 5
token: ${{ secrets.WINGET_TOKEN }}
send_release_notes_email:
if: false && github.repository_owner == 'zed-industries' && !github.event.release.prerelease
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
fetch-depth: 0
- name: Check if release was promoted from preview
id: check-promotion-from-preview
run: |
VERSION="${{ github.event.release.tag_name }}"
PREVIEW_TAG="${VERSION}-pre"
if git rev-parse "$PREVIEW_TAG" > /dev/null 2>&1; then
echo "was_promoted_from_preview=true" >> "$GITHUB_OUTPUT"
else
echo "was_promoted_from_preview=false" >> "$GITHUB_OUTPUT"
fi
- name: Send release notes email
if: steps.check-promotion-from-preview.outputs.was_promoted_from_preview == 'true'
run: |
TAG="${{ github.event.release.tag_name }}"
cat << 'EOF' > release_body.txt
${{ github.event.release.body }}
EOF
jq -n --arg tag "$TAG" --rawfile body release_body.txt '{version: $tag, markdown_body: $body}' \
> release_data.json
curl -X POST "https://zed.dev/api/send_release_notes_email" \
-H "Authorization: Bearer ${{ secrets.RELEASE_NOTES_API_TOKEN }}" \
-H "Content-Type: application/json" \
-d @release_data.json

View File

@@ -35,11 +35,9 @@ jobs:
- name: steps::install_mold - name: steps::install_mold
run: ./script/install-mold run: ./script/install-mold
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
- name: compare_perf::run_perf::install_hyperfine - name: compare_perf::run_perf::install_hyperfine
uses: taiki-e/install-action@hyperfine run: cargo install hyperfine
shell: bash -euxo pipefail {0}
- name: steps::git_checkout - name: steps::git_checkout
run: git fetch origin ${{ inputs.base }} && git checkout ${{ inputs.base }} run: git fetch origin ${{ inputs.base }} && git checkout ${{ inputs.base }}
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}

View File

@@ -12,7 +12,7 @@ on:
- main - main
jobs: jobs:
danger: danger:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') if: github.repository_owner == 'zed-industries'
runs-on: namespace-profile-2x4-ubuntu-2404 runs-on: namespace-profile-2x4-ubuntu-2404
steps: steps:
- name: steps::checkout_repo - name: steps::checkout_repo

View File

@@ -43,7 +43,9 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Install cargo nextest - name: Install cargo nextest
uses: taiki-e/install-action@nextest shell: bash -euxo pipefail {0}
run: |
cargo install cargo-nextest --locked
- name: Limit target directory size - name: Limit target directory size
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}

View File

@@ -1,138 +0,0 @@
# Generated from xtask::workflows::extension_tests
# Rebuild with `cargo xtask workflows`.
name: extension_tests
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: '1'
CARGO_INCREMENTAL: '0'
ZED_EXTENSION_CLI_SHA: 7cfce605704d41ca247e3f84804bf323f6c6caaf
on:
workflow_call:
inputs:
run_tests:
description: Whether the workflow should run rust tests
required: true
type: boolean
jobs:
orchestrate:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
fetch-depth: ${{ github.ref == 'refs/heads/main' && 2 || 350 }}
- id: filter
name: filter
run: |
if [ -z "$GITHUB_BASE_REF" ]; then
echo "Not in a PR context (i.e., push to main/stable/preview)"
COMPARE_REV="$(git rev-parse HEAD~1)"
else
echo "In a PR context comparing to pull_request.base.ref"
git fetch origin "$GITHUB_BASE_REF" --depth=350
COMPARE_REV="$(git merge-base "origin/${GITHUB_BASE_REF}" HEAD)"
fi
CHANGED_FILES="$(git diff --name-only "$COMPARE_REV" ${{ github.sha }})"
check_pattern() {
local output_name="$1"
local pattern="$2"
local grep_arg="$3"
echo "$CHANGED_FILES" | grep "$grep_arg" "$pattern" && \
echo "${output_name}=true" >> "$GITHUB_OUTPUT" || \
echo "${output_name}=false" >> "$GITHUB_OUTPUT"
}
check_pattern "check_rust" '^(Cargo.lock|Cargo.toml|.*\.rs)$' -qP
check_pattern "check_extension" '^.*\.scm$' -qP
shell: bash -euxo pipefail {0}
outputs:
check_rust: ${{ steps.filter.outputs.check_rust }}
check_extension: ${{ steps.filter.outputs.check_extension }}
check_rust:
needs:
- orchestrate
if: needs.orchestrate.outputs.check_rust == 'true'
runs-on: namespace-profile-16x32-ubuntu-2204
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: steps::cache_rust_dependencies_namespace
uses: namespacelabs/nscloud-cache-action@v1
with:
cache: rust
- name: steps::cargo_fmt
run: cargo fmt --all -- --check
shell: bash -euxo pipefail {0}
- name: extension_tests::run_clippy
run: cargo clippy --release --all-targets --all-features -- --deny warnings
shell: bash -euxo pipefail {0}
- name: steps::cargo_install_nextest
if: inputs.run_tests
uses: taiki-e/install-action@nextest
- name: steps::cargo_nextest
if: inputs.run_tests
run: cargo nextest run --workspace --no-fail-fast --failure-output immediate-final
shell: bash -euxo pipefail {0}
timeout-minutes: 3
check_extension:
needs:
- orchestrate
if: needs.orchestrate.outputs.check_extension == 'true'
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- id: cache-zed-extension-cli
name: extension_tests::cache_zed_extension_cli
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830
with:
path: zed-extension
key: zed-extension-${{ env.ZED_EXTENSION_CLI_SHA }}
- name: extension_tests::download_zed_extension_cli
if: steps.cache-zed-extension-cli.outputs.cache-hit != 'true'
run: |
wget --quiet "https://zed-extension-cli.nyc3.digitaloceanspaces.com/$ZED_EXTENSION_CLI_SHA/x86_64-unknown-linux-gnu/zed-extension"
chmod +x zed-extension
shell: bash -euxo pipefail {0}
- name: extension_tests::check
run: |
mkdir -p /tmp/ext-scratch
mkdir -p /tmp/ext-output
./zed-extension --source-dir . --scratch-dir /tmp/ext-scratch --output-dir /tmp/ext-output
shell: bash -euxo pipefail {0}
timeout-minutes: 1
tests_pass:
needs:
- orchestrate
- check_rust
- check_extension
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') && always()
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: run_tests::tests_pass
run: |
set +x
EXIT_CODE=0
check_result() {
echo "* $1: $2"
if [[ "$2" != "skipped" && "$2" != "success" ]]; then EXIT_CODE=1; fi
}
check_result "orchestrate" "${{ needs.orchestrate.result }}"
check_result "check_rust" "${{ needs.check_rust.result }}"
check_result "check_extension" "${{ needs.check_extension.result }}"
exit $EXIT_CODE
shell: bash -euxo pipefail {0}
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
cancel-in-progress: true

View File

@@ -10,7 +10,7 @@ on:
- v* - v*
jobs: jobs:
run_tests_mac: run_tests_mac:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') if: github.repository_owner == 'zed-industries'
runs-on: self-mini-macos runs-on: self-mini-macos
steps: steps:
- name: steps::checkout_repo - name: steps::checkout_repo
@@ -29,6 +29,9 @@ jobs:
- name: steps::clippy - name: steps::clippy
run: ./script/clippy run: ./script/clippy
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
- name: steps::cargo_install_nextest
run: cargo install cargo-nextest --locked
shell: bash -euxo pipefail {0}
- name: steps::clear_target_dir_if_large - name: steps::clear_target_dir_if_large
run: ./script/clear-target-dir-if-larger-than 300 run: ./script/clear-target-dir-if-larger-than 300
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
@@ -42,7 +45,7 @@ jobs:
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
timeout-minutes: 60 timeout-minutes: 60
run_tests_linux: run_tests_linux:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') if: github.repository_owner == 'zed-industries'
runs-on: namespace-profile-16x32-ubuntu-2204 runs-on: namespace-profile-16x32-ubuntu-2204
steps: steps:
- name: steps::checkout_repo - name: steps::checkout_repo
@@ -54,19 +57,16 @@ jobs:
mkdir -p ./../.cargo mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml cp ./.cargo/ci-config.toml ./../.cargo/config.toml
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
- name: steps::cache_rust_dependencies_namespace
uses: namespacelabs/nscloud-cache-action@v1
with:
cache: rust
- name: steps::setup_linux - name: steps::setup_linux
run: ./script/linux run: ./script/linux
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
- name: steps::install_mold - name: steps::install_mold
run: ./script/install-mold run: ./script/install-mold
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
- name: steps::download_wasi_sdk - name: steps::cache_rust_dependencies_namespace
run: ./script/download-wasi-sdk uses: namespacelabs/nscloud-cache-action@v1
shell: bash -euxo pipefail {0} with:
cache: rust
- name: steps::setup_node - name: steps::setup_node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with: with:
@@ -75,7 +75,8 @@ jobs:
run: ./script/clippy run: ./script/clippy
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
- name: steps::cargo_install_nextest - name: steps::cargo_install_nextest
uses: taiki-e/install-action@nextest run: cargo install cargo-nextest --locked
shell: bash -euxo pipefail {0}
- name: steps::clear_target_dir_if_large - name: steps::clear_target_dir_if_large
run: ./script/clear-target-dir-if-larger-than 250 run: ./script/clear-target-dir-if-larger-than 250
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
@@ -89,7 +90,7 @@ jobs:
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
timeout-minutes: 60 timeout-minutes: 60
run_tests_windows: run_tests_windows:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') if: github.repository_owner == 'zed-industries'
runs-on: self-32vcpu-windows-2022 runs-on: self-32vcpu-windows-2022
steps: steps:
- name: steps::checkout_repo - name: steps::checkout_repo
@@ -108,6 +109,9 @@ jobs:
- name: steps::clippy - name: steps::clippy
run: ./script/clippy.ps1 run: ./script/clippy.ps1
shell: pwsh shell: pwsh
- name: steps::cargo_install_nextest
run: cargo install cargo-nextest --locked
shell: pwsh
- name: steps::clear_target_dir_if_large - name: steps::clear_target_dir_if_large
run: ./script/clear-target-dir-if-larger-than.ps1 250 run: ./script/clear-target-dir-if-larger-than.ps1 250
shell: pwsh shell: pwsh
@@ -121,7 +125,7 @@ jobs:
shell: pwsh shell: pwsh
timeout-minutes: 60 timeout-minutes: 60
check_scripts: check_scripts:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') if: github.repository_owner == 'zed-industries'
runs-on: namespace-profile-2x4-ubuntu-2404 runs-on: namespace-profile-2x4-ubuntu-2404
steps: steps:
- name: steps::checkout_repo - name: steps::checkout_repo
@@ -150,7 +154,7 @@ jobs:
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
timeout-minutes: 60 timeout-minutes: 60
create_draft_release: create_draft_release:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') if: github.repository_owner == 'zed-industries'
runs-on: namespace-profile-2x4-ubuntu-2404 runs-on: namespace-profile-2x4-ubuntu-2404
steps: steps:
- name: steps::checkout_repo - name: steps::checkout_repo
@@ -198,9 +202,6 @@ jobs:
- name: steps::install_mold - name: steps::install_mold
run: ./script/install-mold run: ./script/install-mold
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
- name: ./script/bundle-linux - name: ./script/bundle-linux
run: ./script/bundle-linux run: ./script/bundle-linux
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
@@ -241,9 +242,6 @@ jobs:
- name: steps::install_mold - name: steps::install_mold
run: ./script/install-mold run: ./script/install-mold
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
- name: ./script/bundle-linux - name: ./script/bundle-linux
run: ./script/bundle-linux run: ./script/bundle-linux
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
@@ -477,20 +475,14 @@ jobs:
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
notify_on_failure: - name: release::create_sentry_release
needs: uses: getsentry/action-release@526942b68292201ac6bbb99b9a0747d4abee354c
- upload_release_assets with:
- auto_release_preview environment: production
if: failure()
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: release::notify_on_failure::notify_slack
run: |-
curl -X POST -H 'Content-type: application/json'\
--data '{"text":"${{ github.workflow }} failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}' "$SLACK_WEBHOOK"
shell: bash -euxo pipefail {0}
env: env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_WORKFLOW_FAILURES }} SENTRY_ORG: zed-dev
SENTRY_PROJECT: zed
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }} group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
cancel-in-progress: true cancel-in-progress: true

View File

@@ -12,7 +12,7 @@ on:
- cron: 0 7 * * * - cron: 0 7 * * *
jobs: jobs:
check_style: check_style:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') if: github.repository_owner == 'zed-industries'
runs-on: self-mini-macos runs-on: self-mini-macos
steps: steps:
- name: steps::checkout_repo - name: steps::checkout_repo
@@ -28,7 +28,7 @@ jobs:
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
timeout-minutes: 60 timeout-minutes: 60
run_tests_windows: run_tests_windows:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') if: github.repository_owner == 'zed-industries'
runs-on: self-32vcpu-windows-2022 runs-on: self-32vcpu-windows-2022
steps: steps:
- name: steps::checkout_repo - name: steps::checkout_repo
@@ -47,6 +47,9 @@ jobs:
- name: steps::clippy - name: steps::clippy
run: ./script/clippy.ps1 run: ./script/clippy.ps1
shell: pwsh shell: pwsh
- name: steps::cargo_install_nextest
run: cargo install cargo-nextest --locked
shell: pwsh
- name: steps::clear_target_dir_if_large - name: steps::clear_target_dir_if_large
run: ./script/clear-target-dir-if-larger-than.ps1 250 run: ./script/clear-target-dir-if-larger-than.ps1 250
shell: pwsh shell: pwsh
@@ -90,9 +93,6 @@ jobs:
- name: steps::install_mold - name: steps::install_mold
run: ./script/install-mold run: ./script/install-mold
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
- name: ./script/bundle-linux - name: ./script/bundle-linux
run: ./script/bundle-linux run: ./script/bundle-linux
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
@@ -140,9 +140,6 @@ jobs:
- name: steps::install_mold - name: steps::install_mold
run: ./script/install-mold run: ./script/install-mold
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
- name: ./script/bundle-linux - name: ./script/bundle-linux
run: ./script/bundle-linux run: ./script/bundle-linux
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
@@ -361,7 +358,7 @@ jobs:
needs: needs:
- check_style - check_style
- run_tests_windows - run_tests_windows
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') if: github.repository_owner == 'zed-industries'
runs-on: namespace-profile-32x64-ubuntu-2004 runs-on: namespace-profile-32x64-ubuntu-2004
env: env:
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }} ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
@@ -392,7 +389,7 @@ jobs:
needs: needs:
- check_style - check_style
- run_tests_windows - run_tests_windows
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') if: github.repository_owner == 'zed-industries'
runs-on: self-mini-macos runs-on: self-mini-macos
env: env:
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }} ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
@@ -434,7 +431,7 @@ jobs:
- bundle_mac_x86_64 - bundle_mac_x86_64
- bundle_windows_aarch64 - bundle_windows_aarch64
- bundle_windows_x86_64 - bundle_windows_x86_64
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') if: github.repository_owner == 'zed-industries'
runs-on: namespace-profile-4x8-ubuntu-2204 runs-on: namespace-profile-4x8-ubuntu-2204
steps: steps:
- name: steps::checkout_repo - name: steps::checkout_repo
@@ -490,21 +487,3 @@ jobs:
SENTRY_PROJECT: zed SENTRY_PROJECT: zed
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
timeout-minutes: 60 timeout-minutes: 60
notify_on_failure:
needs:
- bundle_linux_aarch64
- bundle_linux_x86_64
- bundle_mac_aarch64
- bundle_mac_x86_64
- bundle_windows_aarch64
- bundle_windows_x86_64
if: failure()
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: release::notify_on_failure::notify_slack
run: |-
curl -X POST -H 'Content-type: application/json'\
--data '{"text":"${{ github.workflow }} failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}' "$SLACK_WEBHOOK"
shell: bash -euxo pipefail {0}
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_WORKFLOW_FAILURES }}

View File

@@ -6,21 +6,24 @@ env:
CARGO_INCREMENTAL: '0' CARGO_INCREMENTAL: '0'
RUST_BACKTRACE: '1' RUST_BACKTRACE: '1'
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
GOOGLE_AI_API_KEY: ${{ secrets.GOOGLE_AI_API_KEY }}
GOOGLE_CLOUD_PROJECT: ${{ secrets.GOOGLE_CLOUD_PROJECT }}
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }} ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_EVAL_TELEMETRY: '1' ZED_EVAL_TELEMETRY: '1'
MODEL_NAME: ${{ inputs.model_name }}
on: on:
workflow_dispatch: pull_request:
inputs: types:
model_name: - synchronize
description: model_name - reopened
required: true - labeled
type: string branches:
- '**'
schedule:
- cron: 0 0 * * *
workflow_dispatch: {}
jobs: jobs:
agent_evals: agent_evals:
if: |
github.repository_owner == 'zed-industries' &&
(github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-eval'))
runs-on: namespace-profile-16x32-ubuntu-2204 runs-on: namespace-profile-16x32-ubuntu-2204
steps: steps:
- name: steps::checkout_repo - name: steps::checkout_repo
@@ -37,9 +40,6 @@ jobs:
- name: steps::install_mold - name: steps::install_mold
run: ./script/install-mold run: ./script/install-mold
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
- name: steps::setup_cargo_config - name: steps::setup_cargo_config
run: | run: |
mkdir -p ./../.cargo mkdir -p ./../.cargo
@@ -49,19 +49,14 @@ jobs:
run: cargo build --package=eval run: cargo build --package=eval
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
- name: run_agent_evals::agent_evals::run_eval - name: run_agent_evals::agent_evals::run_eval
run: cargo run --package=eval -- --repetitions=8 --concurrency=1 --model "${MODEL_NAME}" run: cargo run --package=eval -- --repetitions=8 --concurrency=1
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
GOOGLE_AI_API_KEY: ${{ secrets.GOOGLE_AI_API_KEY }}
GOOGLE_CLOUD_PROJECT: ${{ secrets.GOOGLE_CLOUD_PROJECT }}
- name: steps::cleanup_cargo_config - name: steps::cleanup_cargo_config
if: always() if: always()
run: | run: |
rm -rf ./../.cargo rm -rf ./../.cargo
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
timeout-minutes: 600 timeout-minutes: 60
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }} group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
cancel-in-progress: true cancel-in-progress: true

View File

@@ -34,9 +34,6 @@ jobs:
- name: steps::install_mold - name: steps::install_mold
run: ./script/install-mold run: ./script/install-mold
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
- name: ./script/bundle-linux - name: ./script/bundle-linux
run: ./script/bundle-linux run: ./script/bundle-linux
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
@@ -77,9 +74,6 @@ jobs:
- name: steps::install_mold - name: steps::install_mold
run: ./script/install-mold run: ./script/install-mold
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
- name: ./script/bundle-linux - name: ./script/bundle-linux
run: ./script/bundle-linux run: ./script/bundle-linux
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}

View File

@@ -1,68 +0,0 @@
# Generated from xtask::workflows::run_cron_unit_evals
# Rebuild with `cargo xtask workflows`.
name: run_cron_unit_evals
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: '0'
RUST_BACKTRACE: '1'
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
on:
schedule:
- cron: 47 1 * * 2
workflow_dispatch: {}
jobs:
cron_unit_evals:
runs-on: namespace-profile-16x32-ubuntu-2204
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- 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::cargo_install_nextest
uses: taiki-e/install-action@nextest
- name: steps::clear_target_dir_if_large
run: ./script/clear-target-dir-if-larger-than 250
shell: bash -euxo pipefail {0}
- name: ./script/run-unit-evals
run: ./script/run-unit-evals
shell: bash -euxo pipefail {0}
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
GOOGLE_AI_API_KEY: ${{ secrets.GOOGLE_AI_API_KEY }}
GOOGLE_CLOUD_PROJECT: ${{ secrets.GOOGLE_CLOUD_PROJECT }}
- name: steps::cleanup_cargo_config
if: always()
run: |
rm -rf ./../.cargo
shell: bash -euxo pipefail {0}
- name: run_agent_evals::cron_unit_evals::send_failure_to_slack
if: ${{ failure() }}
uses: slackapi/slack-github-action@b0fa283ad8fea605de13dc3f449259339835fc52
with:
method: chat.postMessage
token: ${{ secrets.SLACK_APP_ZED_UNIT_EVALS_BOT_TOKEN }}
payload: |
channel: C04UDRNNJFQ
text: "Unit Evals Failed: https://github.com/zed-industries/zed/actions/runs/${{ github.run_id }}"
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
cancel-in-progress: true

View File

@@ -15,7 +15,7 @@ on:
- v[0-9]+.[0-9]+.x - v[0-9]+.[0-9]+.x
jobs: jobs:
orchestrate: orchestrate:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') if: github.repository_owner == 'zed-industries'
runs-on: namespace-profile-2x4-ubuntu-2404 runs-on: namespace-profile-2x4-ubuntu-2404
steps: steps:
- name: steps::checkout_repo - name: steps::checkout_repo
@@ -59,7 +59,7 @@ jobs:
run_nix: ${{ steps.filter.outputs.run_nix }} run_nix: ${{ steps.filter.outputs.run_nix }}
run_tests: ${{ steps.filter.outputs.run_tests }} run_tests: ${{ steps.filter.outputs.run_tests }}
check_style: check_style:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') if: github.repository_owner == 'zed-industries'
runs-on: namespace-profile-4x8-ubuntu-2204 runs-on: namespace-profile-4x8-ubuntu-2204
steps: steps:
- name: steps::checkout_repo - name: steps::checkout_repo
@@ -113,6 +113,9 @@ jobs:
- name: steps::clippy - name: steps::clippy
run: ./script/clippy.ps1 run: ./script/clippy.ps1
shell: pwsh shell: pwsh
- name: steps::cargo_install_nextest
run: cargo install cargo-nextest --locked
shell: pwsh
- name: steps::clear_target_dir_if_large - name: steps::clear_target_dir_if_large
run: ./script/clear-target-dir-if-larger-than.ps1 250 run: ./script/clear-target-dir-if-larger-than.ps1 250
shell: pwsh shell: pwsh
@@ -140,19 +143,16 @@ jobs:
mkdir -p ./../.cargo mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml cp ./.cargo/ci-config.toml ./../.cargo/config.toml
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
- name: steps::cache_rust_dependencies_namespace
uses: namespacelabs/nscloud-cache-action@v1
with:
cache: rust
- name: steps::setup_linux - name: steps::setup_linux
run: ./script/linux run: ./script/linux
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
- name: steps::install_mold - name: steps::install_mold
run: ./script/install-mold run: ./script/install-mold
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
- name: steps::download_wasi_sdk - name: steps::cache_rust_dependencies_namespace
run: ./script/download-wasi-sdk uses: namespacelabs/nscloud-cache-action@v1
shell: bash -euxo pipefail {0} with:
cache: rust
- name: steps::setup_node - name: steps::setup_node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with: with:
@@ -161,7 +161,8 @@ jobs:
run: ./script/clippy run: ./script/clippy
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
- name: steps::cargo_install_nextest - name: steps::cargo_install_nextest
uses: taiki-e/install-action@nextest run: cargo install cargo-nextest --locked
shell: bash -euxo pipefail {0}
- name: steps::clear_target_dir_if_large - name: steps::clear_target_dir_if_large
run: ./script/clear-target-dir-if-larger-than 250 run: ./script/clear-target-dir-if-larger-than 250
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
@@ -196,6 +197,9 @@ jobs:
- name: steps::clippy - name: steps::clippy
run: ./script/clippy run: ./script/clippy
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
- name: steps::cargo_install_nextest
run: cargo install cargo-nextest --locked
shell: bash -euxo pipefail {0}
- name: steps::clear_target_dir_if_large - name: steps::clear_target_dir_if_large
run: ./script/clear-target-dir-if-larger-than 300 run: ./script/clear-target-dir-if-larger-than 300
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
@@ -228,9 +232,6 @@ jobs:
- name: steps::install_mold - name: steps::install_mold
run: ./script/install-mold run: ./script/install-mold
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
- name: steps::setup_cargo_config - name: steps::setup_cargo_config
run: | run: |
mkdir -p ./../.cargo mkdir -p ./../.cargo
@@ -262,19 +263,16 @@ jobs:
mkdir -p ./../.cargo mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml cp ./.cargo/ci-config.toml ./../.cargo/config.toml
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
- name: steps::cache_rust_dependencies_namespace
uses: namespacelabs/nscloud-cache-action@v1
with:
cache: rust
- name: steps::setup_linux - name: steps::setup_linux
run: ./script/linux run: ./script/linux
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
- name: steps::install_mold - name: steps::install_mold
run: ./script/install-mold run: ./script/install-mold
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
- name: steps::download_wasi_sdk - name: steps::cache_rust_dependencies_namespace
run: ./script/download-wasi-sdk uses: namespacelabs/nscloud-cache-action@v1
shell: bash -euxo pipefail {0} with:
cache: rust
- name: cargo build -p collab - name: cargo build -p collab
run: cargo build -p collab run: cargo build -p collab
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
@@ -287,6 +285,40 @@ jobs:
rm -rf ./../.cargo rm -rf ./../.cargo
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
timeout-minutes: 60 timeout-minutes: 60
check_postgres_and_protobuf_migrations:
needs:
- orchestrate
if: needs.orchestrate.outputs.run_tests == 'true'
runs-on: self-mini-macos
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
fetch-depth: 0
- name: run_tests::check_postgres_and_protobuf_migrations::remove_untracked_files
run: git clean -df
shell: bash -euxo pipefail {0}
- name: run_tests::check_postgres_and_protobuf_migrations::ensure_fresh_merge
run: |
if [ -z "$GITHUB_BASE_REF" ];
then
echo "BUF_BASE_BRANCH=$(git merge-base origin/main HEAD)" >> "$GITHUB_ENV"
else
git checkout -B temp
git merge -q "origin/$GITHUB_BASE_REF" -m "merge main into temp"
echo "BUF_BASE_BRANCH=$GITHUB_BASE_REF" >> "$GITHUB_ENV"
fi
shell: bash -euxo pipefail {0}
- name: run_tests::check_postgres_and_protobuf_migrations::bufbuild_setup_action
uses: bufbuild/buf-setup-action@v1
with:
version: v1.29.0
- name: run_tests::check_postgres_and_protobuf_migrations::bufbuild_breaking_action
uses: bufbuild/buf-breaking-action@v1
with:
input: crates/proto/proto/
against: https://github.com/${GITHUB_REPOSITORY}.git#branch=${BUF_BASE_BRANCH},subdir=crates/proto/proto/
timeout-minutes: 60
check_dependencies: check_dependencies:
needs: needs:
- orchestrate - orchestrate
@@ -350,9 +382,6 @@ jobs:
- name: steps::install_mold - name: steps::install_mold
run: ./script/install-mold run: ./script/install-mold
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
- name: run_tests::check_docs::install_mdbook - name: run_tests::check_docs::install_mdbook
uses: peaceiris/actions-mdbook@ee69d230fe19748b7abf22df32acaa93833fad08 uses: peaceiris/actions-mdbook@ee69d230fe19748b7abf22df32acaa93833fad08
with: with:
@@ -489,43 +518,6 @@ jobs:
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
timeout-minutes: 60 timeout-minutes: 60
continue-on-error: true continue-on-error: true
check_postgres_and_protobuf_migrations:
needs:
- orchestrate
if: needs.orchestrate.outputs.run_tests == 'true'
runs-on: namespace-profile-16x32-ubuntu-2204
env:
GIT_AUTHOR_NAME: Protobuf Action
GIT_AUTHOR_EMAIL: ci@zed.dev
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
fetch-depth: 0
- name: run_tests::check_postgres_and_protobuf_migrations::remove_untracked_files
run: git clean -df
shell: bash -euxo pipefail {0}
- name: run_tests::check_postgres_and_protobuf_migrations::ensure_fresh_merge
run: |
if [ -z "$GITHUB_BASE_REF" ];
then
echo "BUF_BASE_BRANCH=$(git merge-base origin/main HEAD)" >> "$GITHUB_ENV"
else
git checkout -B temp
git merge -q "origin/$GITHUB_BASE_REF" -m "merge main into temp"
echo "BUF_BASE_BRANCH=$GITHUB_BASE_REF" >> "$GITHUB_ENV"
fi
shell: bash -euxo pipefail {0}
- name: run_tests::check_postgres_and_protobuf_migrations::bufbuild_setup_action
uses: bufbuild/buf-setup-action@v1
with:
version: v1.29.0
- name: run_tests::check_postgres_and_protobuf_migrations::bufbuild_breaking_action
uses: bufbuild/buf-breaking-action@v1
with:
input: crates/proto/proto/
against: https://github.com/${GITHUB_REPOSITORY}.git#branch=${BUF_BASE_BRANCH},subdir=crates/proto/proto/
timeout-minutes: 60
tests_pass: tests_pass:
needs: needs:
- orchestrate - orchestrate
@@ -535,13 +527,14 @@ jobs:
- run_tests_mac - run_tests_mac
- doctests - doctests
- check_workspace_binaries - check_workspace_binaries
- check_postgres_and_protobuf_migrations
- check_dependencies - check_dependencies
- check_docs - check_docs
- check_licenses - check_licenses
- check_scripts - check_scripts
- build_nix_linux_x86_64 - build_nix_linux_x86_64
- build_nix_mac_aarch64 - build_nix_mac_aarch64
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') && always() if: github.repository_owner == 'zed-industries' && always()
runs-on: namespace-profile-2x4-ubuntu-2404 runs-on: namespace-profile-2x4-ubuntu-2404
steps: steps:
- name: run_tests::tests_pass - name: run_tests::tests_pass
@@ -561,6 +554,7 @@ jobs:
check_result "run_tests_mac" "${{ needs.run_tests_mac.result }}" check_result "run_tests_mac" "${{ needs.run_tests_mac.result }}"
check_result "doctests" "${{ needs.doctests.result }}" check_result "doctests" "${{ needs.doctests.result }}"
check_result "check_workspace_binaries" "${{ needs.check_workspace_binaries.result }}" check_result "check_workspace_binaries" "${{ needs.check_workspace_binaries.result }}"
check_result "check_postgres_and_protobuf_migrations" "${{ needs.check_postgres_and_protobuf_migrations.result }}"
check_result "check_dependencies" "${{ needs.check_dependencies.result }}" check_result "check_dependencies" "${{ needs.check_dependencies.result }}"
check_result "check_docs" "${{ needs.check_docs.result }}" check_result "check_docs" "${{ needs.check_docs.result }}"
check_result "check_licenses" "${{ needs.check_licenses.result }}" check_result "check_licenses" "${{ needs.check_licenses.result }}"

View File

@@ -1,26 +1,17 @@
# Generated from xtask::workflows::run_unit_evals # Generated from xtask::workflows::run_agent_evals
# Rebuild with `cargo xtask workflows`. # Rebuild with `cargo xtask workflows`.
name: run_unit_evals name: run_agent_evals
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: '0' CARGO_INCREMENTAL: '0'
RUST_BACKTRACE: '1' RUST_BACKTRACE: '1'
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }} ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_EVAL_TELEMETRY: '1'
MODEL_NAME: ${{ inputs.model_name }}
on: on:
workflow_dispatch: schedule:
inputs: - cron: 47 1 * * 2
model_name: workflow_dispatch: {}
description: model_name
required: true
type: string
commit_sha:
description: commit_sha
required: true
type: string
jobs: jobs:
run_unit_evals: unit_evals:
runs-on: namespace-profile-16x32-ubuntu-2204 runs-on: namespace-profile-16x32-ubuntu-2204
steps: steps:
- name: steps::checkout_repo - name: steps::checkout_repo
@@ -42,11 +33,9 @@ jobs:
- name: steps::install_mold - name: steps::install_mold
run: ./script/install-mold run: ./script/install-mold
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
- name: steps::cargo_install_nextest - name: steps::cargo_install_nextest
uses: taiki-e/install-action@nextest run: cargo install cargo-nextest --locked
shell: bash -euxo pipefail {0}
- name: steps::clear_target_dir_if_large - name: steps::clear_target_dir_if_large
run: ./script/clear-target-dir-if-larger-than 250 run: ./script/clear-target-dir-if-larger-than 250
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
@@ -55,15 +44,20 @@ jobs:
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
env: env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - name: run_agent_evals::unit_evals::send_failure_to_slack
GOOGLE_AI_API_KEY: ${{ secrets.GOOGLE_AI_API_KEY }} if: ${{ failure() }}
GOOGLE_CLOUD_PROJECT: ${{ secrets.GOOGLE_CLOUD_PROJECT }} uses: slackapi/slack-github-action@b0fa283ad8fea605de13dc3f449259339835fc52
UNIT_EVAL_COMMIT: ${{ inputs.commit_sha }} with:
method: chat.postMessage
token: ${{ secrets.SLACK_APP_ZED_UNIT_EVALS_BOT_TOKEN }}
payload: |
channel: C04UDRNNJFQ
text: "Unit Evals Failed: https://github.com/zed-industries/zed/actions/runs/${{ github.run_id }}"
- name: steps::cleanup_cargo_config - name: steps::cleanup_cargo_config
if: always() if: always()
run: | run: |
rm -rf ./../.cargo rm -rf ./../.cargo
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.run_id }} group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
cancel-in-progress: true cancel-in-progress: true

184
Cargo.lock generated
View File

@@ -32,7 +32,6 @@ dependencies = [
"settings", "settings",
"smol", "smol",
"task", "task",
"telemetry",
"tempfile", "tempfile",
"terminal", "terminal",
"ui", "ui",
@@ -40,7 +39,6 @@ dependencies = [
"util", "util",
"uuid", "uuid",
"watch", "watch",
"zlog",
] ]
[[package]] [[package]]
@@ -81,7 +79,6 @@ dependencies = [
"rand 0.9.2", "rand 0.9.2",
"serde_json", "serde_json",
"settings", "settings",
"telemetry",
"text", "text",
"util", "util",
"watch", "watch",
@@ -96,7 +93,6 @@ dependencies = [
"auto_update", "auto_update",
"editor", "editor",
"extension_host", "extension_host",
"fs",
"futures 0.3.31", "futures 0.3.31",
"gpui", "gpui",
"language", "language",
@@ -251,6 +247,7 @@ dependencies = [
"acp_tools", "acp_tools",
"action_log", "action_log",
"agent-client-protocol", "agent-client-protocol",
"agent_settings",
"anyhow", "anyhow",
"async-trait", "async-trait",
"client", "client",
@@ -322,7 +319,6 @@ dependencies = [
"assistant_slash_command", "assistant_slash_command",
"assistant_slash_commands", "assistant_slash_commands",
"assistant_text_thread", "assistant_text_thread",
"async-fs",
"audio", "audio",
"buffer_diff", "buffer_diff",
"chrono", "chrono",
@@ -344,7 +340,6 @@ dependencies = [
"gpui", "gpui",
"html_to_markdown", "html_to_markdown",
"http_client", "http_client",
"image",
"indoc", "indoc",
"itertools 0.14.0", "itertools 0.14.0",
"jsonschema", "jsonschema",
@@ -1240,15 +1235,15 @@ dependencies = [
[[package]] [[package]]
name = "async_zip" name = "async_zip"
version = "0.0.18" version = "0.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d8c50d65ce1b0e0cb65a785ff615f78860d7754290647d3b983208daa4f85e6" checksum = "00b9f7252833d5ed4b00aa9604b563529dd5e11de9c23615de2dcdf91eb87b52"
dependencies = [ dependencies = [
"async-compression", "async-compression",
"crc32fast", "crc32fast",
"futures-lite 2.6.1", "futures-lite 2.6.1",
"pin-project", "pin-project",
"thiserror 2.0.17", "thiserror 1.0.69",
] ]
[[package]] [[package]]
@@ -1333,14 +1328,10 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"client", "client",
"clock",
"ctor",
"db", "db",
"futures 0.3.31",
"gpui", "gpui",
"http_client", "http_client",
"log", "log",
"parking_lot",
"paths", "paths",
"release_channel", "release_channel",
"serde", "serde",
@@ -1351,7 +1342,6 @@ dependencies = [
"util", "util",
"which 6.0.3", "which 6.0.3",
"workspace", "workspace",
"zlog",
] ]
[[package]] [[package]]
@@ -1463,7 +1453,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879b6c89592deb404ba4dc0ae6b58ffd1795c78991cbb5b8bc441c48a070440d" checksum = "879b6c89592deb404ba4dc0ae6b58ffd1795c78991cbb5b8bc441c48a070440d"
dependencies = [ dependencies = [
"aws-lc-sys", "aws-lc-sys",
"untrusted 0.7.1",
"zeroize", "zeroize",
] ]
@@ -2617,24 +2606,26 @@ dependencies = [
[[package]] [[package]]
name = "calloop" name = "calloop"
version = "0.14.3" version = "0.13.0"
source = "git+https://github.com/zed-industries/calloop#eb6b4fd17b9af5ecc226546bdd04185391b3e265" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec"
dependencies = [ dependencies = [
"bitflags 2.9.4", "bitflags 2.9.4",
"log",
"polling", "polling",
"rustix 1.1.2", "rustix 0.38.44",
"slab", "slab",
"tracing", "thiserror 1.0.69",
] ]
[[package]] [[package]]
name = "calloop-wayland-source" name = "calloop-wayland-source"
version = "0.4.1" version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20"
dependencies = [ dependencies = [
"calloop", "calloop",
"rustix 1.1.2", "rustix 0.38.44",
"wayland-backend", "wayland-backend",
"wayland-client", "wayland-client",
] ]
@@ -3207,9 +3198,7 @@ dependencies = [
"indoc", "indoc",
"ordered-float 2.10.1", "ordered-float 2.10.1",
"rustc-hash 2.1.1", "rustc-hash 2.1.1",
"schemars 1.0.4",
"serde", "serde",
"serde_json",
"strum 0.27.2", "strum 0.27.2",
] ]
@@ -3689,7 +3678,6 @@ dependencies = [
"collections", "collections",
"futures 0.3.31", "futures 0.3.31",
"gpui", "gpui",
"http_client",
"log", "log",
"net", "net",
"parking_lot", "parking_lot",
@@ -5320,7 +5308,6 @@ dependencies = [
"workspace", "workspace",
"zed_actions", "zed_actions",
"zeta", "zeta",
"zeta2",
] ]
[[package]] [[package]]
@@ -5864,7 +5851,6 @@ dependencies = [
"lsp", "lsp",
"parking_lot", "parking_lot",
"pretty_assertions", "pretty_assertions",
"proto",
"semantic_version", "semantic_version",
"serde", "serde",
"serde_json", "serde_json",
@@ -6253,7 +6239,7 @@ dependencies = [
"futures-core", "futures-core",
"futures-sink", "futures-sink",
"nanorand", "nanorand",
"spin 0.9.8", "spin",
] ]
[[package]] [[package]]
@@ -6364,9 +6350,9 @@ checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
[[package]] [[package]]
name = "fork" name = "fork"
version = "0.4.0" version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30268f1eefccc9d72f43692e8b89e659aeb52e84016c3b32b6e7e9f1c8f38f94" checksum = "05dc8b302e04a1c27f4fe694439ef0f29779ca4edc205b7b58f00db04e29656d"
dependencies = [ dependencies = [
"libc", "libc",
] ]
@@ -6418,7 +6404,7 @@ dependencies = [
"ignore", "ignore",
"libc", "libc",
"log", "log",
"notify 8.2.0", "notify 8.0.0",
"objc", "objc",
"parking_lot", "parking_lot",
"paths", "paths",
@@ -7105,6 +7091,7 @@ dependencies = [
"askpass", "askpass",
"buffer_diff", "buffer_diff",
"call", "call",
"chrono",
"cloud_llm_client", "cloud_llm_client",
"collections", "collections",
"command_palette_hooks", "command_palette_hooks",
@@ -7292,7 +7279,6 @@ dependencies = [
"calloop", "calloop",
"calloop-wayland-source", "calloop-wayland-source",
"cbindgen", "cbindgen",
"circular-buffer",
"cocoa 0.26.0", "cocoa 0.26.0",
"cocoa-foundation 0.2.0", "cocoa-foundation 0.2.0",
"collections", "collections",
@@ -7348,7 +7334,6 @@ dependencies = [
"slotmap", "slotmap",
"smallvec", "smallvec",
"smol", "smol",
"spin 0.10.0",
"stacksafe", "stacksafe",
"strum 0.27.2", "strum 0.27.2",
"sum_tree", "sum_tree",
@@ -7812,7 +7797,6 @@ dependencies = [
"parking_lot", "parking_lot",
"serde", "serde",
"serde_json", "serde_json",
"serde_urlencoded",
"sha2", "sha2",
"tempfile", "tempfile",
"url", "url",
@@ -8663,25 +8647,23 @@ dependencies = [
[[package]] [[package]]
name = "jupyter-protocol" name = "jupyter-protocol"
version = "0.10.0" version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/ConradIrwin/runtimed?rev=7130c804216b6914355d15d0b91ea91f6babd734#7130c804216b6914355d15d0b91ea91f6babd734"
checksum = "d9c047f6b5e551563af2ddb13dafed833f0ec5a5b0f9621d5ad740a9ff1e1095"
dependencies = [ dependencies = [
"anyhow",
"async-trait", "async-trait",
"bytes 1.10.1", "bytes 1.10.1",
"chrono", "chrono",
"futures 0.3.31", "futures 0.3.31",
"serde", "serde",
"serde_json", "serde_json",
"thiserror 2.0.17",
"uuid", "uuid",
] ]
[[package]] [[package]]
name = "jupyter-websocket-client" name = "jupyter-websocket-client"
version = "0.15.0" version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/ConradIrwin/runtimed?rev=7130c804216b6914355d15d0b91ea91f6babd734#7130c804216b6914355d15d0b91ea91f6babd734"
checksum = "4197fa926a6b0bddfed7377d9fed3d00a0dec44a1501e020097bd26604699cae"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@@ -8690,7 +8672,6 @@ dependencies = [
"jupyter-protocol", "jupyter-protocol",
"serde", "serde",
"serde_json", "serde_json",
"tokio",
"url", "url",
"uuid", "uuid",
] ]
@@ -8728,6 +8709,7 @@ dependencies = [
"ui", "ui",
"ui_input", "ui_input",
"util", "util",
"vim",
"workspace", "workspace",
"zed_actions", "zed_actions",
] ]
@@ -8879,11 +8861,9 @@ dependencies = [
"icons", "icons",
"image", "image",
"log", "log",
"open_ai",
"open_router", "open_router",
"parking_lot", "parking_lot",
"proto", "proto",
"schemars 1.0.4",
"serde", "serde",
"serde_json", "serde_json",
"settings", "settings",
@@ -9048,7 +9028,6 @@ dependencies = [
"settings", "settings",
"smol", "smol",
"task", "task",
"terminal",
"text", "text",
"theme", "theme",
"toml 0.8.23", "toml 0.8.23",
@@ -9082,7 +9061,7 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
dependencies = [ dependencies = [
"spin 0.9.8", "spin",
] ]
[[package]] [[package]]
@@ -9696,7 +9675,6 @@ dependencies = [
"settings", "settings",
"theme", "theme",
"ui", "ui",
"urlencoding",
"util", "util",
"workspace", "workspace",
] ]
@@ -10024,18 +10002,6 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniprofiler_ui"
version = "0.1.0"
dependencies = [
"gpui",
"serde_json",
"smol",
"util",
"workspace",
"zed_actions",
]
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.8.9" version = "0.8.9"
@@ -10240,9 +10206,8 @@ dependencies = [
[[package]] [[package]]
name = "nbformat" name = "nbformat"
version = "0.15.0" version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/ConradIrwin/runtimed?rev=7130c804216b6914355d15d0b91ea91f6babd734#7130c804216b6914355d15d0b91ea91f6babd734"
checksum = "89c7229d604d847227002715e1235cd84e81919285d904ccb290a42ecc409348"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@@ -10444,10 +10409,11 @@ dependencies = [
[[package]] [[package]]
name = "notify" name = "notify"
version = "8.2.0" version = "8.0.0"
source = "git+https://github.com/zed-industries/notify.git?rev=b4588b2e5aee68f4c0e100f140e808cbce7b1419#b4588b2e5aee68f4c0e100f140e808cbce7b1419" source = "git+https://github.com/zed-industries/notify.git?rev=bbb9ea5ae52b253e095737847e367c30653a2e96#bbb9ea5ae52b253e095737847e367c30653a2e96"
dependencies = [ dependencies = [
"bitflags 2.9.4", "bitflags 2.9.4",
"filetime",
"fsevent-sys 4.1.0", "fsevent-sys 4.1.0",
"inotify 0.11.0", "inotify 0.11.0",
"kqueue", "kqueue",
@@ -10456,7 +10422,7 @@ dependencies = [
"mio 1.1.0", "mio 1.1.0",
"notify-types", "notify-types",
"walkdir", "walkdir",
"windows-sys 0.60.2", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@@ -10473,7 +10439,7 @@ dependencies = [
[[package]] [[package]]
name = "notify-types" name = "notify-types"
version = "2.0.0" version = "2.0.0"
source = "git+https://github.com/zed-industries/notify.git?rev=b4588b2e5aee68f4c0e100f140e808cbce7b1419#b4588b2e5aee68f4c0e100f140e808cbce7b1419" source = "git+https://github.com/zed-industries/notify.git?rev=bbb9ea5ae52b253e095737847e367c30653a2e96#bbb9ea5ae52b253e095737847e367c30653a2e96"
[[package]] [[package]]
name = "now" name = "now"
@@ -10528,10 +10494,11 @@ dependencies = [
[[package]] [[package]]
name = "num-bigint-dig" name = "num-bigint-dig"
version = "0.8.6" version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151"
dependencies = [ dependencies = [
"byteorder",
"lazy_static", "lazy_static",
"libm", "libm",
"num-integer", "num-integer",
@@ -11034,7 +11001,6 @@ dependencies = [
"serde_json", "serde_json",
"settings", "settings",
"strum 0.27.2", "strum 0.27.2",
"thiserror 2.0.17",
] ]
[[package]] [[package]]
@@ -13074,23 +13040,6 @@ dependencies = [
"zlog", "zlog",
] ]
[[package]]
name = "project_benchmarks"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"client",
"futures 0.3.31",
"gpui",
"http_client",
"language",
"node_runtime",
"project",
"settings",
"watch",
]
[[package]] [[package]]
name = "project_panel" name = "project_panel"
version = "0.1.0" version = "0.1.0"
@@ -13118,7 +13067,6 @@ dependencies = [
"settings", "settings",
"smallvec", "smallvec",
"telemetry", "telemetry",
"tempfile",
"theme", "theme",
"ui", "ui",
"util", "util",
@@ -14023,7 +13971,6 @@ dependencies = [
"gpui", "gpui",
"gpui_tokio", "gpui_tokio",
"http_client", "http_client",
"image",
"json_schema_store", "json_schema_store",
"language", "language",
"language_extension", "language_extension",
@@ -14037,7 +13984,6 @@ dependencies = [
"paths", "paths",
"pretty_assertions", "pretty_assertions",
"project", "project",
"prompt_store",
"proto", "proto",
"rayon", "rayon",
"release_channel", "release_channel",
@@ -14283,7 +14229,7 @@ dependencies = [
"cfg-if", "cfg-if",
"getrandom 0.2.16", "getrandom 0.2.16",
"libc", "libc",
"untrusted 0.9.0", "untrusted",
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
@@ -14412,9 +14358,9 @@ dependencies = [
[[package]] [[package]]
name = "rsa" name = "rsa"
version = "0.9.9" version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88" checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b"
dependencies = [ dependencies = [
"const-oid", "const-oid",
"digest", "digest",
@@ -14464,26 +14410,25 @@ dependencies = [
[[package]] [[package]]
name = "runtimelib" name = "runtimelib"
version = "0.30.0" version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/ConradIrwin/runtimed?rev=7130c804216b6914355d15d0b91ea91f6babd734#7130c804216b6914355d15d0b91ea91f6babd734"
checksum = "481b48894073a0096f28cbe9860af01fc1b861e55b3bc96afafc645ee3de62dc"
dependencies = [ dependencies = [
"anyhow",
"async-dispatcher", "async-dispatcher",
"async-std", "async-std",
"aws-lc-rs",
"base64 0.22.1", "base64 0.22.1",
"bytes 1.10.1", "bytes 1.10.1",
"chrono", "chrono",
"data-encoding", "data-encoding",
"dirs 6.0.0", "dirs 5.0.1",
"futures 0.3.31", "futures 0.3.31",
"glob", "glob",
"jupyter-protocol", "jupyter-protocol",
"ring",
"serde", "serde",
"serde_json", "serde_json",
"shellexpand 3.1.1", "shellexpand 3.1.1",
"smol", "smol",
"thiserror 2.0.17",
"uuid", "uuid",
"zeromq", "zeromq",
] ]
@@ -14751,7 +14696,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
dependencies = [ dependencies = [
"ring", "ring",
"untrusted 0.9.0", "untrusted",
] ]
[[package]] [[package]]
@@ -14763,7 +14708,7 @@ dependencies = [
"aws-lc-rs", "aws-lc-rs",
"ring", "ring",
"rustls-pki-types", "rustls-pki-types",
"untrusted 0.9.0", "untrusted",
] ]
[[package]] [[package]]
@@ -14993,7 +14938,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
dependencies = [ dependencies = [
"ring", "ring",
"untrusted 0.9.0", "untrusted",
] ]
[[package]] [[package]]
@@ -15896,15 +15841,6 @@ dependencies = [
"lock_api", "lock_api",
] ]
[[package]]
name = "spin"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
dependencies = [
"lock_api",
]
[[package]] [[package]]
name = "spirv" name = "spirv"
version = "0.3.0+sdk-1.3.268.0" version = "0.3.0+sdk-1.3.268.0"
@@ -16276,6 +16212,7 @@ dependencies = [
"log", "log",
"menu", "menu",
"picker", "picker",
"project",
"reqwest_client", "reqwest_client",
"rust-embed", "rust-embed",
"settings", "settings",
@@ -16285,6 +16222,7 @@ dependencies = [
"theme", "theme",
"title_bar", "title_bar",
"ui", "ui",
"workspace",
] ]
[[package]] [[package]]
@@ -16561,10 +16499,10 @@ checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb"
name = "svg_preview" name = "svg_preview"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"editor",
"file_icons", "file_icons",
"gpui", "gpui",
"language", "language",
"multi_buffer",
"ui", "ui",
"workspace", "workspace",
] ]
@@ -18586,12 +18524,6 @@ version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]]
name = "untrusted"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]] [[package]]
name = "untrusted" name = "untrusted"
version = "0.9.0" version = "0.9.0"
@@ -18686,7 +18618,6 @@ dependencies = [
"itertools 0.14.0", "itertools 0.14.0",
"libc", "libc",
"log", "log",
"mach2 0.5.0",
"nix 0.29.0", "nix 0.29.0",
"pretty_assertions", "pretty_assertions",
"rand 0.9.2", "rand 0.9.2",
@@ -18876,6 +18807,7 @@ dependencies = [
name = "vim_mode_setting" name = "vim_mode_setting"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"gpui",
"settings", "settings",
] ]
@@ -21019,7 +20951,6 @@ dependencies = [
"gh-workflow", "gh-workflow",
"indexmap 2.11.4", "indexmap 2.11.4",
"indoc", "indoc",
"serde",
"toml 0.8.23", "toml 0.8.23",
"toml_edit 0.22.27", "toml_edit 0.22.27",
] ]
@@ -21204,7 +21135,7 @@ dependencies = [
[[package]] [[package]]
name = "zed" name = "zed"
version = "0.215.0" version = "0.212.0"
dependencies = [ dependencies = [
"acp_tools", "acp_tools",
"activity_indicator", "activity_indicator",
@@ -21217,11 +21148,11 @@ dependencies = [
"audio", "audio",
"auto_update", "auto_update",
"auto_update_ui", "auto_update_ui",
"backtrace",
"bincode 1.3.3", "bincode 1.3.3",
"breadcrumbs", "breadcrumbs",
"call", "call",
"channel", "channel",
"chrono",
"clap", "clap",
"cli", "cli",
"client", "client",
@@ -21279,8 +21210,8 @@ dependencies = [
"menu", "menu",
"migrator", "migrator",
"mimalloc", "mimalloc",
"miniprofiler_ui",
"nc", "nc",
"nix 0.29.0",
"node_runtime", "node_runtime",
"notifications", "notifications",
"onboarding", "onboarding",
@@ -21322,6 +21253,7 @@ dependencies = [
"task", "task",
"tasks_ui", "tasks_ui",
"telemetry", "telemetry",
"telemetry_events",
"terminal_view", "terminal_view",
"theme", "theme",
"theme_extension", "theme_extension",
@@ -21726,7 +21658,6 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"arrayvec", "arrayvec",
"brotli",
"chrono", "chrono",
"client", "client",
"clock", "clock",
@@ -21743,21 +21674,18 @@ dependencies = [
"language_model", "language_model",
"log", "log",
"lsp", "lsp",
"open_ai",
"pretty_assertions", "pretty_assertions",
"project", "project",
"release_channel", "release_channel",
"schemars 1.0.4",
"serde", "serde",
"serde_json", "serde_json",
"settings", "settings",
"smol",
"strsim",
"thiserror 2.0.17", "thiserror 2.0.17",
"util", "util",
"uuid", "uuid",
"workspace", "workspace",
"worktree", "worktree",
"zlog",
] ]
[[package]] [[package]]
@@ -21769,7 +21697,6 @@ dependencies = [
"clap", "clap",
"client", "client",
"cloud_llm_client", "cloud_llm_client",
"cloud_zeta2_prompt",
"collections", "collections",
"edit_prediction_context", "edit_prediction_context",
"editor", "editor",
@@ -21783,6 +21710,7 @@ dependencies = [
"ordered-float 2.10.1", "ordered-float 2.10.1",
"pretty_assertions", "pretty_assertions",
"project", "project",
"regex-syntax",
"serde", "serde",
"serde_json", "serde_json",
"settings", "settings",

View File

@@ -110,7 +110,6 @@ members = [
"crates/menu", "crates/menu",
"crates/migrator", "crates/migrator",
"crates/mistral", "crates/mistral",
"crates/miniprofiler_ui",
"crates/multi_buffer", "crates/multi_buffer",
"crates/nc", "crates/nc",
"crates/net", "crates/net",
@@ -127,7 +126,6 @@ members = [
"crates/picker", "crates/picker",
"crates/prettier", "crates/prettier",
"crates/project", "crates/project",
"crates/project_benchmarks",
"crates/project_panel", "crates/project_panel",
"crates/project_symbols", "crates/project_symbols",
"crates/prompt_store", "crates/prompt_store",
@@ -343,7 +341,6 @@ menu = { path = "crates/menu" }
migrator = { path = "crates/migrator" } migrator = { path = "crates/migrator" }
mistral = { path = "crates/mistral" } mistral = { path = "crates/mistral" }
multi_buffer = { path = "crates/multi_buffer" } multi_buffer = { path = "crates/multi_buffer" }
miniprofiler_ui = { path = "crates/miniprofiler_ui" }
nc = { path = "crates/nc" } nc = { path = "crates/nc" }
net = { path = "crates/net" } net = { path = "crates/net" }
node_runtime = { path = "crates/node_runtime" } node_runtime = { path = "crates/node_runtime" }
@@ -461,7 +458,7 @@ async-tar = "0.5.1"
async-task = "4.7" async-task = "4.7"
async-trait = "0.1" async-trait = "0.1"
async-tungstenite = "0.31.0" async-tungstenite = "0.31.0"
async_zip = { version = "0.0.18", features = ["deflate", "deflate64"] } async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
aws-config = { version = "1.6.1", features = ["behavior-version-latest"] } aws-config = { version = "1.6.1", features = ["behavior-version-latest"] }
aws-credential-types = { version = "1.2.2", features = [ aws-credential-types = { version = "1.2.2", features = [
"hardcoded-credentials", "hardcoded-credentials",
@@ -478,7 +475,6 @@ bitflags = "2.6.0"
blade-graphics = { version = "0.7.0" } blade-graphics = { version = "0.7.0" }
blade-macros = { version = "0.3.0" } blade-macros = { version = "0.3.0" }
blade-util = { version = "0.3.0" } blade-util = { version = "0.3.0" }
brotli = "8.0.2"
bytes = "1.0" bytes = "1.0"
cargo_metadata = "0.19" cargo_metadata = "0.19"
cargo_toml = "0.21" cargo_toml = "0.21"
@@ -486,7 +482,7 @@ cfg-if = "1.0.3"
chrono = { version = "0.4", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }
ciborium = "0.2" ciborium = "0.2"
circular-buffer = "1.0" circular-buffer = "1.0"
clap = { version = "4.4", features = ["derive", "wrap_help"] } clap = { version = "4.4", features = ["derive"] }
cocoa = "=0.26.0" cocoa = "=0.26.0"
cocoa-foundation = "=0.2.0" cocoa-foundation = "=0.2.0"
convert_case = "0.8.0" convert_case = "0.8.0"
@@ -508,7 +504,7 @@ emojis = "0.6.1"
env_logger = "0.11" env_logger = "0.11"
exec = "0.3.1" exec = "0.3.1"
fancy-regex = "0.14.0" fancy-regex = "0.14.0"
fork = "0.4.0" fork = "0.2.0"
futures = "0.3" futures = "0.3"
futures-batch = "0.6.1" futures-batch = "0.6.1"
futures-lite = "1.13" futures-lite = "1.13"
@@ -535,8 +531,8 @@ itertools = "0.14.0"
json_dotpath = "1.1" json_dotpath = "1.1"
jsonschema = "0.30.0" jsonschema = "0.30.0"
jsonwebtoken = "9.3" jsonwebtoken = "9.3"
jupyter-protocol = "0.10.0" jupyter-protocol = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
jupyter-websocket-client = "0.15.0" jupyter-websocket-client = { git = "https://github.com/ConradIrwin/runtimed" ,rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
libc = "0.2" libc = "0.2"
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] } libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
linkify = "0.10.0" linkify = "0.10.0"
@@ -549,7 +545,7 @@ minidumper = "0.8"
moka = { version = "0.12.10", features = ["sync"] } moka = { version = "0.12.10", features = ["sync"] }
naga = { version = "25.0", features = ["wgsl-in"] } naga = { version = "25.0", features = ["wgsl-in"] }
nanoid = "0.4" nanoid = "0.4"
nbformat = "0.15.0" nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
nix = "0.29" nix = "0.29"
num-format = "0.4.4" num-format = "0.4.4"
num-traits = "0.2" num-traits = "0.2"
@@ -620,8 +616,8 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "c15662
"stream", "stream",
], package = "zed-reqwest", version = "0.12.15-zed" } ], package = "zed-reqwest", version = "0.12.15-zed" }
rsa = "0.9.6" rsa = "0.9.6"
runtimelib = { version = "0.30.0", default-features = false, features = [ runtimelib = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734", default-features = false, features = [
"async-dispatcher-runtime", "aws-lc-rs" "async-dispatcher-runtime",
] } ] }
rust-embed = { version = "8.4", features = ["include-exclude"] } rust-embed = { version = "8.4", features = ["include-exclude"] }
rustc-hash = "2.1.0" rustc-hash = "2.1.0"
@@ -632,7 +628,6 @@ scap = { git = "https://github.com/zed-industries/scap", rev = "4afea48c3b002197
schemars = { version = "1.0", features = ["indexmap2"] } schemars = { version = "1.0", features = ["indexmap2"] }
semver = "1.0" semver = "1.0"
serde = { version = "1.0.221", features = ["derive", "rc"] } serde = { version = "1.0.221", features = ["derive", "rc"] }
serde_derive = "1.0.221"
serde_json = { version = "1.0.144", features = ["preserve_order", "raw_value"] } serde_json = { version = "1.0.144", features = ["preserve_order", "raw_value"] }
serde_json_lenient = { version = "0.2", features = [ serde_json_lenient = { version = "0.2", features = [
"preserve_order", "preserve_order",
@@ -668,7 +663,6 @@ time = { version = "0.3", features = [
"serde", "serde",
"serde-well-known", "serde-well-known",
"formatting", "formatting",
"local-offset",
] } ] }
tiny_http = "0.8" tiny_http = "0.8"
tokio = { version = "1" } tokio = { version = "1" }
@@ -726,7 +720,6 @@ yawc = "0.2.5"
zeroize = "1.8" zeroize = "1.8"
zstd = "0.11" zstd = "0.11"
[workspace.dependencies.windows] [workspace.dependencies.windows]
version = "0.61" version = "0.61"
features = [ features = [
@@ -779,10 +772,9 @@ features = [
] ]
[patch.crates-io] [patch.crates-io]
notify = { git = "https://github.com/zed-industries/notify.git", rev = "b4588b2e5aee68f4c0e100f140e808cbce7b1419" } notify = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" }
notify-types = { git = "https://github.com/zed-industries/notify.git", rev = "b4588b2e5aee68f4c0e100f140e808cbce7b1419" } notify-types = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" }
windows-capture = { git = "https://github.com/zed-industries/windows-capture.git", rev = "f0d6c1b6691db75461b732f6d5ff56eed002eeb9" } windows-capture = { git = "https://github.com/zed-industries/windows-capture.git", rev = "f0d6c1b6691db75461b732f6d5ff56eed002eeb9" }
calloop = { git = "https://github.com/zed-industries/calloop" }
[profile.dev] [profile.dev]
split-debuginfo = "unpacked" split-debuginfo = "unpacked"
@@ -796,19 +788,6 @@ codegen-units = 16
codegen-units = 16 codegen-units = 16
[profile.dev.package] [profile.dev.package]
# proc-macros start
gpui_macros = { opt-level = 3 }
derive_refineable = { opt-level = 3 }
settings_macros = { opt-level = 3 }
sqlez_macros = { opt-level = 3, codegen-units = 1 }
ui_macros = { opt-level = 3 }
util_macros = { opt-level = 3 }
serde_derive = { opt-level = 3 }
quote = { opt-level = 3 }
syn = { opt-level = 3 }
proc-macro2 = { opt-level = 3 }
# proc-macros end
taffy = { opt-level = 3 } taffy = { opt-level = 3 }
cranelift-codegen = { opt-level = 3 } cranelift-codegen = { opt-level = 3 }
cranelift-codegen-meta = { opt-level = 3 } cranelift-codegen-meta = { opt-level = 3 }
@@ -850,6 +829,7 @@ semantic_version = { codegen-units = 1 }
session = { codegen-units = 1 } session = { codegen-units = 1 }
snippet = { codegen-units = 1 } snippet = { codegen-units = 1 }
snippets_ui = { codegen-units = 1 } snippets_ui = { codegen-units = 1 }
sqlez_macros = { codegen-units = 1 }
story = { codegen-units = 1 } story = { codegen-units = 1 }
supermaven_api = { codegen-units = 1 } supermaven_api = { codegen-units = 1 }
telemetry_events = { codegen-units = 1 } telemetry_events = { codegen-units = 1 }

View File

@@ -1,6 +1,6 @@
# syntax = docker/dockerfile:1.2 # syntax = docker/dockerfile:1.2
FROM rust:1.91.1-bookworm as builder FROM rust:1.90-bookworm as builder
WORKDIR app WORKDIR app
COPY . . COPY . .

View File

@@ -1,7 +1,7 @@
# Zed # Zed
[![Zed](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/zed-industries/zed/main/assets/badge/v0.json)](https://zed.dev) [![Zed](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/zed-industries/zed/main/assets/badge/v0.json)](https://zed.dev)
[![CI](https://github.com/zed-industries/zed/actions/workflows/run_tests.yml/badge.svg)](https://github.com/zed-industries/zed/actions/workflows/run_tests.yml) [![CI](https://github.com/zed-industries/zed/actions/workflows/ci.yml/badge.svg)](https://github.com/zed-industries/zed/actions/workflows/ci.yml)
Welcome to Zed, a high-performance, multiplayer code editor from the creators of [Atom](https://github.com/atom/atom) and [Tree-sitter](https://github.com/tree-sitter/tree-sitter). Welcome to Zed, a high-performance, multiplayer code editor from the creators of [Atom](https://github.com/atom/atom) and [Tree-sitter](https://github.com/tree-sitter/tree-sitter).

View File

@@ -44,7 +44,6 @@ design
docs docs
= @probably-neb = @probably-neb
= @miguelraz
extension extension
= @kubkon = @kubkon
@@ -99,9 +98,6 @@ settings_ui
= @danilo-leal = @danilo-leal
= @probably-neb = @probably-neb
support
= @miguelraz
tasks tasks
= @SomeoneToIgnore = @SomeoneToIgnore
= @Veykril = @Veykril

View File

@@ -1,4 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.00156 10.3996C9.32705 10.3996 10.4016 9.32509 10.4016 7.99961C10.4016 6.67413 9.32705 5.59961 8.00156 5.59961C6.67608 5.59961 5.60156 6.67413 5.60156 7.99961C5.60156 9.32509 6.67608 10.3996 8.00156 10.3996Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.4 5.6V8.6C10.4 9.07739 10.5896 9.53523 10.9272 9.8728C11.2648 10.2104 11.7226 10.4 12.2 10.4C12.6774 10.4 13.1352 10.2104 13.4728 9.8728C13.8104 9.53523 14 9.07739 14 8.6V8C14 6.64839 13.5436 5.33636 12.7048 4.27651C11.8661 3.21665 10.694 2.47105 9.37852 2.16051C8.06306 1.84997 6.68129 1.99269 5.45707 2.56554C4.23285 3.13838 3.23791 4.1078 2.63344 5.31672C2.02898 6.52565 1.85041 7.90325 2.12667 9.22633C2.40292 10.5494 3.11782 11.7405 4.15552 12.6065C5.19323 13.4726 6.49295 13.9629 7.84411 13.998C9.19527 14.0331 10.5187 13.611 11.6 12.8" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1,32 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_3348_16)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.97419 6.27207C8.44653 6.29114 8.86622 6.27046 9.23628 6.22425C9.08884 7.48378 8.7346 8.72903 8.16697 9.90688C8.04459 9.83861 7.92582 9.76008 7.81193 9.67108C7.64539 9.54099 7.49799 9.39549 7.37015 9.23818C7.5282 9.54496 7.64901 9.86752 7.73175 10.1986C7.35693 10.6656 6.90663 11.0373 6.412 11.3101C5.01165 10.8075 4.03638 9.63089 4.03638 7.93001C4.03638 6.96185 4.35234 6.07053 4.88281 5.36157C5.34001 5.69449 6.30374 6.20455 7.97419 6.27207ZM8.27511 11.5815C10.3762 11.5349 11.8115 10.7826 12.8347 7.93001C11.6992 7.93001 11.4246 7.10731 11.1188 6.19149C11.0669 6.03596 11.0141 5.87771 10.956 5.72037C10.6733 5.86733 10.2753 6.02782 9.74834 6.13895C9.59658 7.49345 9.20592 8.83238 8.56821 10.0897C8.89933 10.2093 9.24674 10.262 9.5908 10.2502C9.08928 10.4803 8.62468 10.8066 8.22655 11.2255C8.2457 11.3438 8.26186 11.4625 8.27511 11.5815ZM6.62702 7.75422C6.62702 7.50604 6.82821 7.30485 7.07639 7.30485C7.32457 7.30485 7.52576 7.50604 7.52576 7.75422V8.23616C7.52576 8.48435 7.32457 8.68554 7.07639 8.68554C6.82821 8.68554 6.62702 8.48435 6.62702 8.23616V7.75422ZM5.27746 7.30485C5.05086 7.30485 4.86716 7.48854 4.86716 7.71513V8.27525C4.86716 8.50185 5.05086 8.68554 5.27746 8.68554C5.50406 8.68554 5.68776 8.50185 5.68776 8.27525V7.71513C5.68776 7.48854 5.50406 7.30485 5.27746 7.30485Z" fill="white"/>
<mask id="mask0_3348_16" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="4" y="5" width="9" height="7">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.97419 6.27207C8.44653 6.29114 8.86622 6.27046 9.23628 6.22425C9.08884 7.48378 8.7346 8.72903 8.16697 9.90688C8.04459 9.83861 7.92582 9.76008 7.81193 9.67108C7.64539 9.54099 7.49799 9.39549 7.37015 9.23818C7.5282 9.54496 7.64901 9.86752 7.73175 10.1986C7.35693 10.6656 6.90663 11.0373 6.412 11.3101C5.01165 10.8075 4.03638 9.63089 4.03638 7.93001C4.03638 6.96185 4.35234 6.07053 4.88281 5.36157C5.34001 5.69449 6.30374 6.20455 7.97419 6.27207ZM8.27511 11.5815C10.3762 11.5349 11.8115 10.7826 12.8347 7.93001C11.6992 7.93001 11.4246 7.10731 11.1188 6.19149C11.0669 6.03596 11.0141 5.87771 10.956 5.72037C10.6733 5.86733 10.2753 6.02782 9.74834 6.13895C9.59658 7.49345 9.20592 8.83238 8.56821 10.0897C8.89933 10.2093 9.24674 10.262 9.5908 10.2502C9.08928 10.4803 8.62468 10.8066 8.22655 11.2255C8.2457 11.3438 8.26186 11.4625 8.27511 11.5815ZM6.62702 7.75422C6.62702 7.50604 6.82821 7.30485 7.07639 7.30485C7.32457 7.30485 7.52576 7.50604 7.52576 7.75422V8.23616C7.52576 8.48435 7.32457 8.68554 7.07639 8.68554C6.82821 8.68554 6.62702 8.48435 6.62702 8.23616V7.75422ZM5.27746 7.30485C5.05086 7.30485 4.86716 7.48854 4.86716 7.71513V8.27525C4.86716 8.50185 5.05086 8.68554 5.27746 8.68554C5.50406 8.68554 5.68776 8.50185 5.68776 8.27525V7.71513C5.68776 7.48854 5.50406 7.30485 5.27746 7.30485Z" fill="white"/>
</mask>
<g mask="url(#mask0_3348_16)">
<path d="M9.23617 6.22425L9.39588 6.24293L9.41971 6.0393L9.21624 6.06471L9.23617 6.22425ZM8.16687 9.90688L8.08857 10.0473L8.23765 10.1305L8.31174 9.97669L8.16687 9.90688ZM7.37005 9.23819L7.49487 9.13676L7.22714 9.3118L7.37005 9.23819ZM7.73165 10.1986L7.85702 10.2993L7.90696 10.2371L7.88761 10.1597L7.73165 10.1986ZM6.41189 11.3101L6.35758 11.4615L6.42594 11.486L6.48954 11.4509L6.41189 11.3101ZM4.88271 5.36157L4.97736 5.23159L4.84905 5.13817L4.75397 5.26525L4.88271 5.36157ZM8.27501 11.5815L8.11523 11.5993L8.13151 11.7456L8.27859 11.7423L8.27501 11.5815ZM12.8346 7.93001L12.986 7.98428L13.0631 7.76921H12.8346V7.93001ZM10.9559 5.72037L11.1067 5.66469L11.0436 5.49354L10.8817 5.5777L10.9559 5.72037ZM9.74824 6.13896L9.71508 5.98161L9.60139 6.0056L9.58846 6.12102L9.74824 6.13896ZM8.56811 10.0897L8.42469 10.017L8.34242 10.1792L8.51348 10.241L8.56811 10.0897ZM9.5907 10.2502L9.65775 10.3964L9.58519 10.0896L9.5907 10.2502ZM8.22644 11.2255L8.10992 11.1147L8.05502 11.1725L8.06773 11.2512L8.22644 11.2255ZM9.21624 6.06471C8.85519 6.10978 8.44439 6.13015 7.98058 6.11139L7.96756 6.43272C8.44852 6.45215 8.87701 6.43111 9.25607 6.3838L9.21624 6.06471ZM8.31174 9.97669C8.88724 8.78244 9.2464 7.51988 9.39588 6.24293L9.07647 6.20557C8.93108 7.44772 8.58175 8.67563 8.02203 9.83708L8.31174 9.97669ZM8.2452 9.76645C8.12998 9.70219 8.01817 9.62826 7.91082 9.54438L7.71285 9.79779C7.8333 9.8919 7.95895 9.97503 8.08857 10.0473L8.2452 9.76645ZM7.91082 9.54438C7.75387 9.4218 7.61512 9.28479 7.49487 9.13676L7.24526 9.33957C7.38066 9.50619 7.53671 9.66023 7.71285 9.79779L7.91082 9.54438ZM7.22714 9.3118C7.37944 9.60746 7.49589 9.91837 7.57564 10.2376L7.88761 10.1597C7.80196 9.81663 7.67679 9.48248 7.513 9.16453L7.22714 9.3118ZM7.60624 10.098C7.24483 10.5482 6.81083 10.9065 6.33425 11.1693L6.48954 11.4509C7.00223 11.1682 7.46887 10.7829 7.85702 10.2993L7.60624 10.098ZM3.87549 7.93001C3.87548 9.7042 4.89861 10.9378 6.35758 11.4615L6.46622 11.1588C5.12449 10.6772 4.19707 9.55763 4.19707 7.93001H3.87549ZM4.75397 5.26525C4.20309 6.00147 3.87549 6.92646 3.87549 7.93001H4.19707C4.19707 6.99724 4.50139 6.13959 5.01145 5.45791L4.75397 5.26525ZM7.98058 6.11139C6.34236 6.04516 5.40922 5.54604 4.97736 5.23159L4.78806 5.49157C5.27058 5.84291 6.26491 6.3639 7.96756 6.43272L7.98058 6.11139ZM8.27859 11.7423C9.34696 11.7185 10.2682 11.515 11.0542 10.9376C11.8388 10.3612 12.4683 9.4273 12.986 7.98428L12.6833 7.8757C12.1776 9.28534 11.5779 10.1539 10.8638 10.6784C10.1511 11.202 9.30417 11.3978 8.27143 11.4208L8.27859 11.7423ZM12.8346 7.76921C12.3148 7.76921 12.0098 7.58516 11.7925 7.30552C11.5639 7.0114 11.4266 6.60587 11.2712 6.14061L10.9662 6.24242C11.1166 6.69294 11.2695 7.15667 11.5385 7.50285C11.8188 7.86347 12.2189 8.09078 12.8346 8.09078V7.76921ZM11.2712 6.14061C11.2195 5.98543 11.1658 5.82478 11.1067 5.66469L10.805 5.77606C10.8621 5.93065 10.9142 6.0865 10.9662 6.24242L11.2712 6.14061ZM10.8817 5.5777C10.6115 5.71821 10.2273 5.87362 9.71508 5.98161L9.78143 6.29626C10.3232 6.18206 10.735 6.0165 11.0301 5.86301L10.8817 5.5777ZM9.58846 6.12102C9.43882 7.45684 9.05355 8.77717 8.42469 10.017L8.71149 10.1625C9.35809 8.88764 9.75417 7.53011 9.90806 6.15685L9.58846 6.12102ZM9.58519 10.0896C9.26119 10.1006 8.93423 10.051 8.62269 9.93854L8.51348 10.241C8.86427 10.3677 9.23205 10.4234 9.5962 10.4109L9.58519 10.0896ZM8.34301 11.3363C8.72675 10.9325 9.17443 10.6181 9.65775 10.3964L9.52365 10.1041C9.00392 10.3425 8.52241 10.6807 8.10992 11.1147L8.34301 11.3363ZM8.43483 11.5638C8.4213 11.4421 8.40475 11.3207 8.3852 11.1998L8.06773 11.2512C8.08644 11.3668 8.10225 11.4829 8.11523 11.5993L8.43483 11.5638ZM7.07629 7.14405C6.73931 7.14405 6.46613 7.41724 6.46613 7.75423H6.7877C6.7877 7.59484 6.91691 7.46561 7.07629 7.46561V7.14405ZM7.68646 7.75423C7.68646 7.41724 7.41326 7.14405 7.07629 7.14405V7.46561C7.23567 7.46561 7.36489 7.59484 7.36489 7.75423H7.68646ZM7.68646 8.23616V7.75423H7.36489V8.23616H7.68646ZM7.07629 8.84634C7.41326 8.84634 7.68646 8.57315 7.68646 8.23616H7.36489C7.36489 8.39555 7.23567 8.52474 7.07629 8.52474V8.84634ZM6.46613 8.23616C6.46613 8.57315 6.73931 8.84634 7.07629 8.84634V8.52474C6.91691 8.52474 6.7877 8.39555 6.7877 8.23616H6.46613ZM6.46613 7.75423V8.23616H6.7877V7.75423H6.46613ZM5.02785 7.71514C5.02785 7.57734 5.13956 7.46561 5.27736 7.46561V7.14405C4.96196 7.14405 4.70627 7.39974 4.70627 7.71514H5.02785ZM5.02785 8.27525V7.71514H4.70627V8.27525H5.02785ZM5.27736 8.52474C5.13956 8.52474 5.02785 8.41305 5.02785 8.27525H4.70627C4.70627 8.59065 4.96196 8.84634 5.27736 8.84634V8.52474ZM5.52687 8.27525C5.52687 8.41305 5.41516 8.52474 5.27736 8.52474V8.84634C5.59277 8.84634 5.84845 8.59065 5.84845 8.27525H5.52687ZM5.52687 7.71514V8.27525H5.84845V7.71514H5.52687ZM5.27736 7.46561C5.41516 7.46561 5.52687 7.57734 5.52687 7.71514H5.84845C5.84845 7.39974 5.59277 7.14405 5.27736 7.14405V7.46561Z" fill="white"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.12635 14.5901C7.22369 14.3749 7.3069 14.1501 7.37454 13.9167C7.54132 13.3412 7.5998 12.7599 7.56197 12.1948C7.53665 12.5349 7.47589 12.8775 7.37718 13.2181C7.23926 13.694 7.03667 14.1336 6.78174 14.5301C6.89605 14.5547 7.01101 14.5747 7.12635 14.5901Z" fill="white"/>
<path d="M9.71984 7.74796C9.50296 7.74796 9.29496 7.83412 9.14159 7.98745C8.98822 8.14082 8.9021 8.34882 8.9021 8.5657C8.9021 8.78258 8.98822 8.99057 9.14159 9.14394C9.29496 9.29728 9.50296 9.38344 9.71984 9.38344V8.5657V7.74796Z" fill="white"/>
<mask id="mask1_3348_16" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="5" y="2" width="8" height="9">
<path d="M12.3783 2.9985H5.36792V10.3954H12.3783V2.9985Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.75733 3.61999C9.98577 5.80374 9.60089 8.05373 8.56819 10.0898C8.43122 10.0403 8.29704 9.9794 8.16699 9.90688C9.15325 7.86033 9.49538 5.61026 9.22757 3.43526C9.39923 3.51584 9.57682 3.57729 9.75733 3.61999Z" fill="black"/>
</mask>
<g mask="url(#mask1_3348_16)">
<path d="M8.56815 10.0898L8.67689 10.1449L8.62812 10.241L8.52678 10.2044L8.56815 10.0898ZM9.75728 3.61998L9.78536 3.50136L9.86952 3.52127L9.87853 3.6073L9.75728 3.61998ZM8.16695 9.90687L8.1076 10.0133L8.00732 9.9574L8.05715 9.85398L8.16695 9.90687ZM9.22753 3.43524L9.10656 3.45014L9.07958 3.23116L9.27932 3.32491L9.22753 3.43524ZM8.45945 10.0346C9.48122 8.02009 9.86217 5.79374 9.63608 3.63266L9.87853 3.6073C10.1093 5.81372 9.72048 8.0873 8.67689 10.1449L8.45945 10.0346ZM8.22633 9.80041C8.35056 9.86971 8.47876 9.92791 8.60956 9.97514L8.52678 10.2044C8.38363 10.1527 8.24344 10.0891 8.1076 10.0133L8.22633 9.80041ZM9.34849 3.42035C9.61905 5.61792 9.27346 7.89158 8.27675 9.9598L8.05715 9.85398C9.03298 7.82905 9.37158 5.60258 9.10656 3.45014L9.34849 3.42035ZM9.72925 3.7386C9.54064 3.69399 9.3551 3.62977 9.17573 3.54558L9.27932 3.32491C9.44327 3.40188 9.61288 3.46058 9.78536 3.50136L9.72925 3.7386Z" fill="white"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.4118 3.46925L11.2416 3.39926L11.1904 3.57611L11.349 3.62202C11.1904 3.57611 11.1904 3.57615 11.1904 3.5762L11.1903 3.57631L11.1902 3.57658L11.19 3.57741L11.1893 3.58009C11.1886 3.58233 11.1878 3.58548 11.1867 3.58949C11.1845 3.5975 11.1814 3.60897 11.1777 3.62359C11.1703 3.6528 11.1603 3.69464 11.1493 3.74656C11.1275 3.85017 11.102 3.99505 11.0869 4.16045C11.0573 4.4847 11.0653 4.91594 11.2489 5.26595C11.2613 5.28944 11.2643 5.31174 11.2625 5.32629C11.261 5.33849 11.2572 5.34226 11.2536 5.3449C11.0412 5.50026 10.5639 5.78997 9.76653 5.96607C9.76095 6.02373 9.75493 6.08134 9.74848 6.13895C10.601 5.95915 11.1161 5.65017 11.3511 5.4782C11.4413 5.41219 11.4471 5.28823 11.3952 5.18922C11.1546 4.73063 11.2477 4.08248 11.3103 3.78401C11.3314 3.68298 11.349 3.62202 11.349 3.62202C11.3745 3.6325 11.4002 3.63983 11.4259 3.64425C11.9083 3.72709 12.4185 2.78249 12.6294 2.33939C12.6852 2.22212 12.6234 2.08843 12.497 2.05837C11.2595 1.76399 5.46936 0.631807 4.57214 4.96989C4.55907 5.03307 4.57607 5.10106 4.62251 5.14584C4.87914 5.39322 5.86138 6.18665 7.9743 6.27207C8.44664 6.29114 8.86633 6.27046 9.23638 6.22425C9.24295 6.16797 9.24912 6.1117 9.25491 6.05534C8.88438 6.10391 8.46092 6.12641 7.98094 6.10702C5.91152 6.02337 4.96693 5.24843 4.73714 5.02692C4.73701 5.02679 4.73545 5.02525 4.73422 5.0208C4.73292 5.01611 4.73254 5.00987 4.73388 5.00334C4.94996 3.95861 5.4573 3.25195 6.11188 2.77714C6.77039 2.29947 7.58745 2.04983 8.42824 1.94075C10.1122 1.72228 11.8454 2.07312 12.4588 2.21906C12.4722 2.22225 12.4787 2.22927 12.4819 2.2362C12.4853 2.24342 12.4869 2.25443 12.4803 2.2684C12.3706 2.49879 12.183 2.85746 11.9656 3.13057C11.8564 3.26783 11.7479 3.37295 11.6469 3.43216C11.5491 3.48956 11.4752 3.49529 11.4118 3.46925Z" fill="white"/>
<mask id="mask2_3348_16" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="3" y="9" width="7" height="6">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.22654 11.2255C8.62463 10.8066 9.08923 10.4803 9.59075 10.2502C8.97039 10.2715 8.33933 10.0831 7.81189 9.67109C7.64534 9.541 7.49795 9.39549 7.37014 9.23819C7.52815 9.54497 7.64896 9.86752 7.7317 10.1986C6.70151 11.4821 5.1007 12.0466 3.57739 11.8125C3.85909 12.527 4.32941 13.178 4.97849 13.6851C5.8625 14.3756 6.92544 14.6799 7.96392 14.6227C8.32513 13.5174 8.4085 12.351 8.22654 11.2255Z" fill="white"/>
</mask>
<g mask="url(#mask2_3348_16)">
<path d="M9.59085 10.2502L9.58389 10.0472L9.67556 10.4349L9.59085 10.2502ZM8.22663 11.2255L8.02607 11.258L8.00999 11.1585L8.07936 11.0856L8.22663 11.2255ZM7.37024 9.23819L7.18961 9.33119L7.52789 9.11006L7.37024 9.23819ZM7.7318 10.1986L7.92886 10.1494L7.95328 10.2472L7.8902 10.3258L7.7318 10.1986ZM3.57749 11.8125L3.3885 11.887L3.25879 11.5579L3.60835 11.6117L3.57749 11.8125ZM7.96402 14.6227L8.15711 14.6858L8.11397 14.8179L7.97519 14.8255L7.96402 14.6227ZM9.67556 10.4349C9.19708 10.6544 8.7538 10.9657 8.37387 11.3655L8.07936 11.0856C8.49566 10.6475 8.98161 10.3062 9.50614 10.0656L9.67556 10.4349ZM7.93704 9.51099C8.42551 9.89261 9.00942 10.0669 9.58389 10.0472L9.59781 10.4533C8.93151 10.4761 8.25334 10.2737 7.68693 9.83118L7.93704 9.51099ZM7.52789 9.11006C7.64615 9.25565 7.78261 9.39038 7.93704 9.51099L7.68693 9.83118C7.50827 9.69161 7.34994 9.53537 7.21254 9.36627L7.52789 9.11006ZM7.5347 10.2479C7.45573 9.93178 7.34043 9.62393 7.18961 9.33119L7.55082 9.14514C7.71611 9.466 7.84242 9.80326 7.92886 10.1494L7.5347 10.2479ZM3.60835 11.6117C5.06278 11.8352 6.59038 11.2962 7.57335 10.0715L7.8902 10.3258C6.81284 11.6681 5.1388 12.258 3.54663 12.0133L3.60835 11.6117ZM4.85352 13.8452C4.17512 13.3152 3.68312 12.6343 3.3885 11.887L3.76648 11.738C4.03524 12.4197 4.4839 13.0409 5.10364 13.525L4.85352 13.8452ZM7.97519 14.8255C6.8895 14.8853 5.77774 14.5672 4.85352 13.8452L5.10364 13.525C5.94745 14.1842 6.96157 14.4744 7.95285 14.4198L7.97519 14.8255ZM8.42716 11.1931C8.61419 12.3499 8.52858 13.5491 8.15711 14.6858L7.77093 14.5596C8.12191 13.4857 8.20296 12.352 8.02607 11.258L8.42716 11.1931Z" fill="white"/>
</g>
</g>
<defs>
<clipPath id="clip0_3348_16">
<rect width="9.63483" height="14" fill="white" transform="translate(3.19995 1.5)"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -43,7 +43,8 @@
"f11": "zed::ToggleFullScreen", "f11": "zed::ToggleFullScreen",
"ctrl-alt-z": "edit_prediction::RateCompletions", "ctrl-alt-z": "edit_prediction::RateCompletions",
"ctrl-alt-shift-i": "edit_prediction::ToggleMenu", "ctrl-alt-shift-i": "edit_prediction::ToggleMenu",
"ctrl-alt-l": "lsp_tool::ToggleMenu" "ctrl-alt-l": "lsp_tool::ToggleMenu",
"ctrl-alt-.": "project_panel::ToggleHideHidden"
} }
}, },
{ {
@@ -735,17 +736,11 @@
} }
}, },
{ {
"context": "Editor && in_snippet && has_next_tabstop && !showing_completions", "context": "Editor && in_snippet",
"use_key_equivalents": true, "use_key_equivalents": true,
"bindings": { "bindings": {
"tab": "editor::NextSnippetTabstop" "alt-right": "editor::NextSnippetTabstop",
} "alt-left": "editor::PreviousSnippetTabstop"
},
{
"context": "Editor && in_snippet && has_previous_tabstop && !showing_completions",
"use_key_equivalents": true,
"bindings": {
"shift-tab": "editor::PreviousSnippetTabstop"
} }
}, },
// Bindings for accepting edit predictions // Bindings for accepting edit predictions
@@ -865,7 +860,6 @@
"context": "ProjectPanel", "context": "ProjectPanel",
"bindings": { "bindings": {
"left": "project_panel::CollapseSelectedEntry", "left": "project_panel::CollapseSelectedEntry",
"ctrl-left": "project_panel::CollapseAllEntries",
"right": "project_panel::ExpandSelectedEntry", "right": "project_panel::ExpandSelectedEntry",
"new": "project_panel::NewFile", "new": "project_panel::NewFile",
"ctrl-n": "project_panel::NewFile", "ctrl-n": "project_panel::NewFile",

View File

@@ -49,7 +49,8 @@
"ctrl-cmd-f": "zed::ToggleFullScreen", "ctrl-cmd-f": "zed::ToggleFullScreen",
"ctrl-cmd-z": "edit_prediction::RateCompletions", "ctrl-cmd-z": "edit_prediction::RateCompletions",
"ctrl-cmd-i": "edit_prediction::ToggleMenu", "ctrl-cmd-i": "edit_prediction::ToggleMenu",
"ctrl-cmd-l": "lsp_tool::ToggleMenu" "ctrl-cmd-l": "lsp_tool::ToggleMenu",
"cmd-alt-.": "project_panel::ToggleHideHidden"
} }
}, },
{ {
@@ -312,7 +313,7 @@
"use_key_equivalents": true, "use_key_equivalents": true,
"bindings": { "bindings": {
"cmd-n": "agent::NewTextThread", "cmd-n": "agent::NewTextThread",
"cmd-alt-n": "agent::NewExternalAgentThread" "cmd-alt-t": "agent::NewThread"
} }
}, },
{ {
@@ -805,17 +806,11 @@
} }
}, },
{ {
"context": "Editor && in_snippet && has_next_tabstop && !showing_completions", "context": "Editor && in_snippet",
"use_key_equivalents": true, "use_key_equivalents": true,
"bindings": { "bindings": {
"tab": "editor::NextSnippetTabstop" "alt-right": "editor::NextSnippetTabstop",
} "alt-left": "editor::PreviousSnippetTabstop"
},
{
"context": "Editor && in_snippet && has_previous_tabstop && !showing_completions",
"use_key_equivalents": true,
"bindings": {
"shift-tab": "editor::PreviousSnippetTabstop"
} }
}, },
{ {
@@ -935,7 +930,6 @@
"use_key_equivalents": true, "use_key_equivalents": true,
"bindings": { "bindings": {
"left": "project_panel::CollapseSelectedEntry", "left": "project_panel::CollapseSelectedEntry",
"cmd-left": "project_panel::CollapseAllEntries",
"right": "project_panel::ExpandSelectedEntry", "right": "project_panel::ExpandSelectedEntry",
"cmd-n": "project_panel::NewFile", "cmd-n": "project_panel::NewFile",
"cmd-d": "project_panel::Duplicate", "cmd-d": "project_panel::Duplicate",

View File

@@ -41,7 +41,8 @@
"shift-f11": "debugger::StepOut", "shift-f11": "debugger::StepOut",
"f11": "zed::ToggleFullScreen", "f11": "zed::ToggleFullScreen",
"ctrl-shift-i": "edit_prediction::ToggleMenu", "ctrl-shift-i": "edit_prediction::ToggleMenu",
"shift-alt-l": "lsp_tool::ToggleMenu" "shift-alt-l": "lsp_tool::ToggleMenu",
"ctrl-alt-.": "project_panel::ToggleHideHidden"
} }
}, },
{ {
@@ -739,17 +740,11 @@
} }
}, },
{ {
"context": "Editor && in_snippet && has_next_tabstop && !showing_completions", "context": "Editor && in_snippet",
"use_key_equivalents": true, "use_key_equivalents": true,
"bindings": { "bindings": {
"tab": "editor::NextSnippetTabstop" "alt-right": "editor::NextSnippetTabstop",
} "alt-left": "editor::PreviousSnippetTabstop"
},
{
"context": "Editor && in_snippet && has_previous_tabstop && !showing_completions",
"use_key_equivalents": true,
"bindings": {
"shift-tab": "editor::PreviousSnippetTabstop"
} }
}, },
// Bindings for accepting edit predictions // Bindings for accepting edit predictions
@@ -879,7 +874,6 @@
"use_key_equivalents": true, "use_key_equivalents": true,
"bindings": { "bindings": {
"left": "project_panel::CollapseSelectedEntry", "left": "project_panel::CollapseSelectedEntry",
"ctrl-left": "project_panel::CollapseAllEntries",
"right": "project_panel::ExpandSelectedEntry", "right": "project_panel::ExpandSelectedEntry",
"ctrl-n": "project_panel::NewFile", "ctrl-n": "project_panel::NewFile",
"alt-n": "project_panel::NewDirectory", "alt-n": "project_panel::NewDirectory",

View File

@@ -455,7 +455,6 @@
"<": "vim::Outdent", "<": "vim::Outdent",
"=": "vim::AutoIndent", "=": "vim::AutoIndent",
"d": "vim::HelixDelete", "d": "vim::HelixDelete",
"alt-d": "editor::Delete", // Delete selection, without yanking
"c": "vim::HelixSubstitute", "c": "vim::HelixSubstitute",
"alt-c": "vim::HelixSubstituteNoYank", "alt-c": "vim::HelixSubstituteNoYank",
@@ -476,9 +475,6 @@
"alt-p": "editor::SelectPreviousSyntaxNode", "alt-p": "editor::SelectPreviousSyntaxNode",
"alt-n": "editor::SelectNextSyntaxNode", "alt-n": "editor::SelectNextSyntaxNode",
"n": "vim::HelixSelectNext",
"shift-n": "vim::HelixSelectPrevious",
// Goto mode // Goto mode
"g e": "vim::EndOfDocument", "g e": "vim::EndOfDocument",
"g h": "vim::StartOfLine", "g h": "vim::StartOfLine",

View File

@@ -605,10 +605,6 @@
// to both the horizontal and vertical delta values while scrolling. Fast scrolling // to both the horizontal and vertical delta values while scrolling. Fast scrolling
// happens when a user holds the alt or option key while scrolling. // happens when a user holds the alt or option key while scrolling.
"fast_scroll_sensitivity": 4.0, "fast_scroll_sensitivity": 4.0,
"sticky_scroll": {
// Whether to stick scopes to the top of the editor.
"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.
"search_wrap": true, "search_wrap": true,
@@ -616,13 +612,9 @@
"search": { "search": {
// Whether to show the project search button in the status bar. // Whether to show the project search button in the status bar.
"button": true, "button": true,
// Whether to only match on whole words.
"whole_word": false, "whole_word": false,
// Whether to match case sensitively.
"case_sensitive": false, "case_sensitive": false,
// Whether to include gitignored files in search results.
"include_ignored": false, "include_ignored": false,
// 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
@@ -742,31 +734,14 @@
// "never" // "never"
"show": "always" "show": "always"
}, },
// Sort order for entries in the project panel.
// This setting can take three values:
//
// 1. Show directories first, then files:
// "directories_first"
// 2. Mix directories and files together:
// "mixed"
// 3. Show files first, then directories:
// "files_first"
"sort_mode": "directories_first",
// Whether to enable drag-and-drop operations in the project panel. // Whether to enable drag-and-drop operations in the project panel.
"drag_and_drop": true, "drag_and_drop": true,
// Whether to hide the root entry when only one folder is open in the window. // Whether to hide the root entry when only one folder is open in the window.
"hide_root": false, "hide_root": false,
// Whether to hide the hidden entries in the project panel. // Whether to hide the hidden entries in the project panel.
"hide_hidden": false, "hide_hidden": false,
// Settings for automatically opening files. // Whether to automatically open files when pasting them in the project panel.
"auto_open": { "open_file_on_paste": true
// Whether to automatically open newly created files in the editor.
"on_create": true,
// Whether to automatically open files after pasting or duplicating them.
"on_paste": true,
// Whether to automatically open files dropped from external sources.
"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
@@ -1316,10 +1291,7 @@
// "hunk_style": "staged_hollow" // "hunk_style": "staged_hollow"
// 2. Show unstaged hunks hollow and staged hunks filled: // 2. Show unstaged hunks hollow and staged hunks filled:
// "hunk_style": "unstaged_hollow" // "hunk_style": "unstaged_hollow"
"hunk_style": "staged_hollow", "hunk_style": "staged_hollow"
// 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"
}, },
// The list of custom Git hosting providers. // The list of custom Git hosting providers.
"git_hosting_providers": [ "git_hosting_providers": [
@@ -1515,11 +1487,7 @@
// in your project's settings, rather than globally. // in your project's settings, rather than globally.
"directories": [".env", "env", ".venv", "venv"], "directories": [".env", "env", ".venv", "venv"],
// Can also be `csh`, `fish`, `nushell` and `power_shell` // Can also be `csh`, `fish`, `nushell` and `power_shell`
"activate_script": "default", "activate_script": "default"
// Preferred Conda manager to use when activating Conda environments.
// Values: "auto", "conda", "mamba", "micromamba"
// Default: "auto"
"conda_manager": "auto"
} }
}, },
"toolbar": { "toolbar": {
@@ -1563,8 +1531,6 @@
// Default: 10_000, maximum: 100_000 (all bigger values set will be treated as 100_000), 0 disables the scrolling. // Default: 10_000, maximum: 100_000 (all bigger values set will be treated as 100_000), 0 disables the scrolling.
// Existing terminals will not pick up this change until they are recreated. // Existing terminals will not pick up this change until they are recreated.
"max_scroll_history_lines": 10000, "max_scroll_history_lines": 10000,
// The multiplier for scrolling speed in the terminal.
"scroll_multiplier": 1.0,
// The minimum APCA perceptual contrast between foreground and background colors. // The minimum APCA perceptual contrast between foreground and background colors.
// APCA (Accessible Perceptual Contrast Algorithm) is more accurate than WCAG 2.x, // APCA (Accessible Perceptual Contrast Algorithm) is more accurate than WCAG 2.x,
// especially for dark mode. Values range from 0 to 106. // especially for dark mode. Values range from 0 to 106.
@@ -2062,18 +2028,6 @@
"dev": { "dev": {
// "theme": "Andromeda" // "theme": "Andromeda"
}, },
// Settings overrides to use when using linux
"linux": {},
// Settings overrides to use when using macos
"macos": {},
// Settings overrides to use when using windows
"windows": {
"languages": {
"PHP": {
"language_servers": ["intelephense", "!phpactor", "..."]
}
}
},
// Whether to show full labels in line indicator or short ones // Whether to show full labels in line indicator or short ones
// //
// Values: // Values:

View File

@@ -39,7 +39,6 @@ serde_json.workspace = true
settings.workspace = true settings.workspace = true
smol.workspace = true smol.workspace = true
task.workspace = true task.workspace = true
telemetry.workspace = true
terminal.workspace = true terminal.workspace = true
ui.workspace = true ui.workspace = true
url.workspace = true url.workspace = true
@@ -57,4 +56,3 @@ rand.workspace = true
tempfile.workspace = true tempfile.workspace = true
util.workspace = true util.workspace = true
settings.workspace = true settings.workspace = true
zlog.workspace = true

View File

@@ -15,7 +15,7 @@ use settings::Settings as _;
use task::{Shell, ShellBuilder}; use task::{Shell, ShellBuilder};
pub use terminal::*; pub use terminal::*;
use action_log::{ActionLog, ActionLogTelemetry}; use action_log::ActionLog;
use agent_client_protocol::{self as acp}; use agent_client_protocol::{self as acp};
use anyhow::{Context as _, Result, anyhow}; use anyhow::{Context as _, Result, anyhow};
use editor::Bias; use editor::Bias;
@@ -820,15 +820,6 @@ pub struct AcpThread {
pending_terminal_exit: HashMap<acp::TerminalId, acp::TerminalExitStatus>, pending_terminal_exit: HashMap<acp::TerminalId, acp::TerminalExitStatus>,
} }
impl From<&AcpThread> for ActionLogTelemetry {
fn from(value: &AcpThread) -> Self {
Self {
agent_telemetry_id: value.connection().telemetry_id(),
session_id: value.session_id.0.clone(),
}
}
}
#[derive(Debug)] #[derive(Debug)]
pub enum AcpThreadEvent { pub enum AcpThreadEvent {
NewEntry, NewEntry,
@@ -1355,17 +1346,6 @@ 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.id.clone(); let id = update.id.clone();
let agent = self.connection().telemetry_id();
let session = self.session_id();
if let ToolCallStatus::Completed | ToolCallStatus::Failed = status {
let status = if matches!(status, ToolCallStatus::Completed) {
"completed"
} else {
"failed"
};
telemetry::event!("Agent Tool Call Completed", agent, session, status);
}
if let Some(ix) = self.index_for_tool_call(&id) { if let Some(ix) = self.index_for_tool_call(&id) {
let AgentThreadEntry::ToolCall(call) = &mut self.entries[ix] else { let AgentThreadEntry::ToolCall(call) = &mut self.entries[ix] else {
unreachable!() unreachable!()
@@ -1866,14 +1846,10 @@ impl AcpThread {
.checkpoint .checkpoint
.as_ref() .as_ref()
.map(|c| c.git_checkpoint.clone()); .map(|c| c.git_checkpoint.clone());
// Cancel any in-progress generation before restoring
let cancel_task = self.cancel(cx);
let rewind = self.rewind(id.clone(), cx); let rewind = self.rewind(id.clone(), cx);
let git_store = self.project.read(cx).git_store().clone(); let git_store = self.project.read(cx).git_store().clone();
cx.spawn(async move |_, cx| { cx.spawn(async move |_, cx| {
cancel_task.await;
rewind.await?; rewind.await?;
if let Some(checkpoint) = checkpoint { if let Some(checkpoint) = checkpoint {
git_store git_store
@@ -1893,34 +1869,16 @@ impl AcpThread {
return Task::ready(Err(anyhow!("not supported"))); return Task::ready(Err(anyhow!("not supported")));
}; };
let telemetry = ActionLogTelemetry::from(&*self);
cx.spawn(async move |this, cx| { cx.spawn(async move |this, cx| {
cx.update(|cx| truncate.run(id.clone(), cx))?.await?; cx.update(|cx| truncate.run(id.clone(), cx))?.await?;
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
if let Some((ix, _)) = this.user_message_mut(&id) { if let Some((ix, _)) = this.user_message_mut(&id) {
// Collect all terminals from entries that will be removed
let terminals_to_remove: Vec<acp::TerminalId> = this.entries[ix..]
.iter()
.flat_map(|entry| entry.terminals())
.filter_map(|terminal| terminal.read(cx).id().clone().into())
.collect();
let range = ix..this.entries.len(); let range = ix..this.entries.len();
this.entries.truncate(ix); this.entries.truncate(ix);
cx.emit(AcpThreadEvent::EntriesRemoved(range)); cx.emit(AcpThreadEvent::EntriesRemoved(range));
// Kill and remove the terminals
for terminal_id in terminals_to_remove {
if let Some(terminal) = this.terminals.remove(&terminal_id) {
terminal.update(cx, |terminal, cx| {
terminal.kill(cx);
});
}
}
} }
this.action_log().update(cx, |action_log, cx| { this.action_log()
action_log.reject_all_edits(Some(telemetry), cx) .update(cx, |action_log, cx| action_log.reject_all_edits(cx))
})
})? })?
.await; .await;
Ok(()) Ok(())
@@ -2397,6 +2355,8 @@ mod tests {
cx.update(|cx| { cx.update(|cx| {
let settings_store = SettingsStore::test(cx); let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store); cx.set_global(settings_store);
Project::init_settings(cx);
language::init(cx);
}); });
} }
@@ -3654,10 +3614,6 @@ mod tests {
} }
impl AgentConnection for FakeAgentConnection { impl AgentConnection for FakeAgentConnection {
fn telemetry_id(&self) -> &'static str {
"fake"
}
fn auth_methods(&self) -> &[acp::AuthMethod] { fn auth_methods(&self) -> &[acp::AuthMethod] {
&self.auth_methods &self.auth_methods
} }
@@ -3823,314 +3779,4 @@ mod tests {
} }
}); });
} }
/// Tests that restoring a checkpoint properly cleans up terminals that were
/// created after that checkpoint, and cancels any in-progress generation.
///
/// Reproduces issue #35142: When a checkpoint is restored, any terminal processes
/// that were started after that checkpoint should be terminated, and any in-progress
/// AI generation should be canceled.
#[gpui::test]
async fn test_restore_checkpoint_kills_terminal(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, [], cx).await;
let connection = Rc::new(FakeAgentConnection::new());
let thread = cx
.update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx))
.await
.unwrap();
// Send first user message to create a checkpoint
cx.update(|cx| {
thread.update(cx, |thread, cx| {
thread.send(vec!["first message".into()], cx)
})
})
.await
.unwrap();
// Send second message (creates another checkpoint) - we'll restore to this one
cx.update(|cx| {
thread.update(cx, |thread, cx| {
thread.send(vec!["second message".into()], cx)
})
})
.await
.unwrap();
// Create 2 terminals BEFORE the checkpoint that have completed running
let terminal_id_1 = acp::TerminalId(uuid::Uuid::new_v4().to_string().into());
let mock_terminal_1 = cx.new(|cx| {
let builder = ::terminal::TerminalBuilder::new_display_only(
::terminal::terminal_settings::CursorShape::default(),
::terminal::terminal_settings::AlternateScroll::On,
None,
0,
)
.unwrap();
builder.subscribe(cx)
});
thread.update(cx, |thread, cx| {
thread.on_terminal_provider_event(
TerminalProviderEvent::Created {
terminal_id: terminal_id_1.clone(),
label: "echo 'first'".to_string(),
cwd: Some(PathBuf::from("/test")),
output_byte_limit: None,
terminal: mock_terminal_1.clone(),
},
cx,
);
});
thread.update(cx, |thread, cx| {
thread.on_terminal_provider_event(
TerminalProviderEvent::Output {
terminal_id: terminal_id_1.clone(),
data: b"first\n".to_vec(),
},
cx,
);
});
thread.update(cx, |thread, cx| {
thread.on_terminal_provider_event(
TerminalProviderEvent::Exit {
terminal_id: terminal_id_1.clone(),
status: acp::TerminalExitStatus {
exit_code: Some(0),
signal: None,
meta: None,
},
},
cx,
);
});
let terminal_id_2 = acp::TerminalId(uuid::Uuid::new_v4().to_string().into());
let mock_terminal_2 = cx.new(|cx| {
let builder = ::terminal::TerminalBuilder::new_display_only(
::terminal::terminal_settings::CursorShape::default(),
::terminal::terminal_settings::AlternateScroll::On,
None,
0,
)
.unwrap();
builder.subscribe(cx)
});
thread.update(cx, |thread, cx| {
thread.on_terminal_provider_event(
TerminalProviderEvent::Created {
terminal_id: terminal_id_2.clone(),
label: "echo 'second'".to_string(),
cwd: Some(PathBuf::from("/test")),
output_byte_limit: None,
terminal: mock_terminal_2.clone(),
},
cx,
);
});
thread.update(cx, |thread, cx| {
thread.on_terminal_provider_event(
TerminalProviderEvent::Output {
terminal_id: terminal_id_2.clone(),
data: b"second\n".to_vec(),
},
cx,
);
});
thread.update(cx, |thread, cx| {
thread.on_terminal_provider_event(
TerminalProviderEvent::Exit {
terminal_id: terminal_id_2.clone(),
status: acp::TerminalExitStatus {
exit_code: Some(0),
signal: None,
meta: None,
},
},
cx,
);
});
// Get the second message ID to restore to
let second_message_id = thread.read_with(cx, |thread, _| {
// At this point we have:
// - Index 0: First user message (with checkpoint)
// - Index 1: Second user message (with checkpoint)
// No assistant responses because FakeAgentConnection just returns EndTurn
let AgentThreadEntry::UserMessage(message) = &thread.entries[1] else {
panic!("expected user message at index 1");
};
message.id.clone().unwrap()
});
// Create a terminal AFTER the checkpoint we'll restore to.
// This simulates the AI agent starting a long-running terminal command.
let terminal_id = acp::TerminalId(uuid::Uuid::new_v4().to_string().into());
let mock_terminal = cx.new(|cx| {
let builder = ::terminal::TerminalBuilder::new_display_only(
::terminal::terminal_settings::CursorShape::default(),
::terminal::terminal_settings::AlternateScroll::On,
None,
0,
)
.unwrap();
builder.subscribe(cx)
});
// Register the terminal as created
thread.update(cx, |thread, cx| {
thread.on_terminal_provider_event(
TerminalProviderEvent::Created {
terminal_id: terminal_id.clone(),
label: "sleep 1000".to_string(),
cwd: Some(PathBuf::from("/test")),
output_byte_limit: None,
terminal: mock_terminal.clone(),
},
cx,
);
});
// Simulate the terminal producing output (still running)
thread.update(cx, |thread, cx| {
thread.on_terminal_provider_event(
TerminalProviderEvent::Output {
terminal_id: terminal_id.clone(),
data: b"terminal is running...\n".to_vec(),
},
cx,
);
});
// Create a tool call entry that references this terminal
// This represents the agent requesting a terminal command
thread.update(cx, |thread, cx| {
thread
.handle_session_update(
acp::SessionUpdate::ToolCall(acp::ToolCall {
id: acp::ToolCallId("terminal-tool-1".into()),
title: "Running command".into(),
kind: acp::ToolKind::Execute,
status: acp::ToolCallStatus::InProgress,
content: vec![acp::ToolCallContent::Terminal {
terminal_id: terminal_id.clone(),
}],
locations: vec![],
raw_input: Some(
serde_json::json!({"command": "sleep 1000", "cd": "/test"}),
),
raw_output: None,
meta: None,
}),
cx,
)
.unwrap();
});
// Verify terminal exists and is in the thread
let terminal_exists_before =
thread.read_with(cx, |thread, _| thread.terminals.contains_key(&terminal_id));
assert!(
terminal_exists_before,
"Terminal should exist before checkpoint restore"
);
// Verify the terminal's underlying task is still running (not completed)
let terminal_running_before = thread.read_with(cx, |thread, _cx| {
let terminal_entity = thread.terminals.get(&terminal_id).unwrap();
terminal_entity.read_with(cx, |term, _cx| {
term.output().is_none() // output is None means it's still running
})
});
assert!(
terminal_running_before,
"Terminal should be running before checkpoint restore"
);
// Verify we have the expected entries before restore
let entry_count_before = thread.read_with(cx, |thread, _| thread.entries.len());
assert!(
entry_count_before > 1,
"Should have multiple entries before restore"
);
// Restore the checkpoint to the second message.
// This should:
// 1. Cancel any in-progress generation (via the cancel() call)
// 2. Remove the terminal that was created after that point
thread
.update(cx, |thread, cx| {
thread.restore_checkpoint(second_message_id, cx)
})
.await
.unwrap();
// Verify that no send_task is in progress after restore
// (cancel() clears the send_task)
let has_send_task_after = thread.read_with(cx, |thread, _| thread.send_task.is_some());
assert!(
!has_send_task_after,
"Should not have a send_task after restore (cancel should have cleared it)"
);
// Verify the entries were truncated (restoring to index 1 truncates at 1, keeping only index 0)
let entry_count = thread.read_with(cx, |thread, _| thread.entries.len());
assert_eq!(
entry_count, 1,
"Should have 1 entry after restore (only the first user message)"
);
// Verify the 2 completed terminals from before the checkpoint still exist
let terminal_1_exists = thread.read_with(cx, |thread, _| {
thread.terminals.contains_key(&terminal_id_1)
});
assert!(
terminal_1_exists,
"Terminal 1 (from before checkpoint) should still exist"
);
let terminal_2_exists = thread.read_with(cx, |thread, _| {
thread.terminals.contains_key(&terminal_id_2)
});
assert!(
terminal_2_exists,
"Terminal 2 (from before checkpoint) should still exist"
);
// Verify they're still in completed state
let terminal_1_completed = thread.read_with(cx, |thread, _cx| {
let terminal_entity = thread.terminals.get(&terminal_id_1).unwrap();
terminal_entity.read_with(cx, |term, _cx| term.output().is_some())
});
assert!(terminal_1_completed, "Terminal 1 should still be completed");
let terminal_2_completed = thread.read_with(cx, |thread, _cx| {
let terminal_entity = thread.terminals.get(&terminal_id_2).unwrap();
terminal_entity.read_with(cx, |term, _cx| term.output().is_some())
});
assert!(terminal_2_completed, "Terminal 2 should still be completed");
// Verify the running terminal (created after checkpoint) was removed
let terminal_3_exists =
thread.read_with(cx, |thread, _| thread.terminals.contains_key(&terminal_id));
assert!(
!terminal_3_exists,
"Terminal 3 (created after checkpoint) should have been removed"
);
// Verify total count is 2 (the two from before the checkpoint)
let terminal_count = thread.read_with(cx, |thread, _| thread.terminals.len());
assert_eq!(
terminal_count, 2,
"Should have exactly 2 terminals (the completed ones from before checkpoint)"
);
}
} }

View File

@@ -20,8 +20,6 @@ impl UserMessageId {
} }
pub trait AgentConnection { pub trait AgentConnection {
fn telemetry_id(&self) -> &'static str;
fn new_thread( fn new_thread(
self: Rc<Self>, self: Rc<Self>,
project: Entity<Project>, project: Entity<Project>,
@@ -108,6 +106,9 @@ pub trait AgentSessionSetTitle {
} }
pub trait AgentTelemetry { pub trait AgentTelemetry {
/// The name of the agent used for telemetry.
fn agent_name(&self) -> String;
/// A representation of the current thread state that can be serialized for /// A representation of the current thread state that can be serialized for
/// storage with telemetry events. /// storage with telemetry events.
fn thread_data( fn thread_data(
@@ -317,10 +318,6 @@ mod test_support {
} }
impl AgentConnection for StubAgentConnection { impl AgentConnection for StubAgentConnection {
fn telemetry_id(&self) -> &'static str {
"stub"
}
fn auth_methods(&self) -> &[acp::AuthMethod] { fn auth_methods(&self) -> &[acp::AuthMethod] {
&[] &[]
} }

View File

@@ -20,7 +20,6 @@ futures.workspace = true
gpui.workspace = true gpui.workspace = true
language.workspace = true language.workspace = true
project.workspace = true project.workspace = true
telemetry.workspace = true
text.workspace = true text.workspace = true
util.workspace = true util.workspace = true
watch.workspace = true watch.workspace = true

View File

@@ -3,9 +3,7 @@ use buffer_diff::BufferDiff;
use clock; use clock;
use collections::BTreeMap; use collections::BTreeMap;
use futures::{FutureExt, StreamExt, channel::mpsc}; use futures::{FutureExt, StreamExt, channel::mpsc};
use gpui::{ use gpui::{App, AppContext, AsyncApp, Context, Entity, Subscription, Task, WeakEntity};
App, AppContext, AsyncApp, Context, Entity, SharedString, Subscription, Task, WeakEntity,
};
use language::{Anchor, Buffer, BufferEvent, DiskState, Point, ToPoint}; use language::{Anchor, Buffer, BufferEvent, DiskState, Point, ToPoint};
use project::{Project, ProjectItem, lsp_store::OpenLspBufferHandle}; use project::{Project, ProjectItem, lsp_store::OpenLspBufferHandle};
use std::{cmp, ops::Range, sync::Arc}; use std::{cmp, ops::Range, sync::Arc};
@@ -33,6 +31,71 @@ impl ActionLog {
&self.project &self.project
} }
pub fn latest_snapshot(&self, buffer: &Entity<Buffer>) -> Option<text::BufferSnapshot> {
Some(self.tracked_buffers.get(buffer)?.snapshot.clone())
}
/// Return a unified diff patch with user edits made since last read or notification
pub fn unnotified_user_edits(&self, cx: &Context<Self>) -> Option<String> {
let diffs = self
.tracked_buffers
.values()
.filter_map(|tracked| {
if !tracked.may_have_unnotified_user_edits {
return None;
}
let text_with_latest_user_edits = tracked.diff_base.to_string();
let text_with_last_seen_user_edits = tracked.last_seen_base.to_string();
if text_with_latest_user_edits == text_with_last_seen_user_edits {
return None;
}
let patch = language::unified_diff(
&text_with_last_seen_user_edits,
&text_with_latest_user_edits,
);
let buffer = tracked.buffer.clone();
let file_path = buffer
.read(cx)
.file()
.map(|file| {
let mut path = file.full_path(cx).to_string_lossy().into_owned();
if file.path_style(cx).is_windows() {
path = path.replace('\\', "/");
}
path
})
.unwrap_or_else(|| format!("buffer_{}", buffer.entity_id()));
let mut result = String::new();
result.push_str(&format!("--- a/{}\n", file_path));
result.push_str(&format!("+++ b/{}\n", file_path));
result.push_str(&patch);
Some(result)
})
.collect::<Vec<_>>();
if diffs.is_empty() {
return None;
}
let unified_diff = diffs.join("\n\n");
Some(unified_diff)
}
/// Return a unified diff patch with user edits made since last read/notification
/// and mark them as notified
pub fn flush_unnotified_user_edits(&mut self, cx: &Context<Self>) -> Option<String> {
let patch = self.unnotified_user_edits(cx);
self.tracked_buffers.values_mut().for_each(|tracked| {
tracked.may_have_unnotified_user_edits = false;
tracked.last_seen_base = tracked.diff_base.clone();
});
patch
}
fn track_buffer_internal( fn track_buffer_internal(
&mut self, &mut self,
buffer: Entity<Buffer>, buffer: Entity<Buffer>,
@@ -82,26 +145,31 @@ impl ActionLog {
let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx)); let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
let (diff_update_tx, diff_update_rx) = mpsc::unbounded(); let (diff_update_tx, diff_update_rx) = mpsc::unbounded();
let diff_base; let diff_base;
let last_seen_base;
let unreviewed_edits; let unreviewed_edits;
if is_created { if is_created {
diff_base = Rope::default(); diff_base = Rope::default();
last_seen_base = Rope::default();
unreviewed_edits = Patch::new(vec![Edit { unreviewed_edits = Patch::new(vec![Edit {
old: 0..1, old: 0..1,
new: 0..text_snapshot.max_point().row + 1, new: 0..text_snapshot.max_point().row + 1,
}]) }])
} else { } else {
diff_base = buffer.read(cx).as_rope().clone(); diff_base = buffer.read(cx).as_rope().clone();
last_seen_base = diff_base.clone();
unreviewed_edits = Patch::default(); unreviewed_edits = Patch::default();
} }
TrackedBuffer { TrackedBuffer {
buffer: buffer.clone(), buffer: buffer.clone(),
diff_base, diff_base,
last_seen_base,
unreviewed_edits, unreviewed_edits,
snapshot: text_snapshot, snapshot: text_snapshot,
status, status,
version: buffer.read(cx).version(), version: buffer.read(cx).version(),
diff, diff,
diff_update: diff_update_tx, diff_update: diff_update_tx,
may_have_unnotified_user_edits: false,
_open_lsp_handle: open_lsp_handle, _open_lsp_handle: open_lsp_handle,
_maintain_diff: cx.spawn({ _maintain_diff: cx.spawn({
let buffer = buffer.clone(); let buffer = buffer.clone();
@@ -252,9 +320,10 @@ impl ActionLog {
let new_snapshot = buffer_snapshot.clone(); let new_snapshot = buffer_snapshot.clone();
let unreviewed_edits = tracked_buffer.unreviewed_edits.clone(); let unreviewed_edits = tracked_buffer.unreviewed_edits.clone();
let edits = diff_snapshots(&old_snapshot, &new_snapshot); let edits = diff_snapshots(&old_snapshot, &new_snapshot);
let mut has_user_changes = false;
async move { async move {
if let ChangeAuthor::User = author { if let ChangeAuthor::User = author {
apply_non_conflicting_edits( has_user_changes = apply_non_conflicting_edits(
&unreviewed_edits, &unreviewed_edits,
edits, edits,
&mut base_text, &mut base_text,
@@ -262,13 +331,22 @@ impl ActionLog {
); );
} }
(Arc::new(base_text.to_string()), base_text) (Arc::new(base_text.to_string()), base_text, has_user_changes)
} }
}); });
anyhow::Ok(rebase) anyhow::Ok(rebase)
})??; })??;
let (new_base_text, new_diff_base) = rebase.await; let (new_base_text, new_diff_base, has_user_changes) = rebase.await;
this.update(cx, |this, _| {
let tracked_buffer = this
.tracked_buffers
.get_mut(buffer)
.context("buffer not tracked")
.unwrap();
tracked_buffer.may_have_unnotified_user_edits |= has_user_changes;
})?;
Self::update_diff( Self::update_diff(
this, this,
@@ -487,17 +565,14 @@ impl ActionLog {
&mut self, &mut self,
buffer: Entity<Buffer>, buffer: Entity<Buffer>,
buffer_range: Range<impl language::ToPoint>, buffer_range: Range<impl language::ToPoint>,
telemetry: Option<ActionLogTelemetry>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else { let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
return; return;
}; };
let mut metrics = ActionLogMetrics::for_buffer(buffer.read(cx));
match tracked_buffer.status { match tracked_buffer.status {
TrackedBufferStatus::Deleted => { TrackedBufferStatus::Deleted => {
metrics.add_edits(tracked_buffer.unreviewed_edits.edits());
self.tracked_buffers.remove(&buffer); self.tracked_buffers.remove(&buffer);
cx.notify(); cx.notify();
} }
@@ -506,6 +581,7 @@ impl ActionLog {
let buffer_range = let buffer_range =
buffer_range.start.to_point(buffer)..buffer_range.end.to_point(buffer); buffer_range.start.to_point(buffer)..buffer_range.end.to_point(buffer);
let mut delta = 0i32; let mut delta = 0i32;
tracked_buffer.unreviewed_edits.retain_mut(|edit| { tracked_buffer.unreviewed_edits.retain_mut(|edit| {
edit.old.start = (edit.old.start as i32 + delta) as u32; edit.old.start = (edit.old.start as i32 + delta) as u32;
edit.old.end = (edit.old.end as i32 + delta) as u32; edit.old.end = (edit.old.end as i32 + delta) as u32;
@@ -537,7 +613,6 @@ impl ActionLog {
.collect::<String>(), .collect::<String>(),
); );
delta += edit.new_len() as i32 - edit.old_len() as i32; delta += edit.new_len() as i32 - edit.old_len() as i32;
metrics.add_edit(edit);
false false
} }
}); });
@@ -549,24 +624,19 @@ impl ActionLog {
tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx); tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
} }
} }
if let Some(telemetry) = telemetry {
telemetry_report_accepted_edits(&telemetry, metrics);
}
} }
pub fn reject_edits_in_ranges( pub fn reject_edits_in_ranges(
&mut self, &mut self,
buffer: Entity<Buffer>, buffer: Entity<Buffer>,
buffer_ranges: Vec<Range<impl language::ToPoint>>, buffer_ranges: Vec<Range<impl language::ToPoint>>,
telemetry: Option<ActionLogTelemetry>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Task<Result<()>> { ) -> Task<Result<()>> {
let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else { let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
return Task::ready(Ok(())); return Task::ready(Ok(()));
}; };
let mut metrics = ActionLogMetrics::for_buffer(buffer.read(cx)); match &tracked_buffer.status {
let task = match &tracked_buffer.status {
TrackedBufferStatus::Created { TrackedBufferStatus::Created {
existing_file_content, existing_file_content,
} => { } => {
@@ -616,7 +686,6 @@ impl ActionLog {
} }
}; };
metrics.add_edits(tracked_buffer.unreviewed_edits.edits());
self.tracked_buffers.remove(&buffer); self.tracked_buffers.remove(&buffer);
cx.notify(); cx.notify();
task task
@@ -630,7 +699,6 @@ impl ActionLog {
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)); .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx));
// Clear all tracked edits for this buffer and start over as if we just read it. // Clear all tracked edits for this buffer and start over as if we just read it.
metrics.add_edits(tracked_buffer.unreviewed_edits.edits());
self.tracked_buffers.remove(&buffer); self.tracked_buffers.remove(&buffer);
self.buffer_read(buffer.clone(), cx); self.buffer_read(buffer.clone(), cx);
cx.notify(); cx.notify();
@@ -670,7 +738,6 @@ impl ActionLog {
} }
if revert { if revert {
metrics.add_edit(edit);
let old_range = tracked_buffer let old_range = tracked_buffer
.diff_base .diff_base
.point_to_offset(Point::new(edit.old.start, 0)) .point_to_offset(Point::new(edit.old.start, 0))
@@ -691,25 +758,12 @@ impl ActionLog {
self.project self.project
.update(cx, |project, cx| project.save_buffer(buffer, cx)) .update(cx, |project, cx| project.save_buffer(buffer, cx))
} }
};
if let Some(telemetry) = telemetry {
telemetry_report_rejected_edits(&telemetry, metrics);
} }
task
} }
pub fn keep_all_edits( pub fn keep_all_edits(&mut self, cx: &mut Context<Self>) {
&mut self, self.tracked_buffers
telemetry: Option<ActionLogTelemetry>, .retain(|_buffer, tracked_buffer| match tracked_buffer.status {
cx: &mut Context<Self>,
) {
self.tracked_buffers.retain(|buffer, tracked_buffer| {
let mut metrics = ActionLogMetrics::for_buffer(buffer.read(cx));
metrics.add_edits(tracked_buffer.unreviewed_edits.edits());
if let Some(telemetry) = telemetry.as_ref() {
telemetry_report_accepted_edits(telemetry, metrics);
}
match tracked_buffer.status {
TrackedBufferStatus::Deleted => false, TrackedBufferStatus::Deleted => false,
_ => { _ => {
if let TrackedBufferStatus::Created { .. } = &mut tracked_buffer.status { if let TrackedBufferStatus::Created { .. } = &mut tracked_buffer.status {
@@ -720,24 +774,13 @@ impl ActionLog {
tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx); tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
true true
} }
} });
});
cx.notify(); cx.notify();
} }
pub fn reject_all_edits( pub fn reject_all_edits(&mut self, cx: &mut Context<Self>) -> Task<()> {
&mut self,
telemetry: Option<ActionLogTelemetry>,
cx: &mut Context<Self>,
) -> Task<()> {
let futures = self.changed_buffers(cx).into_keys().map(|buffer| { let futures = self.changed_buffers(cx).into_keys().map(|buffer| {
let reject = self.reject_edits_in_ranges( let reject = self.reject_edits_in_ranges(buffer, vec![Anchor::MIN..Anchor::MAX], cx);
buffer,
vec![Anchor::MIN..Anchor::MAX],
telemetry.clone(),
cx,
);
async move { async move {
reject.await.log_err(); reject.await.log_err();
@@ -745,7 +788,8 @@ impl ActionLog {
}); });
let task = futures::future::join_all(futures); let task = futures::future::join_all(futures);
cx.background_spawn(async move {
cx.spawn(async move |_, _| {
task.await; task.await;
}) })
} }
@@ -775,61 +819,6 @@ impl ActionLog {
} }
} }
#[derive(Clone)]
pub struct ActionLogTelemetry {
pub agent_telemetry_id: &'static str,
pub session_id: Arc<str>,
}
struct ActionLogMetrics {
lines_removed: u32,
lines_added: u32,
language: Option<SharedString>,
}
impl ActionLogMetrics {
fn for_buffer(buffer: &Buffer) -> Self {
Self {
language: buffer.language().map(|l| l.name().0),
lines_removed: 0,
lines_added: 0,
}
}
fn add_edits(&mut self, edits: &[Edit<u32>]) {
for edit in edits {
self.add_edit(edit);
}
}
fn add_edit(&mut self, edit: &Edit<u32>) {
self.lines_added += edit.new_len();
self.lines_removed += edit.old_len();
}
}
fn telemetry_report_accepted_edits(telemetry: &ActionLogTelemetry, metrics: ActionLogMetrics) {
telemetry::event!(
"Agent Edits Accepted",
agent = telemetry.agent_telemetry_id,
session = telemetry.session_id,
language = metrics.language,
lines_added = metrics.lines_added,
lines_removed = metrics.lines_removed
);
}
fn telemetry_report_rejected_edits(telemetry: &ActionLogTelemetry, metrics: ActionLogMetrics) {
telemetry::event!(
"Agent Edits Rejected",
agent = telemetry.agent_telemetry_id,
session = telemetry.session_id,
language = metrics.language,
lines_added = metrics.lines_added,
lines_removed = metrics.lines_removed
);
}
fn apply_non_conflicting_edits( fn apply_non_conflicting_edits(
patch: &Patch<u32>, patch: &Patch<u32>,
edits: Vec<Edit<u32>>, edits: Vec<Edit<u32>>,
@@ -960,12 +949,14 @@ enum TrackedBufferStatus {
struct TrackedBuffer { struct TrackedBuffer {
buffer: Entity<Buffer>, buffer: Entity<Buffer>,
diff_base: Rope, diff_base: Rope,
last_seen_base: Rope,
unreviewed_edits: Patch<u32>, unreviewed_edits: Patch<u32>,
status: TrackedBufferStatus, status: TrackedBufferStatus,
version: clock::Global, version: clock::Global,
diff: Entity<BufferDiff>, diff: Entity<BufferDiff>,
snapshot: text::BufferSnapshot, snapshot: text::BufferSnapshot,
diff_update: mpsc::UnboundedSender<(ChangeAuthor, text::BufferSnapshot)>, diff_update: mpsc::UnboundedSender<(ChangeAuthor, text::BufferSnapshot)>,
may_have_unnotified_user_edits: bool,
_open_lsp_handle: OpenLspBufferHandle, _open_lsp_handle: OpenLspBufferHandle,
_maintain_diff: Task<()>, _maintain_diff: Task<()>,
_subscription: Subscription, _subscription: Subscription,
@@ -996,6 +987,7 @@ mod tests {
use super::*; use super::*;
use buffer_diff::DiffHunkStatusKind; use buffer_diff::DiffHunkStatusKind;
use gpui::TestAppContext; use gpui::TestAppContext;
use indoc::indoc;
use language::Point; use language::Point;
use project::{FakeFs, Fs, Project, RemoveOptions}; use project::{FakeFs, Fs, Project, RemoveOptions};
use rand::prelude::*; use rand::prelude::*;
@@ -1013,6 +1005,8 @@ mod tests {
cx.update(|cx| { cx.update(|cx| {
let settings_store = SettingsStore::test(cx); let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store); cx.set_global(settings_store);
language::init(cx);
Project::init_settings(cx);
}); });
} }
@@ -1072,7 +1066,7 @@ mod tests {
); );
action_log.update(cx, |log, cx| { action_log.update(cx, |log, cx| {
log.keep_edits_in_range(buffer.clone(), Point::new(3, 0)..Point::new(4, 3), None, cx) log.keep_edits_in_range(buffer.clone(), Point::new(3, 0)..Point::new(4, 3), cx)
}); });
cx.run_until_parked(); cx.run_until_parked();
assert_eq!( assert_eq!(
@@ -1088,7 +1082,7 @@ mod tests {
); );
action_log.update(cx, |log, cx| { action_log.update(cx, |log, cx| {
log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(4, 3), None, cx) log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(4, 3), cx)
}); });
cx.run_until_parked(); cx.run_until_parked();
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
@@ -1173,7 +1167,7 @@ mod tests {
); );
action_log.update(cx, |log, cx| { action_log.update(cx, |log, cx| {
log.keep_edits_in_range(buffer.clone(), Point::new(1, 0)..Point::new(1, 0), None, cx) log.keep_edits_in_range(buffer.clone(), Point::new(1, 0)..Point::new(1, 0), cx)
}); });
cx.run_until_parked(); cx.run_until_parked();
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
@@ -1270,7 +1264,111 @@ mod tests {
); );
action_log.update(cx, |log, cx| { action_log.update(cx, |log, cx| {
log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(1, 0), None, cx) log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(1, 0), cx)
});
cx.run_until_parked();
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
}
#[gpui::test(iterations = 10)]
async fn test_user_edits_notifications(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/dir"),
json!({"file": indoc! {"
abc
def
ghi
jkl
mno"}}),
)
.await;
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let file_path = project
.read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
.unwrap();
let buffer = project
.update(cx, |project, cx| project.open_buffer(file_path, cx))
.await
.unwrap();
// Agent edits
cx.update(|cx| {
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
buffer.update(cx, |buffer, cx| {
buffer
.edit([(Point::new(1, 2)..Point::new(2, 3), "F\nGHI")], None, cx)
.unwrap()
});
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
});
cx.run_until_parked();
assert_eq!(
buffer.read_with(cx, |buffer, _| buffer.text()),
indoc! {"
abc
deF
GHI
jkl
mno"}
);
assert_eq!(
unreviewed_hunks(&action_log, cx),
vec![(
buffer.clone(),
vec![HunkStatus {
range: Point::new(1, 0)..Point::new(3, 0),
diff_status: DiffHunkStatusKind::Modified,
old_text: "def\nghi\n".into(),
}],
)]
);
// User edits
buffer.update(cx, |buffer, cx| {
buffer.edit(
[
(Point::new(0, 2)..Point::new(0, 2), "X"),
(Point::new(3, 0)..Point::new(3, 0), "Y"),
],
None,
cx,
)
});
cx.run_until_parked();
assert_eq!(
buffer.read_with(cx, |buffer, _| buffer.text()),
indoc! {"
abXc
deF
GHI
Yjkl
mno"}
);
// User edits should be stored separately from agent's
let user_edits = action_log.update(cx, |log, cx| log.unnotified_user_edits(cx));
assert_eq!(
user_edits.expect("should have some user edits"),
indoc! {"
--- a/dir/file
+++ b/dir/file
@@ -1,5 +1,5 @@
-abc
+abXc
def
ghi
-jkl
+Yjkl
mno
"}
);
action_log.update(cx, |log, cx| {
log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(1, 0), cx)
}); });
cx.run_until_parked(); cx.run_until_parked();
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
@@ -1329,7 +1427,7 @@ mod tests {
); );
action_log.update(cx, |log, cx| { action_log.update(cx, |log, cx| {
log.keep_edits_in_range(buffer.clone(), 0..5, None, cx) log.keep_edits_in_range(buffer.clone(), 0..5, cx)
}); });
cx.run_until_parked(); cx.run_until_parked();
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
@@ -1381,7 +1479,7 @@ mod tests {
action_log action_log
.update(cx, |log, cx| { .update(cx, |log, cx| {
log.reject_edits_in_ranges(buffer.clone(), vec![2..5], None, cx) log.reject_edits_in_ranges(buffer.clone(), vec![2..5], cx)
}) })
.await .await
.unwrap(); .unwrap();
@@ -1461,7 +1559,7 @@ mod tests {
action_log action_log
.update(cx, |log, cx| { .update(cx, |log, cx| {
log.reject_edits_in_ranges(buffer.clone(), vec![2..5], None, cx) log.reject_edits_in_ranges(buffer.clone(), vec![2..5], cx)
}) })
.await .await
.unwrap(); .unwrap();
@@ -1644,7 +1742,6 @@ mod tests {
log.reject_edits_in_ranges( log.reject_edits_in_ranges(
buffer.clone(), buffer.clone(),
vec![Point::new(4, 0)..Point::new(4, 0)], vec![Point::new(4, 0)..Point::new(4, 0)],
None,
cx, cx,
) )
}) })
@@ -1679,7 +1776,6 @@ mod tests {
log.reject_edits_in_ranges( log.reject_edits_in_ranges(
buffer.clone(), buffer.clone(),
vec![Point::new(0, 0)..Point::new(1, 0)], vec![Point::new(0, 0)..Point::new(1, 0)],
None,
cx, cx,
) )
}) })
@@ -1707,7 +1803,6 @@ mod tests {
log.reject_edits_in_ranges( log.reject_edits_in_ranges(
buffer.clone(), buffer.clone(),
vec![Point::new(4, 0)..Point::new(4, 0)], vec![Point::new(4, 0)..Point::new(4, 0)],
None,
cx, cx,
) )
}) })
@@ -1782,7 +1877,7 @@ mod tests {
let range_2 = buffer.read(cx).anchor_before(Point::new(5, 0)) let range_2 = buffer.read(cx).anchor_before(Point::new(5, 0))
..buffer.read(cx).anchor_before(Point::new(5, 3)); ..buffer.read(cx).anchor_before(Point::new(5, 3));
log.reject_edits_in_ranges(buffer.clone(), vec![range_1, range_2], None, cx) log.reject_edits_in_ranges(buffer.clone(), vec![range_1, range_2], cx)
.detach(); .detach();
assert_eq!( assert_eq!(
buffer.read_with(cx, |buffer, _| buffer.text()), buffer.read_with(cx, |buffer, _| buffer.text()),
@@ -1843,7 +1938,6 @@ mod tests {
log.reject_edits_in_ranges( log.reject_edits_in_ranges(
buffer.clone(), buffer.clone(),
vec![Point::new(0, 0)..Point::new(0, 0)], vec![Point::new(0, 0)..Point::new(0, 0)],
None,
cx, cx,
) )
}) })
@@ -1899,7 +1993,6 @@ mod tests {
log.reject_edits_in_ranges( log.reject_edits_in_ranges(
buffer.clone(), buffer.clone(),
vec![Point::new(0, 0)..Point::new(0, 11)], vec![Point::new(0, 0)..Point::new(0, 11)],
None,
cx, cx,
) )
}) })
@@ -1962,7 +2055,6 @@ mod tests {
log.reject_edits_in_ranges( log.reject_edits_in_ranges(
buffer.clone(), buffer.clone(),
vec![Point::new(0, 0)..Point::new(100, 0)], vec![Point::new(0, 0)..Point::new(100, 0)],
None,
cx, cx,
) )
}) })
@@ -2010,7 +2102,7 @@ mod tests {
// User accepts the single hunk // User accepts the single hunk
action_log.update(cx, |log, cx| { action_log.update(cx, |log, cx| {
log.keep_edits_in_range(buffer.clone(), Anchor::MIN..Anchor::MAX, None, cx) log.keep_edits_in_range(buffer.clone(), Anchor::MIN..Anchor::MAX, cx)
}); });
cx.run_until_parked(); cx.run_until_parked();
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
@@ -2031,7 +2123,7 @@ mod tests {
// User rejects the hunk // User rejects the hunk
action_log action_log
.update(cx, |log, cx| { .update(cx, |log, cx| {
log.reject_edits_in_ranges(buffer.clone(), vec![Anchor::MIN..Anchor::MAX], None, cx) log.reject_edits_in_ranges(buffer.clone(), vec![Anchor::MIN..Anchor::MAX], cx)
}) })
.await .await
.unwrap(); .unwrap();
@@ -2075,7 +2167,7 @@ mod tests {
cx.run_until_parked(); cx.run_until_parked();
// User clicks "Accept All" // User clicks "Accept All"
action_log.update(cx, |log, cx| log.keep_all_edits(None, cx)); action_log.update(cx, |log, cx| log.keep_all_edits(cx));
cx.run_until_parked(); cx.run_until_parked();
assert!(fs.is_file(path!("/dir/new_file").as_ref()).await); assert!(fs.is_file(path!("/dir/new_file").as_ref()).await);
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); // Hunks are cleared assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); // Hunks are cleared
@@ -2094,7 +2186,7 @@ mod tests {
// User clicks "Reject All" // User clicks "Reject All"
action_log action_log
.update(cx, |log, cx| log.reject_all_edits(None, cx)) .update(cx, |log, cx| log.reject_all_edits(cx))
.await; .await;
cx.run_until_parked(); cx.run_until_parked();
assert!(fs.is_file(path!("/dir/new_file").as_ref()).await); assert!(fs.is_file(path!("/dir/new_file").as_ref()).await);
@@ -2134,7 +2226,7 @@ mod tests {
action_log.update(cx, |log, cx| { action_log.update(cx, |log, cx| {
let range = buffer.read(cx).random_byte_range(0, &mut rng); let range = buffer.read(cx).random_byte_range(0, &mut rng);
log::info!("keeping edits in range {:?}", range); log::info!("keeping edits in range {:?}", range);
log.keep_edits_in_range(buffer.clone(), range, None, cx) log.keep_edits_in_range(buffer.clone(), range, cx)
}); });
} }
25..50 => { 25..50 => {
@@ -2142,7 +2234,7 @@ mod tests {
.update(cx, |log, cx| { .update(cx, |log, cx| {
let range = buffer.read(cx).random_byte_range(0, &mut rng); let range = buffer.read(cx).random_byte_range(0, &mut rng);
log::info!("rejecting edits in range {:?}", range); log::info!("rejecting edits in range {:?}", range);
log.reject_edits_in_ranges(buffer.clone(), vec![range], None, cx) log.reject_edits_in_ranges(buffer.clone(), vec![range], cx)
}) })
.await .await
.unwrap(); .unwrap();
@@ -2396,4 +2488,61 @@ mod tests {
.collect() .collect()
}) })
} }
#[gpui::test]
async fn test_format_patch(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/dir"),
json!({"test.txt": "line 1\nline 2\nline 3\n"}),
)
.await;
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let file_path = project
.read_with(cx, |project, cx| {
project.find_project_path("dir/test.txt", cx)
})
.unwrap();
let buffer = project
.update(cx, |project, cx| project.open_buffer(file_path, cx))
.await
.unwrap();
cx.update(|cx| {
// Track the buffer and mark it as read first
action_log.update(cx, |log, cx| {
log.buffer_read(buffer.clone(), cx);
});
// Make some edits to create a patch
buffer.update(cx, |buffer, cx| {
buffer
.edit([(Point::new(1, 0)..Point::new(1, 6), "CHANGED")], None, cx)
.unwrap(); // Replace "line2" with "CHANGED"
});
});
cx.run_until_parked();
// Get the patch
let patch = action_log.update(cx, |log, cx| log.unnotified_user_edits(cx));
// Verify the patch format contains expected unified diff elements
assert_eq!(
patch.unwrap(),
indoc! {"
--- a/dir/test.txt
+++ b/dir/test.txt
@@ -1,3 +1,3 @@
line 1
-line 2
+CHANGED
line 3
"}
);
}
} }

View File

@@ -17,7 +17,6 @@ anyhow.workspace = true
auto_update.workspace = true auto_update.workspace = true
editor.workspace = true editor.workspace = true
extension_host.workspace = true extension_host.workspace = true
fs.workspace = true
futures.workspace = true futures.workspace = true
gpui.workspace = true gpui.workspace = true
language.workspace = true language.workspace = true

View File

@@ -51,7 +51,6 @@ pub struct ActivityIndicator {
project: Entity<Project>, project: Entity<Project>,
auto_updater: Option<Entity<AutoUpdater>>, auto_updater: Option<Entity<AutoUpdater>>,
context_menu_handle: PopoverMenuHandle<ContextMenu>, context_menu_handle: PopoverMenuHandle<ContextMenu>,
fs_jobs: Vec<fs::JobInfo>,
} }
#[derive(Debug)] #[derive(Debug)]
@@ -100,27 +99,6 @@ impl ActivityIndicator {
}) })
.detach(); .detach();
let fs = project.read(cx).fs().clone();
let mut job_events = fs.subscribe_to_jobs();
cx.spawn(async move |this, cx| {
while let Some(job_event) = job_events.next().await {
this.update(cx, |this: &mut ActivityIndicator, cx| {
match job_event {
fs::JobEvent::Started { info } => {
this.fs_jobs.retain(|j| j.id != info.id);
this.fs_jobs.push(info);
}
fs::JobEvent::Completed { id } => {
this.fs_jobs.retain(|j| j.id != id);
}
}
cx.notify();
})?;
}
anyhow::Ok(())
})
.detach();
cx.subscribe( cx.subscribe(
&project.read(cx).lsp_store(), &project.read(cx).lsp_store(),
|activity_indicator, _, event, cx| { |activity_indicator, _, event, cx| {
@@ -223,8 +201,7 @@ impl ActivityIndicator {
statuses: Vec::new(), statuses: Vec::new(),
project: project.clone(), project: project.clone(),
auto_updater, auto_updater,
context_menu_handle: PopoverMenuHandle::default(), context_menu_handle: Default::default(),
fs_jobs: Vec::new(),
} }
}); });
@@ -455,23 +432,6 @@ impl ActivityIndicator {
}); });
} }
// Show any long-running fs command
for fs_job in &self.fs_jobs {
if Instant::now().duration_since(fs_job.start) >= GIT_OPERATION_DELAY {
return Some(Content {
icon: Some(
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.with_rotate_animation(2)
.into_any_element(),
),
message: fs_job.message.clone().into(),
on_click: None,
tooltip_message: None,
});
}
}
// Show any language server installation info. // Show any language server installation info.
let mut downloading = SmallVec::<[_; 3]>::new(); let mut downloading = SmallVec::<[_; 3]>::new();
let mut checking_for_update = SmallVec::<[_; 3]>::new(); let mut checking_for_update = SmallVec::<[_; 3]>::new();

View File

@@ -63,6 +63,7 @@ streaming_diff.workspace = true
strsim.workspace = true strsim.workspace = true
task.workspace = true task.workspace = true
telemetry.workspace = true telemetry.workspace = true
terminal.workspace = true
text.workspace = true text.workspace = true
thiserror.workspace = true thiserror.workspace = true
ui.workspace = true ui.workspace = true

View File

@@ -6,6 +6,7 @@ mod native_agent_server;
pub mod outline; pub mod outline;
mod templates; mod templates;
mod thread; mod thread;
mod tool_schema;
mod tools; mod tools;
#[cfg(test)] #[cfg(test)]
@@ -133,7 +134,9 @@ impl LanguageModels {
for model in provider.provided_models(cx) { for model in provider.provided_models(cx) {
let model_info = Self::map_language_model_to_info(&model, &provider); let model_info = Self::map_language_model_to_info(&model, &provider);
let model_id = model_info.id.clone(); let model_id = model_info.id.clone();
provider_models.push(model_info); if !recommended_models.contains(&(model.provider_id(), model.id())) {
provider_models.push(model_info);
}
models.insert(model_id, model); models.insert(model_id, model);
} }
if !provider_models.is_empty() { if !provider_models.is_empty() {
@@ -215,7 +218,7 @@ impl LanguageModels {
} }
_ => { _ => {
log::error!( log::error!(
"Failed to authenticate provider: {}: {err:#}", "Failed to authenticate provider: {}: {err}",
provider_name.0 provider_name.0
); );
} }
@@ -964,10 +967,6 @@ impl acp_thread::AgentModelSelector for NativeAgentModelSelector {
} }
impl acp_thread::AgentConnection for NativeAgentConnection { impl acp_thread::AgentConnection for NativeAgentConnection {
fn telemetry_id(&self) -> &'static str {
"zed"
}
fn new_thread( fn new_thread(
self: Rc<Self>, self: Rc<Self>,
project: Entity<Project>, project: Entity<Project>,
@@ -1108,6 +1107,10 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
} }
impl acp_thread::AgentTelemetry for NativeAgentConnection { impl acp_thread::AgentTelemetry for NativeAgentConnection {
fn agent_name(&self) -> String {
"Zed".into()
}
fn thread_data( fn thread_data(
&self, &self,
session_id: &acp::SessionId, session_id: &acp::SessionId,
@@ -1624,7 +1627,9 @@ mod internal_tests {
cx.update(|cx| { cx.update(|cx| {
let settings_store = SettingsStore::test(cx); let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store); cx.set_global(settings_store);
Project::init_settings(cx);
agent_settings::init(cx);
language::init(cx);
LanguageModelRegistry::test(cx); LanguageModelRegistry::test(cx);
}); });
} }

View File

@@ -150,7 +150,6 @@ impl DbThread {
.unwrap_or_default(), .unwrap_or_default(),
input: tool_use.input, input: tool_use.input,
is_input_complete: true, is_input_complete: true,
thought_signature: None,
}, },
)); ));
} }

View File

@@ -1394,7 +1394,7 @@ mod tests {
async fn init_test(cx: &mut TestAppContext) -> EditAgent { async fn init_test(cx: &mut TestAppContext) -> EditAgent {
cx.update(settings::init); cx.update(settings::init);
cx.update(Project::init_settings);
let project = Project::test(FakeFs::new(cx.executor()), [], cx).await; let project = Project::test(FakeFs::new(cx.executor()), [], cx).await;
let model = Arc::new(FakeLanguageModel::default()); let model = Arc::new(FakeLanguageModel::default());
let action_log = cx.new(|_| ActionLog::new(project.clone())); let action_log = cx.new(|_| ActionLog::new(project.clone()));

View File

@@ -15,14 +15,12 @@ const SEPARATOR_MARKER: &str = "=======";
const REPLACE_MARKER: &str = ">>>>>>> REPLACE"; const REPLACE_MARKER: &str = ">>>>>>> REPLACE";
const SONNET_PARAMETER_INVOKE_1: &str = "</parameter>\n</invoke>"; const SONNET_PARAMETER_INVOKE_1: &str = "</parameter>\n</invoke>";
const SONNET_PARAMETER_INVOKE_2: &str = "</parameter></invoke>"; const SONNET_PARAMETER_INVOKE_2: &str = "</parameter></invoke>";
const SONNET_PARAMETER_INVOKE_3: &str = "</parameter>"; const END_TAGS: [&str; 5] = [
const END_TAGS: [&str; 6] = [
OLD_TEXT_END_TAG, OLD_TEXT_END_TAG,
NEW_TEXT_END_TAG, NEW_TEXT_END_TAG,
EDITS_END_TAG, EDITS_END_TAG,
SONNET_PARAMETER_INVOKE_1, // Remove these after switching to streaming tool call SONNET_PARAMETER_INVOKE_1, // Remove this after switching to streaming tool call
SONNET_PARAMETER_INVOKE_2, SONNET_PARAMETER_INVOKE_2,
SONNET_PARAMETER_INVOKE_3,
]; ];
#[derive(Debug)] #[derive(Debug)]
@@ -569,29 +567,21 @@ mod tests {
parse_random_chunks( parse_random_chunks(
indoc! {" indoc! {"
<old_text>some text</old_text><new_text>updated text</parameter></invoke> <old_text>some text</old_text><new_text>updated text</parameter></invoke>
<old_text>more text</old_text><new_text>upd</parameter></new_text>
"}, "},
&mut parser, &mut parser,
&mut rng &mut rng
), ),
vec![ vec![Edit {
Edit { old_text: "some text".to_string(),
old_text: "some text".to_string(), new_text: "updated text".to_string(),
new_text: "updated text".to_string(), line_hint: None,
line_hint: None, },]
},
Edit {
old_text: "more text".to_string(),
new_text: "upd".to_string(),
line_hint: None,
},
]
); );
assert_eq!( assert_eq!(
parser.finish(), parser.finish(),
EditParserMetrics { EditParserMetrics {
tags: 4, tags: 2,
mismatched_tags: 2 mismatched_tags: 1
} }
); );
} }

View File

@@ -1108,7 +1108,6 @@ fn tool_use(
raw_input: serde_json::to_string_pretty(&input).unwrap(), raw_input: serde_json::to_string_pretty(&input).unwrap(),
input: serde_json::to_value(input).unwrap(), input: serde_json::to_value(input).unwrap(),
is_input_complete: true, is_input_complete: true,
thought_signature: None,
}) })
} }
@@ -1469,9 +1468,14 @@ impl EditAgentTest {
gpui_tokio::init(cx); gpui_tokio::init(cx);
let http_client = Arc::new(ReqwestClient::user_agent("agent tests").unwrap()); let http_client = Arc::new(ReqwestClient::user_agent("agent tests").unwrap());
cx.set_http_client(http_client); cx.set_http_client(http_client);
client::init_settings(cx);
let client = Client::production(cx); let client = Client::production(cx);
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
settings::init(cx); settings::init(cx);
Project::init_settings(cx);
language::init(cx);
language_model::init(client.clone(), cx); language_model::init(client.clone(), cx);
language_models::init(user_store, client.clone(), cx); language_models::init(user_store, client.clone(), cx);
}); });

View File

@@ -88,6 +88,8 @@ mod tests {
async |fs, project, cx| { async |fs, project, cx| {
let auth = cx.update(|cx| { let auth = cx.update(|cx| {
prompt_store::init(cx); prompt_store::init(cx);
terminal::init(cx);
let registry = language_model::LanguageModelRegistry::read_global(cx); let registry = language_model::LanguageModelRegistry::read_global(cx);
let auth = registry let auth = registry
.provider(&language_model::ANTHROPIC_PROVIDER_ID) .provider(&language_model::ANTHROPIC_PROVIDER_ID)

View File

@@ -1,6 +1,6 @@
use anyhow::Result; use anyhow::Result;
use gpui::{AsyncApp, Entity}; use gpui::{AsyncApp, Entity};
use language::{Buffer, OutlineItem}; use language::{Buffer, OutlineItem, ParseStatus};
use regex::Regex; use regex::Regex;
use std::fmt::Write; use std::fmt::Write;
use text::Point; use text::Point;
@@ -30,9 +30,10 @@ pub async fn get_buffer_content_or_outline(
if file_size > AUTO_OUTLINE_SIZE { if file_size > AUTO_OUTLINE_SIZE {
// For large files, use outline instead of full content // For large files, use outline instead of full content
// Wait until the buffer has been fully parsed, so we can read its outline // Wait until the buffer has been fully parsed, so we can read its outline
buffer let mut parse_status = buffer.read_with(cx, |buffer, _| buffer.parse_status())?;
.read_with(cx, |buffer, _| buffer.parsing_idle())? while *parse_status.borrow() != ParseStatus::Idle {
.await; parse_status.changed().await?;
}
let outline_items = buffer.read_with(cx, |buffer, _| { let outline_items = buffer.read_with(cx, |buffer, _| {
let snapshot = buffer.snapshot(); let snapshot = buffer.snapshot();
@@ -44,25 +45,6 @@ pub async fn get_buffer_content_or_outline(
.collect::<Vec<_>>() .collect::<Vec<_>>()
})?; })?;
// If no outline exists, fall back to first 1KB so the agent has some context
if outline_items.is_empty() {
let text = buffer.read_with(cx, |buffer, _| {
let snapshot = buffer.snapshot();
let len = snapshot.len().min(1024);
let content = snapshot.text_for_range(0..len).collect::<String>();
if let Some(path) = path {
format!("# First 1KB of {path} (file too large to show full content, and no outline available)\n\n{content}")
} else {
format!("# First 1KB of file (file too large to show full content, and no outline available)\n\n{content}")
}
})?;
return Ok(BufferContent {
text,
is_outline: false,
});
}
let outline_text = render_outline(outline_items, None, 0, usize::MAX).await?; let outline_text = render_outline(outline_items, None, 0, usize::MAX).await?;
let text = if let Some(path) = path { let text = if let Some(path) = path {
@@ -159,62 +141,3 @@ fn render_entries(
entries_rendered entries_rendered
} }
#[cfg(test)]
mod tests {
use super::*;
use fs::FakeFs;
use gpui::TestAppContext;
use project::Project;
use settings::SettingsStore;
#[gpui::test]
async fn test_large_file_fallback_to_subset(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings = SettingsStore::test(cx);
cx.set_global(settings);
});
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, [], cx).await;
let content = "A".repeat(100 * 1024); // 100KB
let content_len = content.len();
let buffer = project
.update(cx, |project, cx| project.create_buffer(true, cx))
.await
.expect("failed to create buffer");
buffer.update(cx, |buffer, cx| buffer.set_text(content, cx));
let result = cx
.spawn(|cx| async move { get_buffer_content_or_outline(buffer, None, &cx).await })
.await
.unwrap();
// Should contain some of the actual file content
assert!(
result.text.contains("AAAAAAAAAA"),
"Result did not contain content subset"
);
// Should be marked as not an outline (it's truncated content)
assert!(
!result.is_outline,
"Large file without outline should not be marked as outline"
);
// Should be reasonably sized (much smaller than original)
assert!(
result.text.len() < 50 * 1024,
"Result size {} should be smaller than 50KB",
result.text.len()
);
// Should be significantly smaller than the original content
assert!(
result.text.len() < content_len / 10,
"Result should be much smaller than original content"
);
}
}

View File

@@ -274,7 +274,6 @@ async fn test_prompt_caching(cx: &mut TestAppContext) {
raw_input: json!({"text": "test"}).to_string(), raw_input: json!({"text": "test"}).to_string(),
input: json!({"text": "test"}), input: json!({"text": "test"}),
is_input_complete: true, is_input_complete: true,
thought_signature: None,
}; };
fake_model fake_model
.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(tool_use.clone())); .send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(tool_use.clone()));
@@ -462,7 +461,6 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
raw_input: "{}".into(), raw_input: "{}".into(),
input: json!({}), input: json!({}),
is_input_complete: true, is_input_complete: true,
thought_signature: None,
}, },
)); ));
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
@@ -472,7 +470,6 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
raw_input: "{}".into(), raw_input: "{}".into(),
input: json!({}), input: json!({}),
is_input_complete: true, is_input_complete: true,
thought_signature: None,
}, },
)); ));
fake_model.end_last_completion_stream(); fake_model.end_last_completion_stream();
@@ -523,7 +520,6 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
raw_input: "{}".into(), raw_input: "{}".into(),
input: json!({}), input: json!({}),
is_input_complete: true, is_input_complete: true,
thought_signature: None,
}, },
)); ));
fake_model.end_last_completion_stream(); fake_model.end_last_completion_stream();
@@ -558,7 +554,6 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
raw_input: "{}".into(), raw_input: "{}".into(),
input: json!({}), input: json!({}),
is_input_complete: true, is_input_complete: true,
thought_signature: None,
}, },
)); ));
fake_model.end_last_completion_stream(); fake_model.end_last_completion_stream();
@@ -597,7 +592,6 @@ async fn test_tool_hallucination(cx: &mut TestAppContext) {
raw_input: "{}".into(), raw_input: "{}".into(),
input: json!({}), input: json!({}),
is_input_complete: true, is_input_complete: true,
thought_signature: None,
}, },
)); ));
fake_model.end_last_completion_stream(); fake_model.end_last_completion_stream();
@@ -627,7 +621,6 @@ async fn test_resume_after_tool_use_limit(cx: &mut TestAppContext) {
raw_input: "{}".into(), raw_input: "{}".into(),
input: serde_json::to_value(&EchoToolInput { text: "def".into() }).unwrap(), input: serde_json::to_value(&EchoToolInput { text: "def".into() }).unwrap(),
is_input_complete: true, is_input_complete: true,
thought_signature: None,
}; };
fake_model fake_model
.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(tool_use.clone())); .send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(tool_use.clone()));
@@ -738,7 +731,6 @@ async fn test_send_after_tool_use_limit(cx: &mut TestAppContext) {
raw_input: "{}".into(), raw_input: "{}".into(),
input: serde_json::to_value(&EchoToolInput { text: "def".into() }).unwrap(), input: serde_json::to_value(&EchoToolInput { text: "def".into() }).unwrap(),
is_input_complete: true, is_input_complete: true,
thought_signature: None,
}; };
let tool_result = LanguageModelToolResult { let tool_result = LanguageModelToolResult {
tool_use_id: "tool_id_1".into(), tool_use_id: "tool_id_1".into(),
@@ -941,7 +933,7 @@ async fn test_profiles(cx: &mut TestAppContext) {
// Test that test-1 profile (default) has echo and delay tools // Test that test-1 profile (default) has echo and delay tools
thread thread
.update(cx, |thread, cx| { .update(cx, |thread, cx| {
thread.set_profile(AgentProfileId("test-1".into()), cx); thread.set_profile(AgentProfileId("test-1".into()));
thread.send(UserMessageId::new(), ["test"], cx) thread.send(UserMessageId::new(), ["test"], cx)
}) })
.unwrap(); .unwrap();
@@ -961,7 +953,7 @@ async fn test_profiles(cx: &mut TestAppContext) {
// Switch to test-2 profile, and verify that it has only the infinite tool. // Switch to test-2 profile, and verify that it has only the infinite tool.
thread thread
.update(cx, |thread, cx| { .update(cx, |thread, cx| {
thread.set_profile(AgentProfileId("test-2".into()), cx); thread.set_profile(AgentProfileId("test-2".into()));
thread.send(UserMessageId::new(), ["test2"], cx) thread.send(UserMessageId::new(), ["test2"], cx)
}) })
.unwrap(); .unwrap();
@@ -1010,8 +1002,8 @@ async fn test_mcp_tools(cx: &mut TestAppContext) {
) )
.await; .await;
cx.run_until_parked(); cx.run_until_parked();
thread.update(cx, |thread, cx| { thread.update(cx, |thread, _| {
thread.set_profile(AgentProfileId("test".into()), cx) thread.set_profile(AgentProfileId("test".into()))
}); });
let mut mcp_tool_calls = setup_context_server( let mut mcp_tool_calls = setup_context_server(
@@ -1045,7 +1037,6 @@ async fn test_mcp_tools(cx: &mut TestAppContext) {
raw_input: json!({"text": "test"}).to_string(), raw_input: json!({"text": "test"}).to_string(),
input: json!({"text": "test"}), input: json!({"text": "test"}),
is_input_complete: true, is_input_complete: true,
thought_signature: None,
}, },
)); ));
fake_model.end_last_completion_stream(); fake_model.end_last_completion_stream();
@@ -1089,7 +1080,6 @@ async fn test_mcp_tools(cx: &mut TestAppContext) {
raw_input: json!({"text": "mcp"}).to_string(), raw_input: json!({"text": "mcp"}).to_string(),
input: json!({"text": "mcp"}), input: json!({"text": "mcp"}),
is_input_complete: true, is_input_complete: true,
thought_signature: None,
}, },
)); ));
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
@@ -1099,7 +1089,6 @@ async fn test_mcp_tools(cx: &mut TestAppContext) {
raw_input: json!({"text": "native"}).to_string(), raw_input: json!({"text": "native"}).to_string(),
input: json!({"text": "native"}), input: json!({"text": "native"}),
is_input_complete: true, is_input_complete: true,
thought_signature: None,
}, },
)); ));
fake_model.end_last_completion_stream(); fake_model.end_last_completion_stream();
@@ -1180,8 +1169,8 @@ async fn test_mcp_tool_truncation(cx: &mut TestAppContext) {
.await; .await;
cx.run_until_parked(); cx.run_until_parked();
thread.update(cx, |thread, cx| { thread.update(cx, |thread, _| {
thread.set_profile(AgentProfileId("test".into()), cx); thread.set_profile(AgentProfileId("test".into()));
thread.add_tool(EchoTool); thread.add_tool(EchoTool);
thread.add_tool(DelayTool); thread.add_tool(DelayTool);
thread.add_tool(WordListTool); thread.add_tool(WordListTool);
@@ -1799,7 +1788,6 @@ async fn test_building_request_with_pending_tools(cx: &mut TestAppContext) {
raw_input: "{}".into(), raw_input: "{}".into(),
input: json!({}), input: json!({}),
is_input_complete: true, is_input_complete: true,
thought_signature: None,
}; };
let echo_tool_use = LanguageModelToolUse { let echo_tool_use = LanguageModelToolUse {
id: "tool_id_2".into(), id: "tool_id_2".into(),
@@ -1807,7 +1795,6 @@ async fn test_building_request_with_pending_tools(cx: &mut TestAppContext) {
raw_input: json!({"text": "test"}).to_string(), raw_input: json!({"text": "test"}).to_string(),
input: json!({"text": "test"}), input: json!({"text": "test"}),
is_input_complete: true, is_input_complete: true,
thought_signature: None,
}; };
fake_model.send_last_completion_stream_text_chunk("Hi!"); fake_model.send_last_completion_stream_text_chunk("Hi!");
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
@@ -1864,6 +1851,7 @@ async fn test_agent_connection(cx: &mut TestAppContext) {
// Initialize language model system with test provider // Initialize language model system with test provider
cx.update(|cx| { cx.update(|cx| {
gpui_tokio::init(cx); gpui_tokio::init(cx);
client::init_settings(cx);
let http_client = FakeHttpClient::with_404_response(); let http_client = FakeHttpClient::with_404_response();
let clock = Arc::new(clock::FakeSystemClock::new()); let clock = Arc::new(clock::FakeSystemClock::new());
@@ -1871,7 +1859,9 @@ async fn test_agent_connection(cx: &mut TestAppContext) {
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
language_model::init(client.clone(), cx); language_model::init(client.clone(), cx);
language_models::init(user_store, client.clone(), cx); language_models::init(user_store, client.clone(), cx);
Project::init_settings(cx);
LanguageModelRegistry::test(cx); LanguageModelRegistry::test(cx);
agent_settings::init(cx);
}); });
cx.executor().forbid_parking(); cx.executor().forbid_parking();
@@ -2013,7 +2003,6 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
raw_input: input.to_string(), raw_input: input.to_string(),
input, input,
is_input_complete: false, is_input_complete: false,
thought_signature: None,
}, },
)); ));
@@ -2026,7 +2015,6 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
raw_input: input.to_string(), raw_input: input.to_string(),
input, input,
is_input_complete: true, is_input_complete: true,
thought_signature: None,
}, },
)); ));
fake_model.end_last_completion_stream(); fake_model.end_last_completion_stream();
@@ -2229,7 +2217,6 @@ async fn test_send_retry_finishes_tool_calls_on_error(cx: &mut TestAppContext) {
raw_input: json!({"text": "test"}).to_string(), raw_input: json!({"text": "test"}).to_string(),
input: json!({"text": "test"}), input: json!({"text": "test"}),
is_input_complete: true, is_input_complete: true,
thought_signature: None,
}; };
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
tool_use_1.clone(), tool_use_1.clone(),
@@ -2408,6 +2395,8 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest {
cx.update(|cx| { cx.update(|cx| {
settings::init(cx); settings::init(cx);
Project::init_settings(cx);
agent_settings::init(cx);
match model { match model {
TestModel::Fake => {} TestModel::Fake => {}
@@ -2415,6 +2404,7 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest {
gpui_tokio::init(cx); gpui_tokio::init(cx);
let http_client = ReqwestClient::user_agent("agent tests").unwrap(); let http_client = ReqwestClient::user_agent("agent tests").unwrap();
cx.set_http_client(Arc::new(http_client)); cx.set_http_client(Arc::new(http_client));
client::init_settings(cx);
let client = Client::production(cx); let client = Client::production(cx);
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
language_model::init(client.clone(), cx); language_model::init(client.clone(), cx);

View File

@@ -30,17 +30,16 @@ use gpui::{
}; };
use language_model::{ use language_model::{
LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelExt, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelExt,
LanguageModelId, LanguageModelImage, LanguageModelProviderId, LanguageModelRegistry, LanguageModelImage, LanguageModelProviderId, LanguageModelRegistry, LanguageModelRequest,
LanguageModelRequest, LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
LanguageModelToolResult, LanguageModelToolResultContent, LanguageModelToolSchemaFormat, LanguageModelToolResultContent, LanguageModelToolSchemaFormat, LanguageModelToolUse,
LanguageModelToolUse, LanguageModelToolUseId, Role, SelectedModel, StopReason, TokenUsage, LanguageModelToolUseId, Role, SelectedModel, StopReason, TokenUsage, ZED_CLOUD_PROVIDER_ID,
ZED_CLOUD_PROVIDER_ID,
}; };
use project::Project; use project::Project;
use prompt_store::ProjectContext; use prompt_store::ProjectContext;
use schemars::{JsonSchema, Schema}; use schemars::{JsonSchema, Schema};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use settings::{LanguageModelSelection, Settings, update_settings_file}; use settings::{Settings, update_settings_file};
use smol::stream::StreamExt; use smol::stream::StreamExt;
use std::{ use std::{
collections::BTreeMap, collections::BTreeMap,
@@ -607,8 +606,6 @@ pub struct Thread {
pub(crate) prompt_capabilities_rx: watch::Receiver<acp::PromptCapabilities>, pub(crate) prompt_capabilities_rx: watch::Receiver<acp::PromptCapabilities>,
pub(crate) project: Entity<Project>, pub(crate) project: Entity<Project>,
pub(crate) action_log: Entity<ActionLog>, pub(crate) action_log: Entity<ActionLog>,
/// Tracks the last time files were read by the agent, to detect external modifications
pub(crate) file_read_times: HashMap<PathBuf, fs::MTime>,
} }
impl Thread { impl Thread {
@@ -667,7 +664,6 @@ impl Thread {
prompt_capabilities_rx, prompt_capabilities_rx,
project, project,
action_log, action_log,
file_read_times: HashMap::default(),
} }
} }
@@ -802,8 +798,7 @@ impl Thread {
let profile_id = db_thread let profile_id = db_thread
.profile .profile
.unwrap_or_else(|| AgentSettings::get_global(cx).default_profile.clone()); .unwrap_or_else(|| AgentSettings::get_global(cx).default_profile.clone());
let model = LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
let mut model = LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
db_thread db_thread
.model .model
.and_then(|model| { .and_then(|model| {
@@ -816,16 +811,6 @@ impl Thread {
.or_else(|| registry.default_model()) .or_else(|| registry.default_model())
.map(|model| model.model) .map(|model| model.model)
}); });
if model.is_none() {
model = Self::resolve_profile_model(&profile_id, cx);
}
if model.is_none() {
model = LanguageModelRegistry::global(cx).update(cx, |registry, _cx| {
registry.default_model().map(|model| model.model)
});
}
let (prompt_capabilities_tx, prompt_capabilities_rx) = let (prompt_capabilities_tx, prompt_capabilities_rx) =
watch::channel(Self::prompt_capabilities(model.as_deref())); watch::channel(Self::prompt_capabilities(model.as_deref()));
@@ -863,7 +848,6 @@ impl Thread {
updated_at: db_thread.updated_at, updated_at: db_thread.updated_at,
prompt_capabilities_tx, prompt_capabilities_tx,
prompt_capabilities_rx, prompt_capabilities_rx,
file_read_times: HashMap::default(),
} }
} }
@@ -1003,7 +987,6 @@ impl Thread {
self.add_tool(NowTool); self.add_tool(NowTool);
self.add_tool(OpenTool::new(self.project.clone())); self.add_tool(OpenTool::new(self.project.clone()));
self.add_tool(ReadFileTool::new( self.add_tool(ReadFileTool::new(
cx.weak_entity(),
self.project.clone(), self.project.clone(),
self.action_log.clone(), self.action_log.clone(),
)); ));
@@ -1024,17 +1007,8 @@ impl Thread {
&self.profile_id &self.profile_id
} }
pub fn set_profile(&mut self, profile_id: AgentProfileId, cx: &mut Context<Self>) { pub fn set_profile(&mut self, profile_id: AgentProfileId) {
if self.profile_id == profile_id {
return;
}
self.profile_id = profile_id; self.profile_id = profile_id;
// Swap to the profile's preferred model when available.
if let Some(model) = Self::resolve_profile_model(&self.profile_id, cx) {
self.set_model(model, cx);
}
} }
pub fn cancel(&mut self, cx: &mut Context<Self>) { pub fn cancel(&mut self, cx: &mut Context<Self>) {
@@ -1091,35 +1065,6 @@ impl Thread {
}) })
} }
/// Look up the active profile and resolve its preferred model if one is configured.
fn resolve_profile_model(
profile_id: &AgentProfileId,
cx: &mut Context<Self>,
) -> Option<Arc<dyn LanguageModel>> {
let selection = AgentSettings::get_global(cx)
.profiles
.get(profile_id)?
.default_model
.clone()?;
Self::resolve_model_from_selection(&selection, cx)
}
/// Translate a stored model selection into the configured model from the registry.
fn resolve_model_from_selection(
selection: &LanguageModelSelection,
cx: &mut Context<Self>,
) -> Option<Arc<dyn LanguageModel>> {
let selected = SelectedModel {
provider: LanguageModelProviderId::from(selection.provider.0.clone()),
model: LanguageModelId::from(selection.model.clone()),
};
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
registry
.select_model(&selected, cx)
.map(|configured| configured.model)
})
}
pub fn resume( pub fn resume(
&mut self, &mut self,
cx: &mut Context<Self>, cx: &mut Context<Self>,
@@ -2194,7 +2139,7 @@ where
/// Returns the JSON schema that describes the tool's input. /// Returns the JSON schema that describes the tool's input.
fn input_schema(format: LanguageModelToolSchemaFormat) -> Schema { fn input_schema(format: LanguageModelToolSchemaFormat) -> Schema {
language_model::tool_schema::root_schema_for::<Self::Input>(format) crate::tool_schema::root_schema_for::<Self::Input>(format)
} }
/// Some tools rely on a provider for the underlying billing or other reasons. /// Some tools rely on a provider for the underlying billing or other reasons.
@@ -2281,7 +2226,7 @@ where
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> { fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
let mut json = serde_json::to_value(T::input_schema(format))?; let mut json = serde_json::to_value(T::input_schema(format))?;
language_model::tool_schema::adapt_schema_to_format(&mut json, format)?; crate::tool_schema::adapt_schema_to_format(&mut json, format)?;
Ok(json) Ok(json)
} }

View File

@@ -1,4 +1,5 @@
use anyhow::Result; use anyhow::Result;
use language_model::LanguageModelToolSchemaFormat;
use schemars::{ use schemars::{
JsonSchema, Schema, JsonSchema, Schema,
generate::SchemaSettings, generate::SchemaSettings,
@@ -6,16 +7,7 @@ use schemars::{
}; };
use serde_json::Value; use serde_json::Value;
/// Indicates the format used to define the input schema for a language model tool. pub(crate) fn root_schema_for<T: JsonSchema>(format: LanguageModelToolSchemaFormat) -> Schema {
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
pub enum LanguageModelToolSchemaFormat {
/// A JSON schema, see https://json-schema.org
JsonSchema,
/// A subset of an OpenAPI 3.0 schema object supported by Google AI, see https://ai.google.dev/api/caching#Schema
JsonSchemaSubset,
}
pub fn root_schema_for<T: JsonSchema>(format: LanguageModelToolSchemaFormat) -> Schema {
let mut generator = match format { let mut generator = match format {
LanguageModelToolSchemaFormat::JsonSchema => SchemaSettings::draft07().into_generator(), LanguageModelToolSchemaFormat::JsonSchema => SchemaSettings::draft07().into_generator(),
LanguageModelToolSchemaFormat::JsonSchemaSubset => SchemaSettings::openapi3() LanguageModelToolSchemaFormat::JsonSchemaSubset => SchemaSettings::openapi3()

View File

@@ -165,7 +165,7 @@ impl AnyAgentTool for ContextServerTool {
format: language_model::LanguageModelToolSchemaFormat, format: language_model::LanguageModelToolSchemaFormat,
) -> Result<serde_json::Value> { ) -> Result<serde_json::Value> {
let mut schema = self.tool.input_schema.clone(); let mut schema = self.tool.input_schema.clone();
language_model::tool_schema::adapt_schema_to_format(&mut schema, format)?; crate::tool_schema::adapt_schema_to_format(&mut schema, format)?;
Ok(match schema { Ok(match schema {
serde_json::Value::Null => { serde_json::Value::Null => {
serde_json::json!({ "type": "object", "properties": [] }) serde_json::json!({ "type": "object", "properties": [] })

View File

@@ -309,40 +309,6 @@ impl AgentTool for EditFileTool {
})? })?
.await?; .await?;
// Check if the file has been modified since the agent last read it
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 = thread.file_read_times.get(abs_path).copied();
let current = buffer.read(cx).file().and_then(|file| file.disk_state().mtime());
let dirty = buffer.read(cx).is_dirty();
(last_read, current, dirty)
})?;
// Check for unsaved changes first - these indicate modifications we don't know about
if is_dirty {
anyhow::bail!(
"This file cannot be written to because it has unsaved changes. \
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. \
Ask the user to save that buffer's changes and to inform you when it's ok to proceed."
);
}
// Check if the file was modified on disk since we last read it
if let (Some(last_read), Some(current)) = (last_read_mtime, current_mtime) {
// MTime can be unreliable for comparisons, so our newtype intentionally
// doesn't support comparing them. If the mtime at all different
// (which could be because of a modification or because e.g. system clock changed),
// we pessimistically assume it was modified.
if current != last_read {
anyhow::bail!(
"The file {} has been modified since you last read it. \
Please read the file again to get the current state before editing it.",
input.path.display()
);
}
}
}
let diff = cx.new(|cx| Diff::new(buffer.clone(), cx))?; let diff = cx.new(|cx| Diff::new(buffer.clone(), cx))?;
event_stream.update_diff(diff.clone()); event_stream.update_diff(diff.clone());
let _finalize_diff = util::defer({ let _finalize_diff = util::defer({
@@ -455,17 +421,6 @@ impl AgentTool for EditFileTool {
log.buffer_edited(buffer.clone(), cx); log.buffer_edited(buffer.clone(), cx);
})?; })?;
// Update the recorded read time after a successful edit so consecutive edits work
if let Some(abs_path) = abs_path.as_ref() {
if let Some(new_mtime) = buffer.read_with(cx, |buffer, _| {
buffer.file().and_then(|file| file.disk_state().mtime())
})? {
self.thread.update(cx, |thread, _| {
thread.file_read_times.insert(abs_path.to_path_buf(), new_mtime);
})?;
}
}
let new_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; let new_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
let (new_text, unified_diff) = cx let (new_text, unified_diff) = cx
.background_spawn({ .background_spawn({
@@ -607,6 +562,7 @@ fn resolve_path(
mod tests { mod tests {
use super::*; use super::*;
use crate::{ContextServerRegistry, Templates}; use crate::{ContextServerRegistry, Templates};
use client::TelemetrySettings;
use fs::Fs; use fs::Fs;
use gpui::{TestAppContext, UpdateGlobal}; use gpui::{TestAppContext, UpdateGlobal};
use language_model::fake_provider::FakeLanguageModel; use language_model::fake_provider::FakeLanguageModel;
@@ -1793,426 +1749,14 @@ mod tests {
} }
} }
#[gpui::test]
async fn test_file_read_times_tracking(cx: &mut TestAppContext) {
init_test(cx);
let fs = project::FakeFs::new(cx.executor());
fs.insert_tree(
"/root",
json!({
"test.txt": "original content"
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
Templates::new(),
Some(model.clone()),
cx,
)
});
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
// Initially, file_read_times should be empty
let is_empty = thread.read_with(cx, |thread, _| thread.file_read_times.is_empty());
assert!(is_empty, "file_read_times should start empty");
// Create read tool
let read_tool = Arc::new(crate::ReadFileTool::new(
thread.downgrade(),
project.clone(),
action_log,
));
// Read the file to record the read time
cx.update(|cx| {
read_tool.clone().run(
crate::ReadFileToolInput {
path: "root/test.txt".to_string(),
start_line: None,
end_line: None,
},
ToolCallEventStream::test().0,
cx,
)
})
.await
.unwrap();
// Verify that file_read_times now contains an entry for the file
let has_entry = thread.read_with(cx, |thread, _| {
thread.file_read_times.len() == 1
&& thread
.file_read_times
.keys()
.any(|path| path.ends_with("test.txt"))
});
assert!(
has_entry,
"file_read_times should contain an entry after reading the file"
);
// Read the file again - should update the entry
cx.update(|cx| {
read_tool.clone().run(
crate::ReadFileToolInput {
path: "root/test.txt".to_string(),
start_line: None,
end_line: None,
},
ToolCallEventStream::test().0,
cx,
)
})
.await
.unwrap();
// Should still have exactly one entry
let has_one_entry = thread.read_with(cx, |thread, _| thread.file_read_times.len() == 1);
assert!(
has_one_entry,
"file_read_times should still have one entry after re-reading"
);
}
fn init_test(cx: &mut TestAppContext) { fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| { cx.update(|cx| {
let settings_store = SettingsStore::test(cx); let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store); cx.set_global(settings_store);
language::init(cx);
TelemetrySettings::register(cx);
agent_settings::AgentSettings::register(cx);
Project::init_settings(cx);
}); });
} }
#[gpui::test]
async fn test_consecutive_edits_work(cx: &mut TestAppContext) {
init_test(cx);
let fs = project::FakeFs::new(cx.executor());
fs.insert_tree(
"/root",
json!({
"test.txt": "original content"
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
Templates::new(),
Some(model.clone()),
cx,
)
});
let languages = project.read_with(cx, |project, _| project.languages().clone());
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
let read_tool = Arc::new(crate::ReadFileTool::new(
thread.downgrade(),
project.clone(),
action_log,
));
let edit_tool = Arc::new(EditFileTool::new(
project.clone(),
thread.downgrade(),
languages,
Templates::new(),
));
// Read the file first
cx.update(|cx| {
read_tool.clone().run(
crate::ReadFileToolInput {
path: "root/test.txt".to_string(),
start_line: None,
end_line: None,
},
ToolCallEventStream::test().0,
cx,
)
})
.await
.unwrap();
// First edit should work
let edit_result = {
let edit_task = cx.update(|cx| {
edit_tool.clone().run(
EditFileToolInput {
display_description: "First edit".into(),
path: "root/test.txt".into(),
mode: EditFileMode::Edit,
},
ToolCallEventStream::test().0,
cx,
)
});
cx.executor().run_until_parked();
model.send_last_completion_stream_text_chunk(
"<old_text>original content</old_text><new_text>modified content</new_text>"
.to_string(),
);
model.end_last_completion_stream();
edit_task.await
};
assert!(
edit_result.is_ok(),
"First edit should succeed, got error: {:?}",
edit_result.as_ref().err()
);
// Second edit should also work because the edit updated the recorded read time
let edit_result = {
let edit_task = cx.update(|cx| {
edit_tool.clone().run(
EditFileToolInput {
display_description: "Second edit".into(),
path: "root/test.txt".into(),
mode: EditFileMode::Edit,
},
ToolCallEventStream::test().0,
cx,
)
});
cx.executor().run_until_parked();
model.send_last_completion_stream_text_chunk(
"<old_text>modified content</old_text><new_text>further modified content</new_text>".to_string(),
);
model.end_last_completion_stream();
edit_task.await
};
assert!(
edit_result.is_ok(),
"Second consecutive edit should succeed, got error: {:?}",
edit_result.as_ref().err()
);
}
#[gpui::test]
async fn test_external_modification_detected(cx: &mut TestAppContext) {
init_test(cx);
let fs = project::FakeFs::new(cx.executor());
fs.insert_tree(
"/root",
json!({
"test.txt": "original content"
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
Templates::new(),
Some(model.clone()),
cx,
)
});
let languages = project.read_with(cx, |project, _| project.languages().clone());
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
let read_tool = Arc::new(crate::ReadFileTool::new(
thread.downgrade(),
project.clone(),
action_log,
));
let edit_tool = Arc::new(EditFileTool::new(
project.clone(),
thread.downgrade(),
languages,
Templates::new(),
));
// Read the file first
cx.update(|cx| {
read_tool.clone().run(
crate::ReadFileToolInput {
path: "root/test.txt".to_string(),
start_line: None,
end_line: None,
},
ToolCallEventStream::test().0,
cx,
)
})
.await
.unwrap();
// Simulate external modification - advance time and save file
cx.background_executor
.advance_clock(std::time::Duration::from_secs(2));
fs.save(
path!("/root/test.txt").as_ref(),
&"externally modified content".into(),
language::LineEnding::Unix,
)
.await
.unwrap();
// Reload the buffer to pick up the new mtime
let project_path = project
.read_with(cx, |project, cx| {
project.find_project_path("root/test.txt", cx)
})
.expect("Should find project path");
let buffer = project
.update(cx, |project, cx| project.open_buffer(project_path, cx))
.await
.unwrap();
buffer
.update(cx, |buffer, cx| buffer.reload(cx))
.await
.unwrap();
cx.executor().run_until_parked();
// Try to edit - should fail because file was modified externally
let result = cx
.update(|cx| {
edit_tool.clone().run(
EditFileToolInput {
display_description: "Edit after external change".into(),
path: "root/test.txt".into(),
mode: EditFileMode::Edit,
},
ToolCallEventStream::test().0,
cx,
)
})
.await;
assert!(
result.is_err(),
"Edit should fail after external modification"
);
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("has been modified since you last read it"),
"Error should mention file modification, got: {}",
error_msg
);
}
#[gpui::test]
async fn test_dirty_buffer_detected(cx: &mut TestAppContext) {
init_test(cx);
let fs = project::FakeFs::new(cx.executor());
fs.insert_tree(
"/root",
json!({
"test.txt": "original content"
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
Templates::new(),
Some(model.clone()),
cx,
)
});
let languages = project.read_with(cx, |project, _| project.languages().clone());
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
let read_tool = Arc::new(crate::ReadFileTool::new(
thread.downgrade(),
project.clone(),
action_log,
));
let edit_tool = Arc::new(EditFileTool::new(
project.clone(),
thread.downgrade(),
languages,
Templates::new(),
));
// Read the file first
cx.update(|cx| {
read_tool.clone().run(
crate::ReadFileToolInput {
path: "root/test.txt".to_string(),
start_line: None,
end_line: None,
},
ToolCallEventStream::test().0,
cx,
)
})
.await
.unwrap();
// Open the buffer and make it dirty by editing without saving
let project_path = project
.read_with(cx, |project, cx| {
project.find_project_path("root/test.txt", cx)
})
.expect("Should find project path");
let buffer = project
.update(cx, |project, cx| project.open_buffer(project_path, cx))
.await
.unwrap();
// Make an in-memory edit to the buffer (making it dirty)
buffer.update(cx, |buffer, cx| {
let end_point = buffer.max_point();
buffer.edit([(end_point..end_point, " added text")], None, cx);
});
// Verify buffer is dirty
let is_dirty = buffer.read_with(cx, |buffer, _| buffer.is_dirty());
assert!(is_dirty, "Buffer should be dirty after in-memory edit");
// Try to edit - should fail because buffer has unsaved changes
let result = cx
.update(|cx| {
edit_tool.clone().run(
EditFileToolInput {
display_description: "Edit with dirty buffer".into(),
path: "root/test.txt".into(),
mode: EditFileMode::Edit,
},
ToolCallEventStream::test().0,
cx,
)
})
.await;
assert!(result.is_err(), "Edit should fail when buffer is dirty");
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("cannot be written to because it has unsaved changes"),
"Error should mention unsaved changes, got: {}",
error_msg
);
}
} }

View File

@@ -246,6 +246,8 @@ mod test {
cx.update(|cx| { cx.update(|cx| {
let settings_store = SettingsStore::test(cx); let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store); cx.set_global(settings_store);
language::init(cx);
Project::init_settings(cx);
}); });
} }
} }

View File

@@ -778,6 +778,8 @@ mod tests {
cx.update(|cx| { cx.update(|cx| {
let settings_store = SettingsStore::test(cx); let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store); cx.set_global(settings_store);
language::init(cx);
Project::init_settings(cx);
}); });
} }

View File

@@ -223,6 +223,8 @@ mod tests {
cx.update(|cx| { cx.update(|cx| {
let settings_store = SettingsStore::test(cx); let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store); cx.set_global(settings_store);
language::init(cx);
Project::init_settings(cx);
}); });
} }

View File

@@ -163,6 +163,8 @@ mod tests {
cx.update(|cx| { cx.update(|cx| {
let settings_store = SettingsStore::test(cx); let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store); cx.set_global(settings_store);
language::init(cx);
Project::init_settings(cx);
}); });
} }
} }

View File

@@ -1,7 +1,7 @@
use action_log::ActionLog; use action_log::ActionLog;
use agent_client_protocol::{self as acp, ToolCallUpdateFields}; use agent_client_protocol::{self as acp, ToolCallUpdateFields};
use anyhow::{Context as _, Result, anyhow}; use anyhow::{Context as _, Result, anyhow};
use gpui::{App, Entity, SharedString, Task, WeakEntity}; use gpui::{App, Entity, SharedString, Task};
use indoc::formatdoc; use indoc::formatdoc;
use language::Point; use language::Point;
use language_model::{LanguageModelImage, LanguageModelToolResultContent}; use language_model::{LanguageModelImage, LanguageModelToolResultContent};
@@ -12,7 +12,7 @@ use settings::Settings;
use std::sync::Arc; use std::sync::Arc;
use util::markdown::MarkdownCodeBlock; use util::markdown::MarkdownCodeBlock;
use crate::{AgentTool, Thread, ToolCallEventStream, outline}; use crate::{AgentTool, ToolCallEventStream, outline};
/// Reads the content of the given file in the project. /// Reads the content of the given file in the project.
/// ///
@@ -42,19 +42,13 @@ pub struct ReadFileToolInput {
} }
pub struct ReadFileTool { pub struct ReadFileTool {
thread: WeakEntity<Thread>,
project: Entity<Project>, project: Entity<Project>,
action_log: Entity<ActionLog>, action_log: Entity<ActionLog>,
} }
impl ReadFileTool { impl ReadFileTool {
pub fn new( pub fn new(project: Entity<Project>, action_log: Entity<ActionLog>) -> Self {
thread: WeakEntity<Thread>,
project: Entity<Project>,
action_log: Entity<ActionLog>,
) -> Self {
Self { Self {
thread,
project, project,
action_log, action_log,
} }
@@ -201,17 +195,6 @@ impl AgentTool for ReadFileTool {
anyhow::bail!("{file_path} not found"); anyhow::bail!("{file_path} not found");
} }
// Record the file read time and mtime
if let Some(mtime) = buffer.read_with(cx, |buffer, _| {
buffer.file().and_then(|file| file.disk_state().mtime())
})? {
self.thread
.update(cx, |thread, _| {
thread.file_read_times.insert(abs_path.to_path_buf(), mtime);
})
.ok();
}
let mut anchor = None; let mut anchor = None;
// Check if specific line ranges are provided // Check if specific line ranges are provided
@@ -302,15 +285,11 @@ impl AgentTool for ReadFileTool {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use crate::{ContextServerRegistry, Templates, Thread};
use gpui::{AppContext, TestAppContext, UpdateGlobal as _}; use gpui::{AppContext, TestAppContext, UpdateGlobal as _};
use language::{Language, LanguageConfig, LanguageMatcher, tree_sitter_rust}; use language::{Language, LanguageConfig, LanguageMatcher, tree_sitter_rust};
use language_model::fake_provider::FakeLanguageModel;
use project::{FakeFs, Project}; use project::{FakeFs, Project};
use prompt_store::ProjectContext;
use serde_json::json; use serde_json::json;
use settings::SettingsStore; use settings::SettingsStore;
use std::sync::Arc;
use util::path; use util::path;
#[gpui::test] #[gpui::test]
@@ -321,20 +300,7 @@ mod test {
fs.insert_tree(path!("/root"), json!({})).await; fs.insert_tree(path!("/root"), json!({})).await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone())); let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry = let tool = Arc::new(ReadFileTool::new(project, action_log));
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
Templates::new(),
Some(model),
cx,
)
});
let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log));
let (event_stream, _) = ToolCallEventStream::test(); let (event_stream, _) = ToolCallEventStream::test();
let result = cx let result = cx
@@ -367,20 +333,7 @@ mod test {
.await; .await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone())); let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry = let tool = Arc::new(ReadFileTool::new(project, action_log));
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
Templates::new(),
Some(model),
cx,
)
});
let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log));
let result = cx let result = cx
.update(|cx| { .update(|cx| {
let input = ReadFileToolInput { let input = ReadFileToolInput {
@@ -410,20 +363,7 @@ mod test {
let language_registry = project.read_with(cx, |project, _| project.languages().clone()); let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(Arc::new(rust_lang())); language_registry.add(Arc::new(rust_lang()));
let action_log = cx.new(|_| ActionLog::new(project.clone())); let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry = let tool = Arc::new(ReadFileTool::new(project, action_log));
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
Templates::new(),
Some(model),
cx,
)
});
let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log));
let result = cx let result = cx
.update(|cx| { .update(|cx| {
let input = ReadFileToolInput { let input = ReadFileToolInput {
@@ -495,20 +435,7 @@ mod test {
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone())); let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry = let tool = Arc::new(ReadFileTool::new(project, action_log));
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
Templates::new(),
Some(model),
cx,
)
});
let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log));
let result = cx let result = cx
.update(|cx| { .update(|cx| {
let input = ReadFileToolInput { let input = ReadFileToolInput {
@@ -536,20 +463,7 @@ mod test {
.await; .await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone())); let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry = let tool = Arc::new(ReadFileTool::new(project, action_log));
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
Templates::new(),
Some(model),
cx,
)
});
let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log));
// start_line of 0 should be treated as 1 // start_line of 0 should be treated as 1
let result = cx let result = cx
@@ -595,6 +509,8 @@ mod test {
cx.update(|cx| { cx.update(|cx| {
let settings_store = SettingsStore::test(cx); let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store); cx.set_global(settings_store);
language::init(cx);
Project::init_settings(cx);
}); });
} }
@@ -693,20 +609,7 @@ mod test {
let project = Project::test(fs.clone(), [path!("/project_root").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/project_root").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone())); let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry = let tool = Arc::new(ReadFileTool::new(project, action_log));
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
Templates::new(),
Some(model),
cx,
)
});
let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log));
// Reading a file outside the project worktree should fail // Reading a file outside the project worktree should fail
let result = cx let result = cx
@@ -920,24 +823,7 @@ mod test {
.await; .await;
let action_log = cx.new(|_| ActionLog::new(project.clone())); let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry = let tool = Arc::new(ReadFileTool::new(project.clone(), action_log.clone()));
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
Templates::new(),
Some(model),
cx,
)
});
let tool = Arc::new(ReadFileTool::new(
thread.downgrade(),
project.clone(),
action_log.clone(),
));
// Test reading allowed files in worktree1 // Test reading allowed files in worktree1
let result = cx let result = cx

View File

@@ -21,6 +21,7 @@ acp_tools.workspace = true
acp_thread.workspace = true acp_thread.workspace = true
action_log.workspace = true action_log.workspace = true
agent-client-protocol.workspace = true agent-client-protocol.workspace = true
agent_settings.workspace = true
anyhow.workspace = true anyhow.workspace = true
async-trait.workspace = true async-trait.workspace = true
client.workspace = true client.workspace = true
@@ -32,6 +33,7 @@ gpui.workspace = true
gpui_tokio = { workspace = true, optional = true } gpui_tokio = { workspace = true, optional = true }
http_client.workspace = true http_client.workspace = true
indoc.workspace = true indoc.workspace = true
language.workspace = true
language_model.workspace = true language_model.workspace = true
language_models.workspace = true language_models.workspace = true
log.workspace = true log.workspace = true

View File

@@ -29,13 +29,11 @@ pub struct UnsupportedVersion;
pub struct AcpConnection { pub struct AcpConnection {
server_name: SharedString, server_name: SharedString,
telemetry_id: &'static str,
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>,
agent_capabilities: acp::AgentCapabilities, agent_capabilities: acp::AgentCapabilities,
default_mode: Option<acp::SessionModeId>, default_mode: Option<acp::SessionModeId>,
default_model: Option<acp::ModelId>,
root_dir: PathBuf, root_dir: PathBuf,
// NB: Don't move this into the wait_task, since we need to ensure the process is // NB: Don't move this into the wait_task, since we need to ensure the process is
// killed on drop (setting kill_on_drop on the command seems to not always work). // killed on drop (setting kill_on_drop on the command seems to not always work).
@@ -54,21 +52,17 @@ 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>,
default_model: Option<acp::ModelId>,
is_remote: bool, is_remote: bool,
cx: &mut AsyncApp, cx: &mut AsyncApp,
) -> 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,
default_model,
is_remote, is_remote,
cx, cx,
) )
@@ -81,11 +75,9 @@ const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::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>,
default_model: Option<acp::ModelId>,
is_remote: bool, is_remote: bool,
cx: &mut AsyncApp, cx: &mut AsyncApp,
) -> Result<Self> { ) -> Result<Self> {
@@ -140,7 +132,7 @@ impl AcpConnection {
while let Ok(n) = stderr.read_line(&mut line).await while let Ok(n) = stderr.read_line(&mut line).await
&& n > 0 && n > 0
{ {
log::warn!("agent stderr: {}", line.trim()); log::warn!("agent stderr: {}", &line);
line.clear(); line.clear();
} }
Ok(()) Ok(())
@@ -207,11 +199,9 @@ impl AcpConnection {
root_dir: root_dir.to_owned(), root_dir: root_dir.to_owned(),
connection, connection,
server_name, server_name,
telemetry_id,
sessions, sessions,
agent_capabilities: response.agent_capabilities, agent_capabilities: response.agent_capabilities,
default_mode, default_mode,
default_model,
_io_task: io_task, _io_task: io_task,
_wait_task: wait_task, _wait_task: wait_task,
_stderr_task: stderr_task, _stderr_task: stderr_task,
@@ -236,10 +226,6 @@ impl Drop for AcpConnection {
} }
impl AgentConnection for AcpConnection { impl AgentConnection for AcpConnection {
fn telemetry_id(&self) -> &'static str {
self.telemetry_id
}
fn new_thread( fn new_thread(
self: Rc<Self>, self: Rc<Self>,
project: Entity<Project>, project: Entity<Project>,
@@ -250,61 +236,39 @@ impl AgentConnection for AcpConnection {
let conn = self.connection.clone(); let conn = self.connection.clone();
let sessions = self.sessions.clone(); let sessions = self.sessions.clone();
let default_mode = self.default_mode.clone(); let default_mode = self.default_mode.clone();
let default_model = self.default_model.clone();
let cwd = cwd.to_path_buf(); let cwd = cwd.to_path_buf();
let context_server_store = project.read(cx).context_server_store().read(cx); let context_server_store = project.read(cx).context_server_store().read(cx);
let mcp_servers = let mcp_servers = if project.read(cx).is_local() {
if project.read(cx).is_local() { context_server_store
context_server_store .configured_server_ids()
.configured_server_ids() .iter()
.iter() .filter_map(|id| {
.filter_map(|id| { let configuration = context_server_store.configuration_for_server(id)?;
let configuration = context_server_store.configuration_for_server(id)?; let command = configuration.command();
match &*configuration { Some(acp::McpServer::Stdio {
project::context_server_store::ContextServerConfiguration::Custom { name: id.0.to_string(),
command, command: command.path.clone(),
.. args: command.args.clone(),
} env: if let Some(env) = command.env.as_ref() {
| project::context_server_store::ContextServerConfiguration::Extension { env.iter()
command, .map(|(name, value)| acp::EnvVariable {
.. name: name.clone(),
} => Some(acp::McpServer::Stdio { value: value.clone(),
name: id.0.to_string(), meta: None,
command: command.path.clone(), })
args: command.args.clone(), .collect()
env: if let Some(env) = command.env.as_ref() { } else {
env.iter() vec![]
.map(|(name, value)| acp::EnvVariable { },
name: name.clone(),
value: value.clone(),
meta: None,
})
.collect()
} else {
vec![]
},
}),
project::context_server_store::ContextServerConfiguration::Http {
url,
headers,
} => Some(acp::McpServer::Http {
name: id.0.to_string(),
url: url.to_string(),
headers: headers.iter().map(|(name, value)| acp::HttpHeader {
name: name.clone(),
value: value.clone(),
meta: None,
}).collect(),
}),
}
}) })
.collect() })
} else { .collect()
// In SSH projects, the external agent is running on the remote } else {
// machine, and currently we only run MCP servers on the local // In SSH projects, the external agent is running on the remote
// machine. So don't pass any MCP servers to the agent in that case. // machine, and currently we only run MCP servers on the local
Vec::new() // machine. So don't pass any MCP servers to the agent in that case.
}; Vec::new()
};
cx.spawn(async move |cx| { cx.spawn(async move |cx| {
let response = conn let response = conn
@@ -339,7 +303,6 @@ impl AgentConnection for AcpConnection {
let default_mode = default_mode.clone(); let default_mode = default_mode.clone();
let session_id = response.session_id.clone(); let session_id = response.session_id.clone();
let modes = modes.clone(); let modes = modes.clone();
let conn = conn.clone();
async move |_| { async move |_| {
let result = conn.set_session_mode(acp::SetSessionModeRequest { let result = conn.set_session_mode(acp::SetSessionModeRequest {
session_id, session_id,
@@ -374,53 +337,6 @@ impl AgentConnection for AcpConnection {
} }
} }
if let Some(default_model) = default_model {
if let Some(models) = models.as_ref() {
let mut models_ref = models.borrow_mut();
let has_model = models_ref.available_models.iter().any(|model| model.model_id == default_model);
if has_model {
let initial_model_id = models_ref.current_model_id.clone();
cx.spawn({
let default_model = default_model.clone();
let session_id = response.session_id.clone();
let models = models.clone();
let conn = conn.clone();
async move |_| {
let result = conn.set_session_model(acp::SetSessionModelRequest {
session_id,
model_id: default_model,
meta: None,
})
.await.log_err();
if result.is_none() {
models.borrow_mut().current_model_id = initial_model_id;
}
}
}).detach();
models_ref.current_model_id = default_model;
} else {
let available_models = models_ref
.available_models
.iter()
.map(|model| format!("- `{}`: {}", model.model_id, model.name))
.collect::<Vec<_>>()
.join("\n");
log::warn!(
"`{default_model}` is not a valid {name} model. Available options:\n{available_models}",
);
}
} else {
log::warn!(
"`{name}` does not support model selection, but `default_model` was set in settings.",
);
}
}
let session_id = response.session_id; let session_id = response.session_id;
let action_log = cx.new(|_| ActionLog::new(project.clone()))?; let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
let thread = cx.new(|cx| { let thread = cx.new(|cx| {

View File

@@ -68,18 +68,6 @@ pub trait AgentServer: Send {
) { ) {
} }
fn default_model(&self, _cx: &mut App) -> Option<agent_client_protocol::ModelId> {
None
}
fn set_default_model(
&self,
_model_id: Option<agent_client_protocol::ModelId>,
_fs: Arc<dyn Fs>,
_cx: &mut App,
) {
}
fn connect( fn connect(
&self, &self,
root_dir: Option<&Path>, root_dir: Option<&Path>,

View File

@@ -55,27 +55,6 @@ impl AgentServer for ClaudeCode {
}); });
} }
fn default_model(&self, cx: &mut App) -> Option<acp::ModelId> {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<AllAgentServersSettings>(None).claude.clone()
});
settings
.as_ref()
.and_then(|s| s.default_model.clone().map(|m| acp::ModelId(m.into())))
}
fn set_default_model(&self, model_id: Option<acp::ModelId>, fs: Arc<dyn Fs>, cx: &mut App) {
update_settings_file(fs, cx, |settings, _| {
settings
.agent_servers
.get_or_insert_default()
.claude
.get_or_insert_default()
.default_model = model_id.map(|m| m.to_string())
});
}
fn connect( fn connect(
&self, &self,
root_dir: Option<&Path>, root_dir: Option<&Path>,
@@ -83,13 +62,11 @@ 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();
let extra_env = load_proxy_env(cx); let extra_env = load_proxy_env(cx);
let default_mode = self.default_mode(cx); let default_mode = self.default_mode(cx);
let default_model = self.default_model(cx);
cx.spawn(async move |cx| { cx.spawn(async move |cx| {
let (command, root_dir, login) = store let (command, root_dir, login) = store
@@ -108,11 +85,9 @@ 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,
default_model,
is_remote, is_remote,
cx, cx,
) )

View File

@@ -56,27 +56,6 @@ impl AgentServer for Codex {
}); });
} }
fn default_model(&self, cx: &mut App) -> Option<acp::ModelId> {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<AllAgentServersSettings>(None).codex.clone()
});
settings
.as_ref()
.and_then(|s| s.default_model.clone().map(|m| acp::ModelId(m.into())))
}
fn set_default_model(&self, model_id: Option<acp::ModelId>, fs: Arc<dyn Fs>, cx: &mut App) {
update_settings_file(fs, cx, |settings, _| {
settings
.agent_servers
.get_or_insert_default()
.codex
.get_or_insert_default()
.default_model = model_id.map(|m| m.to_string())
});
}
fn connect( fn connect(
&self, &self,
root_dir: Option<&Path>, root_dir: Option<&Path>,
@@ -84,13 +63,11 @@ 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();
let extra_env = load_proxy_env(cx); let extra_env = load_proxy_env(cx);
let default_mode = self.default_mode(cx); let default_mode = self.default_mode(cx);
let default_model = self.default_model(cx);
cx.spawn(async move |cx| { cx.spawn(async move |cx| {
let (command, root_dir, login) = store let (command, root_dir, login) = store
@@ -110,11 +87,9 @@ 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,
default_model,
is_remote, is_remote,
cx, cx,
) )

View File

@@ -50,42 +50,13 @@ impl crate::AgentServer for CustomAgentServer {
fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) { fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
let name = self.name(); let name = self.name();
update_settings_file(fs, cx, move |settings, _| { update_settings_file(fs, cx, move |settings, _| {
if let Some(settings) = settings
.agent_servers
.get_or_insert_default()
.custom
.get_mut(&name)
{
settings.default_mode = mode_id.map(|m| m.to_string())
}
});
}
fn default_model(&self, cx: &mut App) -> Option<acp::ModelId> {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings settings
.get::<AllAgentServersSettings>(None)
.custom
.get(&self.name())
.cloned()
});
settings
.as_ref()
.and_then(|s| s.default_model.clone().map(|m| acp::ModelId(m.into())))
}
fn set_default_model(&self, model_id: Option<acp::ModelId>, fs: Arc<dyn Fs>, cx: &mut App) {
let name = self.name();
update_settings_file(fs, cx, move |settings, _| {
if let Some(settings) = settings
.agent_servers .agent_servers
.get_or_insert_default() .get_or_insert_default()
.custom .custom
.get_mut(&name) .get_mut(&name)
{ .unwrap()
settings.default_model = model_id.map(|m| m.to_string()) .default_mode = mode_id.map(|m| m.to_string())
}
}); });
} }
@@ -96,11 +67,9 @@ 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 store = delegate.store.downgrade(); let store = delegate.store.downgrade();
let extra_env = load_proxy_env(cx); let extra_env = load_proxy_env(cx);
@@ -123,11 +92,9 @@ 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,
default_model,
is_remote, is_remote,
cx, cx,
) )

View File

@@ -6,9 +6,7 @@ use gpui::{AppContext, Entity, TestAppContext};
use indoc::indoc; use indoc::indoc;
#[cfg(test)] #[cfg(test)]
use project::agent_server_store::BuiltinAgentServerSettings; use project::agent_server_store::BuiltinAgentServerSettings;
use project::{FakeFs, Project}; use project::{FakeFs, Project, agent_server_store::AllAgentServersSettings};
#[cfg(test)]
use settings::Settings;
use std::{ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::Arc, sync::Arc,
@@ -454,29 +452,35 @@ pub use common_e2e_tests;
// Helpers // Helpers
pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> { pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
use settings::Settings;
env_logger::try_init().ok(); env_logger::try_init().ok();
cx.update(|cx| { cx.update(|cx| {
let settings_store = settings::SettingsStore::test(cx); let settings_store = settings::SettingsStore::test(cx);
cx.set_global(settings_store); cx.set_global(settings_store);
Project::init_settings(cx);
language::init(cx);
gpui_tokio::init(cx); gpui_tokio::init(cx);
let http_client = reqwest_client::ReqwestClient::user_agent("agent tests").unwrap(); let http_client = reqwest_client::ReqwestClient::user_agent("agent tests").unwrap();
cx.set_http_client(Arc::new(http_client)); cx.set_http_client(Arc::new(http_client));
client::init_settings(cx);
let client = client::Client::production(cx); let client = client::Client::production(cx);
let user_store = cx.new(|cx| client::UserStore::new(client.clone(), cx)); let user_store = cx.new(|cx| client::UserStore::new(client.clone(), cx));
language_model::init(client.clone(), cx); language_model::init(client.clone(), cx);
language_models::init(user_store, client, cx); language_models::init(user_store, client, cx);
agent_settings::init(cx);
AllAgentServersSettings::register(cx);
#[cfg(test)] #[cfg(test)]
project::agent_server_store::AllAgentServersSettings::override_global( AllAgentServersSettings::override_global(
project::agent_server_store::AllAgentServersSettings { AllAgentServersSettings {
claude: Some(BuiltinAgentServerSettings { claude: Some(BuiltinAgentServerSettings {
path: Some("claude-code-acp".into()), path: Some("claude-code-acp".into()),
args: None, args: None,
env: None, env: None,
ignore_system_version: None, ignore_system_version: None,
default_mode: None, default_mode: None,
default_model: None,
}), }),
gemini: Some(crate::gemini::tests::local_command().into()), gemini: Some(crate::gemini::tests::local_command().into()),
codex: Some(BuiltinAgentServerSettings { codex: Some(BuiltinAgentServerSettings {
@@ -485,7 +489,6 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
env: None, env: None,
ignore_system_version: None, ignore_system_version: None,
default_mode: None, default_mode: None,
default_model: None,
}), }),
custom: collections::HashMap::default(), custom: collections::HashMap::default(),
}, },

View File

@@ -31,13 +31,11 @@ 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();
let mut extra_env = load_proxy_env(cx); let mut extra_env = load_proxy_env(cx);
let default_mode = self.default_mode(cx); let default_mode = self.default_mode(cx);
let default_model = self.default_model(cx);
cx.spawn(async move |cx| { cx.spawn(async move |cx| {
extra_env.insert("SURFACE".to_owned(), "zed".to_owned()); extra_env.insert("SURFACE".to_owned(), "zed".to_owned());
@@ -66,11 +64,9 @@ 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,
default_model,
is_remote, is_remote,
cx, cx,
) )

View File

@@ -6,8 +6,8 @@ use convert_case::{Case, Casing as _};
use fs::Fs; use fs::Fs;
use gpui::{App, SharedString}; use gpui::{App, SharedString};
use settings::{ use settings::{
AgentProfileContent, ContextServerPresetContent, LanguageModelSelection, Settings as _, AgentProfileContent, ContextServerPresetContent, Settings as _, SettingsContent,
SettingsContent, update_settings_file, update_settings_file,
}; };
use util::ResultExt as _; use util::ResultExt as _;
@@ -53,30 +53,19 @@ impl AgentProfile {
let base_profile = let base_profile =
base_profile_id.and_then(|id| AgentSettings::get_global(cx).profiles.get(&id).cloned()); base_profile_id.and_then(|id| AgentSettings::get_global(cx).profiles.get(&id).cloned());
// Copy toggles from the base profile so the new profile starts with familiar defaults.
let tools = base_profile
.as_ref()
.map(|profile| profile.tools.clone())
.unwrap_or_default();
let enable_all_context_servers = base_profile
.as_ref()
.map(|profile| profile.enable_all_context_servers)
.unwrap_or_default();
let context_servers = base_profile
.as_ref()
.map(|profile| profile.context_servers.clone())
.unwrap_or_default();
// Preserve the base profile's model preference when cloning into a new profile.
let default_model = base_profile
.as_ref()
.and_then(|profile| profile.default_model.clone());
let profile_settings = AgentProfileSettings { let profile_settings = AgentProfileSettings {
name: name.into(), name: name.into(),
tools, tools: base_profile
enable_all_context_servers, .as_ref()
context_servers, .map(|profile| profile.tools.clone())
default_model, .unwrap_or_default(),
enable_all_context_servers: base_profile
.as_ref()
.map(|profile| profile.enable_all_context_servers)
.unwrap_or_default(),
context_servers: base_profile
.map(|profile| profile.context_servers)
.unwrap_or_default(),
}; };
update_settings_file(fs, cx, { update_settings_file(fs, cx, {
@@ -107,8 +96,6 @@ pub struct AgentProfileSettings {
pub tools: IndexMap<Arc<str>, bool>, pub tools: IndexMap<Arc<str>, bool>,
pub enable_all_context_servers: bool, pub enable_all_context_servers: bool,
pub context_servers: IndexMap<Arc<str>, ContextServerPreset>, pub context_servers: IndexMap<Arc<str>, ContextServerPreset>,
/// Default language model to apply when this profile becomes active.
pub default_model: Option<LanguageModelSelection>,
} }
impl AgentProfileSettings { impl AgentProfileSettings {
@@ -157,7 +144,6 @@ impl AgentProfileSettings {
) )
}) })
.collect(), .collect(),
default_model: self.default_model.clone(),
}, },
); );
@@ -167,23 +153,15 @@ impl AgentProfileSettings {
impl From<AgentProfileContent> for AgentProfileSettings { impl From<AgentProfileContent> for AgentProfileSettings {
fn from(content: AgentProfileContent) -> Self { fn from(content: AgentProfileContent) -> Self {
let AgentProfileContent {
name,
tools,
enable_all_context_servers,
context_servers,
default_model,
} = content;
Self { Self {
name: name.into(), name: content.name.into(),
tools, tools: content.tools,
enable_all_context_servers: enable_all_context_servers.unwrap_or_default(), enable_all_context_servers: content.enable_all_context_servers.unwrap_or_default(),
context_servers: context_servers context_servers: content
.context_servers
.into_iter() .into_iter()
.map(|(server_id, preset)| (server_id, preset.into())) .map(|(server_id, preset)| (server_id, preset.into()))
.collect(), .collect(),
default_model,
} }
} }
} }

View File

@@ -10,7 +10,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use settings::{ use settings::{
DefaultAgentView, DockPosition, LanguageModelParameters, LanguageModelSelection, DefaultAgentView, DockPosition, LanguageModelParameters, LanguageModelSelection,
NotifyWhenAgentWaiting, RegisterSetting, Settings, NotifyWhenAgentWaiting, Settings,
}; };
pub use crate::agent_profile::*; pub use crate::agent_profile::*;
@@ -19,7 +19,11 @@ pub const SUMMARIZE_THREAD_PROMPT: &str = include_str!("prompts/summarize_thread
pub const SUMMARIZE_THREAD_DETAILED_PROMPT: &str = pub const SUMMARIZE_THREAD_DETAILED_PROMPT: &str =
include_str!("prompts/summarize_thread_detailed_prompt.txt"); include_str!("prompts/summarize_thread_detailed_prompt.txt");
#[derive(Clone, Debug, RegisterSetting)] pub fn init(cx: &mut App) {
AgentSettings::register(cx);
}
#[derive(Clone, Debug)]
pub struct AgentSettings { pub struct AgentSettings {
pub enabled: bool, pub enabled: bool,
pub button: bool, pub button: bool,

View File

@@ -98,8 +98,6 @@ util.workspace = true
watch.workspace = true watch.workspace = true
workspace.workspace = true workspace.workspace = true
zed_actions.workspace = true zed_actions.workspace = true
image.workspace = true
async-fs.workspace = true
[dev-dependencies] [dev-dependencies]
acp_thread = { workspace = true, features = ["test-support"] } acp_thread = { workspace = true, features = ["test-support"] }

View File

@@ -109,8 +109,6 @@ impl ContextPickerCompletionProvider {
icon_path: Some(mode.icon().path().into()), icon_path: Some(mode.icon().path().into()),
documentation: None, documentation: None,
source: project::CompletionSource::Custom, source: project::CompletionSource::Custom,
match_start: None,
snippet_deduplication_key: None,
insert_text_mode: None, insert_text_mode: None,
// This ensures that when a user accepts this completion, the // This ensures that when a user accepts this completion, the
// completion menu will still be shown after "@category " is // completion menu will still be shown after "@category " is
@@ -148,8 +146,6 @@ impl ContextPickerCompletionProvider {
documentation: None, documentation: None,
insert_text_mode: None, insert_text_mode: None,
source: project::CompletionSource::Custom, source: project::CompletionSource::Custom,
match_start: None,
snippet_deduplication_key: None,
icon_path: Some(icon_for_completion), icon_path: Some(icon_for_completion),
confirm: Some(confirm_completion_callback( confirm: Some(confirm_completion_callback(
thread_entry.title().clone(), thread_entry.title().clone(),
@@ -181,8 +177,6 @@ impl ContextPickerCompletionProvider {
documentation: None, documentation: None,
insert_text_mode: None, insert_text_mode: None,
source: project::CompletionSource::Custom, source: project::CompletionSource::Custom,
match_start: None,
snippet_deduplication_key: None,
icon_path: Some(icon_path), icon_path: Some(icon_path),
confirm: Some(confirm_completion_callback( confirm: Some(confirm_completion_callback(
rule.title, rule.title,
@@ -239,8 +233,6 @@ impl ContextPickerCompletionProvider {
documentation: None, documentation: None,
source: project::CompletionSource::Custom, source: project::CompletionSource::Custom,
icon_path: Some(completion_icon_path), icon_path: Some(completion_icon_path),
match_start: None,
snippet_deduplication_key: None,
insert_text_mode: None, insert_text_mode: None,
confirm: Some(confirm_completion_callback( confirm: Some(confirm_completion_callback(
file_name, file_name,
@@ -292,8 +284,6 @@ impl ContextPickerCompletionProvider {
documentation: None, documentation: None,
source: project::CompletionSource::Custom, source: project::CompletionSource::Custom,
icon_path: Some(icon_path), icon_path: Some(icon_path),
match_start: None,
snippet_deduplication_key: None,
insert_text_mode: None, insert_text_mode: None,
confirm: Some(confirm_completion_callback( confirm: Some(confirm_completion_callback(
symbol.name.into(), symbol.name.into(),
@@ -326,8 +316,6 @@ impl ContextPickerCompletionProvider {
documentation: None, documentation: None,
source: project::CompletionSource::Custom, source: project::CompletionSource::Custom,
icon_path: Some(icon_path), icon_path: Some(icon_path),
match_start: None,
snippet_deduplication_key: None,
insert_text_mode: None, insert_text_mode: None,
confirm: Some(confirm_completion_callback( confirm: Some(confirm_completion_callback(
url_to_fetch.to_string().into(), url_to_fetch.to_string().into(),
@@ -396,8 +384,6 @@ impl ContextPickerCompletionProvider {
icon_path: Some(action.icon().path().into()), icon_path: Some(action.icon().path().into()),
documentation: None, documentation: None,
source: project::CompletionSource::Custom, source: project::CompletionSource::Custom,
match_start: None,
snippet_deduplication_key: None,
insert_text_mode: None, insert_text_mode: None,
// This ensures that when a user accepts this completion, the // This ensures that when a user accepts this completion, the
// completion menu will still be shown after "@category " is // completion menu will still be shown after "@category " is
@@ -660,14 +646,16 @@ impl ContextPickerCompletionProvider {
cx: &mut App, cx: &mut App,
) -> Vec<ContextPickerEntry> { ) -> Vec<ContextPickerEntry> {
let embedded_context = self.prompt_capabilities.borrow().embedded_context; let embedded_context = self.prompt_capabilities.borrow().embedded_context;
let mut entries = vec![ let mut entries = if embedded_context {
ContextPickerEntry::Mode(ContextPickerMode::File), vec![
ContextPickerEntry::Mode(ContextPickerMode::Symbol), ContextPickerEntry::Mode(ContextPickerMode::File),
]; ContextPickerEntry::Mode(ContextPickerMode::Symbol),
ContextPickerEntry::Mode(ContextPickerMode::Thread),
if embedded_context { ]
entries.push(ContextPickerEntry::Mode(ContextPickerMode::Thread)); } else {
} // File is always available, but we don't need a mode entry
vec![]
};
let has_selection = workspace let has_selection = workspace
.read(cx) .read(cx)
@@ -708,18 +696,14 @@ fn build_symbol_label(symbol_name: &str, file_name: &str, line: u32, cx: &App) -
} }
fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: &App) -> CodeLabel { fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: &App) -> CodeLabel {
let path = cx let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
.theme()
.syntax()
.highlight_id("variable")
.map(HighlightId);
let mut label = CodeLabelBuilder::default(); let mut label = CodeLabelBuilder::default();
label.push_str(file_name, None); label.push_str(file_name, None);
label.push_str(" ", None); label.push_str(" ", None);
if let Some(directory) = directory { if let Some(directory) = directory {
label.push_str(directory, path); label.push_str(directory, comment_id);
} }
label.build() label.build()
@@ -788,8 +772,6 @@ impl CompletionProvider for ContextPickerCompletionProvider {
)), )),
source: project::CompletionSource::Custom, source: project::CompletionSource::Custom,
icon_path: None, icon_path: None,
match_start: None,
snippet_deduplication_key: None,
insert_text_mode: None, insert_text_mode: None,
confirm: Some(Arc::new({ confirm: Some(Arc::new({
let editor = editor.clone(); let editor = editor.clone();

View File

@@ -401,9 +401,10 @@ mod tests {
use acp_thread::{AgentConnection, StubAgentConnection}; use acp_thread::{AgentConnection, StubAgentConnection};
use agent::HistoryStore; use agent::HistoryStore;
use agent_client_protocol as acp; use agent_client_protocol as acp;
use agent_settings::AgentSettings;
use assistant_text_thread::TextThreadStore; use assistant_text_thread::TextThreadStore;
use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind}; use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
use editor::RowInfo; use editor::{EditorSettings, RowInfo};
use fs::FakeFs; use fs::FakeFs;
use gpui::{AppContext as _, SemanticVersion, TestAppContext}; use gpui::{AppContext as _, SemanticVersion, TestAppContext};
@@ -412,7 +413,7 @@ mod tests {
use pretty_assertions::assert_matches; use pretty_assertions::assert_matches;
use project::Project; use project::Project;
use serde_json::json; use serde_json::json;
use settings::SettingsStore; use settings::{Settings as _, SettingsStore};
use util::path; use util::path;
use workspace::Workspace; use workspace::Workspace;
@@ -538,8 +539,13 @@ mod tests {
cx.update(|cx| { cx.update(|cx| {
let settings_store = SettingsStore::test(cx); let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store); cx.set_global(settings_store);
language::init(cx);
Project::init_settings(cx);
AgentSettings::register(cx);
workspace::init_settings(cx);
theme::init(theme::LoadThemes::JustBase, cx); theme::init(theme::LoadThemes::JustBase, cx);
release_channel::init(SemanticVersion::default(), cx); release_channel::init(SemanticVersion::default(), cx);
EditorSettings::register(cx);
}); });
} }
} }

View File

@@ -13,9 +13,8 @@ use collections::{HashMap, HashSet};
use editor::{ use editor::{
Addon, Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, Addon, Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement,
EditorEvent, EditorMode, EditorSnapshot, EditorStyle, ExcerptId, FoldPlaceholder, Inlay, EditorEvent, EditorMode, EditorSnapshot, EditorStyle, ExcerptId, FoldPlaceholder, Inlay,
MultiBuffer, MultiBufferOffset, ToOffset, MultiBuffer, ToOffset,
actions::Paste, actions::Paste,
code_context_menus::CodeContextMenu,
display_map::{Crease, CreaseId, FoldId}, display_map::{Crease, CreaseId, FoldId},
scroll::Autoscroll, scroll::Autoscroll,
}; };
@@ -28,7 +27,6 @@ use gpui::{
EventEmitter, FocusHandle, Focusable, Image, ImageFormat, Img, KeyContext, SharedString, EventEmitter, FocusHandle, Focusable, Image, ImageFormat, Img, KeyContext, SharedString,
Subscription, Task, TextStyle, WeakEntity, pulsating_between, Subscription, Task, TextStyle, WeakEntity, pulsating_between,
}; };
use itertools::Either;
use language::{Buffer, Language, language_settings::InlayHintKind}; use language::{Buffer, Language, language_settings::InlayHintKind};
use language_model::LanguageModelImage; use language_model::LanguageModelImage;
use postage::stream::Stream as _; use postage::stream::Stream as _;
@@ -209,7 +207,7 @@ impl MessageEditor {
let acp::AvailableCommandInput::Unstructured { mut hint } = let acp::AvailableCommandInput::Unstructured { mut hint } =
available_command.input.clone()?; available_command.input.clone()?;
let mut hint_pos = MultiBufferOffset(parsed_command.source_range.end) + 1usize; let mut hint_pos = parsed_command.source_range.end + 1;
if hint_pos > snapshot.len() { if hint_pos > snapshot.len() {
hint_pos = snapshot.len(); hint_pos = snapshot.len();
hint.insert(0, ' '); hint.insert(0, ' ');
@@ -274,15 +272,6 @@ impl MessageEditor {
self.editor.read(cx).is_empty(cx) self.editor.read(cx).is_empty(cx)
} }
pub fn is_completions_menu_visible(&self, cx: &App) -> bool {
self.editor
.read(cx)
.context_menu()
.borrow()
.as_ref()
.is_some_and(|menu| matches!(menu, CodeContextMenu::Completions(_)) && menu.visible())
}
pub fn mentions(&self) -> HashSet<MentionUri> { pub fn mentions(&self) -> HashSet<MentionUri> {
self.mention_set self.mention_set
.mentions .mentions
@@ -307,9 +296,9 @@ impl MessageEditor {
return Task::ready(()); return Task::ready(());
}; };
let excerpt_id = start_anchor.excerpt_id; let excerpt_id = start_anchor.excerpt_id;
let end_anchor = snapshot.buffer_snapshot().anchor_before( let end_anchor = snapshot
start_anchor.to_offset(&snapshot.buffer_snapshot()) + content_len + 1usize, .buffer_snapshot()
); .anchor_before(start_anchor.to_offset(&snapshot.buffer_snapshot()) + content_len + 1);
let crease = if let MentionUri::File { abs_path } = &mention_uri let crease = if let MentionUri::File { abs_path } = &mention_uri
&& let Some(extension) = abs_path.extension() && let Some(extension) = abs_path.extension()
@@ -367,7 +356,7 @@ impl MessageEditor {
let task = match mention_uri.clone() { let task = match mention_uri.clone() {
MentionUri::Fetch { url } => self.confirm_mention_for_fetch(url, cx), MentionUri::Fetch { url } => self.confirm_mention_for_fetch(url, cx),
MentionUri::Directory { .. } => Task::ready(Ok(Mention::Link)), MentionUri::Directory { .. } => Task::ready(Ok(Mention::UriOnly)),
MentionUri::Thread { id, .. } => self.confirm_mention_for_thread(id, cx), MentionUri::Thread { id, .. } => self.confirm_mention_for_thread(id, cx),
MentionUri::TextThread { path, .. } => self.confirm_mention_for_text_thread(path, cx), MentionUri::TextThread { path, .. } => self.confirm_mention_for_text_thread(path, cx),
MentionUri::File { abs_path } => self.confirm_mention_for_file(abs_path, cx), MentionUri::File { abs_path } => self.confirm_mention_for_file(abs_path, cx),
@@ -384,6 +373,7 @@ impl MessageEditor {
))) )))
} }
MentionUri::Selection { .. } => { MentionUri::Selection { .. } => {
// Handled elsewhere
debug_panic!("unexpected selection URI"); debug_panic!("unexpected selection URI");
Task::ready(Err(anyhow!("unexpected selection URI"))) Task::ready(Err(anyhow!("unexpected selection URI")))
} }
@@ -714,21 +704,20 @@ impl MessageEditor {
return Task::ready(Err(err)); return Task::ready(Err(err));
} }
let contents = self let contents = self.mention_set.contents(
.mention_set &self.prompt_capabilities.borrow(),
.contents(full_mention_content, self.project.clone(), cx); full_mention_content,
self.project.clone(),
cx,
);
let editor = self.editor.clone(); let editor = self.editor.clone();
let supports_embedded_context = self.prompt_capabilities.borrow().embedded_context;
cx.spawn(async move |_, cx| { cx.spawn(async move |_, cx| {
let contents = contents.await?; let contents = contents.await?;
let mut all_tracked_buffers = Vec::new(); let mut all_tracked_buffers = Vec::new();
let result = editor.update(cx, |editor, cx| { let result = editor.update(cx, |editor, cx| {
let (mut ix, _) = text let mut ix = text.chars().position(|c| !c.is_whitespace()).unwrap_or(0);
.char_indices()
.find(|(_, c)| !c.is_whitespace())
.unwrap_or((0, '\0'));
let mut chunks: Vec<acp::ContentBlock> = Vec::new(); let mut chunks: Vec<acp::ContentBlock> = Vec::new();
let text = editor.text(cx); let text = editor.text(cx);
editor.display_map.update(cx, |map, cx| { editor.display_map.update(cx, |map, cx| {
@@ -739,8 +728,8 @@ impl MessageEditor {
}; };
let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot()); let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot());
if crease_range.start.0 > ix { if crease_range.start > ix {
let chunk = text[ix..crease_range.start.0].into(); let chunk = text[ix..crease_range.start].into();
chunks.push(chunk); chunks.push(chunk);
} }
let chunk = match mention { let chunk = match mention {
@@ -749,32 +738,18 @@ impl MessageEditor {
tracked_buffers, tracked_buffers,
} => { } => {
all_tracked_buffers.extend(tracked_buffers.iter().cloned()); all_tracked_buffers.extend(tracked_buffers.iter().cloned());
if supports_embedded_context { acp::ContentBlock::Resource(acp::EmbeddedResource {
acp::ContentBlock::Resource(acp::EmbeddedResource { annotations: None,
annotations: None, resource: acp::EmbeddedResourceResource::TextResourceContents(
resource: acp::TextResourceContents {
acp::EmbeddedResourceResource::TextResourceContents( mime_type: None,
acp::TextResourceContents { text: content.clone(),
mime_type: None, uri: uri.to_uri().to_string(),
text: content.clone(), meta: None,
uri: uri.to_uri().to_string(), },
meta: None, ),
}, meta: None,
), })
meta: None,
})
} else {
acp::ContentBlock::ResourceLink(acp::ResourceLink {
name: uri.name(),
uri: uri.to_uri().to_string(),
annotations: None,
description: None,
mime_type: None,
size: None,
title: None,
meta: None,
})
}
} }
Mention::Image(mention_image) => { Mention::Image(mention_image) => {
let uri = match uri { let uri = match uri {
@@ -796,19 +771,21 @@ impl MessageEditor {
meta: None, meta: None,
}) })
} }
Mention::Link => acp::ContentBlock::ResourceLink(acp::ResourceLink { Mention::UriOnly => {
name: uri.name(), acp::ContentBlock::ResourceLink(acp::ResourceLink {
uri: uri.to_uri().to_string(), name: uri.name(),
annotations: None, uri: uri.to_uri().to_string(),
description: None, annotations: None,
mime_type: None, description: None,
size: None, mime_type: None,
title: None, size: None,
meta: None, title: None,
}), meta: None,
})
}
}; };
chunks.push(chunk); chunks.push(chunk);
ix = crease_range.end.0; ix = crease_range.end;
} }
if ix < text.len() { if ix < text.len() {
@@ -847,45 +824,6 @@ impl MessageEditor {
cx.emit(MessageEditorEvent::Send) cx.emit(MessageEditorEvent::Send)
} }
pub fn trigger_completion_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let editor = self.editor.clone();
cx.spawn_in(window, async move |_, cx| {
editor
.update_in(cx, |editor, window, cx| {
let menu_is_open =
editor.context_menu().borrow().as_ref().is_some_and(|menu| {
matches!(menu, CodeContextMenu::Completions(_)) && menu.visible()
});
let has_at_sign = {
let snapshot = editor.display_snapshot(cx);
let cursor = editor.selections.newest::<text::Point>(&snapshot).head();
let offset = cursor.to_offset(&snapshot);
if offset.0 > 0 {
snapshot
.buffer_snapshot()
.reversed_chars_at(offset)
.next()
.map(|sign| sign == '@')
.unwrap_or(false)
} else {
false
}
};
if menu_is_open && has_at_sign {
return;
}
editor.insert("@", window, cx);
editor.show_completions(&editor::actions::ShowCompletions, window, cx);
})
.log_err();
})
.detach();
}
fn chat(&mut self, _: &Chat, _: &mut Window, cx: &mut Context<Self>) { fn chat(&mut self, _: &Chat, _: &mut Window, cx: &mut Context<Self>) {
self.send(cx); self.send(cx);
} }
@@ -913,114 +851,74 @@ impl MessageEditor {
if !self.prompt_capabilities.borrow().image { if !self.prompt_capabilities.borrow().image {
return; return;
} }
let Some(clipboard) = cx.read_from_clipboard() else {
return;
};
cx.spawn_in(window, async move |this, cx| {
use itertools::Itertools;
let (mut images, paths) = clipboard
.into_entries()
.filter_map(|entry| match entry {
ClipboardEntry::Image(image) => Some(Either::Left(image)),
ClipboardEntry::ExternalPaths(paths) => Some(Either::Right(paths)),
_ => None,
})
.partition_map::<Vec<_>, Vec<_>, _, _, _>(std::convert::identity);
if !paths.is_empty() { let images = cx
images.extend( .read_from_clipboard()
cx.background_spawn(async move { .map(|item| {
let mut images = vec![]; item.into_entries()
for path in paths.into_iter().flat_map(|paths| paths.paths().to_owned()) { .filter_map(|entry| {
let Ok(content) = async_fs::read(path).await else { if let ClipboardEntry::Image(image) = entry {
continue; Some(image)
}; } else {
let Ok(format) = image::guess_format(&content) else { None
continue;
};
images.push(gpui::Image::from_bytes(
match format {
image::ImageFormat::Png => gpui::ImageFormat::Png,
image::ImageFormat::Jpeg => gpui::ImageFormat::Jpeg,
image::ImageFormat::WebP => gpui::ImageFormat::Webp,
image::ImageFormat::Gif => gpui::ImageFormat::Gif,
image::ImageFormat::Bmp => gpui::ImageFormat::Bmp,
image::ImageFormat::Tiff => gpui::ImageFormat::Tiff,
image::ImageFormat::Ico => gpui::ImageFormat::Ico,
_ => continue,
},
content,
));
} }
images
}) })
.await, .collect::<Vec<_>>()
); })
} .unwrap_or_default();
if images.is_empty() { if images.is_empty() {
return; return;
} }
cx.stop_propagation();
let replacement_text = MentionUri::PastedImage.as_link().to_string(); let replacement_text = MentionUri::PastedImage.as_link().to_string();
let Ok(editor) = this.update(cx, |this, cx| { for image in images {
cx.stop_propagation(); let (excerpt_id, text_anchor, multibuffer_anchor) =
this.editor.clone() self.editor.update(cx, |message_editor, cx| {
}) else { let snapshot = message_editor.snapshot(window, cx);
return; let (excerpt_id, _, buffer_snapshot) =
}; snapshot.buffer_snapshot().as_singleton().unwrap();
for image in images {
let Ok((excerpt_id, text_anchor, multibuffer_anchor)) =
editor.update_in(cx, |message_editor, window, cx| {
let snapshot = message_editor.snapshot(window, cx);
let (excerpt_id, _, buffer_snapshot) =
snapshot.buffer_snapshot().as_singleton().unwrap();
let text_anchor = buffer_snapshot.anchor_before(buffer_snapshot.len()); let text_anchor = buffer_snapshot.anchor_before(buffer_snapshot.len());
let multibuffer_anchor = snapshot let multibuffer_anchor = snapshot
.buffer_snapshot() .buffer_snapshot()
.anchor_in_excerpt(*excerpt_id, text_anchor); .anchor_in_excerpt(*excerpt_id, text_anchor);
message_editor.edit( message_editor.edit(
[( [(
multi_buffer::Anchor::max()..multi_buffer::Anchor::max(), multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
format!("{replacement_text} "), format!("{replacement_text} "),
)], )],
cx,
);
(*excerpt_id, text_anchor, multibuffer_anchor)
})
else {
break;
};
let content_len = replacement_text.len();
let Some(start_anchor) = multibuffer_anchor else {
continue;
};
let Ok(end_anchor) = editor.update(cx, |editor, cx| {
let snapshot = editor.buffer().read(cx).snapshot(cx);
snapshot.anchor_before(start_anchor.to_offset(&snapshot) + content_len)
}) else {
continue;
};
let image = Arc::new(image);
let Ok(Some((crease_id, tx))) = cx.update(|window, cx| {
insert_crease_for_mention(
excerpt_id,
text_anchor,
content_len,
MentionUri::PastedImage.name().into(),
IconName::Image.path().into(),
Some(Task::ready(Ok(image.clone())).shared()),
editor.clone(),
window,
cx, cx,
) );
}) else { (*excerpt_id, text_anchor, multibuffer_anchor)
continue; });
};
let task = cx let content_len = replacement_text.len();
.spawn(async move |cx| { let Some(start_anchor) = multibuffer_anchor else {
continue;
};
let end_anchor = self.editor.update(cx, |editor, cx| {
let snapshot = editor.buffer().read(cx).snapshot(cx);
snapshot.anchor_before(start_anchor.to_offset(&snapshot) + content_len)
});
let image = Arc::new(image);
let Some((crease_id, tx)) = insert_crease_for_mention(
excerpt_id,
text_anchor,
content_len,
MentionUri::PastedImage.name().into(),
IconName::Image.path().into(),
Some(Task::ready(Ok(image.clone())).shared()),
self.editor.clone(),
window,
cx,
) else {
continue;
};
let task = cx
.spawn_in(window, {
async move |_, cx| {
let format = image.format; let format = image.format;
let image = cx let image = cx
.update(|_, cx| LanguageModelImage::from_image(image, cx)) .update(|_, cx| LanguageModelImage::from_image(image, cx))
@@ -1035,16 +933,15 @@ impl MessageEditor {
} else { } else {
Err("Failed to convert image".into()) Err("Failed to convert image".into())
} }
}) }
.shared();
this.update(cx, |this, _| {
this.mention_set
.mentions
.insert(crease_id, (MentionUri::PastedImage, task.clone()))
}) })
.ok(); .shared();
self.mention_set
.mentions
.insert(crease_id, (MentionUri::PastedImage, task.clone()));
cx.spawn_in(window, async move |this, cx| {
if task.await.notify_async_err(cx).is_none() { if task.await.notify_async_err(cx).is_none() {
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
this.editor.update(cx, |editor, cx| { this.editor.update(cx, |editor, cx| {
@@ -1054,9 +951,9 @@ impl MessageEditor {
}) })
.ok(); .ok();
} }
} })
}) .detach();
.detach(); }
} }
pub fn insert_dragged_files( pub fn insert_dragged_files(
@@ -1132,7 +1029,7 @@ impl MessageEditor {
let cursor_anchor = editor.selections.newest_anchor().head(); let cursor_anchor = editor.selections.newest_anchor().head();
let cursor_offset = cursor_anchor.to_offset(&editor_buffer.snapshot(cx)); let cursor_offset = cursor_anchor.to_offset(&editor_buffer.snapshot(cx));
let anchor = buffer.update(cx, |buffer, _cx| { let anchor = buffer.update(cx, |buffer, _cx| {
buffer.anchor_before(cursor_offset.0.min(buffer.len())) buffer.anchor_before(cursor_offset.min(buffer.len()))
}); });
let Some(workspace) = self.workspace.upgrade() else { let Some(workspace) = self.workspace.upgrade() else {
return; return;
@@ -1214,7 +1111,7 @@ impl MessageEditor {
let start = text.len(); let start = text.len();
write!(&mut text, "{}", mention_uri.as_link()).ok(); write!(&mut text, "{}", mention_uri.as_link()).ok();
let end = text.len(); let end = text.len();
mentions.push((start..end, mention_uri, Mention::Link)); mentions.push((start..end, mention_uri, Mention::UriOnly));
} }
} }
acp::ContentBlock::Image(acp::ImageContent { acp::ContentBlock::Image(acp::ImageContent {
@@ -1258,7 +1155,7 @@ impl MessageEditor {
}); });
for (range, mention_uri, mention) in mentions { for (range, mention_uri, mention) in mentions {
let anchor = snapshot.anchor_before(MultiBufferOffset(range.start)); let anchor = snapshot.anchor_before(range.start);
let Some((crease_id, tx)) = insert_crease_for_mention( let Some((crease_id, tx)) = insert_crease_for_mention(
anchor.excerpt_id, anchor.excerpt_id,
anchor.text_anchor, anchor.text_anchor,
@@ -1286,17 +1183,6 @@ impl MessageEditor {
self.editor.read(cx).text(cx) self.editor.read(cx).text(cx)
} }
pub fn set_placeholder_text(
&mut self,
placeholder: &str,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.editor.update(cx, |editor, cx| {
editor.set_placeholder_text(placeholder, window, cx);
});
}
#[cfg(test)] #[cfg(test)]
pub fn set_text(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) { pub fn set_text(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
self.editor.update(cx, |editor, cx| { self.editor.update(cx, |editor, cx| {
@@ -1631,7 +1517,7 @@ pub enum Mention {
tracked_buffers: Vec<Entity<Buffer>>, tracked_buffers: Vec<Entity<Buffer>>,
}, },
Image(MentionImage), Image(MentionImage),
Link, UriOnly,
} }
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
@@ -1648,10 +1534,21 @@ pub struct MentionSet {
impl MentionSet { impl MentionSet {
fn contents( fn contents(
&self, &self,
prompt_capabilities: &acp::PromptCapabilities,
full_mention_content: bool, full_mention_content: bool,
project: Entity<Project>, project: Entity<Project>,
cx: &mut App, cx: &mut App,
) -> Task<Result<HashMap<CreaseId, (MentionUri, Mention)>>> { ) -> Task<Result<HashMap<CreaseId, (MentionUri, Mention)>>> {
if !prompt_capabilities.embedded_context {
let mentions = self
.mentions
.iter()
.map(|(crease_id, (uri, _))| (*crease_id, (uri.clone(), Mention::UriOnly)))
.collect();
return Task::ready(Ok(mentions));
}
let mentions = self.mentions.clone(); let mentions = self.mentions.clone();
cx.spawn(async move |cx| { cx.spawn(async move |cx| {
let mut contents = HashMap::default(); let mut contents = HashMap::default();
@@ -1713,7 +1610,7 @@ mod tests {
use agent::{HistoryStore, outline}; use agent::{HistoryStore, outline};
use agent_client_protocol as acp; use agent_client_protocol as acp;
use assistant_text_thread::TextThreadStore; use assistant_text_thread::TextThreadStore;
use editor::{AnchorRangeExt as _, Editor, EditorMode, MultiBufferOffset}; use editor::{AnchorRangeExt as _, Editor, EditorMode};
use fs::FakeFs; use fs::FakeFs;
use futures::StreamExt as _; use futures::StreamExt as _;
use gpui::{ use gpui::{
@@ -2001,8 +1898,10 @@ mod tests {
let app_state = cx.update(AppState::test); let app_state = cx.update(AppState::test);
cx.update(|cx| { cx.update(|cx| {
language::init(cx);
editor::init(cx); editor::init(cx);
workspace::init(app_state.clone(), cx); workspace::init(app_state.clone(), cx);
Project::init_settings(cx);
}); });
let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
@@ -2175,8 +2074,10 @@ mod tests {
let app_state = cx.update(AppState::test); let app_state = cx.update(AppState::test);
cx.update(|cx| { cx.update(|cx| {
language::init(cx);
editor::init(cx); editor::init(cx);
workspace::init(app_state.clone(), cx); workspace::init(app_state.clone(), cx);
Project::init_settings(cx);
}); });
app_state app_state
@@ -2301,8 +2202,6 @@ mod tests {
format!("seven.txt b{slash}"), format!("seven.txt b{slash}"),
format!("six.txt b{slash}"), format!("six.txt b{slash}"),
format!("five.txt b{slash}"), format!("five.txt b{slash}"),
"Files & Directories".into(),
"Symbols".into()
] ]
); );
editor.set_text("", window, cx); editor.set_text("", window, cx);
@@ -2387,11 +2286,21 @@ mod tests {
assert_eq!(fold_ranges(editor, cx).len(), 1); assert_eq!(fold_ranges(editor, cx).len(), 1);
}); });
let all_prompt_capabilities = acp::PromptCapabilities {
image: true,
audio: true,
embedded_context: true,
meta: None,
};
let contents = message_editor let contents = message_editor
.update(&mut cx, |message_editor, cx| { .update(&mut cx, |message_editor, cx| {
message_editor message_editor.mention_set().contents(
.mention_set() &all_prompt_capabilities,
.contents(false, project.clone(), cx) false,
project.clone(),
cx,
)
}) })
.await .await
.unwrap() .unwrap()
@@ -2409,6 +2318,30 @@ mod tests {
); );
} }
let contents = message_editor
.update(&mut cx, |message_editor, cx| {
message_editor.mention_set().contents(
&acp::PromptCapabilities::default(),
false,
project.clone(),
cx,
)
})
.await
.unwrap()
.into_values()
.collect::<Vec<_>>();
{
let [(uri, Mention::UriOnly)] = contents.as_slice() else {
panic!("Unexpected mentions");
};
pretty_assertions::assert_eq!(
uri,
&MentionUri::parse(&url_one, PathStyle::local()).unwrap()
);
}
cx.simulate_input(" "); cx.simulate_input(" ");
editor.update(&mut cx, |editor, cx| { editor.update(&mut cx, |editor, cx| {
@@ -2444,9 +2377,12 @@ mod tests {
let contents = message_editor let contents = message_editor
.update(&mut cx, |message_editor, cx| { .update(&mut cx, |message_editor, cx| {
message_editor message_editor.mention_set().contents(
.mention_set() &all_prompt_capabilities,
.contents(false, project.clone(), cx) false,
project.clone(),
cx,
)
}) })
.await .await
.unwrap() .unwrap()
@@ -2567,9 +2503,12 @@ mod tests {
let contents = message_editor let contents = message_editor
.update(&mut cx, |message_editor, cx| { .update(&mut cx, |message_editor, cx| {
message_editor message_editor.mention_set().contents(
.mention_set() &all_prompt_capabilities,
.contents(false, project.clone(), cx) false,
project.clone(),
cx,
)
}) })
.await .await
.unwrap() .unwrap()
@@ -2615,9 +2554,12 @@ mod tests {
// Getting the message contents fails // Getting the message contents fails
message_editor message_editor
.update(&mut cx, |message_editor, cx| { .update(&mut cx, |message_editor, cx| {
message_editor message_editor.mention_set().contents(
.mention_set() &all_prompt_capabilities,
.contents(false, project.clone(), cx) false,
project.clone(),
cx,
)
}) })
.await .await
.expect_err("Should fail to load x.png"); .expect_err("Should fail to load x.png");
@@ -2668,9 +2610,12 @@ mod tests {
// Now getting the contents succeeds, because the invalid mention was removed // Now getting the contents succeeds, because the invalid mention was removed
let contents = message_editor let contents = message_editor
.update(&mut cx, |message_editor, cx| { .update(&mut cx, |message_editor, cx| {
message_editor message_editor.mention_set().contents(
.mention_set() &all_prompt_capabilities,
.contents(false, project.clone(), cx) false,
project.clone(),
cx,
)
}) })
.await .await
.unwrap(); .unwrap();
@@ -2682,7 +2627,7 @@ mod tests {
editor.display_map.update(cx, |display_map, cx| { editor.display_map.update(cx, |display_map, cx| {
display_map display_map
.snapshot(cx) .snapshot(cx)
.folds_in_range(MultiBufferOffset(0)..snapshot.len()) .folds_in_range(0..snapshot.len())
.map(|fold| fold.range.to_point(&snapshot)) .map(|fold| fold.range.to_point(&snapshot))
.collect() .collect()
}) })
@@ -2713,14 +2658,13 @@ mod tests {
} }
#[gpui::test] #[gpui::test]
async fn test_large_file_mention_fallback(cx: &mut TestAppContext) { async fn test_large_file_mention_uses_outline(cx: &mut TestAppContext) {
init_test(cx); init_test(cx);
let fs = FakeFs::new(cx.executor()); let fs = FakeFs::new(cx.executor());
// Create a large file that exceeds AUTO_OUTLINE_SIZE // Create a large file that exceeds AUTO_OUTLINE_SIZE
// Using plain text without a configured language, so no outline is available const LINE: &str = "fn example_function() { /* some code */ }\n";
const LINE: &str = "This is a line of text in the file\n";
let large_content = LINE.repeat(2 * (outline::AUTO_OUTLINE_SIZE / LINE.len())); let large_content = LINE.repeat(2 * (outline::AUTO_OUTLINE_SIZE / LINE.len()));
assert!(large_content.len() > outline::AUTO_OUTLINE_SIZE); assert!(large_content.len() > outline::AUTO_OUTLINE_SIZE);
@@ -2731,8 +2675,8 @@ mod tests {
fs.insert_tree( fs.insert_tree(
"/project", "/project",
json!({ json!({
"large_file.txt": large_content.clone(), "large_file.rs": large_content.clone(),
"small_file.txt": small_content, "small_file.rs": small_content,
}), }),
) )
.await; .await;
@@ -2778,7 +2722,7 @@ mod tests {
let large_file_abs_path = project.read_with(cx, |project, cx| { let large_file_abs_path = project.read_with(cx, |project, cx| {
let worktree = project.worktrees(cx).next().unwrap(); let worktree = project.worktrees(cx).next().unwrap();
let worktree_root = worktree.read(cx).abs_path(); let worktree_root = worktree.read(cx).abs_path();
worktree_root.join("large_file.txt") worktree_root.join("large_file.rs")
}); });
let large_file_task = message_editor.update(cx, |editor, cx| { let large_file_task = message_editor.update(cx, |editor, cx| {
editor.confirm_mention_for_file(large_file_abs_path, cx) editor.confirm_mention_for_file(large_file_abs_path, cx)
@@ -2787,20 +2731,11 @@ mod tests {
let large_file_mention = large_file_task.await.unwrap(); let large_file_mention = large_file_task.await.unwrap();
match large_file_mention { match large_file_mention {
Mention::Text { content, .. } => { Mention::Text { content, .. } => {
// Should contain some of the content but not all of it // Should contain outline header for large files
assert!( assert!(content.contains("File outline for"));
content.contains(LINE), assert!(content.contains("file too large to show full content"));
"Should contain some of the file content" // Should not contain the full repeated content
); assert!(!content.contains(&LINE.repeat(100)));
assert!(
!content.contains(&LINE.repeat(100)),
"Should not contain the full file"
);
// Should be much smaller than original
assert!(
content.len() < large_content.len() / 10,
"Should be significantly truncated"
);
} }
_ => panic!("Expected Text mention for large file"), _ => panic!("Expected Text mention for large file"),
} }
@@ -2810,7 +2745,7 @@ mod tests {
let small_file_abs_path = project.read_with(cx, |project, cx| { let small_file_abs_path = project.read_with(cx, |project, cx| {
let worktree = project.worktrees(cx).next().unwrap(); let worktree = project.worktrees(cx).next().unwrap();
let worktree_root = worktree.read(cx).abs_path(); let worktree_root = worktree.read(cx).abs_path();
worktree_root.join("small_file.txt") worktree_root.join("small_file.rs")
}); });
let small_file_task = message_editor.update(cx, |editor, cx| { let small_file_task = message_editor.update(cx, |editor, cx| {
editor.confirm_mention_for_file(small_file_abs_path, cx) editor.confirm_mention_for_file(small_file_abs_path, cx)
@@ -2819,8 +2754,10 @@ mod tests {
let small_file_mention = small_file_task.await.unwrap(); let small_file_mention = small_file_task.await.unwrap();
match small_file_mention { match small_file_mention {
Mention::Text { content, .. } => { Mention::Text { content, .. } => {
// Should contain the full actual content // Should contain the actual content
assert_eq!(content, small_content); assert_eq!(content, small_content);
// Should not contain outline header
assert!(!content.contains("File outline for"));
} }
_ => panic!("Expected Text mention for small file"), _ => panic!("Expected Text mention for small file"),
} }
@@ -2942,7 +2879,7 @@ mod tests {
cx.run_until_parked(); cx.run_until_parked();
editor.update_in(cx, |editor, window, cx| { editor.update_in(cx, |editor, window, cx| {
editor.set_text(" \u{A0}してhello world ", window, cx); editor.set_text(" hello world ", window, cx);
}); });
let (content, _) = message_editor let (content, _) = message_editor
@@ -2953,154 +2890,13 @@ mod tests {
assert_eq!( assert_eq!(
content, content,
vec![acp::ContentBlock::Text(acp::TextContent { vec![acp::ContentBlock::Text(acp::TextContent {
text: "してhello world".into(), text: "hello world".into(),
annotations: None, annotations: None,
meta: None meta: None
})] })]
); );
} }
#[gpui::test]
async fn test_editor_respects_embedded_context_capability(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
let file_content = "fn main() { println!(\"Hello, world!\"); }\n";
fs.insert_tree(
"/project",
json!({
"src": {
"main.rs": file_content,
}
}),
)
.await;
let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx));
let (message_editor, editor) = workspace.update_in(cx, |workspace, window, cx| {
let workspace_handle = cx.weak_entity();
let message_editor = cx.new(|cx| {
MessageEditor::new(
workspace_handle,
project.clone(),
history_store.clone(),
None,
Default::default(),
Default::default(),
"Test Agent".into(),
"Test",
EditorMode::AutoHeight {
max_lines: None,
min_lines: 1,
},
window,
cx,
)
});
workspace.active_pane().update(cx, |pane, cx| {
pane.add_item(
Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
true,
true,
None,
window,
cx,
);
});
message_editor.read(cx).focus_handle(cx).focus(window);
let editor = message_editor.read(cx).editor().clone();
(message_editor, editor)
});
cx.simulate_input("What is in @file main");
editor.update_in(cx, |editor, window, cx| {
assert!(editor.has_visible_completions_menu());
assert_eq!(editor.text(cx), "What is in @file main");
editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
});
let content = message_editor
.update(cx, |editor, cx| editor.contents(false, cx))
.await
.unwrap()
.0;
let main_rs_uri = if cfg!(windows) {
"file:///C:/project/src/main.rs".to_string()
} else {
"file:///project/src/main.rs".to_string()
};
// When embedded context is `false` we should get a resource link
pretty_assertions::assert_eq!(
content,
vec![
acp::ContentBlock::Text(acp::TextContent {
text: "What is in ".to_string(),
annotations: None,
meta: None
}),
acp::ContentBlock::ResourceLink(acp::ResourceLink {
uri: main_rs_uri.clone(),
name: "main.rs".to_string(),
annotations: None,
meta: None,
description: None,
mime_type: None,
size: None,
title: None,
})
]
);
message_editor.update(cx, |editor, _cx| {
editor.prompt_capabilities.replace(acp::PromptCapabilities {
embedded_context: true,
..Default::default()
})
});
let content = message_editor
.update(cx, |editor, cx| editor.contents(false, cx))
.await
.unwrap()
.0;
// When embedded context is `true` we should get a resource
pretty_assertions::assert_eq!(
content,
vec![
acp::ContentBlock::Text(acp::TextContent {
text: "What is in ".to_string(),
annotations: None,
meta: None
}),
acp::ContentBlock::Resource(acp::EmbeddedResource {
resource: acp::EmbeddedResourceResource::TextResourceContents(
acp::TextResourceContents {
text: file_content.to_string(),
uri: main_rs_uri,
mime_type: None,
meta: None
}
),
annotations: None,
meta: None
})
]
);
}
#[gpui::test] #[gpui::test]
async fn test_autoscroll_after_insert_selections(cx: &mut TestAppContext) { async fn test_autoscroll_after_insert_selections(cx: &mut TestAppContext) {
init_test(cx); init_test(cx);
@@ -3108,8 +2904,10 @@ mod tests {
let app_state = cx.update(AppState::test); let app_state = cx.update(AppState::test);
cx.update(|cx| { cx.update(|cx| {
language::init(cx);
editor::init(cx); editor::init(cx);
workspace::init(app_state.clone(), cx); workspace::init(app_state.clone(), cx);
Project::init_settings(cx);
}); });
app_state app_state

View File

@@ -11,7 +11,7 @@ use ui::{
PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*,
}; };
use crate::{CycleModeSelector, ToggleProfileSelector, ui::HoldForDefault}; use crate::{CycleModeSelector, ToggleProfileSelector};
pub struct ModeSelector { pub struct ModeSelector {
connection: Rc<dyn AgentSessionModes>, connection: Rc<dyn AgentSessionModes>,
@@ -56,10 +56,6 @@ impl ModeSelector {
self.set_mode(all_modes[next_index].id.clone(), cx); self.set_mode(all_modes[next_index].id.clone(), cx);
} }
pub fn mode(&self) -> acp::SessionModeId {
self.connection.current_mode()
}
pub fn set_mode(&mut self, mode: acp::SessionModeId, cx: &mut Context<Self>) { pub fn set_mode(&mut self, mode: acp::SessionModeId, cx: &mut Context<Self>) {
let task = self.connection.set_mode(mode, cx); let task = self.connection.set_mode(mode, cx);
self.setting_mode = true; self.setting_mode = true;
@@ -108,11 +104,36 @@ impl ModeSelector {
entry.documentation_aside(side, DocumentationEdge::Bottom, { entry.documentation_aside(side, DocumentationEdge::Bottom, {
let description = description.clone(); let description = description.clone();
move |_| { move |cx| {
v_flex() v_flex()
.gap_1() .gap_1()
.child(Label::new(description.clone())) .child(Label::new(description.clone()))
.child(HoldForDefault::new(is_default)) .child(
h_flex()
.pt_1()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.gap_0p5()
.text_sm()
.text_color(Color::Muted.color(cx))
.child("Hold")
.child(h_flex().flex_shrink_0().children(
ui::render_modifiers(
&gpui::Modifiers::secondary_key(),
PlatformStyle::platform(),
None,
Some(ui::TextSize::Default.rems(cx).into()),
true,
),
))
.child(div().map(|this| {
if is_default {
this.child("to also unset as default")
} else {
this.child("to also set as default")
}
})),
)
.into_any_element() .into_any_element()
} }
}) })

View File

@@ -1,10 +1,8 @@
use std::{cmp::Reverse, rc::Rc, sync::Arc}; use std::{cmp::Reverse, rc::Rc, sync::Arc};
use acp_thread::{AgentModelInfo, AgentModelList, AgentModelSelector}; use acp_thread::{AgentModelInfo, AgentModelList, AgentModelSelector};
use agent_servers::AgentServer;
use anyhow::Result; use anyhow::Result;
use collections::IndexMap; use collections::IndexMap;
use fs::Fs;
use futures::FutureExt; use futures::FutureExt;
use fuzzy::{StringMatchCandidate, match_strings}; use fuzzy::{StringMatchCandidate, match_strings};
use gpui::{AsyncWindowContext, BackgroundExecutor, DismissEvent, Task, WeakEntity}; use gpui::{AsyncWindowContext, BackgroundExecutor, DismissEvent, Task, WeakEntity};
@@ -16,18 +14,14 @@ use ui::{
}; };
use util::ResultExt; use util::ResultExt;
use crate::ui::HoldForDefault;
pub type AcpModelSelector = Picker<AcpModelPickerDelegate>; pub type AcpModelSelector = Picker<AcpModelPickerDelegate>;
pub fn acp_model_selector( pub fn acp_model_selector(
selector: Rc<dyn AgentModelSelector>, selector: Rc<dyn AgentModelSelector>,
agent_server: Rc<dyn AgentServer>,
fs: Arc<dyn Fs>,
window: &mut Window, window: &mut Window,
cx: &mut Context<AcpModelSelector>, cx: &mut Context<AcpModelSelector>,
) -> AcpModelSelector { ) -> AcpModelSelector {
let delegate = AcpModelPickerDelegate::new(selector, agent_server, fs, window, cx); let delegate = AcpModelPickerDelegate::new(selector, window, cx);
Picker::list(delegate, window, cx) Picker::list(delegate, window, cx)
.show_scrollbar(true) .show_scrollbar(true)
.width(rems(20.)) .width(rems(20.))
@@ -41,12 +35,10 @@ enum AcpModelPickerEntry {
pub struct AcpModelPickerDelegate { pub struct AcpModelPickerDelegate {
selector: Rc<dyn AgentModelSelector>, selector: Rc<dyn AgentModelSelector>,
agent_server: Rc<dyn AgentServer>,
fs: Arc<dyn Fs>,
filtered_entries: Vec<AcpModelPickerEntry>, filtered_entries: Vec<AcpModelPickerEntry>,
models: Option<AgentModelList>, models: Option<AgentModelList>,
selected_index: usize, selected_index: usize,
selected_description: Option<(usize, SharedString, bool)>, selected_description: Option<(usize, SharedString)>,
selected_model: Option<AgentModelInfo>, selected_model: Option<AgentModelInfo>,
_refresh_models_task: Task<()>, _refresh_models_task: Task<()>,
} }
@@ -54,8 +46,6 @@ pub struct AcpModelPickerDelegate {
impl AcpModelPickerDelegate { impl AcpModelPickerDelegate {
fn new( fn new(
selector: Rc<dyn AgentModelSelector>, selector: Rc<dyn AgentModelSelector>,
agent_server: Rc<dyn AgentServer>,
fs: Arc<dyn Fs>,
window: &mut Window, window: &mut Window,
cx: &mut Context<AcpModelSelector>, cx: &mut Context<AcpModelSelector>,
) -> Self { ) -> Self {
@@ -96,8 +86,6 @@ impl AcpModelPickerDelegate {
Self { Self {
selector, selector,
agent_server,
fs,
filtered_entries: Vec::new(), filtered_entries: Vec::new(),
models: None, models: None,
selected_model: None, selected_model: None,
@@ -193,21 +181,6 @@ impl PickerDelegate for AcpModelPickerDelegate {
if let Some(AcpModelPickerEntry::Model(model_info)) = if let Some(AcpModelPickerEntry::Model(model_info)) =
self.filtered_entries.get(self.selected_index) self.filtered_entries.get(self.selected_index)
{ {
if window.modifiers().secondary() {
let default_model = self.agent_server.default_model(cx);
let is_default = default_model.as_ref() == Some(&model_info.id);
self.agent_server.set_default_model(
if is_default {
None
} else {
Some(model_info.id.clone())
},
self.fs.clone(),
cx,
);
}
self.selector self.selector
.select_model(model_info.id.clone(), cx) .select_model(model_info.id.clone(), cx)
.detach_and_log_err(cx); .detach_and_log_err(cx);
@@ -252,8 +225,6 @@ impl PickerDelegate for AcpModelPickerDelegate {
), ),
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 is_default = default_model.as_ref() == Some(&model_info.id);
let model_icon_color = if is_selected { let model_icon_color = if is_selected {
Color::Accent Color::Accent
@@ -268,8 +239,8 @@ impl PickerDelegate for AcpModelPickerDelegate {
this this
.on_hover(cx.listener(move |menu, hovered, _, cx| { .on_hover(cx.listener(move |menu, hovered, _, cx| {
if *hovered { if *hovered {
menu.delegate.selected_description = Some((ix, description.clone(), is_default)); menu.delegate.selected_description = Some((ix, description.clone()));
} else if matches!(menu.delegate.selected_description, Some((id, _, _)) if id == ix) { } else if matches!(menu.delegate.selected_description, Some((id, _)) if id == ix) {
menu.delegate.selected_description = None; menu.delegate.selected_description = None;
} }
cx.notify(); cx.notify();
@@ -280,17 +251,17 @@ impl PickerDelegate for AcpModelPickerDelegate {
.inset(true) .inset(true)
.spacing(ListItemSpacing::Sparse) .spacing(ListItemSpacing::Sparse)
.toggle_state(selected) .toggle_state(selected)
.start_slot::<Icon>(model_info.icon.map(|icon| {
Icon::new(icon)
.color(model_icon_color)
.size(IconSize::Small)
}))
.child( .child(
h_flex() h_flex()
.w_full() .w_full()
.pl_0p5()
.gap_1p5() .gap_1p5()
.when_some(model_info.icon, |this, icon| { .w(px(240.))
this.child(
Icon::new(icon)
.color(model_icon_color)
.size(IconSize::Small)
)
})
.child(Label::new(model_info.name.clone()).truncate()), .child(Label::new(model_info.name.clone()).truncate()),
) )
.end_slot(div().pr_3().when(is_selected, |this| { .end_slot(div().pr_3().when(is_selected, |this| {
@@ -312,24 +283,14 @@ impl PickerDelegate for AcpModelPickerDelegate {
_window: &mut Window, _window: &mut Window,
_cx: &mut Context<Picker<Self>>, _cx: &mut Context<Picker<Self>>,
) -> Option<ui::DocumentationAside> { ) -> Option<ui::DocumentationAside> {
self.selected_description self.selected_description.as_ref().map(|(_, description)| {
.as_ref() let description = description.clone();
.map(|(_, description, is_default)| { DocumentationAside::new(
let description = description.clone(); DocumentationSide::Left,
let is_default = *is_default; DocumentationEdge::Top,
Rc::new(move |_| Label::new(description.clone()).into_any_element()),
DocumentationAside::new( )
DocumentationSide::Left, })
DocumentationEdge::Top,
Rc::new(move |_| {
v_flex()
.gap_1()
.child(Label::new(description.clone()))
.child(HoldForDefault::new(is_default))
.into_any_element()
}),
)
})
} }
} }

View File

@@ -1,9 +1,6 @@
use std::rc::Rc; use std::rc::Rc;
use std::sync::Arc;
use acp_thread::{AgentModelInfo, AgentModelSelector}; use acp_thread::AgentModelSelector;
use agent_servers::AgentServer;
use fs::Fs;
use gpui::{Entity, FocusHandle}; use gpui::{Entity, FocusHandle};
use picker::popover_menu::PickerPopoverMenu; use picker::popover_menu::PickerPopoverMenu;
use ui::{ use ui::{
@@ -23,15 +20,13 @@ pub struct AcpModelSelectorPopover {
impl AcpModelSelectorPopover { impl AcpModelSelectorPopover {
pub(crate) fn new( pub(crate) fn new(
selector: Rc<dyn AgentModelSelector>, selector: Rc<dyn AgentModelSelector>,
agent_server: Rc<dyn AgentServer>,
fs: Arc<dyn Fs>,
menu_handle: PopoverMenuHandle<AcpModelSelector>, menu_handle: PopoverMenuHandle<AcpModelSelector>,
focus_handle: FocusHandle, focus_handle: FocusHandle,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Self { ) -> Self {
Self { Self {
selector: cx.new(move |cx| acp_model_selector(selector, agent_server, fs, window, cx)), selector: cx.new(move |cx| acp_model_selector(selector, window, cx)),
menu_handle, menu_handle,
focus_handle, focus_handle,
} }
@@ -41,8 +36,12 @@ impl AcpModelSelectorPopover {
self.menu_handle.toggle(window, cx); self.menu_handle.toggle(window, cx);
} }
pub fn active_model<'a>(&self, cx: &'a App) -> Option<&'a AgentModelInfo> { pub fn active_model_name(&self, cx: &App) -> Option<SharedString> {
self.selector.read(cx).delegate.active_model() self.selector
.read(cx)
.delegate
.active_model()
.map(|model| model.name.clone())
} }
} }

View File

@@ -457,23 +457,25 @@ impl Render for AcpThreadHistory {
.on_action(cx.listener(Self::select_last)) .on_action(cx.listener(Self::select_last))
.on_action(cx.listener(Self::confirm)) .on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::remove_selected_thread)) .on_action(cx.listener(Self::remove_selected_thread))
.child( .when(!self.history_store.read(cx).is_empty(cx), |parent| {
h_flex() parent.child(
.h(px(41.)) // Match the toolbar perfectly h_flex()
.w_full() .h(px(41.)) // Match the toolbar perfectly
.py_1() .w_full()
.px_2() .py_1()
.gap_2() .px_2()
.justify_between() .gap_2()
.border_b_1() .justify_between()
.border_color(cx.theme().colors().border) .border_b_1()
.child( .border_color(cx.theme().colors().border)
Icon::new(IconName::MagnifyingGlass) .child(
.color(Color::Muted) Icon::new(IconName::MagnifyingGlass)
.size(IconSize::Small), .color(Color::Muted)
) .size(IconSize::Small),
.child(self.search_editor.clone()), )
) .child(self.search_editor.clone()),
)
})
.child({ .child({
let view = v_flex() let view = v_flex()
.id("list-container") .id("list-container")
@@ -482,15 +484,19 @@ impl Render for AcpThreadHistory {
.flex_grow(); .flex_grow();
if self.history_store.read(cx).is_empty(cx) { if self.history_store.read(cx).is_empty(cx) {
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() view.justify_center()
.items_center() .child(
.child(Label::new("No threads match your search.").size(LabelSize::Small)) h_flex().w_full().justify_center().child(
Label::new("You don't have any past threads yet.")
.size(LabelSize::Small),
),
)
} else if self.search_produced_no_matches() {
view.justify_center().child(
h_flex().w_full().justify_center().child(
Label::new("No threads match your search.").size(LabelSize::Small),
),
)
} else { } else {
view.child( view.child(
uniform_list( uniform_list(
@@ -667,7 +673,7 @@ impl EntryTimeFormat {
timezone, timezone,
time_format::TimestampFormat::EnhancedAbsolute, time_format::TimestampFormat::EnhancedAbsolute,
), ),
EntryTimeFormat::TimeOnly => time_format::format_time(timestamp.to_offset(timezone)), EntryTimeFormat::TimeOnly => time_format::format_time(timestamp),
} }
} }
} }

View File

@@ -4,12 +4,12 @@ use acp_thread::{
ToolCallStatus, UserMessageId, ToolCallStatus, UserMessageId,
}; };
use acp_thread::{AgentConnection, Plan}; use acp_thread::{AgentConnection, Plan};
use action_log::{ActionLog, ActionLogTelemetry}; use action_log::ActionLog;
use agent::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore, NativeAgentServer}; use agent::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore, NativeAgentServer};
use agent_client_protocol::{self as acp, PromptCapabilities}; use agent_client_protocol::{self as acp, PromptCapabilities};
use agent_servers::{AgentServer, AgentServerDelegate}; use agent_servers::{AgentServer, AgentServerDelegate};
use agent_settings::{AgentProfileId, AgentSettings, CompletionMode}; use agent_settings::{AgentProfileId, AgentSettings, CompletionMode};
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow, bail};
use arrayvec::ArrayVec; use arrayvec::ArrayVec;
use audio::{Audio, Sound}; use audio::{Audio, Sound};
use buffer_diff::BufferDiff; use buffer_diff::BufferDiff;
@@ -51,7 +51,7 @@ use ui::{
PopoverMenuHandle, SpinnerLabel, TintColor, Tooltip, WithScrollbar, prelude::*, PopoverMenuHandle, SpinnerLabel, TintColor, Tooltip, WithScrollbar, prelude::*,
}; };
use util::{ResultExt, size::format_file_size, time::duration_alt_display}; use util::{ResultExt, size::format_file_size, time::duration_alt_display};
use workspace::{CollaboratorId, NewTerminal, Workspace}; use workspace::{CollaboratorId, Workspace};
use zed_actions::agent::{Chat, ToggleModelSelector}; use zed_actions::agent::{Chat, ToggleModelSelector};
use zed_actions::assistant::OpenRulesLibrary; use zed_actions::assistant::OpenRulesLibrary;
@@ -69,8 +69,8 @@ use crate::ui::{
}; };
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, OpenAgentDiff, OpenHistory, RejectAll,
RejectAll, RejectOnce, ToggleBurnMode, ToggleProfileSelector, RejectOnce, ToggleBurnMode, ToggleProfileSelector,
}; };
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
@@ -125,9 +125,8 @@ impl ProfileProvider for Entity<agent::Thread> {
} }
fn set_profile(&self, profile_id: AgentProfileId, cx: &mut App) { fn set_profile(&self, profile_id: AgentProfileId, cx: &mut App) {
self.update(cx, |thread, cx| { self.update(cx, |thread, _cx| {
// Apply the profile and let the thread swap to its default model. thread.set_profile(profile_id);
thread.set_profile(profile_id, cx);
}); });
} }
@@ -170,7 +169,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_name = telemetry.agent_name();
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,9 +179,9 @@ impl ThreadFeedbackState {
let thread = task.await?; let thread = task.await?;
telemetry::event!( telemetry::event!(
"Agent Thread Rated", "Agent Thread Rated",
agent = agent,
session_id = session_id, session_id = session_id,
rating = rating, rating = rating,
agent = agent_name,
thread = thread thread = thread
); );
anyhow::Ok(()) anyhow::Ok(())
@@ -207,15 +206,15 @@ 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_name = telemetry.agent_name();
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,
session_id = session_id, session_id = session_id,
comments = comments, comments = comments,
agent = agent_name,
thread = thread thread = thread
); );
anyhow::Ok(()) anyhow::Ok(())
@@ -278,7 +277,6 @@ pub struct AcpThreadView {
notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>, notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>,
thread_retry_status: Option<RetryStatus>, thread_retry_status: Option<RetryStatus>,
thread_error: Option<ThreadError>, thread_error: Option<ThreadError>,
thread_error_markdown: Option<Entity<Markdown>>,
thread_feedback: ThreadFeedbackState, thread_feedback: ThreadFeedbackState,
list_state: ListState, list_state: ListState,
auth_task: Option<Task<()>>, auth_task: Option<Task<()>>,
@@ -296,6 +294,7 @@ pub struct AcpThreadView {
resume_thread_metadata: Option<DbThreadMetadata>, resume_thread_metadata: Option<DbThreadMetadata>,
_cancel_task: Option<Task<()>>, _cancel_task: Option<Task<()>>,
_subscriptions: [Subscription; 5], _subscriptions: [Subscription; 5],
#[cfg(target_os = "windows")]
show_codex_windows_warning: bool, show_codex_windows_warning: bool,
} }
@@ -338,7 +337,19 @@ impl AcpThreadView {
let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default())); let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
let available_commands = Rc::new(RefCell::new(vec![])); let available_commands = Rc::new(RefCell::new(vec![]));
let placeholder = placeholder_text(agent.name().as_ref(), false); let placeholder = if agent.name() == "Zed Agent" {
format!("Message the {} — @ to include context", agent.name())
} else if agent.name() == "Claude Code"
|| agent.name() == "Codex"
|| !available_commands.borrow().is_empty()
{
format!(
"Message {} — @ to include context, / for commands",
agent.name()
)
} else {
format!("Message {} — @ to include context", agent.name())
};
let message_editor = cx.new(|cx| { let message_editor = cx.new(|cx| {
let mut editor = MessageEditor::new( let mut editor = MessageEditor::new(
@@ -390,6 +401,7 @@ impl AcpThreadView {
), ),
]; ];
#[cfg(target_os = "windows")]
let show_codex_windows_warning = crate::ExternalAgent::parse_built_in(agent.as_ref()) let show_codex_windows_warning = crate::ExternalAgent::parse_built_in(agent.as_ref())
== Some(crate::ExternalAgent::Codex); == Some(crate::ExternalAgent::Codex);
@@ -416,7 +428,6 @@ impl AcpThreadView {
list_state: list_state, list_state: list_state,
thread_retry_status: None, thread_retry_status: None,
thread_error: None, thread_error: None,
thread_error_markdown: None,
thread_feedback: Default::default(), thread_feedback: Default::default(),
auth_task: None, auth_task: None,
expanded_tool_calls: HashSet::default(), expanded_tool_calls: HashSet::default(),
@@ -436,6 +447,7 @@ impl AcpThreadView {
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
new_server_version_available: None, new_server_version_available: None,
resume_thread_metadata: resume_thread, resume_thread_metadata: resume_thread,
#[cfg(target_os = "windows")]
show_codex_windows_warning, show_codex_windows_warning,
} }
} }
@@ -529,7 +541,14 @@ impl AcpThreadView {
}) })
.log_err() .log_err()
} else { } else {
let root_dir = root_dir.unwrap_or(paths::home_dir().as_path().into()); let root_dir = if let Some(acp_agent) = connection
.clone()
.downcast::<agent_servers::AcpConnection>()
{
acp_agent.root_dir().into()
} else {
root_dir.unwrap_or(paths::home_dir().as_path().into())
};
cx.update(|_, cx| { cx.update(|_, cx| {
connection connection
.clone() .clone()
@@ -591,13 +610,9 @@ impl AcpThreadView {
.connection() .connection()
.model_selector(thread.read(cx).session_id()) .model_selector(thread.read(cx).session_id())
.map(|selector| { .map(|selector| {
let agent_server = this.agent.clone();
let fs = this.project.read(cx).fs().clone();
cx.new(|cx| { cx.new(|cx| {
AcpModelSelectorPopover::new( AcpModelSelectorPopover::new(
selector, selector,
agent_server,
fs,
PopoverMenuHandle::default(), PopoverMenuHandle::default(),
this.focus_handle(cx), this.focus_handle(cx),
window, window,
@@ -804,7 +819,6 @@ impl AcpThreadView {
if should_retry { if should_retry {
self.thread_error = None; self.thread_error = None;
self.thread_error_markdown = None;
self.reset(window, cx); self.reset(window, cx);
} }
} }
@@ -1119,6 +1133,8 @@ impl AcpThreadView {
message_editor.contents(full_mention_content, cx) message_editor.contents(full_mention_content, cx)
}); });
let agent_telemetry_id = self.agent.telemetry_id();
self.thread_error.take(); self.thread_error.take();
self.editing_message.take(); self.editing_message.take();
self.thread_feedback.clear(); self.thread_feedback.clear();
@@ -1126,8 +1142,6 @@ 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 thread = thread.downgrade(); let thread = thread.downgrade();
if self.should_be_following { if self.should_be_following {
self.workspace self.workspace
@@ -1138,8 +1152,6 @@ impl AcpThreadView {
} }
self.is_loading_contents = true; self.is_loading_contents = true;
let model_id = self.current_model_id(cx);
let mode_id = self.current_mode_id(cx);
let guard = cx.new(|_| ()); let guard = cx.new(|_| ());
cx.observe_release(&guard, |this, _guard, cx| { cx.observe_release(&guard, |this, _guard, cx| {
this.is_loading_contents = false; this.is_loading_contents = false;
@@ -1161,7 +1173,6 @@ impl AcpThreadView {
message_editor.clear(window, cx); message_editor.clear(window, cx);
}); });
})?; })?;
let turn_start_time = Instant::now();
let send = thread.update(cx, |thread, cx| { let send = thread.update(cx, |thread, cx| {
thread.action_log().update(cx, |action_log, cx| { thread.action_log().update(cx, |action_log, cx| {
for buffer in tracked_buffers { for buffer in tracked_buffers {
@@ -1170,29 +1181,11 @@ impl AcpThreadView {
}); });
drop(guard); drop(guard);
telemetry::event!( telemetry::event!("Agent Message Sent", agent = agent_telemetry_id);
"Agent Message Sent",
agent = agent_telemetry_id,
session = session_id,
model = model_id,
mode = mode_id
);
thread.send(contents, cx) thread.send(contents, cx)
})?; })?;
let res = send.await; send.await
let turn_time_ms = turn_start_time.elapsed().as_millis();
let status = if res.is_ok() { "success" } else { "failure" };
telemetry::event!(
"Agent Turn Completed",
agent = agent_telemetry_id,
session = session_id,
model = model_id,
mode = mode_id,
status,
turn_time_ms,
);
res
}); });
cx.spawn(async move |this, cx| { cx.spawn(async move |this, cx| {
@@ -1337,7 +1330,6 @@ impl AcpThreadView {
fn clear_thread_error(&mut self, cx: &mut Context<Self>) { fn clear_thread_error(&mut self, cx: &mut Context<Self>) {
self.thread_error = None; self.thread_error = None;
self.thread_error_markdown = None;
cx.notify(); cx.notify();
} }
@@ -1395,7 +1387,7 @@ impl AcpThreadView {
AcpThreadEvent::Refusal => { AcpThreadEvent::Refusal => {
self.thread_retry_status.take(); self.thread_retry_status.take();
self.thread_error = Some(ThreadError::Refusal); self.thread_error = Some(ThreadError::Refusal);
let model_or_agent_name = self.current_model_name(cx); let model_or_agent_name = self.get_current_model_name(cx);
let notification_message = let notification_message =
format!("{} refused to respond to this request", model_or_agent_name); format!("{} refused to respond to this request", model_or_agent_name);
self.notify_with_sound(&notification_message, IconName::Warning, window, cx); self.notify_with_sound(&notification_message, IconName::Warning, window, cx);
@@ -1455,14 +1447,7 @@ impl AcpThreadView {
}); });
} }
let has_commands = !available_commands.is_empty();
self.available_commands.replace(available_commands); self.available_commands.replace(available_commands);
let new_placeholder = placeholder_text(self.agent.name().as_ref(), has_commands);
self.message_editor.update(cx, |editor, cx| {
editor.set_placeholder_text(&new_placeholder, window, cx);
});
} }
AcpThreadEvent::ModeUpdated(_mode) => { AcpThreadEvent::ModeUpdated(_mode) => {
// The connection keeps track of the mode // The connection keeps track of the mode
@@ -1521,12 +1506,6 @@ impl AcpThreadView {
}) })
.unwrap_or_default(); .unwrap_or_default();
// Run SpawnInTerminal in the same dir as the ACP server
let cwd = connection
.clone()
.downcast::<agent_servers::AcpConnection>()
.map(|acp_conn| acp_conn.root_dir().to_path_buf());
// Build SpawnInTerminal from _meta // Build SpawnInTerminal from _meta
let login = task::SpawnInTerminal { let login = task::SpawnInTerminal {
id: task::TaskId(format!("external-agent-{}-login", label)), id: task::TaskId(format!("external-agent-{}-login", label)),
@@ -1535,7 +1514,6 @@ impl AcpThreadView {
command: Some(command.to_string()), command: Some(command.to_string()),
args, args,
command_label: label.to_string(), command_label: label.to_string(),
cwd,
env, env,
use_new_terminal: true, use_new_terminal: true,
allow_concurrent_runs: true, allow_concurrent_runs: true,
@@ -1548,9 +1526,8 @@ impl AcpThreadView {
pending_auth_method.replace(method.clone()); pending_auth_method.replace(method.clone());
if let Some(workspace) = self.workspace.upgrade() { if let Some(workspace) = self.workspace.upgrade() {
let project = self.project.clone();
let authenticate = Self::spawn_external_agent_login( let authenticate = Self::spawn_external_agent_login(
login, workspace, project, false, true, window, cx, login, workspace, false, window, cx,
); );
cx.notify(); cx.notify();
self.auth_task = Some(cx.spawn_in(window, { self.auth_task = Some(cx.spawn_in(window, {
@@ -1694,10 +1671,7 @@ impl AcpThreadView {
&& let Some(login) = self.login.clone() && let Some(login) = self.login.clone()
{ {
if let Some(workspace) = self.workspace.upgrade() { if let Some(workspace) = self.workspace.upgrade() {
let project = self.project.clone(); Self::spawn_external_agent_login(login, workspace, false, window, cx)
Self::spawn_external_agent_login(
login, workspace, project, false, false, window, cx,
)
} else { } else {
Task::ready(Ok(())) Task::ready(Ok(()))
} }
@@ -1747,40 +1721,17 @@ impl AcpThreadView {
fn spawn_external_agent_login( fn spawn_external_agent_login(
login: task::SpawnInTerminal, login: task::SpawnInTerminal,
workspace: Entity<Workspace>, workspace: Entity<Workspace>,
project: Entity<Project>,
previous_attempt: bool, previous_attempt: bool,
check_exit_code: bool,
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) -> Task<Result<()>> { ) -> Task<Result<()>> {
let Some(terminal_panel) = workspace.read(cx).panel::<TerminalPanel>(cx) else { let Some(terminal_panel) = workspace.read(cx).panel::<TerminalPanel>(cx) else {
return Task::ready(Ok(())); return Task::ready(Ok(()));
}; };
let project = workspace.read(cx).project().clone();
window.spawn(cx, async move |cx| { window.spawn(cx, async move |cx| {
let mut task = login.clone(); let mut task = login.clone();
if let Some(cmd) = &task.command {
// Have "node" command use Zed's managed Node runtime by default
if cmd == "node" {
let resolved_node_runtime = project
.update(cx, |project, cx| {
let agent_server_store = project.agent_server_store().clone();
agent_server_store.update(cx, |store, cx| {
store.node_runtime().map(|node_runtime| {
cx.background_spawn(async move {
node_runtime.binary_path().await
})
})
})
});
if let Ok(Some(resolve_task)) = resolved_node_runtime {
if let Ok(node_path) = resolve_task.await {
task.command = Some(node_path.to_string_lossy().to_string());
}
}
}
}
task.shell = task::Shell::WithArguments { task.shell = task::Shell::WithArguments {
program: task.command.take().expect("login command should be set"), program: task.command.take().expect("login command should be set"),
args: std::mem::take(&mut task.args), args: std::mem::take(&mut task.args),
@@ -1798,65 +1749,44 @@ impl AcpThreadView {
})?; })?;
let terminal = terminal.await?; let terminal = terminal.await?;
let mut exit_status = terminal
.read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
.fuse();
if check_exit_code { let logged_in = cx
// For extension-based auth, wait for the process to exit and check exit code .spawn({
let exit_status = terminal let terminal = terminal.clone();
.read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))? async move |cx| {
.await; loop {
cx.background_executor().timer(Duration::from_secs(1)).await;
match exit_status { let content =
Some(status) if status.success() => { terminal.update(cx, |terminal, _cx| terminal.get_content())?;
Ok(()) if content.contains("Login successful")
} || content.contains("Type your message")
Some(status) => { {
Err(anyhow!("Login command failed with exit code: {:?}", status.code())) return anyhow::Ok(());
}
None => {
Err(anyhow!("Login command terminated without exit status"))
}
}
} else {
// For hardcoded agents (claude-login, gemini-cli): look for specific output
let mut exit_status = terminal
.read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
.fuse();
let logged_in = cx
.spawn({
let terminal = terminal.clone();
async move |cx| {
loop {
cx.background_executor().timer(Duration::from_secs(1)).await;
let content =
terminal.update(cx, |terminal, _cx| terminal.get_content())?;
if content.contains("Login successful")
|| content.contains("Type your message")
{
return anyhow::Ok(());
}
} }
} }
})
.fuse();
futures::pin_mut!(logged_in);
futures::select_biased! {
result = logged_in => {
if let Err(e) = result {
log::error!("{e}");
return Err(anyhow!("exited before logging in"));
}
} }
_ = exit_status => { })
if !previous_attempt && project.read_with(cx, |project, _| project.is_via_remote_server())? && login.label.contains("gemini") { .fuse();
return cx.update(|window, cx| Self::spawn_external_agent_login(login, workspace, project.clone(), true, false, window, cx))?.await futures::pin_mut!(logged_in);
} futures::select_biased! {
result = logged_in => {
if let Err(e) = result {
log::error!("{e}");
return Err(anyhow!("exited before logging in")); return Err(anyhow!("exited before logging in"));
} }
} }
terminal.update(cx, |terminal, _| terminal.kill_active_task())?; _ = exit_status => {
Ok(()) if !previous_attempt && project.read_with(cx, |project, _| project.is_via_remote_server())? && login.label.contains("gemini") {
return cx.update(|window, cx| Self::spawn_external_agent_login(login, workspace, true, window, cx))?.await
}
return Err(anyhow!("exited before logging in"));
}
} }
terminal.update(cx, |terminal, _| terminal.kill_active_task())?;
Ok(())
}) })
} }
@@ -1871,14 +1801,6 @@ impl AcpThreadView {
let Some(thread) = self.thread() else { let Some(thread) = self.thread() else {
return; return;
}; };
telemetry::event!(
"Agent Tool Call Authorized",
agent = self.agent.telemetry_id(),
session = thread.read(cx).session_id(),
option = option_kind
);
thread.update(cx, |thread, cx| { thread.update(cx, |thread, cx| {
thread.authorize_tool_call(tool_call_id, option_id, option_kind, cx); thread.authorize_tool_call(tool_call_id, option_id, option_kind, cx);
}); });
@@ -2129,15 +2051,6 @@ impl AcpThreadView {
.into_any(), .into_any(),
}; };
let needs_confirmation = if let AgentThreadEntry::ToolCall(tool_call) = entry {
matches!(
tool_call.status,
ToolCallStatus::WaitingForConfirmation { .. }
)
} else {
false
};
let Some(thread) = self.thread() else { let Some(thread) = self.thread() else {
return primary; return primary;
}; };
@@ -2146,13 +2059,7 @@ impl AcpThreadView {
v_flex() v_flex()
.w_full() .w_full()
.child(primary) .child(primary)
.map(|this| { .child(self.render_thread_controls(&thread, cx))
if needs_confirmation {
this.child(self.render_generating(true))
} else {
this.child(self.render_thread_controls(&thread, cx))
}
})
.when_some( .when_some(
self.thread_feedback.comments_editor.clone(), self.thread_feedback.comments_editor.clone(),
|this, editor| this.child(Self::render_feedback_feedback_editor(editor, cx)), |this, editor| this.child(Self::render_feedback_feedback_editor(editor, cx)),
@@ -3151,7 +3058,7 @@ impl AcpThreadView {
.text_ui_sm(cx) .text_ui_sm(cx)
.h_full() .h_full()
.children(terminal_view.map(|terminal_view| { .children(terminal_view.map(|terminal_view| {
let element = if terminal_view if terminal_view
.read(cx) .read(cx)
.content_mode(window, cx) .content_mode(window, cx)
.is_scrollable() .is_scrollable()
@@ -3159,15 +3066,7 @@ impl AcpThreadView {
div().h_72().child(terminal_view).into_any_element() div().h_72().child(terminal_view).into_any_element()
} else { } else {
terminal_view.into_any_element() terminal_view.into_any_element()
}; }
div()
.on_action(cx.listener(|_this, _: &NewTerminal, window, cx| {
window.dispatch_action(NewThread.boxed_clone(), cx);
cx.stop_propagation();
}))
.child(element)
.into_any_element()
})), })),
) )
}) })
@@ -3619,7 +3518,6 @@ impl AcpThreadView {
) -> Option<AnyElement> { ) -> Option<AnyElement> {
let thread = thread_entity.read(cx); let thread = thread_entity.read(cx);
let action_log = thread.action_log(); let action_log = thread.action_log();
let telemetry = ActionLogTelemetry::from(thread);
let changed_buffers = action_log.read(cx).changed_buffers(cx); let changed_buffers = action_log.read(cx).changed_buffers(cx);
let plan = thread.plan(); let plan = thread.plan();
@@ -3667,7 +3565,6 @@ impl AcpThreadView {
.when(self.edits_expanded, |parent| { .when(self.edits_expanded, |parent| {
parent.child(self.render_edited_files( parent.child(self.render_edited_files(
action_log, action_log,
telemetry,
&changed_buffers, &changed_buffers,
pending_edits, pending_edits,
cx, cx,
@@ -3948,7 +3845,6 @@ impl AcpThreadView {
fn render_edited_files( fn render_edited_files(
&self, &self,
action_log: &Entity<ActionLog>, action_log: &Entity<ActionLog>,
telemetry: ActionLogTelemetry,
changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>, changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
pending_edits: bool, pending_edits: bool,
cx: &Context<Self>, cx: &Context<Self>,
@@ -4068,14 +3964,12 @@ impl AcpThreadView {
.on_click({ .on_click({
let buffer = buffer.clone(); let buffer = buffer.clone();
let action_log = action_log.clone(); let action_log = action_log.clone();
let telemetry = telemetry.clone();
move |_, _, cx| { move |_, _, cx| {
action_log.update(cx, |action_log, cx| { action_log.update(cx, |action_log, cx| {
action_log action_log
.reject_edits_in_ranges( .reject_edits_in_ranges(
buffer.clone(), buffer.clone(),
vec![Anchor::MIN..Anchor::MAX], vec![Anchor::MIN..Anchor::MAX],
Some(telemetry.clone()),
cx, cx,
) )
.detach_and_log_err(cx); .detach_and_log_err(cx);
@@ -4090,13 +3984,11 @@ impl AcpThreadView {
.on_click({ .on_click({
let buffer = buffer.clone(); let buffer = buffer.clone();
let action_log = action_log.clone(); let action_log = action_log.clone();
let telemetry = telemetry.clone();
move |_, _, cx| { move |_, _, cx| {
action_log.update(cx, |action_log, cx| { action_log.update(cx, |action_log, cx| {
action_log.keep_edits_in_range( action_log.keep_edits_in_range(
buffer.clone(), buffer.clone(),
Anchor::MIN..Anchor::MAX, Anchor::MIN..Anchor::MAX,
Some(telemetry.clone()),
cx, cx,
); );
}) })
@@ -4207,8 +4099,6 @@ impl AcpThreadView {
.justify_between() .justify_between()
.child( .child(
h_flex() h_flex()
.gap_0p5()
.child(self.render_add_context_button(cx))
.child(self.render_follow_toggle(cx)) .child(self.render_follow_toggle(cx))
.children(self.render_burn_mode_toggle(cx)), .children(self.render_burn_mode_toggle(cx)),
) )
@@ -4314,23 +4204,17 @@ impl AcpThreadView {
let Some(thread) = self.thread() else { let Some(thread) = self.thread() else {
return; return;
}; };
let telemetry = ActionLogTelemetry::from(thread.read(cx));
let action_log = thread.read(cx).action_log().clone(); let action_log = thread.read(cx).action_log().clone();
action_log.update(cx, |action_log, cx| { action_log.update(cx, |action_log, cx| action_log.keep_all_edits(cx));
action_log.keep_all_edits(Some(telemetry), cx)
});
} }
fn reject_all(&mut self, _: &RejectAll, _window: &mut Window, cx: &mut Context<Self>) { fn reject_all(&mut self, _: &RejectAll, _window: &mut Window, cx: &mut Context<Self>) {
let Some(thread) = self.thread() else { let Some(thread) = self.thread() else {
return; return;
}; };
let telemetry = ActionLogTelemetry::from(thread.read(cx));
let action_log = thread.read(cx).action_log().clone(); let action_log = thread.read(cx).action_log().clone();
action_log action_log
.update(cx, |action_log, cx| { .update(cx, |action_log, cx| action_log.reject_all_edits(cx))
action_log.reject_all_edits(Some(telemetry), cx)
})
.detach(); .detach();
} }
@@ -4523,29 +4407,6 @@ impl AcpThreadView {
})) }))
} }
fn render_add_context_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
let message_editor = self.message_editor.clone();
let menu_visible = message_editor.read(cx).is_completions_menu_visible(cx);
IconButton::new("add-context", IconName::AtSign)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.when(!menu_visible, |this| {
this.tooltip(move |_window, cx| {
Tooltip::with_meta("Add Context", None, "Or type @ to include context", cx)
})
})
.on_click(cx.listener(move |_this, _, window, cx| {
let message_editor_clone = message_editor.clone();
window.defer(cx, move |window, cx| {
message_editor_clone.update(cx, |message_editor, cx| {
message_editor.trigger_completion_menu(window, cx);
});
});
}))
}
fn render_markdown(&self, markdown: Entity<Markdown>, style: MarkdownStyle) -> MarkdownElement { fn render_markdown(&self, markdown: Entity<Markdown>, style: MarkdownStyle) -> MarkdownElement {
let workspace = self.workspace.clone(); let workspace = self.workspace.clone();
MarkdownElement::new(markdown, style).on_url_click(move |text, window, cx| { MarkdownElement::new(markdown, style).on_url_click(move |text, window, cx| {
@@ -4749,36 +4610,35 @@ impl AcpThreadView {
.languages .languages
.language_for_name("Markdown"); .language_for_name("Markdown");
let (thread_title, markdown) = if let Some(thread) = self.thread() { let (thread_summary, markdown) = if let Some(thread) = self.thread() {
let thread = thread.read(cx); let thread = thread.read(cx);
(thread.title().to_string(), thread.to_markdown(cx)) (thread.title().to_string(), thread.to_markdown(cx))
} else { } else {
return Task::ready(Ok(())); return Task::ready(Ok(()));
}; };
let project = workspace.read(cx).project().clone();
window.spawn(cx, async move |cx| { window.spawn(cx, async move |cx| {
let markdown_language = markdown_language_task.await?; let markdown_language = markdown_language_task.await?;
let buffer = project
.update(cx, |project, cx| project.create_buffer(false, cx))?
.await?;
buffer.update(cx, |buffer, cx| {
buffer.set_text(markdown, cx);
buffer.set_language(Some(markdown_language), cx);
buffer.set_capability(language::Capability::ReadOnly, cx);
})?;
workspace.update_in(cx, |workspace, window, cx| { workspace.update_in(cx, |workspace, window, cx| {
let buffer = cx let project = workspace.project().clone();
.new(|cx| MultiBuffer::singleton(buffer, cx).with_title(thread_title.clone()));
if !project.read(cx).is_local() {
bail!("failed to open active thread as markdown in remote project");
}
let buffer = project.update(cx, |project, cx| {
project.create_local_buffer(&markdown, Some(markdown_language), true, cx)
});
let buffer = cx.new(|cx| {
MultiBuffer::singleton(buffer, cx).with_title(thread_summary.clone())
});
workspace.add_item_to_active_pane( workspace.add_item_to_active_pane(
Box::new(cx.new(|cx| { Box::new(cx.new(|cx| {
let mut editor = let mut editor =
Editor::for_multibuffer(buffer, Some(project.clone()), window, cx); Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
editor.set_breadcrumb_header(thread_title); editor.set_breadcrumb_header(thread_summary);
editor editor
})), })),
None, None,
@@ -4786,7 +4646,9 @@ impl AcpThreadView {
window, window,
cx, cx,
); );
})?;
anyhow::Ok(())
})??;
anyhow::Ok(()) anyhow::Ok(())
}) })
} }
@@ -4967,31 +4829,6 @@ impl AcpThreadView {
} }
} }
fn render_generating(&self, confirmation: bool) -> impl IntoElement {
h_flex()
.id("generating-spinner")
.py_2()
.px(rems_from_px(22.))
.map(|this| {
if confirmation {
this.gap_2()
.child(
h_flex()
.w_2()
.child(SpinnerLabel::sand().size(LabelSize::Small)),
)
.child(
LoadingLabel::new("Waiting Confirmation")
.size(LabelSize::Small)
.color(Color::Muted),
)
} else {
this.child(SpinnerLabel::new().size(LabelSize::Small))
}
})
.into_any_element()
}
fn render_thread_controls( fn render_thread_controls(
&self, &self,
thread: &Entity<AcpThread>, thread: &Entity<AcpThread>,
@@ -4999,7 +4836,12 @@ impl AcpThreadView {
) -> impl IntoElement { ) -> impl IntoElement {
let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating); let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating);
if is_generating { if is_generating {
return self.render_generating(false).into_any_element(); return h_flex().id("thread-controls-container").child(
div()
.py_2()
.px(rems_from_px(22.))
.child(SpinnerLabel::new().size(LabelSize::Small)),
);
} }
let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown) let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown)
@@ -5087,10 +4929,7 @@ impl AcpThreadView {
); );
} }
container container.child(open_as_markdown).child(scroll_to_top)
.child(open_as_markdown)
.child(scroll_to_top)
.into_any_element()
} }
fn render_feedback_feedback_editor(editor: Entity<Editor>, cx: &Context<Self>) -> Div { fn render_feedback_feedback_editor(editor: Entity<Editor>, cx: &Context<Self>) -> Div {
@@ -5320,6 +5159,7 @@ impl AcpThreadView {
) )
} }
#[cfg(target_os = "windows")]
fn render_codex_windows_warning(&self, cx: &mut Context<Self>) -> Option<Callout> { fn render_codex_windows_warning(&self, cx: &mut Context<Self>) -> Option<Callout> {
if self.show_codex_windows_warning { if self.show_codex_windows_warning {
Some( Some(
@@ -5335,9 +5175,8 @@ impl AcpThreadView {
.icon_size(IconSize::Small) .icon_size(IconSize::Small)
.icon_color(Color::Muted) .icon_color(Color::Muted)
.on_click(cx.listener({ .on_click(cx.listener({
move |_, _, _window, cx| { move |_, _, window, cx| {
#[cfg(windows)] window.dispatch_action(
_window.dispatch_action(
zed_actions::wsl_actions::OpenWsl::default().boxed_clone(), zed_actions::wsl_actions::OpenWsl::default().boxed_clone(),
cx, cx,
); );
@@ -5363,9 +5202,9 @@ impl AcpThreadView {
} }
} }
fn render_thread_error(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> { fn render_thread_error(&self, cx: &mut Context<Self>) -> Option<Div> {
let content = match self.thread_error.as_ref()? { let content = match self.thread_error.as_ref()? {
ThreadError::Other(error) => self.render_any_thread_error(error.clone(), window, cx), ThreadError::Other(error) => self.render_any_thread_error(error.clone(), cx),
ThreadError::Refusal => self.render_refusal_error(cx), ThreadError::Refusal => self.render_refusal_error(cx),
ThreadError::AuthenticationRequired(error) => { ThreadError::AuthenticationRequired(error) => {
self.render_authentication_required_error(error.clone(), cx) self.render_authentication_required_error(error.clone(), cx)
@@ -5412,31 +5251,20 @@ impl AcpThreadView {
) )
} }
fn current_mode_id(&self, cx: &App) -> Option<Arc<str>> { fn get_current_model_name(&self, cx: &App) -> SharedString {
if let Some(thread) = self.as_native_thread(cx) {
Some(thread.read(cx).profile().0.clone())
} else if let Some(mode_selector) = self.mode_selector() {
Some(mode_selector.read(cx).mode().0)
} else {
None
}
}
fn current_model_id(&self, cx: &App) -> Option<String> {
self.model_selector
.as_ref()
.and_then(|selector| selector.read(cx).active_model(cx).map(|m| m.id.to_string()))
}
fn current_model_name(&self, cx: &App) -> SharedString {
// For native agent (Zed Agent), use the specific model name (e.g., "Claude 3.5 Sonnet") // For native agent (Zed Agent), use the specific model name (e.g., "Claude 3.5 Sonnet")
// For ACP agents, use the agent name (e.g., "Claude Code", "Gemini CLI") // For ACP agents, use the agent name (e.g., "Claude Code", "Gemini CLI")
// This provides better clarity about what refused the request // This provides better clarity about what refused the request
if self.as_native_connection(cx).is_some() { if self
.agent
.clone()
.downcast::<agent::NativeAgentServer>()
.is_some()
{
// Native agent - use the model name
self.model_selector self.model_selector
.as_ref() .as_ref()
.and_then(|selector| selector.read(cx).active_model(cx)) .and_then(|selector| selector.read(cx).active_model_name(cx))
.map(|model| model.name.clone())
.unwrap_or_else(|| SharedString::from("The model")) .unwrap_or_else(|| SharedString::from("The model"))
} else { } else {
// ACP agent - use the agent name (e.g., "Claude Code", "Gemini CLI") // ACP agent - use the agent name (e.g., "Claude Code", "Gemini CLI")
@@ -5445,7 +5273,7 @@ impl AcpThreadView {
} }
fn render_refusal_error(&self, cx: &mut Context<'_, Self>) -> Callout { fn render_refusal_error(&self, cx: &mut Context<'_, Self>) -> Callout {
let model_or_agent_name = self.current_model_name(cx); let model_or_agent_name = self.get_current_model_name(cx);
let refusal_message = format!( let refusal_message = format!(
"{} refused to respond to this prompt. This can happen when a model believes the prompt violates its content policy or safety guidelines, so rephrasing it can sometimes address the issue.", "{} refused to respond to this prompt. This can happen when a model believes the prompt violates its content policy or safety guidelines, so rephrasing it can sometimes address the issue.",
model_or_agent_name model_or_agent_name
@@ -5460,12 +5288,7 @@ impl AcpThreadView {
.dismiss_action(self.dismiss_error_button(cx)) .dismiss_action(self.dismiss_error_button(cx))
} }
fn render_any_thread_error( fn render_any_thread_error(&self, error: SharedString, cx: &mut Context<'_, Self>) -> Callout {
&mut self,
error: SharedString,
window: &mut Window,
cx: &mut Context<'_, Self>,
) -> Callout {
let can_resume = self let can_resume = self
.thread() .thread()
.map_or(false, |thread| thread.read(cx).can_resume(cx)); .map_or(false, |thread| thread.read(cx).can_resume(cx));
@@ -5478,24 +5301,11 @@ impl AcpThreadView {
supports_burn_mode && thread.completion_mode() == CompletionMode::Normal supports_burn_mode && thread.completion_mode() == CompletionMode::Normal
}); });
let markdown = if let Some(markdown) = &self.thread_error_markdown {
markdown.clone()
} else {
let markdown = cx.new(|cx| Markdown::new(error.clone(), None, None, cx));
self.thread_error_markdown = Some(markdown.clone());
markdown
};
let markdown_style = default_markdown_style(false, true, window, cx);
let description = self
.render_markdown(markdown, markdown_style)
.into_any_element();
Callout::new() Callout::new()
.severity(Severity::Error) .severity(Severity::Error)
.title("Error")
.icon(IconName::XCircle) .icon(IconName::XCircle)
.title("An Error Happened") .description(error.clone())
.description_slot(description)
.actions_slot( .actions_slot(
h_flex() h_flex()
.gap_0p5() .gap_0p5()
@@ -5514,9 +5324,11 @@ impl AcpThreadView {
}) })
.when(can_resume, |this| { .when(can_resume, |this| {
this.child( this.child(
IconButton::new("retry", IconName::RotateCw) Button::new("retry", "Retry")
.icon(IconName::RotateCw)
.icon_position(IconPosition::Start)
.icon_size(IconSize::Small) .icon_size(IconSize::Small)
.tooltip(Tooltip::text("Retry Generation")) .label_size(LabelSize::Small)
.on_click(cx.listener(|this, _, _window, cx| { .on_click(cx.listener(|this, _, _window, cx| {
this.resume_chat(cx); this.resume_chat(cx);
})), })),
@@ -5658,6 +5470,7 @@ impl AcpThreadView {
IconButton::new("copy", IconName::Copy) IconButton::new("copy", IconName::Copy)
.icon_size(IconSize::Small) .icon_size(IconSize::Small)
.icon_color(Color::Muted)
.tooltip(Tooltip::text("Copy Error Message")) .tooltip(Tooltip::text("Copy Error Message"))
.on_click(move |_, _, cx| { .on_click(move |_, _, cx| {
cx.write_to_clipboard(ClipboardItem::new_string(message.clone())) cx.write_to_clipboard(ClipboardItem::new_string(message.clone()))
@@ -5667,6 +5480,7 @@ impl AcpThreadView {
fn dismiss_error_button(&self, cx: &mut Context<Self>) -> impl IntoElement { fn dismiss_error_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
IconButton::new("dismiss", IconName::Close) IconButton::new("dismiss", IconName::Close)
.icon_size(IconSize::Small) .icon_size(IconSize::Small)
.icon_color(Color::Muted)
.tooltip(Tooltip::text("Dismiss Error")) .tooltip(Tooltip::text("Dismiss Error"))
.on_click(cx.listener({ .on_click(cx.listener({
move |this, _, _, cx| { move |this, _, _, cx| {
@@ -5771,19 +5585,6 @@ fn loading_contents_spinner(size: IconSize) -> AnyElement {
.into_any_element() .into_any_element()
} }
fn placeholder_text(agent_name: &str, has_commands: bool) -> String {
if agent_name == "Zed Agent" {
format!("Message the {} — @ to include context", agent_name)
} else if has_commands {
format!(
"Message {} — @ to include context, / for commands",
agent_name
)
} else {
format!("Message {} — @ to include context", agent_name)
}
}
impl Focusable for AcpThreadView { impl Focusable for AcpThreadView {
fn focus_handle(&self, cx: &App) -> FocusHandle { fn focus_handle(&self, cx: &App) -> FocusHandle {
match self.thread_state { match self.thread_state {
@@ -5878,13 +5679,16 @@ impl Render for AcpThreadView {
}) })
.children(self.render_thread_retry_status_callout(window, cx)) .children(self.render_thread_retry_status_callout(window, cx))
.children({ .children({
if cfg!(windows) && self.project.read(cx).is_local() { #[cfg(target_os = "windows")]
{
self.render_codex_windows_warning(cx) self.render_codex_windows_warning(cx)
} else { }
None #[cfg(not(target_os = "windows"))]
{
Vec::<Empty>::new()
} }
}) })
.children(self.render_thread_error(window, cx)) .children(self.render_thread_error(cx))
.when_some( .when_some(
self.new_server_version_available.as_ref().filter(|_| { self.new_server_version_available.as_ref().filter(|_| {
!has_messages || !matches!(self.thread_state, ThreadState::Ready { .. }) !has_messages || !matches!(self.thread_state, ThreadState::Ready { .. })
@@ -5950,6 +5754,7 @@ fn default_markdown_style(
syntax: cx.theme().syntax().clone(), syntax: cx.theme().syntax().clone(),
selection_background_color: colors.element_selection_background, selection_background_color: colors.element_selection_background,
code_block_overflow_x_scroll: true, code_block_overflow_x_scroll: true,
table_overflow_x_scroll: true,
heading_level_styles: Some(HeadingLevelStyles { heading_level_styles: Some(HeadingLevelStyles {
h1: Some(TextStyleRefinement { h1: Some(TextStyleRefinement {
font_size: Some(rems(1.15).into()), font_size: Some(rems(1.15).into()),
@@ -6017,7 +5822,6 @@ fn default_markdown_style(
}, },
link: TextStyleRefinement { link: TextStyleRefinement {
background_color: Some(colors.editor_foreground.opacity(0.025)), background_color: Some(colors.editor_foreground.opacity(0.025)),
color: Some(colors.text_accent),
underline: Some(UnderlineStyle { underline: Some(UnderlineStyle {
color: Some(colors.text_accent.opacity(0.5)), color: Some(colors.text_accent.opacity(0.5)),
thickness: px(1.), thickness: px(1.),
@@ -6070,7 +5874,7 @@ pub(crate) mod tests {
use acp_thread::StubAgentConnection; use acp_thread::StubAgentConnection;
use agent_client_protocol::SessionId; use agent_client_protocol::SessionId;
use assistant_text_thread::TextThreadStore; use assistant_text_thread::TextThreadStore;
use editor::MultiBufferOffset; use editor::EditorSettings;
use fs::FakeFs; use fs::FakeFs;
use gpui::{EventEmitter, SemanticVersion, TestAppContext, VisualTestContext}; use gpui::{EventEmitter, SemanticVersion, TestAppContext, VisualTestContext};
use project::Project; use project::Project;
@@ -6458,10 +6262,6 @@ pub(crate) mod tests {
struct SaboteurAgentConnection; struct SaboteurAgentConnection;
impl AgentConnection for SaboteurAgentConnection { impl AgentConnection for SaboteurAgentConnection {
fn telemetry_id(&self) -> &'static str {
"saboteur"
}
fn new_thread( fn new_thread(
self: Rc<Self>, self: Rc<Self>,
project: Entity<Project>, project: Entity<Project>,
@@ -6522,10 +6322,6 @@ pub(crate) mod tests {
struct RefusalAgentConnection; struct RefusalAgentConnection;
impl AgentConnection for RefusalAgentConnection { impl AgentConnection for RefusalAgentConnection {
fn telemetry_id(&self) -> &'static str {
"refusal"
}
fn new_thread( fn new_thread(
self: Rc<Self>, self: Rc<Self>,
project: Entity<Project>, project: Entity<Project>,
@@ -6588,8 +6384,13 @@ pub(crate) mod tests {
cx.update(|cx| { cx.update(|cx| {
let settings_store = SettingsStore::test(cx); let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store); cx.set_global(settings_store);
language::init(cx);
Project::init_settings(cx);
AgentSettings::register(cx);
workspace::init_settings(cx);
theme::init(theme::LoadThemes::JustBase, cx); theme::init(theme::LoadThemes::JustBase, cx);
release_channel::init(SemanticVersion::default(), cx); release_channel::init(SemanticVersion::default(), cx);
EditorSettings::register(cx);
prompt_store::init(cx) prompt_store::init(cx)
}); });
} }
@@ -7239,7 +7040,7 @@ pub(crate) mod tests {
Editor::for_buffer(buffer.clone(), Some(project.clone()), window, cx); Editor::for_buffer(buffer.clone(), Some(project.clone()), window, cx);
editor.change_selections(Default::default(), window, cx, |selections| { editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges([MultiBufferOffset(8)..MultiBufferOffset(15)]); selections.select_ranges([8..15]);
}); });
editor editor
@@ -7301,7 +7102,7 @@ pub(crate) mod tests {
Editor::for_buffer(buffer.clone(), Some(project.clone()), window, cx); Editor::for_buffer(buffer.clone(), Some(project.clone()), window, cx);
editor.change_selections(Default::default(), window, cx, |selections| { editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges([MultiBufferOffset(8)..MultiBufferOffset(15)]); selections.select_ranges([8..15]);
}); });
editor editor

View File

@@ -1,5 +1,5 @@
mod add_llm_provider_modal; mod add_llm_provider_modal;
pub mod configure_context_server_modal; mod configure_context_server_modal;
mod configure_context_server_tools_modal; mod configure_context_server_tools_modal;
mod manage_profiles_modal; mod manage_profiles_modal;
mod tool_picker; mod tool_picker;
@@ -8,11 +8,10 @@ use std::{ops::Range, sync::Arc};
use agent::ContextServerRegistry; use agent::ContextServerRegistry;
use anyhow::Result; use anyhow::Result;
use client::zed_urls;
use cloud_llm_client::{Plan, PlanV1, PlanV2}; use cloud_llm_client::{Plan, PlanV1, PlanV2};
use collections::HashMap; use collections::HashMap;
use context_server::ContextServerId; use context_server::ContextServerId;
use editor::{Editor, MultiBufferOffset, SelectionEffects, scroll::Autoscroll}; use editor::{Editor, SelectionEffects, scroll::Autoscroll};
use extension::ExtensionManifest; use extension::ExtensionManifest;
use extension_host::ExtensionStore; use extension_host::ExtensionStore;
use fs::Fs; use fs::Fs;
@@ -27,27 +26,26 @@ use language_model::{
use language_models::AllLanguageModelSettings; use language_models::AllLanguageModelSettings;
use notifications::status_toast::{StatusToast, ToastIcon}; use notifications::status_toast::{StatusToast, ToastIcon};
use project::{ use project::{
agent_server_store::{ agent_server_store::{AgentServerStore, CLAUDE_CODE_NAME, CODEX_NAME, GEMINI_NAME},
AgentServerStore, CLAUDE_CODE_NAME, CODEX_NAME, ExternalAgentServerName, GEMINI_NAME,
},
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore}, context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
}; };
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, Button, ButtonStyle, Chip, CommonAnimationExt, ContextMenu, Disclosure, Divider, DividerColor,
Divider, DividerColor, ElevationIndex, IconName, IconPosition, IconSize, Indicator, LabelSize, ElevationIndex, IconName, IconPosition, IconSize, Indicator, LabelSize, PopoverMenu, Switch,
PopoverMenu, Switch, SwitchColor, Tooltip, WithScrollbar, prelude::*, SwitchColor, Tooltip, 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};
use zed_actions::{ExtensionCategoryFilter, OpenBrowser}; use zed_actions::ExtensionCategoryFilter;
pub(crate) use configure_context_server_modal::ConfigureContextServerModal; pub(crate) use configure_context_server_modal::ConfigureContextServerModal;
pub(crate) use configure_context_server_tools_modal::ConfigureContextServerToolsModal; pub(crate) use configure_context_server_tools_modal::ConfigureContextServerToolsModal;
pub(crate) use manage_profiles_modal::ManageProfilesModal; pub(crate) use manage_profiles_modal::ManageProfilesModal;
use crate::agent_configuration::add_llm_provider_modal::{ use crate::{
AddLlmProviderModal, LlmCompatibleProvider, AddContextServer,
agent_configuration::add_llm_provider_modal::{AddLlmProviderModal, LlmCompatibleProvider},
}; };
pub struct AgentConfiguration { pub struct AgentConfiguration {
@@ -417,7 +415,6 @@ impl AgentConfiguration {
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> impl IntoElement { ) -> impl IntoElement {
let providers = LanguageModelRegistry::read_global(cx).providers(); let providers = LanguageModelRegistry::read_global(cx).providers();
let popover_menu = PopoverMenu::new("add-provider-popover") let popover_menu = PopoverMenu::new("add-provider-popover")
.trigger( .trigger(
Button::new("add-provider", "Add Provider") Button::new("add-provider", "Add Provider")
@@ -428,6 +425,7 @@ impl AgentConfiguration {
.icon_color(Color::Muted) .icon_color(Color::Muted)
.label_size(LabelSize::Small), .label_size(LabelSize::Small),
) )
.anchor(gpui::Corner::TopRight)
.menu({ .menu({
let workspace = self.workspace.clone(); let workspace = self.workspace.clone();
move |window, cx| { move |window, cx| {
@@ -449,11 +447,6 @@ impl AgentConfiguration {
}) })
})) }))
} }
})
.anchor(gpui::Corner::TopRight)
.offset(gpui::Point {
x: px(0.0),
y: px(2.0),
}); });
v_flex() v_flex()
@@ -548,13 +541,12 @@ impl AgentConfiguration {
.icon_color(Color::Muted) .icon_color(Color::Muted)
.label_size(LabelSize::Small), .label_size(LabelSize::Small),
) )
.anchor(gpui::Corner::TopRight)
.menu({ .menu({
move |window, cx| { move |window, cx| {
Some(ContextMenu::build(window, cx, |menu, _window, _cx| { Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
menu.entry("Add Custom Server", None, { menu.entry("Add Custom Server", None, {
|window, cx| { |window, cx| window.dispatch_action(AddContextServer.boxed_clone(), cx)
window.dispatch_action(crate::AddContextServer.boxed_clone(), cx)
}
}) })
.entry("Install from Extensions", None, { .entry("Install from Extensions", None, {
|window, cx| { |window, cx| {
@@ -572,11 +564,6 @@ impl AgentConfiguration {
}) })
})) }))
} }
})
.anchor(gpui::Corner::TopRight)
.offset(gpui::Point {
x: px(0.0),
y: px(2.0),
}); });
v_flex() v_flex()
@@ -651,13 +638,15 @@ impl AgentConfiguration {
let is_running = matches!(server_status, ContextServerStatus::Running); let is_running = matches!(server_status, ContextServerStatus::Running);
let item_id = SharedString::from(context_server_id.0.clone()); let item_id = SharedString::from(context_server_id.0.clone());
// Servers without a configuration can only be provided by extensions. let is_from_extension = server_configuration
let provided_by_extension = server_configuration.as_ref().is_none_or(|config| { .as_ref()
matches!( .map(|config| {
config.as_ref(), matches!(
ContextServerConfiguration::Extension { .. } config.as_ref(),
) ContextServerConfiguration::Extension { .. }
}); )
})
.unwrap_or(false);
let error = if let ContextServerStatus::Error(error) = server_status.clone() { let error = if let ContextServerStatus::Error(error) = server_status.clone() {
Some(error) Some(error)
@@ -671,7 +660,7 @@ impl AgentConfiguration {
.tools_for_server(&context_server_id) .tools_for_server(&context_server_id)
.count(); .count();
let (source_icon, source_tooltip) = if provided_by_extension { let (source_icon, source_tooltip) = if is_from_extension {
( (
IconName::ZedSrcExtension, IconName::ZedSrcExtension,
"This MCP server was installed from an extension.", "This MCP server was installed from an extension.",
@@ -708,10 +697,7 @@ impl AgentConfiguration {
"Server is stopped.", "Server is stopped.",
), ),
}; };
let is_remote = server_configuration
.as_ref()
.map(|config| matches!(config.as_ref(), ContextServerConfiguration::Http { .. }))
.unwrap_or(false);
let context_server_configuration_menu = PopoverMenu::new("context-server-config-menu") let context_server_configuration_menu = PopoverMenu::new("context-server-config-menu")
.trigger_with_tooltip( .trigger_with_tooltip(
IconButton::new("context-server-config-menu", IconName::Settings) IconButton::new("context-server-config-menu", IconName::Settings)
@@ -724,6 +710,7 @@ impl AgentConfiguration {
let fs = self.fs.clone(); let fs = self.fs.clone();
let context_server_id = context_server_id.clone(); let context_server_id = context_server_id.clone();
let language_registry = self.language_registry.clone(); let language_registry = self.language_registry.clone();
let context_server_store = self.context_server_store.clone();
let workspace = self.workspace.clone(); let workspace = self.workspace.clone();
let context_server_registry = self.context_server_registry.clone(); let context_server_registry = self.context_server_registry.clone();
@@ -734,25 +721,14 @@ impl AgentConfiguration {
let language_registry = language_registry.clone(); let language_registry = language_registry.clone();
let workspace = workspace.clone(); let workspace = workspace.clone();
move |window, cx| { move |window, cx| {
if is_remote { ConfigureContextServerModal::show_modal_for_existing_server(
crate::agent_configuration::configure_context_server_modal::ConfigureContextServerModal::show_modal_for_existing_server( context_server_id.clone(),
context_server_id.clone(), language_registry.clone(),
language_registry.clone(), workspace.clone(),
workspace.clone(), window,
window, cx,
cx, )
) .detach_and_log_err(cx);
.detach();
} else {
ConfigureContextServerModal::show_modal_for_existing_server(
context_server_id.clone(),
language_registry.clone(),
workspace.clone(),
window,
cx,
)
.detach();
}
} }
}).when(tool_count > 0, |this| this.entry("View Tools", None, { }).when(tool_count > 0, |this| this.entry("View Tools", None, {
let context_server_id = context_server_id.clone(); let context_server_id = context_server_id.clone();
@@ -776,10 +752,23 @@ impl AgentConfiguration {
.entry("Uninstall", None, { .entry("Uninstall", None, {
let fs = fs.clone(); let fs = fs.clone();
let context_server_id = context_server_id.clone(); let context_server_id = context_server_id.clone();
let context_server_store = context_server_store.clone();
let workspace = workspace.clone(); let workspace = workspace.clone();
move |_, cx| { move |_, cx| {
let is_provided_by_extension = context_server_store
.read(cx)
.configuration_for_server(&context_server_id)
.as_ref()
.map(|config| {
matches!(
config.as_ref(),
ContextServerConfiguration::Extension { .. }
)
})
.unwrap_or(false);
let uninstall_extension_task = match ( let uninstall_extension_task = match (
provided_by_extension, is_provided_by_extension,
resolve_extension_for_context_server(&context_server_id, cx), resolve_extension_for_context_server(&context_server_id, cx),
) { ) {
(true, Some((id, manifest))) => { (true, Some((id, manifest))) => {
@@ -970,7 +959,7 @@ impl AgentConfiguration {
.cloned() .cloned()
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let user_defined_agents: Vec<_> = user_defined_agents let user_defined_agents = user_defined_agents
.into_iter() .into_iter()
.map(|name| { .map(|name| {
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) {
@@ -978,93 +967,27 @@ impl AgentConfiguration {
} else { } else {
AgentIcon::Name(IconName::Ai) AgentIcon::Name(IconName::Ai)
}; };
(name, icon) self.render_agent_server(icon, name, true)
.into_any_element()
}) })
.collect(); .collect::<Vec<_>>();
let add_agent_popover = PopoverMenu::new("add-agent-server-popover") let add_agens_button = Button::new("add-agent", "Add Agent")
.trigger( .style(ButtonStyle::Outlined)
Button::new("add-agent", "Add Agent") .icon_position(IconPosition::Start)
.style(ButtonStyle::Outlined) .icon(IconName::Plus)
.icon_position(IconPosition::Start) .icon_size(IconSize::Small)
.icon(IconName::Plus) .icon_color(Color::Muted)
.icon_size(IconSize::Small) .label_size(LabelSize::Small)
.icon_color(Color::Muted) .on_click(move |_, window, cx| {
.label_size(LabelSize::Small), if let Some(workspace) = window.root().flatten() {
) let workspace = workspace.downgrade();
.menu({ window
move |window, cx| { .spawn(cx, async |cx| {
Some(ContextMenu::build(window, cx, |menu, _window, _cx| { open_new_agent_servers_entry_in_settings_editor(workspace, cx).await
menu.entry("Install from Extensions", None, {
|window, cx| {
window.dispatch_action(
zed_actions::Extensions {
category_filter: Some(
ExtensionCategoryFilter::AgentServers,
),
id: None,
}
.boxed_clone(),
cx,
)
}
}) })
.entry("Add Custom Agent", None, { .detach_and_log_err(cx);
move |window, cx| {
if let Some(workspace) = window.root().flatten() {
let workspace = workspace.downgrade();
window
.spawn(cx, async |cx| {
open_new_agent_servers_entry_in_settings_editor(
workspace, cx,
)
.await
})
.detach_and_log_err(cx);
}
}
})
.separator()
.header("Learn More")
.item(
ContextMenuEntry::new("Agent Servers Docs")
.icon(IconName::ArrowUpRight)
.icon_color(Color::Muted)
.icon_position(IconPosition::End)
.handler({
move |window, cx| {
window.dispatch_action(
Box::new(OpenBrowser {
url: zed_urls::agent_server_docs(cx),
}),
cx,
);
}
}),
)
.item(
ContextMenuEntry::new("ACP Docs")
.icon(IconName::ArrowUpRight)
.icon_color(Color::Muted)
.icon_position(IconPosition::End)
.handler({
move |window, cx| {
window.dispatch_action(
Box::new(OpenBrowser {
url: "https://agentclientprotocol.com/".into(),
}),
cx,
);
}
}),
)
}))
} }
})
.anchor(gpui::Corner::TopRight)
.offset(gpui::Point {
x: px(0.0),
y: px(2.0),
}); });
v_flex() v_flex()
@@ -1075,7 +998,7 @@ impl AgentConfiguration {
.child(self.render_section_title( .child(self.render_section_title(
"External Agents", "External Agents",
"All agents connected through the Agent Client Protocol.", "All agents connected through the Agent Client Protocol.",
add_agent_popover.into_any_element(), add_agens_button.into_any_element(),
)) ))
.child( .child(
v_flex() v_flex()
@@ -1086,29 +1009,26 @@ impl AgentConfiguration {
AgentIcon::Name(IconName::AiClaude), AgentIcon::Name(IconName::AiClaude),
"Claude Code", "Claude Code",
false, false,
cx,
)) ))
.child(Divider::horizontal().color(DividerColor::BorderFaded)) .child(Divider::horizontal().color(DividerColor::BorderFaded))
.child(self.render_agent_server( .child(self.render_agent_server(
AgentIcon::Name(IconName::AiOpenAi), AgentIcon::Name(IconName::AiOpenAi),
"Codex CLI", "Codex",
false, false,
cx,
)) ))
.child(Divider::horizontal().color(DividerColor::BorderFaded)) .child(Divider::horizontal().color(DividerColor::BorderFaded))
.child(self.render_agent_server( .child(self.render_agent_server(
AgentIcon::Name(IconName::AiGemini), AgentIcon::Name(IconName::AiGemini),
"Gemini CLI", "Gemini CLI",
false, false,
cx,
)) ))
.map(|mut parent| { .map(|mut parent| {
for (name, icon) in user_defined_agents { for agent 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(agent);
} }
parent parent
}), }),
@@ -1121,14 +1041,13 @@ impl AgentConfiguration {
icon: AgentIcon, icon: AgentIcon,
name: impl Into<SharedString>, name: impl Into<SharedString>,
external: bool, external: bool,
cx: &mut Context<Self>,
) -> impl IntoElement { ) -> impl IntoElement {
let name = name.into(); let name = 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)
.color(Color::Muted), .color(Color::Muted),
AgentIcon::Path(icon_path) => Icon::from_external_svg(icon_path) AgentIcon::Path(icon_path) => Icon::from_path(icon_path)
.size(IconSize::Small) .size(IconSize::Small)
.color(Color::Muted), .color(Color::Muted),
}; };
@@ -1136,53 +1055,28 @@ impl AgentConfiguration {
let tooltip_id = SharedString::new(format!("agent-source-{}", name)); let tooltip_id = SharedString::new(format!("agent-source-{}", name));
let tooltip_message = format!("The {} agent was installed from an extension.", name); let tooltip_message = format!("The {} agent was installed from an extension.", name);
let agent_server_name = ExternalAgentServerName(name.clone());
let uninstall_btn_id = SharedString::from(format!("uninstall-{}", name));
let uninstall_button = IconButton::new(uninstall_btn_id, IconName::Trash)
.icon_color(Color::Muted)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text("Uninstall Agent Extension"))
.on_click(cx.listener(move |this, _, _window, cx| {
let agent_name = agent_server_name.clone();
if let Some(ext_id) = this.agent_server_store.update(cx, |store, _cx| {
store.get_extension_id_for_agent(&agent_name)
}) {
ExtensionStore::global(cx)
.update(cx, |store, cx| store.uninstall_extension(ext_id, cx))
.detach_and_log_err(cx);
}
}));
h_flex() h_flex()
.gap_1() .gap_1p5()
.justify_between() .child(icon)
.child(Label::new(name))
.when(external, |this| {
this.child(
div()
.id(tooltip_id)
.flex_none()
.tooltip(Tooltip::text(tooltip_message))
.child(
Icon::new(IconName::ZedSrcExtension)
.size(IconSize::Small)
.color(Color::Muted),
),
)
})
.child( .child(
h_flex() Icon::new(IconName::Check)
.gap_1p5() .color(Color::Success)
.child(icon) .size(IconSize::Small),
.child(Label::new(name))
.when(external, |this| {
this.child(
div()
.id(tooltip_id)
.flex_none()
.tooltip(Tooltip::text(tooltip_message))
.child(
Icon::new(IconName::ZedSrcExtension)
.size(IconSize::Small)
.color(Color::Muted),
),
)
})
.child(
Icon::new(IconName::Check)
.color(Color::Success)
.size(IconSize::Small),
),
) )
.when(external, |this| this.child(uninstall_button))
} }
} }
@@ -1348,7 +1242,6 @@ async fn open_new_agent_servers_entry_in_settings_editor(
args: vec![], args: vec![],
env: Some(HashMap::default()), env: Some(HashMap::default()),
default_mode: None, default_mode: None,
default_model: None,
}, },
); );
} }
@@ -1363,15 +1256,7 @@ async fn open_new_agent_servers_entry_in_settings_editor(
.map(|(range, _)| range.clone()) .map(|(range, _)| range.clone())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
item.edit( item.edit(edits, cx);
edits.into_iter().map(|(range, s)| {
(
MultiBufferOffset(range.start)..MultiBufferOffset(range.end),
s,
)
}),
cx,
);
if let Some((unique_server_name, buffer)) = if let Some((unique_server_name, buffer)) =
unique_server_name.zip(item.buffer().read(cx).as_singleton()) unique_server_name.zip(item.buffer().read(cx).as_singleton())
{ {
@@ -1384,9 +1269,7 @@ async fn open_new_agent_servers_entry_in_settings_editor(
window, window,
cx, cx,
|selections| { |selections| {
selections.select_ranges(vec![ selections.select_ranges(vec![range]);
MultiBufferOffset(range.start)..MultiBufferOffset(range.end),
]);
}, },
); );
} }

View File

@@ -3,42 +3,16 @@ use std::sync::Arc;
use anyhow::Result; use anyhow::Result;
use collections::HashSet; use collections::HashSet;
use fs::Fs; use fs::Fs;
use gpui::{ use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, Task};
DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, ScrollHandle, Task,
};
use language_model::LanguageModelRegistry; use language_model::LanguageModelRegistry;
use language_models::provider::open_ai_compatible::{AvailableModel, ModelCapabilities}; use language_models::provider::open_ai_compatible::{AvailableModel, ModelCapabilities};
use settings::{OpenAiCompatibleSettingsContent, update_settings_file}; use settings::{OpenAiCompatibleSettingsContent, update_settings_file};
use ui::{ use ui::{
Banner, Checkbox, KeyBinding, Modal, ModalFooter, ModalHeader, Section, ToggleState, Banner, Checkbox, KeyBinding, Modal, ModalFooter, ModalHeader, Section, ToggleState, prelude::*,
WithScrollbar, prelude::*,
}; };
use ui_input::InputField; use ui_input::InputField;
use workspace::{ModalView, Workspace}; use workspace::{ModalView, Workspace};
fn single_line_input(
label: impl Into<SharedString>,
placeholder: impl Into<SharedString>,
text: Option<&str>,
tab_index: isize,
window: &mut Window,
cx: &mut App,
) -> Entity<InputField> {
cx.new(|cx| {
let input = InputField::new(window, cx, placeholder)
.label(label)
.tab_index(tab_index)
.tab_stop(true);
if let Some(text) = text {
input
.editor()
.update(cx, |editor, cx| editor.set_text(text, window, cx));
}
input
})
}
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub enum LlmCompatibleProvider { pub enum LlmCompatibleProvider {
OpenAi, OpenAi,
@@ -67,14 +41,12 @@ struct AddLlmProviderInput {
impl AddLlmProviderInput { impl AddLlmProviderInput {
fn new(provider: LlmCompatibleProvider, window: &mut Window, cx: &mut App) -> Self { fn new(provider: LlmCompatibleProvider, window: &mut Window, cx: &mut App) -> Self {
let provider_name = let provider_name = single_line_input("Provider Name", provider.name(), None, window, cx);
single_line_input("Provider Name", provider.name(), None, 1, window, cx); let api_url = single_line_input("API URL", provider.api_url(), None, window, cx);
let api_url = single_line_input("API URL", provider.api_url(), None, 2, window, cx);
let api_key = single_line_input( let api_key = single_line_input(
"API Key", "API Key",
"000000000000000000000000000000000000000000000000", "000000000000000000000000000000000000000000000000",
None, None,
3,
window, window,
cx, cx,
); );
@@ -83,13 +55,12 @@ impl AddLlmProviderInput {
provider_name, provider_name,
api_url, api_url,
api_key, api_key,
models: vec![ModelInput::new(0, window, cx)], models: vec![ModelInput::new(window, cx)],
} }
} }
fn add_model(&mut self, window: &mut Window, cx: &mut App) { fn add_model(&mut self, window: &mut Window, cx: &mut App) {
let model_index = self.models.len(); self.models.push(ModelInput::new(window, cx));
self.models.push(ModelInput::new(model_index, window, cx));
} }
fn remove_model(&mut self, index: usize) { fn remove_model(&mut self, index: usize) {
@@ -113,14 +84,11 @@ struct ModelInput {
} }
impl ModelInput { impl ModelInput {
fn new(model_index: usize, window: &mut Window, cx: &mut App) -> Self { fn new(window: &mut Window, cx: &mut App) -> Self {
let base_tab_index = (3 + (model_index * 4)) as isize;
let model_name = single_line_input( let model_name = single_line_input(
"Model Name", "Model Name",
"e.g. gpt-4o, claude-opus-4, gemini-2.5-pro", "e.g. gpt-4o, claude-opus-4, gemini-2.5-pro",
None, None,
base_tab_index + 1,
window, window,
cx, cx,
); );
@@ -128,7 +96,6 @@ impl ModelInput {
"Max Completion Tokens", "Max Completion Tokens",
"200000", "200000",
Some("200000"), Some("200000"),
base_tab_index + 2,
window, window,
cx, cx,
); );
@@ -136,26 +103,16 @@ impl ModelInput {
"Max Output Tokens", "Max Output Tokens",
"Max Output Tokens", "Max Output Tokens",
Some("32000"), Some("32000"),
base_tab_index + 3,
window, window,
cx, cx,
); );
let max_tokens = single_line_input( let max_tokens = single_line_input("Max Tokens", "Max Tokens", Some("200000"), window, cx);
"Max Tokens",
"Max Tokens",
Some("200000"),
base_tab_index + 4,
window,
cx,
);
let ModelCapabilities { let ModelCapabilities {
tools, tools,
images, images,
parallel_tool_calls, parallel_tool_calls,
prompt_cache_key, prompt_cache_key,
} = ModelCapabilities::default(); } = ModelCapabilities::default();
Self { Self {
name: model_name, name: model_name,
max_completion_tokens, max_completion_tokens,
@@ -208,6 +165,24 @@ impl ModelInput {
} }
} }
fn single_line_input(
label: impl Into<SharedString>,
placeholder: impl Into<SharedString>,
text: Option<&str>,
window: &mut Window,
cx: &mut App,
) -> Entity<InputField> {
cx.new(|cx| {
let input = InputField::new(window, cx, placeholder).label(label);
if let Some(text) = text {
input
.editor()
.update(cx, |editor, cx| editor.set_text(text, window, cx));
}
input
})
}
fn save_provider_to_settings( fn save_provider_to_settings(
input: &AddLlmProviderInput, input: &AddLlmProviderInput,
cx: &mut App, cx: &mut App,
@@ -283,7 +258,6 @@ fn save_provider_to_settings(
pub struct AddLlmProviderModal { pub struct AddLlmProviderModal {
provider: LlmCompatibleProvider, provider: LlmCompatibleProvider,
input: AddLlmProviderInput, input: AddLlmProviderInput,
scroll_handle: ScrollHandle,
focus_handle: FocusHandle, focus_handle: FocusHandle,
last_error: Option<SharedString>, last_error: Option<SharedString>,
} }
@@ -304,7 +278,6 @@ impl AddLlmProviderModal {
provider, provider,
last_error: None, last_error: None,
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
scroll_handle: ScrollHandle::new(),
} }
} }
@@ -445,19 +418,6 @@ impl AddLlmProviderModal {
) )
}) })
} }
fn on_tab(&mut self, _: &menu::SelectNext, window: &mut Window, _: &mut Context<Self>) {
window.focus_next();
}
fn on_tab_prev(
&mut self,
_: &menu::SelectPrevious,
window: &mut Window,
_: &mut Context<Self>,
) {
window.focus_prev();
}
} }
impl EventEmitter<DismissEvent> for AddLlmProviderModal {} impl EventEmitter<DismissEvent> for AddLlmProviderModal {}
@@ -471,27 +431,15 @@ impl Focusable for AddLlmProviderModal {
impl ModalView for AddLlmProviderModal {} impl ModalView for AddLlmProviderModal {}
impl Render for AddLlmProviderModal { impl Render for AddLlmProviderModal {
fn render(&mut self, window: &mut ui::Window, cx: &mut ui::Context<Self>) -> impl IntoElement { fn render(&mut self, _window: &mut ui::Window, cx: &mut ui::Context<Self>) -> impl IntoElement {
let focus_handle = self.focus_handle(cx); let focus_handle = self.focus_handle(cx);
let window_size = window.viewport_size(); div()
let rem_size = window.rem_size();
let is_large_window = window_size.height / rem_size > rems_from_px(600.).0;
let modal_max_height = if is_large_window {
rems_from_px(450.)
} else {
rems_from_px(200.)
};
v_flex()
.id("add-llm-provider-modal") .id("add-llm-provider-modal")
.key_context("AddLlmProviderModal") .key_context("AddLlmProviderModal")
.w(rems(34.)) .w(rems(34.))
.elevation_3(cx) .elevation_3(cx)
.on_action(cx.listener(Self::cancel)) .on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::on_tab))
.on_action(cx.listener(Self::on_tab_prev))
.capture_any_mouse_down(cx.listener(|this, _, window, cx| { .capture_any_mouse_down(cx.listener(|this, _, window, cx| {
this.focus_handle(cx).focus(window); this.focus_handle(cx).focus(window);
})) }))
@@ -514,25 +462,17 @@ impl Render for AddLlmProviderModal {
) )
}) })
.child( .child(
div() v_flex()
.id("modal_content")
.size_full() .size_full()
.vertical_scrollbar_for(self.scroll_handle.clone(), window, cx) .max_h_128()
.child( .overflow_y_scroll()
v_flex() .px(DynamicSpacing::Base12.rems(cx))
.id("modal_content") .gap(DynamicSpacing::Base04.rems(cx))
.size_full() .child(self.input.provider_name.clone())
.tab_group() .child(self.input.api_url.clone())
.max_h(modal_max_height) .child(self.input.api_key.clone())
.pl_3() .child(self.render_model_section(cx)),
.pr_4()
.gap_2()
.overflow_y_scroll()
.track_scroll(&self.scroll_handle)
.child(self.input.provider_name.clone())
.child(self.input.api_url.clone())
.child(self.input.api_key.clone())
.child(self.render_model_section(cx)),
),
) )
.footer( .footer(
ModalFooter::new().end_slot( ModalFooter::new().end_slot(
@@ -575,14 +515,16 @@ impl Render for AddLlmProviderModal {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use editor::EditorSettings;
use fs::FakeFs; use fs::FakeFs;
use gpui::{TestAppContext, VisualTestContext}; use gpui::{TestAppContext, VisualTestContext};
use language::language_settings;
use language_model::{ use language_model::{
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderId, LanguageModelProviderName,
fake_provider::FakeLanguageModelProvider, fake_provider::FakeLanguageModelProvider,
}; };
use project::Project; use project::Project;
use settings::SettingsStore; use settings::{Settings as _, SettingsStore};
use util::path; use util::path;
#[gpui::test] #[gpui::test]
@@ -702,7 +644,7 @@ mod tests {
let cx = setup_test(cx).await; let cx = setup_test(cx).await;
cx.update(|window, cx| { cx.update(|window, cx| {
let model_input = ModelInput::new(0, window, cx); let model_input = ModelInput::new(window, cx);
model_input.name.update(cx, |input, cx| { model_input.name.update(cx, |input, cx| {
input.editor().update(cx, |editor, cx| { input.editor().update(cx, |editor, cx| {
editor.set_text("somemodel", window, cx); editor.set_text("somemodel", window, cx);
@@ -738,7 +680,7 @@ mod tests {
let cx = setup_test(cx).await; let cx = setup_test(cx).await;
cx.update(|window, cx| { cx.update(|window, cx| {
let mut model_input = ModelInput::new(0, window, cx); let mut model_input = ModelInput::new(window, cx);
model_input.name.update(cx, |input, cx| { model_input.name.update(cx, |input, cx| {
input.editor().update(cx, |editor, cx| { input.editor().update(cx, |editor, cx| {
editor.set_text("somemodel", window, cx); editor.set_text("somemodel", window, cx);
@@ -763,7 +705,7 @@ mod tests {
let cx = setup_test(cx).await; let cx = setup_test(cx).await;
cx.update(|window, cx| { cx.update(|window, cx| {
let mut model_input = ModelInput::new(0, window, cx); let mut model_input = ModelInput::new(window, cx);
model_input.name.update(cx, |input, cx| { model_input.name.update(cx, |input, cx| {
input.editor().update(cx, |editor, cx| { input.editor().update(cx, |editor, cx| {
editor.set_text("somemodel", window, cx); editor.set_text("somemodel", window, cx);
@@ -788,9 +730,13 @@ mod tests {
cx.update(|cx| { cx.update(|cx| {
let store = SettingsStore::test(cx); let store = SettingsStore::test(cx);
cx.set_global(store); cx.set_global(store);
workspace::init_settings(cx);
Project::init_settings(cx);
theme::init(theme::LoadThemes::JustBase, cx); theme::init(theme::LoadThemes::JustBase, cx);
language_settings::init(cx);
EditorSettings::register(cx);
language_model::init_settings(cx); language_model::init_settings(cx);
language_models::init_settings(cx);
}); });
let fs = FakeFs::new(cx.executor()); let fs = FakeFs::new(cx.executor());
@@ -827,7 +773,7 @@ mod tests {
models.iter().enumerate() models.iter().enumerate()
{ {
if i >= input.models.len() { if i >= input.models.len() {
input.models.push(ModelInput::new(i, window, cx)); input.models.push(ModelInput::new(window, cx));
} }
let model = &mut input.models[i]; let model = &mut input.models[i];
set_text(&model.name, name, window, cx); set_text(&model.name, name, window, cx);

View File

@@ -4,12 +4,11 @@ use std::{
}; };
use anyhow::{Context as _, Result}; use anyhow::{Context as _, Result};
use collections::HashMap;
use context_server::{ContextServerCommand, ContextServerId}; use context_server::{ContextServerCommand, ContextServerId};
use editor::{Editor, EditorElement, EditorStyle}; use editor::{Editor, EditorElement, EditorStyle};
use gpui::{ use gpui::{
AsyncWindowContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, ScrollHandle, AsyncWindowContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task,
Task, TextStyle, TextStyleRefinement, UnderlineStyle, WeakEntity, prelude::*, TextStyle, TextStyleRefinement, UnderlineStyle, WeakEntity, prelude::*,
}; };
use language::{Language, LanguageRegistry}; use language::{Language, LanguageRegistry};
use markdown::{Markdown, MarkdownElement, MarkdownStyle}; use markdown::{Markdown, MarkdownElement, MarkdownStyle};
@@ -21,12 +20,10 @@ use project::{
project_settings::{ContextServerSettings, ProjectSettings}, project_settings::{ContextServerSettings, ProjectSettings},
worktree_store::WorktreeStore, worktree_store::WorktreeStore,
}; };
use serde::Deserialize;
use settings::{Settings as _, update_settings_file}; use settings::{Settings as _, update_settings_file};
use theme::ThemeSettings; use theme::ThemeSettings;
use ui::{ use ui::{
CommonAnimationExt, KeyBinding, Modal, ModalFooter, ModalHeader, Section, Tooltip, CommonAnimationExt, KeyBinding, Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*,
WithScrollbar, prelude::*,
}; };
use util::ResultExt as _; use util::ResultExt as _;
use workspace::{ModalView, Workspace}; use workspace::{ModalView, Workspace};
@@ -39,11 +36,6 @@ enum ConfigurationTarget {
id: ContextServerId, id: ContextServerId,
command: ContextServerCommand, command: ContextServerCommand,
}, },
ExistingHttp {
id: ContextServerId,
url: String,
headers: HashMap<String, String>,
},
Extension { Extension {
id: ContextServerId, id: ContextServerId,
repository_url: Option<SharedString>, repository_url: Option<SharedString>,
@@ -54,11 +46,9 @@ enum ConfigurationTarget {
enum ConfigurationSource { enum ConfigurationSource {
New { New {
editor: Entity<Editor>, editor: Entity<Editor>,
is_http: bool,
}, },
Existing { Existing {
editor: Entity<Editor>, editor: Entity<Editor>,
is_http: bool,
}, },
Extension { Extension {
id: ContextServerId, id: ContextServerId,
@@ -106,7 +96,6 @@ impl ConfigurationSource {
match target { match target {
ConfigurationTarget::New => ConfigurationSource::New { ConfigurationTarget::New => ConfigurationSource::New {
editor: create_editor(context_server_input(None), jsonc_language, window, cx), editor: create_editor(context_server_input(None), jsonc_language, window, cx),
is_http: false,
}, },
ConfigurationTarget::Existing { id, command } => ConfigurationSource::Existing { ConfigurationTarget::Existing { id, command } => ConfigurationSource::Existing {
editor: create_editor( editor: create_editor(
@@ -115,20 +104,6 @@ impl ConfigurationSource {
window, window,
cx, cx,
), ),
is_http: false,
},
ConfigurationTarget::ExistingHttp {
id,
url,
headers: auth,
} => ConfigurationSource::Existing {
editor: create_editor(
context_server_http_input(Some((id, url, auth))),
jsonc_language,
window,
cx,
),
is_http: true,
}, },
ConfigurationTarget::Extension { ConfigurationTarget::Extension {
id, id,
@@ -165,30 +140,16 @@ impl ConfigurationSource {
fn output(&self, cx: &mut App) -> Result<(ContextServerId, ContextServerSettings)> { fn output(&self, cx: &mut App) -> Result<(ContextServerId, ContextServerSettings)> {
match self { match self {
ConfigurationSource::New { editor, is_http } ConfigurationSource::New { editor } | ConfigurationSource::Existing { editor } => {
| ConfigurationSource::Existing { editor, is_http } => { parse_input(&editor.read(cx).text(cx)).map(|(id, command)| {
if *is_http { (
parse_http_input(&editor.read(cx).text(cx)).map(|(id, url, auth)| { id,
( ContextServerSettings::Custom {
id, enabled: true,
ContextServerSettings::Http { command,
enabled: true, },
url, )
headers: auth, })
},
)
})
} else {
parse_input(&editor.read(cx).text(cx)).map(|(id, command)| {
(
id,
ContextServerSettings::Custom {
enabled: true,
command,
},
)
})
}
} }
ConfigurationSource::Extension { ConfigurationSource::Extension {
id, id,
@@ -250,66 +211,6 @@ fn context_server_input(existing: Option<(ContextServerId, ContextServerCommand)
) )
} }
fn context_server_http_input(
existing: Option<(ContextServerId, String, HashMap<String, String>)>,
) -> String {
let (name, url, headers) = match existing {
Some((id, url, headers)) => {
let header = if headers.is_empty() {
r#"// "Authorization": "Bearer <token>"#.to_string()
} else {
let json = serde_json::to_string_pretty(&headers).unwrap();
let mut lines = json.split("\n").collect::<Vec<_>>();
if lines.len() > 1 {
lines.remove(0);
lines.pop();
}
lines
.into_iter()
.map(|line| format!(" {}", line))
.collect::<String>()
};
(id.0.to_string(), url, header)
}
None => (
"some-remote-server".to_string(),
"https://example.com/mcp".to_string(),
r#"// "Authorization": "Bearer <token>"#.to_string(),
),
};
format!(
r#"{{
/// The name of your remote MCP server
"{name}": {{
/// The URL of the remote MCP server
"url": "{url}",
"headers": {{
/// Any headers to send along
{headers}
}}
}}
}}"#
)
}
fn parse_http_input(text: &str) -> Result<(ContextServerId, String, HashMap<String, String>)> {
#[derive(Deserialize)]
struct Temp {
url: String,
#[serde(default)]
headers: HashMap<String, String>,
}
let value: HashMap<String, Temp> = serde_json_lenient::from_str(text)?;
if value.len() != 1 {
anyhow::bail!("Expected exactly one context server configuration");
}
let (key, value) = value.into_iter().next().unwrap();
Ok((ContextServerId(key.into()), value.url, value.headers))
}
fn resolve_context_server_extension( fn resolve_context_server_extension(
id: ContextServerId, id: ContextServerId,
worktree_store: Entity<WorktreeStore>, worktree_store: Entity<WorktreeStore>,
@@ -351,7 +252,6 @@ pub struct ConfigureContextServerModal {
source: ConfigurationSource, source: ConfigurationSource,
state: State, state: State,
original_server_id: Option<ContextServerId>, original_server_id: Option<ContextServerId>,
scroll_handle: ScrollHandle,
} }
impl ConfigureContextServerModal { impl ConfigureContextServerModal {
@@ -410,15 +310,6 @@ impl ConfigureContextServerModal {
id: server_id, id: server_id,
command, command,
}), }),
ContextServerSettings::Http {
enabled: _,
url,
headers,
} => Some(ConfigurationTarget::ExistingHttp {
id: server_id,
url,
headers,
}),
ContextServerSettings::Extension { .. } => { ContextServerSettings::Extension { .. } => {
match workspace match workspace
.update(cx, |workspace, cx| { .update(cx, |workspace, cx| {
@@ -460,7 +351,6 @@ impl ConfigureContextServerModal {
state: State::Idle, state: State::Idle,
original_server_id: match &target { original_server_id: match &target {
ConfigurationTarget::Existing { id, .. } => Some(id.clone()), ConfigurationTarget::Existing { id, .. } => Some(id.clone()),
ConfigurationTarget::ExistingHttp { id, .. } => Some(id.clone()),
ConfigurationTarget::Extension { id, .. } => Some(id.clone()), ConfigurationTarget::Extension { id, .. } => Some(id.clone()),
ConfigurationTarget::New => None, ConfigurationTarget::New => None,
}, },
@@ -471,7 +361,6 @@ impl ConfigureContextServerModal {
window, window,
cx, cx,
), ),
scroll_handle: ScrollHandle::new(),
}) })
}) })
}) })
@@ -589,7 +478,7 @@ impl ModalView for ConfigureContextServerModal {}
impl Focusable for ConfigureContextServerModal { impl Focusable for ConfigureContextServerModal {
fn focus_handle(&self, cx: &App) -> FocusHandle { fn focus_handle(&self, cx: &App) -> FocusHandle {
match &self.source { match &self.source {
ConfigurationSource::New { editor, .. } => editor.focus_handle(cx), ConfigurationSource::New { editor } => editor.focus_handle(cx),
ConfigurationSource::Existing { editor, .. } => editor.focus_handle(cx), ConfigurationSource::Existing { editor, .. } => editor.focus_handle(cx),
ConfigurationSource::Extension { editor, .. } => editor ConfigurationSource::Extension { editor, .. } => editor
.as_ref() .as_ref()
@@ -635,10 +524,9 @@ impl ConfigureContextServerModal {
} }
fn render_modal_content(&self, cx: &App) -> AnyElement { fn render_modal_content(&self, cx: &App) -> AnyElement {
// All variants now use single editor approach
let editor = match &self.source { let editor = match &self.source {
ConfigurationSource::New { editor, .. } => editor, ConfigurationSource::New { editor } => editor,
ConfigurationSource::Existing { editor, .. } => editor, ConfigurationSource::Existing { editor } => editor,
ConfigurationSource::Extension { editor, .. } => { ConfigurationSource::Extension { editor, .. } => {
let Some(editor) = editor else { let Some(editor) = editor else {
return div().into_any_element(); return div().into_any_element();
@@ -710,36 +598,6 @@ impl ConfigureContextServerModal {
move |_, _, cx| cx.open_url(&repository_url) move |_, _, cx| cx.open_url(&repository_url)
}), }),
) )
} else if let ConfigurationSource::New { is_http, .. } = &self.source {
let label = if *is_http {
"Run command"
} else {
"Connect via HTTP"
};
let tooltip = if *is_http {
"Configure an MCP serevr that runs on stdin/stdout."
} else {
"Configure an MCP server that you connect to over HTTP"
};
Some(
Button::new("toggle-kind", label)
.tooltip(Tooltip::text(tooltip))
.on_click(cx.listener(|this, _, window, cx| match &mut this.source {
ConfigurationSource::New { editor, is_http } => {
*is_http = !*is_http;
let new_text = if *is_http {
context_server_http_input(None)
} else {
context_server_input(None)
};
editor.update(cx, |editor, cx| {
editor.set_text(new_text, window, cx);
})
}
_ => {}
})),
)
} else { } else {
None None
}, },
@@ -822,7 +680,6 @@ impl ConfigureContextServerModal {
impl Render for ConfigureContextServerModal { impl Render for ConfigureContextServerModal {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let scroll_handle = self.scroll_handle.clone();
div() div()
.elevation_3(cx) .elevation_3(cx)
.w(rems(34.)) .w(rems(34.))
@@ -842,29 +699,14 @@ impl Render for ConfigureContextServerModal {
Modal::new("configure-context-server", None) Modal::new("configure-context-server", None)
.header(self.render_modal_header()) .header(self.render_modal_header())
.section( .section(
Section::new().child( Section::new()
div() .child(self.render_modal_description(window, cx))
.size_full() .child(self.render_modal_content(cx))
.child( .child(match &self.state {
div() State::Idle => div(),
.id("modal-content") State::Waiting => Self::render_waiting_for_context_server(),
.max_h(vh(0.7, window)) State::Error(error) => Self::render_modal_error(error.clone()),
.overflow_y_scroll() }),
.track_scroll(&scroll_handle)
.child(self.render_modal_description(window, cx))
.child(self.render_modal_content(cx))
.child(match &self.state {
State::Idle => div(),
State::Waiting => {
Self::render_waiting_for_context_server()
}
State::Error(error) => {
Self::render_modal_error(error.clone())
}
}),
)
.vertical_scrollbar_for(scroll_handle, window, cx),
),
) )
.footer(self.render_modal_footer(cx)), .footer(self.render_modal_footer(cx)),
) )

View File

@@ -7,10 +7,8 @@ use agent_settings::{AgentProfile, AgentProfileId, AgentSettings, builtin_profil
use editor::Editor; 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;
use settings::{ use settings::Settings as _;
LanguageModelProviderSetting, LanguageModelSelection, Settings as _, update_settings_file,
};
use ui::{ use ui::{
KeyBinding, ListItem, ListItemSpacing, ListSeparator, Navigable, NavigableEntry, prelude::*, KeyBinding, ListItem, ListItemSpacing, ListSeparator, Navigable, NavigableEntry, prelude::*,
}; };
@@ -18,7 +16,6 @@ use workspace::{ModalView, Workspace};
use crate::agent_configuration::manage_profiles_modal::profile_modal_header::ProfileModalHeader; use crate::agent_configuration::manage_profiles_modal::profile_modal_header::ProfileModalHeader;
use crate::agent_configuration::tool_picker::{ToolPicker, ToolPickerDelegate}; use crate::agent_configuration::tool_picker::{ToolPicker, ToolPickerDelegate};
use crate::language_model_selector::{LanguageModelSelector, language_model_selector};
use crate::{AgentPanel, ManageProfiles}; use crate::{AgentPanel, ManageProfiles};
enum Mode { enum Mode {
@@ -35,11 +32,6 @@ enum Mode {
tool_picker: Entity<ToolPicker>, tool_picker: Entity<ToolPicker>,
_subscription: Subscription, _subscription: Subscription,
}, },
ConfigureDefaultModel {
profile_id: AgentProfileId,
model_picker: Entity<LanguageModelSelector>,
_subscription: Subscription,
},
} }
impl Mode { impl Mode {
@@ -91,7 +83,6 @@ pub struct ChooseProfileMode {
pub struct ViewProfileMode { pub struct ViewProfileMode {
profile_id: AgentProfileId, profile_id: AgentProfileId,
fork_profile: NavigableEntry, fork_profile: NavigableEntry,
configure_default_model: NavigableEntry,
configure_tools: NavigableEntry, configure_tools: NavigableEntry,
configure_mcps: NavigableEntry, configure_mcps: NavigableEntry,
cancel_item: NavigableEntry, cancel_item: NavigableEntry,
@@ -189,7 +180,6 @@ impl ManageProfilesModal {
self.mode = Mode::ViewProfile(ViewProfileMode { self.mode = Mode::ViewProfile(ViewProfileMode {
profile_id, profile_id,
fork_profile: NavigableEntry::focusable(cx), fork_profile: 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),
cancel_item: NavigableEntry::focusable(cx), cancel_item: NavigableEntry::focusable(cx),
@@ -197,83 +187,6 @@ impl ManageProfilesModal {
self.focus_handle(cx).focus(window); self.focus_handle(cx).focus(window);
} }
fn configure_default_model(
&mut self,
profile_id: AgentProfileId,
window: &mut Window,
cx: &mut Context<Self>,
) {
let fs = self.fs.clone();
let profile_id_for_closure = profile_id.clone();
let model_picker = cx.new(|cx| {
let fs = fs.clone();
let profile_id = profile_id_for_closure.clone();
language_model_selector(
{
let profile_id = profile_id.clone();
move |cx| {
let settings = AgentSettings::get_global(cx);
settings
.profiles
.get(&profile_id)
.and_then(|profile| profile.default_model.as_ref())
.and_then(|selection| {
let registry = LanguageModelRegistry::read_global(cx);
let provider_id = language_model::LanguageModelProviderId(
gpui::SharedString::from(selection.provider.0.clone()),
);
let provider = registry.provider(&provider_id)?;
let model = provider
.provided_models(cx)
.iter()
.find(|m| m.id().0 == selection.model.as_str())?
.clone();
Some(language_model::ConfiguredModel { provider, model })
})
}
},
move |model, cx| {
let provider = model.provider_id().0.to_string();
let model_id = model.id().0.to_string();
let profile_id = profile_id.clone();
update_settings_file(fs.clone(), cx, move |settings, _cx| {
let agent_settings = settings.agent.get_or_insert_default();
if let Some(profiles) = agent_settings.profiles.as_mut() {
if let Some(profile) = profiles.get_mut(profile_id.0.as_ref()) {
profile.default_model = Some(LanguageModelSelection {
provider: LanguageModelProviderSetting(provider.clone()),
model: model_id.clone(),
});
}
}
});
},
false, // Do not use popover styles for the model picker
window,
cx,
)
.modal(false)
});
let dismiss_subscription = cx.subscribe_in(&model_picker, window, {
let profile_id = profile_id.clone();
move |this, _picker, _: &DismissEvent, window, cx| {
this.view_profile(profile_id.clone(), window, cx);
}
});
self.mode = Mode::ConfigureDefaultModel {
profile_id,
model_picker,
_subscription: dismiss_subscription,
};
self.focus_handle(cx).focus(window);
}
fn configure_mcp_tools( fn configure_mcp_tools(
&mut self, &mut self,
profile_id: AgentProfileId, profile_id: AgentProfileId,
@@ -364,7 +277,6 @@ impl ManageProfilesModal {
Mode::ViewProfile(_) => {} Mode::ViewProfile(_) => {}
Mode::ConfigureTools { .. } => {} Mode::ConfigureTools { .. } => {}
Mode::ConfigureMcps { .. } => {} Mode::ConfigureMcps { .. } => {}
Mode::ConfigureDefaultModel { .. } => {}
} }
} }
@@ -387,9 +299,6 @@ impl ManageProfilesModal {
Mode::ConfigureMcps { profile_id, .. } => { Mode::ConfigureMcps { profile_id, .. } => {
self.view_profile(profile_id.clone(), window, cx) self.view_profile(profile_id.clone(), window, cx)
} }
Mode::ConfigureDefaultModel { profile_id, .. } => {
self.view_profile(profile_id.clone(), window, cx)
}
} }
} }
} }
@@ -404,7 +313,6 @@ impl Focusable for ManageProfilesModal {
Mode::ViewProfile(_) => self.focus_handle.clone(), Mode::ViewProfile(_) => self.focus_handle.clone(),
Mode::ConfigureTools { tool_picker, .. } => tool_picker.focus_handle(cx), Mode::ConfigureTools { tool_picker, .. } => tool_picker.focus_handle(cx),
Mode::ConfigureMcps { tool_picker, .. } => tool_picker.focus_handle(cx), Mode::ConfigureMcps { tool_picker, .. } => tool_picker.focus_handle(cx),
Mode::ConfigureDefaultModel { model_picker, .. } => model_picker.focus_handle(cx),
} }
} }
} }
@@ -636,47 +544,6 @@ impl ManageProfilesModal {
}), }),
), ),
) )
.child(
div()
.id("configure-default-model")
.track_focus(&mode.configure_default_model.focus_handle)
.on_action({
let profile_id = mode.profile_id.clone();
cx.listener(move |this, _: &menu::Confirm, window, cx| {
this.configure_default_model(
profile_id.clone(),
window,
cx,
);
})
})
.child(
ListItem::new("model-item")
.toggle_state(
mode.configure_default_model
.focus_handle
.contains_focused(window, cx),
)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.start_slot(
Icon::new(IconName::ZedAssistant)
.size(IconSize::Small)
.color(Color::Muted),
)
.child(Label::new("Configure Default Model"))
.on_click({
let profile_id = mode.profile_id.clone();
cx.listener(move |this, _, window, cx| {
this.configure_default_model(
profile_id.clone(),
window,
cx,
);
})
}),
),
)
.child( .child(
div() div()
.id("configure-builtin-tools") .id("configure-builtin-tools")
@@ -801,7 +668,6 @@ impl ManageProfilesModal {
.into_any_element(), .into_any_element(),
) )
.entry(mode.fork_profile) .entry(mode.fork_profile)
.entry(mode.configure_default_model)
.entry(mode.configure_tools) .entry(mode.configure_tools)
.entry(mode.configure_mcps) .entry(mode.configure_mcps)
.entry(mode.cancel_item) .entry(mode.cancel_item)
@@ -887,29 +753,6 @@ impl Render for ManageProfilesModal {
.child(go_back_item) .child(go_back_item)
.into_any_element() .into_any_element()
} }
Mode::ConfigureDefaultModel {
profile_id,
model_picker,
..
} => {
let profile_name = settings
.profiles
.get(profile_id)
.map(|profile| profile.name.clone())
.unwrap_or_else(|| "Unknown".into());
v_flex()
.pb_1()
.child(ProfileModalHeader::new(
format!("{profile_name} — Configure Default Model"),
Some(IconName::Ai),
))
.child(ListSeparator)
.child(v_flex().w(rems(34.)).child(model_picker.clone()))
.child(ListSeparator)
.child(go_back_item)
.into_any_element()
}
Mode::ConfigureMcps { Mode::ConfigureMcps {
profile_id, profile_id,
tool_picker, tool_picker,

View File

@@ -314,7 +314,6 @@ impl PickerDelegate for ToolPickerDelegate {
) )
}) })
.collect(), .collect(),
default_model: default_profile.default_model.clone(),
}); });
if let Some(server_id) = server_id { if let Some(server_id) = server_id {

View File

@@ -1,6 +1,6 @@
use crate::{Keep, KeepAll, OpenAgentDiff, Reject, RejectAll}; use crate::{Keep, KeepAll, OpenAgentDiff, Reject, RejectAll};
use acp_thread::{AcpThread, AcpThreadEvent}; use acp_thread::{AcpThread, AcpThreadEvent};
use action_log::ActionLogTelemetry; use action_log::ActionLog;
use agent_settings::AgentSettings; use agent_settings::AgentSettings;
use anyhow::Result; use anyhow::Result;
use buffer_diff::DiffHunkStatus; use buffer_diff::DiffHunkStatus;
@@ -13,8 +13,8 @@ use editor::{
scroll::Autoscroll, scroll::Autoscroll,
}; };
use gpui::{ use gpui::{
Action, AnyElement, App, AppContext, Empty, Entity, EventEmitter, FocusHandle, Focusable, Action, AnyElement, AnyView, App, AppContext, Empty, Entity, EventEmitter, FocusHandle,
Global, SharedString, Subscription, Task, WeakEntity, Window, prelude::*, Focusable, Global, SharedString, Subscription, Task, WeakEntity, Window, prelude::*,
}; };
use language::{Buffer, Capability, DiskState, OffsetRangeExt, Point}; use language::{Buffer, Capability, DiskState, OffsetRangeExt, Point};
@@ -40,16 +40,79 @@ use zed_actions::assistant::ToggleFocus;
pub struct AgentDiffPane { pub struct AgentDiffPane {
multibuffer: Entity<MultiBuffer>, multibuffer: Entity<MultiBuffer>,
editor: Entity<Editor>, editor: Entity<Editor>,
thread: Entity<AcpThread>, thread: AgentDiffThread,
focus_handle: FocusHandle, focus_handle: FocusHandle,
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
title: SharedString, title: SharedString,
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
} }
#[derive(PartialEq, Eq, Clone)]
pub enum AgentDiffThread {
AcpThread(Entity<AcpThread>),
}
impl AgentDiffThread {
fn project(&self, cx: &App) -> Entity<Project> {
match self {
AgentDiffThread::AcpThread(thread) => thread.read(cx).project().clone(),
}
}
fn action_log(&self, cx: &App) -> Entity<ActionLog> {
match self {
AgentDiffThread::AcpThread(thread) => thread.read(cx).action_log().clone(),
}
}
fn title(&self, cx: &App) -> SharedString {
match self {
AgentDiffThread::AcpThread(thread) => thread.read(cx).title(),
}
}
fn has_pending_edit_tool_uses(&self, cx: &App) -> bool {
match self {
AgentDiffThread::AcpThread(thread) => thread.read(cx).has_pending_edit_tool_calls(),
}
}
fn downgrade(&self) -> WeakAgentDiffThread {
match self {
AgentDiffThread::AcpThread(thread) => {
WeakAgentDiffThread::AcpThread(thread.downgrade())
}
}
}
}
impl From<Entity<AcpThread>> for AgentDiffThread {
fn from(entity: Entity<AcpThread>) -> Self {
AgentDiffThread::AcpThread(entity)
}
}
#[derive(PartialEq, Eq, Clone)]
pub enum WeakAgentDiffThread {
AcpThread(WeakEntity<AcpThread>),
}
impl WeakAgentDiffThread {
pub fn upgrade(&self) -> Option<AgentDiffThread> {
match self {
WeakAgentDiffThread::AcpThread(weak) => weak.upgrade().map(AgentDiffThread::AcpThread),
}
}
}
impl From<WeakEntity<AcpThread>> for WeakAgentDiffThread {
fn from(entity: WeakEntity<AcpThread>) -> Self {
WeakAgentDiffThread::AcpThread(entity)
}
}
impl AgentDiffPane { impl AgentDiffPane {
pub fn deploy( pub fn deploy(
thread: Entity<AcpThread>, thread: impl Into<AgentDiffThread>,
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
@@ -60,11 +123,12 @@ impl AgentDiffPane {
} }
pub fn deploy_in_workspace( pub fn deploy_in_workspace(
thread: Entity<AcpThread>, thread: impl Into<AgentDiffThread>,
workspace: &mut Workspace, workspace: &mut Workspace,
window: &mut Window, window: &mut Window,
cx: &mut Context<Workspace>, cx: &mut Context<Workspace>,
) -> Entity<Self> { ) -> Entity<Self> {
let thread = thread.into();
let existing_diff = workspace let existing_diff = workspace
.items_of_type::<AgentDiffPane>(cx) .items_of_type::<AgentDiffPane>(cx)
.find(|diff| diff.read(cx).thread == thread); .find(|diff| diff.read(cx).thread == thread);
@@ -81,7 +145,7 @@ impl AgentDiffPane {
} }
pub fn new( pub fn new(
thread: Entity<AcpThread>, thread: AgentDiffThread,
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
@@ -89,7 +153,7 @@ impl AgentDiffPane {
let focus_handle = cx.focus_handle(); let focus_handle = cx.focus_handle();
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
let project = thread.read(cx).project().clone(); let project = thread.project(cx);
let editor = cx.new(|cx| { let editor = cx.new(|cx| {
let mut editor = let mut editor =
Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx); Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);
@@ -100,16 +164,19 @@ impl AgentDiffPane {
editor editor
}); });
let action_log = thread.read(cx).action_log().clone(); let action_log = thread.action_log(cx);
let mut this = Self { let mut this = Self {
_subscriptions: vec![ _subscriptions: vec![
cx.observe_in(&action_log, window, |this, _action_log, window, cx| { cx.observe_in(&action_log, window, |this, _action_log, window, cx| {
this.update_excerpts(window, cx) this.update_excerpts(window, cx)
}), }),
cx.subscribe(&thread, |this, _thread, event, cx| { match &thread {
this.handle_acp_thread_event(event, cx) AgentDiffThread::AcpThread(thread) => cx
}), .subscribe(thread, |this, _thread, event, cx| {
this.handle_acp_thread_event(event, cx)
}),
},
], ],
title: SharedString::default(), title: SharedString::default(),
multibuffer, multibuffer,
@@ -124,12 +191,7 @@ impl AgentDiffPane {
} }
fn update_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn update_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let changed_buffers = self let changed_buffers = self.thread.action_log(cx).read(cx).changed_buffers(cx);
.thread
.read(cx)
.action_log()
.read(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().collect::<HashSet<_>>();
for (buffer, diff_handle) in changed_buffers { for (buffer, diff_handle) in changed_buffers {
@@ -216,7 +278,7 @@ impl AgentDiffPane {
} }
fn update_title(&mut self, cx: &mut Context<Self>) { fn update_title(&mut self, cx: &mut Context<Self>) {
let new_title = self.thread.read(cx).title(); let new_title = self.thread.title(cx);
if new_title != self.title { if new_title != self.title {
self.title = new_title; self.title = new_title;
cx.emit(EditorEvent::TitleChanged); cx.emit(EditorEvent::TitleChanged);
@@ -278,18 +340,16 @@ impl AgentDiffPane {
} }
fn keep_all(&mut self, _: &KeepAll, _window: &mut Window, cx: &mut Context<Self>) { fn keep_all(&mut self, _: &KeepAll, _window: &mut Window, cx: &mut Context<Self>) {
let telemetry = ActionLogTelemetry::from(self.thread.read(cx)); self.thread
let action_log = self.thread.read(cx).action_log().clone(); .action_log(cx)
action_log.update(cx, |action_log, cx| { .update(cx, |action_log, cx| action_log.keep_all_edits(cx))
action_log.keep_all_edits(Some(telemetry), cx)
});
} }
} }
fn keep_edits_in_selection( fn keep_edits_in_selection(
editor: &mut Editor, editor: &mut Editor,
buffer_snapshot: &MultiBufferSnapshot, buffer_snapshot: &MultiBufferSnapshot,
thread: &Entity<AcpThread>, thread: &AgentDiffThread,
window: &mut Window, window: &mut Window,
cx: &mut Context<Editor>, cx: &mut Context<Editor>,
) { ) {
@@ -304,7 +364,7 @@ fn keep_edits_in_selection(
fn reject_edits_in_selection( fn reject_edits_in_selection(
editor: &mut Editor, editor: &mut Editor,
buffer_snapshot: &MultiBufferSnapshot, buffer_snapshot: &MultiBufferSnapshot,
thread: &Entity<AcpThread>, thread: &AgentDiffThread,
window: &mut Window, window: &mut Window,
cx: &mut Context<Editor>, cx: &mut Context<Editor>,
) { ) {
@@ -318,7 +378,7 @@ fn reject_edits_in_selection(
fn keep_edits_in_ranges( fn keep_edits_in_ranges(
editor: &mut Editor, editor: &mut Editor,
buffer_snapshot: &MultiBufferSnapshot, buffer_snapshot: &MultiBufferSnapshot,
thread: &Entity<AcpThread>, thread: &AgentDiffThread,
ranges: Vec<Range<editor::Anchor>>, ranges: Vec<Range<editor::Anchor>>,
window: &mut Window, window: &mut Window,
cx: &mut Context<Editor>, cx: &mut Context<Editor>,
@@ -333,15 +393,8 @@ fn keep_edits_in_ranges(
for hunk in &diff_hunks_in_ranges { for hunk in &diff_hunks_in_ranges {
let buffer = multibuffer.read(cx).buffer(hunk.buffer_id); let buffer = multibuffer.read(cx).buffer(hunk.buffer_id);
if let Some(buffer) = buffer { if let Some(buffer) = buffer {
let action_log = thread.read(cx).action_log().clone(); thread.action_log(cx).update(cx, |action_log, cx| {
let telemetry = ActionLogTelemetry::from(thread.read(cx)); action_log.keep_edits_in_range(buffer, hunk.buffer_range.clone(), cx)
action_log.update(cx, |action_log, cx| {
action_log.keep_edits_in_range(
buffer,
hunk.buffer_range.clone(),
Some(telemetry),
cx,
)
}); });
} }
} }
@@ -350,7 +403,7 @@ fn keep_edits_in_ranges(
fn reject_edits_in_ranges( fn reject_edits_in_ranges(
editor: &mut Editor, editor: &mut Editor,
buffer_snapshot: &MultiBufferSnapshot, buffer_snapshot: &MultiBufferSnapshot,
thread: &Entity<AcpThread>, thread: &AgentDiffThread,
ranges: Vec<Range<editor::Anchor>>, ranges: Vec<Range<editor::Anchor>>,
window: &mut Window, window: &mut Window,
cx: &mut Context<Editor>, cx: &mut Context<Editor>,
@@ -374,12 +427,11 @@ fn reject_edits_in_ranges(
} }
} }
let action_log = thread.read(cx).action_log().clone();
let telemetry = ActionLogTelemetry::from(thread.read(cx));
for (buffer, ranges) in ranges_by_buffer { for (buffer, ranges) in ranges_by_buffer {
action_log thread
.action_log(cx)
.update(cx, |action_log, cx| { .update(cx, |action_log, cx| {
action_log.reject_edits_in_ranges(buffer, ranges, Some(telemetry.clone()), cx) action_log.reject_edits_in_ranges(buffer, ranges, cx)
}) })
.detach_and_log_err(cx); .detach_and_log_err(cx);
} }
@@ -479,7 +531,7 @@ impl Item for AgentDiffPane {
} }
fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement { fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
let title = self.thread.read(cx).title(); let title = self.thread.title(cx);
Label::new(format!("Review: {}", title)) Label::new(format!("Review: {}", title))
.color(if params.selected { .color(if params.selected {
Color::Default Color::Default
@@ -580,11 +632,11 @@ impl Item for AgentDiffPane {
type_id: TypeId, type_id: TypeId,
self_handle: &'a Entity<Self>, self_handle: &'a Entity<Self>,
_: &'a App, _: &'a App,
) -> Option<gpui::AnyEntity> { ) -> Option<AnyView> {
if type_id == TypeId::of::<Self>() { if type_id == TypeId::of::<Self>() {
Some(self_handle.clone().into()) Some(self_handle.to_any())
} else if type_id == TypeId::of::<Editor>() { } else if type_id == TypeId::of::<Editor>() {
Some(self.editor.clone().into()) Some(self.editor.to_any())
} else { } else {
None None
} }
@@ -660,7 +712,7 @@ impl Render for AgentDiffPane {
} }
} }
fn diff_hunk_controls(thread: &Entity<AcpThread>) -> editor::RenderDiffHunkControlsFn { fn diff_hunk_controls(thread: &AgentDiffThread) -> editor::RenderDiffHunkControlsFn {
let thread = thread.clone(); let thread = thread.clone();
Arc::new( Arc::new(
@@ -687,7 +739,7 @@ fn render_diff_hunk_controls(
hunk_range: Range<editor::Anchor>, hunk_range: Range<editor::Anchor>,
is_created_file: bool, is_created_file: bool,
line_height: Pixels, line_height: Pixels,
thread: &Entity<AcpThread>, thread: &AgentDiffThread,
editor: &Entity<Editor>, editor: &Entity<Editor>,
cx: &mut App, cx: &mut App,
) -> AnyElement { ) -> AnyElement {
@@ -1101,11 +1153,8 @@ impl Render for AgentDiffToolbar {
return Empty.into_any(); return Empty.into_any();
}; };
let has_pending_edit_tool_use = agent_diff let has_pending_edit_tool_use =
.read(cx) agent_diff.read(cx).thread.has_pending_edit_tool_uses(cx);
.thread
.read(cx)
.has_pending_edit_tool_calls();
if has_pending_edit_tool_use { if has_pending_edit_tool_use {
return div().px_2().child(spinner_icon).into_any(); return div().px_2().child(spinner_icon).into_any();
@@ -1165,7 +1214,7 @@ pub enum EditorState {
} }
struct WorkspaceThread { struct WorkspaceThread {
thread: WeakEntity<AcpThread>, thread: WeakAgentDiffThread,
_thread_subscriptions: (Subscription, Subscription), _thread_subscriptions: (Subscription, Subscription),
singleton_editors: HashMap<WeakEntity<Buffer>, HashMap<WeakEntity<Editor>, Subscription>>, singleton_editors: HashMap<WeakEntity<Buffer>, HashMap<WeakEntity<Editor>, Subscription>>,
_settings_subscription: Subscription, _settings_subscription: Subscription,
@@ -1190,23 +1239,23 @@ impl AgentDiff {
pub fn set_active_thread( pub fn set_active_thread(
workspace: &WeakEntity<Workspace>, workspace: &WeakEntity<Workspace>,
thread: Entity<AcpThread>, thread: impl Into<AgentDiffThread>,
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) { ) {
Self::global(cx).update(cx, |this, cx| { Self::global(cx).update(cx, |this, cx| {
this.register_active_thread_impl(workspace, thread, window, cx); this.register_active_thread_impl(workspace, thread.into(), window, cx);
}); });
} }
fn register_active_thread_impl( fn register_active_thread_impl(
&mut self, &mut self,
workspace: &WeakEntity<Workspace>, workspace: &WeakEntity<Workspace>,
thread: Entity<AcpThread>, thread: AgentDiffThread,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let action_log = thread.read(cx).action_log().clone(); let action_log = thread.action_log(cx);
let action_log_subscription = cx.observe_in(&action_log, window, { let action_log_subscription = cx.observe_in(&action_log, window, {
let workspace = workspace.clone(); let workspace = workspace.clone();
@@ -1215,12 +1264,14 @@ impl AgentDiff {
} }
}); });
let thread_subscription = cx.subscribe_in(&thread, window, { let thread_subscription = match &thread {
let workspace = workspace.clone(); AgentDiffThread::AcpThread(thread) => cx.subscribe_in(thread, window, {
move |this, thread, event, window, cx| { let workspace = workspace.clone();
this.handle_acp_thread_event(&workspace, thread, event, window, cx) move |this, thread, event, window, cx| {
} this.handle_acp_thread_event(&workspace, thread, event, window, cx)
}); }
}),
};
if let Some(workspace_thread) = self.workspace_threads.get_mut(workspace) { if let Some(workspace_thread) = self.workspace_threads.get_mut(workspace) {
// replace thread and action log subscription, but keep editors // replace thread and action log subscription, but keep editors
@@ -1297,7 +1348,7 @@ impl AgentDiff {
fn register_review_action<T: Action>( fn register_review_action<T: Action>(
workspace: &mut Workspace, workspace: &mut Workspace,
review: impl Fn(&Entity<Editor>, &Entity<AcpThread>, &mut Window, &mut App) -> PostReviewState review: impl Fn(&Entity<Editor>, &AgentDiffThread, &mut Window, &mut App) -> PostReviewState
+ 'static, + 'static,
this: &Entity<AgentDiff>, this: &Entity<AgentDiff>,
) { ) {
@@ -1457,7 +1508,7 @@ impl AgentDiff {
return; return;
}; };
let action_log = thread.read(cx).action_log(); let action_log = thread.action_log(cx);
let changed_buffers = action_log.read(cx).changed_buffers(cx); let changed_buffers = action_log.read(cx).changed_buffers(cx);
let mut unaffected = self.reviewing_editors.clone(); let mut unaffected = self.reviewing_editors.clone();
@@ -1576,7 +1627,7 @@ impl AgentDiff {
fn keep_all( fn keep_all(
editor: &Entity<Editor>, editor: &Entity<Editor>,
thread: &Entity<AcpThread>, thread: &AgentDiffThread,
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) -> PostReviewState { ) -> PostReviewState {
@@ -1596,7 +1647,7 @@ impl AgentDiff {
fn reject_all( fn reject_all(
editor: &Entity<Editor>, editor: &Entity<Editor>,
thread: &Entity<AcpThread>, thread: &AgentDiffThread,
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) -> PostReviewState { ) -> PostReviewState {
@@ -1616,7 +1667,7 @@ impl AgentDiff {
fn keep( fn keep(
editor: &Entity<Editor>, editor: &Entity<Editor>,
thread: &Entity<AcpThread>, thread: &AgentDiffThread,
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) -> PostReviewState { ) -> PostReviewState {
@@ -1629,7 +1680,7 @@ impl AgentDiff {
fn reject( fn reject(
editor: &Entity<Editor>, editor: &Entity<Editor>,
thread: &Entity<AcpThread>, thread: &AgentDiffThread,
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) -> PostReviewState { ) -> PostReviewState {
@@ -1652,7 +1703,7 @@ impl AgentDiff {
fn review_in_active_editor( fn review_in_active_editor(
&mut self, &mut self,
workspace: &mut Workspace, workspace: &mut Workspace,
review: impl Fn(&Entity<Editor>, &Entity<AcpThread>, &mut Window, &mut App) -> PostReviewState, review: impl Fn(&Entity<Editor>, &AgentDiffThread, &mut Window, &mut App) -> PostReviewState,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Option<Task<Result<()>>> { ) -> Option<Task<Result<()>>> {
@@ -1674,7 +1725,7 @@ impl AgentDiff {
if let PostReviewState::AllReviewed = review(&editor, &thread, window, cx) if let PostReviewState::AllReviewed = review(&editor, &thread, window, cx)
&& let Some(curr_buffer) = editor.read(cx).buffer().read(cx).as_singleton() && let Some(curr_buffer) = editor.read(cx).buffer().read(cx).as_singleton()
{ {
let changed_buffers = thread.read(cx).action_log().read(cx).changed_buffers(cx); let changed_buffers = thread.action_log(cx).read(cx).changed_buffers(cx);
let mut keys = changed_buffers.keys().cycle(); let mut keys = changed_buffers.keys().cycle();
keys.find(|k| *k == &curr_buffer); keys.find(|k| *k == &curr_buffer);
@@ -1717,11 +1768,12 @@ mod tests {
use super::*; use super::*;
use crate::Keep; use crate::Keep;
use acp_thread::AgentConnection as _; use acp_thread::AgentConnection as _;
use agent_settings::AgentSettings;
use editor::EditorSettings; use editor::EditorSettings;
use gpui::{TestAppContext, UpdateGlobal, VisualTestContext}; use gpui::{TestAppContext, UpdateGlobal, VisualTestContext};
use project::{FakeFs, Project}; use project::{FakeFs, Project};
use serde_json::json; use serde_json::json;
use settings::SettingsStore; use settings::{Settings, SettingsStore};
use std::{path::Path, rc::Rc}; use std::{path::Path, rc::Rc};
use util::path; use util::path;
@@ -1730,8 +1782,13 @@ mod tests {
cx.update(|cx| { cx.update(|cx| {
let settings_store = SettingsStore::test(cx); let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store); cx.set_global(settings_store);
language::init(cx);
Project::init_settings(cx);
AgentSettings::register(cx);
prompt_store::init(cx); prompt_store::init(cx);
workspace::init_settings(cx);
theme::init(theme::LoadThemes::JustBase, cx); theme::init(theme::LoadThemes::JustBase, cx);
EditorSettings::register(cx);
language_model::init_settings(cx); language_model::init_settings(cx);
}); });
@@ -1758,7 +1815,8 @@ mod tests {
.await .await
.unwrap(); .unwrap();
let action_log = cx.read(|cx| thread.read(cx).action_log().clone()); let thread = AgentDiffThread::AcpThread(thread);
let action_log = cx.read(|cx| thread.action_log(cx));
let (workspace, cx) = let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
@@ -1884,8 +1942,13 @@ mod tests {
cx.update(|cx| { cx.update(|cx| {
let settings_store = SettingsStore::test(cx); let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store); cx.set_global(settings_store);
language::init(cx);
Project::init_settings(cx);
AgentSettings::register(cx);
prompt_store::init(cx); prompt_store::init(cx);
workspace::init_settings(cx);
theme::init(theme::LoadThemes::JustBase, cx); theme::init(theme::LoadThemes::JustBase, cx);
EditorSettings::register(cx);
language_model::init_settings(cx); language_model::init_settings(cx);
workspace::register_project_item::<Editor>(cx); workspace::register_project_item::<Editor>(cx);
}); });
@@ -1941,6 +2004,7 @@ mod tests {
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone()); let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
// Set the active thread // Set the active thread
let thread = AgentDiffThread::AcpThread(thread);
cx.update(|window, cx| { cx.update(|window, cx| {
AgentDiff::set_active_thread(&workspace.downgrade(), thread.clone(), window, cx) AgentDiff::set_active_thread(&workspace.downgrade(), thread.clone(), window, cx)
}); });

View File

@@ -47,7 +47,6 @@ impl AgentModelSelector {
} }
} }
}, },
true, // Use popover styles for picker
window, window,
cx, cx,
) )

View File

@@ -16,7 +16,7 @@ use serde::{Deserialize, Serialize};
use settings::{ use settings::{
DefaultAgentView as DefaultView, LanguageModelProviderSetting, LanguageModelSelection, DefaultAgentView as DefaultView, LanguageModelProviderSetting, LanguageModelSelection,
}; };
use zed_actions::OpenBrowser;
use zed_actions::agent::{OpenClaudeCodeOnboardingModal, ReauthenticateAgent}; use zed_actions::agent::{OpenClaudeCodeOnboardingModal, ReauthenticateAgent};
use crate::ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal}; use crate::ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal};
@@ -1880,21 +1880,13 @@ impl AgentPanel {
{ {
let focus_handle = focus_handle.clone(); let focus_handle = focus_handle.clone();
move |_window, cx| { move |_window, cx| {
Tooltip::for_action_in( Tooltip::for_action_in("New…", &ToggleNewThreadMenu, &focus_handle, cx)
"New Thread…",
&ToggleNewThreadMenu,
&focus_handle,
cx,
)
} }
}, },
) )
.anchor(Corner::TopRight) .anchor(Corner::TopRight)
.with_handle(self.new_thread_menu_handle.clone()) .with_handle(self.new_thread_menu_handle.clone())
.menu({ .menu({
let selected_agent = self.selected_agent.clone();
let is_agent_selected = move |agent_type: AgentType| selected_agent == agent_type;
let workspace = self.workspace.clone(); let workspace = self.workspace.clone();
let is_via_collab = workspace let is_via_collab = workspace
.update(cx, |workspace, cx| { .update(cx, |workspace, cx| {
@@ -1908,6 +1900,7 @@ impl AgentPanel {
let active_thread = active_thread.clone(); let active_thread = active_thread.clone();
Some(ContextMenu::build(window, cx, |menu, _window, cx| { Some(ContextMenu::build(window, cx, |menu, _window, cx| {
menu.context(focus_handle.clone()) menu.context(focus_handle.clone())
.header("Zed Agent")
.when_some(active_thread, |this, active_thread| { .when_some(active_thread, |this, active_thread| {
let thread = active_thread.read(cx); let thread = active_thread.read(cx);
@@ -1931,11 +1924,9 @@ impl AgentPanel {
} }
}) })
.item( .item(
ContextMenuEntry::new("Zed Agent") ContextMenuEntry::new("New Thread")
.when(is_agent_selected(AgentType::NativeAgent) | is_agent_selected(AgentType::TextThread) , |this| { .action(NewThread.boxed_clone())
this.action(Box::new(NewExternalAgentThread { agent: None })) .icon(IconName::Thread)
})
.icon(IconName::ZedAgent)
.icon_color(Color::Muted) .icon_color(Color::Muted)
.handler({ .handler({
let workspace = workspace.clone(); let workspace = workspace.clone();
@@ -1959,10 +1950,10 @@ impl AgentPanel {
}), }),
) )
.item( .item(
ContextMenuEntry::new("Text Thread") ContextMenuEntry::new("New Text Thread")
.action(NewTextThread.boxed_clone())
.icon(IconName::TextThread) .icon(IconName::TextThread)
.icon_color(Color::Muted) .icon_color(Color::Muted)
.action(NewTextThread.boxed_clone())
.handler({ .handler({
let workspace = workspace.clone(); let workspace = workspace.clone();
move |window, cx| { move |window, cx| {
@@ -1987,10 +1978,7 @@ impl AgentPanel {
.separator() .separator()
.header("External Agents") .header("External Agents")
.item( .item(
ContextMenuEntry::new("Claude Code") ContextMenuEntry::new("New Claude Code Thread")
.when(is_agent_selected(AgentType::ClaudeCode), |this| {
this.action(Box::new(NewExternalAgentThread { agent: None }))
})
.icon(IconName::AiClaude) .icon(IconName::AiClaude)
.disabled(is_via_collab) .disabled(is_via_collab)
.icon_color(Color::Muted) .icon_color(Color::Muted)
@@ -2016,10 +2004,7 @@ impl AgentPanel {
}), }),
) )
.item( .item(
ContextMenuEntry::new("Codex CLI") ContextMenuEntry::new("New Codex Thread")
.when(is_agent_selected(AgentType::Codex), |this| {
this.action(Box::new(NewExternalAgentThread { agent: None }))
})
.icon(IconName::AiOpenAi) .icon(IconName::AiOpenAi)
.disabled(is_via_collab) .disabled(is_via_collab)
.icon_color(Color::Muted) .icon_color(Color::Muted)
@@ -2045,10 +2030,7 @@ impl AgentPanel {
}), }),
) )
.item( .item(
ContextMenuEntry::new("Gemini CLI") ContextMenuEntry::new("New Gemini CLI Thread")
.when(is_agent_selected(AgentType::Gemini), |this| {
this.action(Box::new(NewExternalAgentThread { agent: None }))
})
.icon(IconName::AiGemini) .icon(IconName::AiGemini)
.icon_color(Color::Muted) .icon_color(Color::Muted)
.disabled(is_via_collab) .disabled(is_via_collab)
@@ -2074,8 +2056,8 @@ impl AgentPanel {
}), }),
) )
.map(|mut menu| { .map(|mut menu| {
let agent_server_store = agent_server_store.read(cx); let agent_server_store_read = agent_server_store.read(cx);
let agent_names = agent_server_store let agent_names = agent_server_store_read
.external_agents() .external_agents()
.filter(|name| { .filter(|name| {
name.0 != GEMINI_NAME name.0 != GEMINI_NAME
@@ -2084,38 +2066,21 @@ impl AgentPanel {
}) })
.cloned() .cloned()
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let custom_settings = cx let custom_settings = cx
.global::<SettingsStore>() .global::<SettingsStore>()
.get::<AllAgentServersSettings>(None) .get::<AllAgentServersSettings>(None)
.custom .custom
.clone(); .clone();
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_read.agent_icon(&agent_name);
let mut entry =
let mut entry = ContextMenuEntry::new(agent_name.clone()); ContextMenuEntry::new(format!("New {} Thread", agent_name));
let command = custom_settings
.get(&agent_name.0)
.map(|settings| settings.command.clone())
.unwrap_or(placeholder_command());
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_path(icon_path);
} else { } else {
entry = entry.icon(IconName::Terminal); entry = entry.icon(IconName::Terminal);
} }
entry = entry entry = entry
.when(
is_agent_selected(AgentType::Custom {
name: agent_name.0.clone(),
command: command.clone(),
}),
|this| {
this.action(Box::new(NewExternalAgentThread { agent: None }))
},
)
.icon_color(Color::Muted) .icon_color(Color::Muted)
.disabled(is_via_collab) .disabled(is_via_collab)
.handler({ .handler({
@@ -2155,27 +2120,18 @@ impl AgentPanel {
} }
} }
}); });
menu = menu.item(entry); menu = menu.item(entry);
} }
menu menu
}) })
.separator() .separator()
.item( .link(
ContextMenuEntry::new("Add More Agents") "Add Other Agents",
.icon(IconName::Plus) OpenBrowser {
.icon_color(Color::Muted) url: zed_urls::external_agents_docs(cx),
.handler({ }
move |window, cx| { .boxed_clone(),
window.dispatch_action(Box::new(zed_actions::Extensions {
category_filter: Some(
zed_actions::ExtensionCategoryFilter::AgentServers,
),
id: None,
}), cx)
}
}),
) )
})) }))
} }
@@ -2188,8 +2144,8 @@ impl AgentPanel {
.id("selected_agent_icon") .id("selected_agent_icon")
.when_some(selected_agent_custom_icon, |this, icon_path| { .when_some(selected_agent_custom_icon, |this, icon_path| {
let label = selected_agent_label.clone(); let label = selected_agent_label.clone();
this.px_1() this.px(DynamicSpacing::Base02.rems(cx))
.child(Icon::from_external_svg(icon_path).color(Color::Muted)) .child(Icon::from_path(icon_path).color(Color::Muted))
.tooltip(move |_window, cx| { .tooltip(move |_window, cx| {
Tooltip::with_meta(label.clone(), None, "Selected Agent", cx) Tooltip::with_meta(label.clone(), None, "Selected Agent", cx)
}) })
@@ -2197,7 +2153,7 @@ impl AgentPanel {
.when(!has_custom_icon, |this| { .when(!has_custom_icon, |this| {
this.when_some(self.selected_agent.icon(), |this, icon| { this.when_some(self.selected_agent.icon(), |this, icon| {
let label = selected_agent_label.clone(); let label = selected_agent_label.clone();
this.px_1() this.px(DynamicSpacing::Base02.rems(cx))
.child(Icon::new(icon).color(Color::Muted)) .child(Icon::new(icon).color(Color::Muted))
.tooltip(move |_window, cx| { .tooltip(move |_window, cx| {
Tooltip::with_meta(label.clone(), None, "Selected Agent", cx) Tooltip::with_meta(label.clone(), None, "Selected Agent", cx)

View File

@@ -12,6 +12,7 @@ mod context_strip;
mod inline_assistant; mod inline_assistant;
mod inline_prompt_editor; mod inline_prompt_editor;
mod language_model_selector; mod language_model_selector;
mod message_editor;
mod profile_selector; mod profile_selector;
mod slash_command; mod slash_command;
mod slash_command_picker; mod slash_command_picker;
@@ -30,10 +31,7 @@ use command_palette_hooks::CommandPaletteFilter;
use feature_flags::FeatureFlagAppExt as _; use feature_flags::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::LanguageRegistry;
LanguageRegistry,
language_settings::{AllLanguageSettings, EditPredictionProvider},
};
use language_model::{ use language_model::{
ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry,
}; };
@@ -250,6 +248,8 @@ pub fn init(
is_eval: bool, is_eval: bool,
cx: &mut App, cx: &mut App,
) { ) {
AgentSettings::register(cx);
assistant_text_thread::init(client.clone(), cx); assistant_text_thread::init(client.clone(), cx);
rules_library::init(cx); rules_library::init(cx);
if !is_eval { if !is_eval {
@@ -289,25 +289,7 @@ pub fn init(
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 edit_prediction_provider = AllLanguageSettings::get_global(cx)
.edit_predictions
.provider;
CommandPaletteFilter::update_global(cx, |filter, _| { CommandPaletteFilter::update_global(cx, |filter, _| {
use editor::actions::{
AcceptEditPrediction, AcceptPartialEditPrediction, NextEditPrediction,
PreviousEditPrediction, ShowEditPrediction, ToggleEditPrediction,
};
let edit_prediction_actions = [
TypeId::of::<AcceptEditPrediction>(),
TypeId::of::<AcceptPartialEditPrediction>(),
TypeId::of::<ShowEditPrediction>(),
TypeId::of::<NextEditPrediction>(),
TypeId::of::<PreviousEditPrediction>(),
TypeId::of::<ToggleEditPrediction>(),
];
if disable_ai { if disable_ai {
filter.hide_namespace("agent"); filter.hide_namespace("agent");
filter.hide_namespace("assistant"); filter.hide_namespace("assistant");
@@ -316,47 +298,42 @@ fn update_command_palette_filter(cx: &mut App) {
filter.hide_namespace("zed_predict_onboarding"); filter.hide_namespace("zed_predict_onboarding");
filter.hide_namespace("edit_prediction"); filter.hide_namespace("edit_prediction");
use editor::actions::{
AcceptEditPrediction, AcceptPartialEditPrediction, NextEditPrediction,
PreviousEditPrediction, ShowEditPrediction, ToggleEditPrediction,
};
let edit_prediction_actions = [
TypeId::of::<AcceptEditPrediction>(),
TypeId::of::<AcceptPartialEditPrediction>(),
TypeId::of::<ShowEditPrediction>(),
TypeId::of::<NextEditPrediction>(),
TypeId::of::<PreviousEditPrediction>(),
TypeId::of::<ToggleEditPrediction>(),
];
filter.hide_action_types(&edit_prediction_actions); filter.hide_action_types(&edit_prediction_actions);
filter.hide_action_types(&[TypeId::of::<zed_actions::OpenZedPredictOnboarding>()]); filter.hide_action_types(&[TypeId::of::<zed_actions::OpenZedPredictOnboarding>()]);
} else { } else {
if agent_enabled { filter.show_namespace("agent");
filter.show_namespace("agent");
} else {
filter.hide_namespace("agent");
}
filter.show_namespace("assistant"); filter.show_namespace("assistant");
filter.show_namespace("copilot");
match edit_prediction_provider {
EditPredictionProvider::None => {
filter.hide_namespace("edit_prediction");
filter.hide_namespace("copilot");
filter.hide_namespace("supermaven");
filter.hide_action_types(&edit_prediction_actions);
}
EditPredictionProvider::Copilot => {
filter.show_namespace("edit_prediction");
filter.show_namespace("copilot");
filter.hide_namespace("supermaven");
filter.show_action_types(edit_prediction_actions.iter());
}
EditPredictionProvider::Supermaven => {
filter.show_namespace("edit_prediction");
filter.hide_namespace("copilot");
filter.show_namespace("supermaven");
filter.show_action_types(edit_prediction_actions.iter());
}
EditPredictionProvider::Zed
| EditPredictionProvider::Codestral
| EditPredictionProvider::Experimental(_) => {
filter.show_namespace("edit_prediction");
filter.hide_namespace("copilot");
filter.hide_namespace("supermaven");
filter.show_action_types(edit_prediction_actions.iter());
}
}
filter.show_namespace("zed_predict_onboarding"); filter.show_namespace("zed_predict_onboarding");
filter.show_namespace("edit_prediction");
use editor::actions::{
AcceptEditPrediction, AcceptPartialEditPrediction, NextEditPrediction,
PreviousEditPrediction, ShowEditPrediction, ToggleEditPrediction,
};
let edit_prediction_actions = [
TypeId::of::<AcceptEditPrediction>(),
TypeId::of::<AcceptPartialEditPrediction>(),
TypeId::of::<ShowEditPrediction>(),
TypeId::of::<NextEditPrediction>(),
TypeId::of::<PreviousEditPrediction>(),
TypeId::of::<ToggleEditPrediction>(),
];
filter.show_action_types(edit_prediction_actions.iter());
filter.show_action_types(&[TypeId::of::<zed_actions::OpenZedPredictOnboarding>()]); filter.show_action_types(&[TypeId::of::<zed_actions::OpenZedPredictOnboarding>()]);
} }
}); });
@@ -446,137 +423,3 @@ fn register_slash_commands(cx: &mut App) {
}) })
.detach(); .detach();
} }
#[cfg(test)]
mod tests {
use super::*;
use agent_settings::{AgentProfileId, AgentSettings, CompletionMode};
use command_palette_hooks::CommandPaletteFilter;
use editor::actions::AcceptEditPrediction;
use gpui::{BorrowAppContext, TestAppContext, px};
use project::DisableAiSettings;
use settings::{
DefaultAgentView, DockPosition, NotifyWhenAgentWaiting, Settings, SettingsStore,
};
#[gpui::test]
fn test_agent_command_palette_visibility(cx: &mut TestAppContext) {
// Init settings
cx.update(|cx| {
let store = SettingsStore::test(cx);
cx.set_global(store);
command_palette_hooks::init(cx);
AgentSettings::register(cx);
DisableAiSettings::register(cx);
AllLanguageSettings::register(cx);
});
let agent_settings = AgentSettings {
enabled: true,
button: true,
dock: DockPosition::Right,
default_width: px(300.),
default_height: px(600.),
default_model: None,
inline_assistant_model: None,
commit_message_model: None,
thread_summary_model: None,
inline_alternatives: vec![],
default_profile: AgentProfileId::default(),
default_view: DefaultAgentView::Thread,
profiles: Default::default(),
always_allow_tool_actions: false,
notify_when_agent_waiting: NotifyWhenAgentWaiting::default(),
play_sound_when_agent_done: false,
single_file_review: false,
model_parameters: vec![],
preferred_completion_mode: CompletionMode::Normal,
enable_feedback: false,
expand_edit_card: true,
expand_terminal_card: true,
use_modifier_to_send: true,
message_editor_min_lines: 1,
};
cx.update(|cx| {
AgentSettings::override_global(agent_settings.clone(), cx);
DisableAiSettings::override_global(DisableAiSettings { disable_ai: false }, cx);
// Initial update
update_command_palette_filter(cx);
});
// Assert visible
cx.update(|cx| {
let filter = CommandPaletteFilter::try_global(cx).unwrap();
assert!(
!filter.is_hidden(&NewThread),
"NewThread should be visible by default"
);
});
// Disable agent
cx.update(|cx| {
let mut new_settings = agent_settings.clone();
new_settings.enabled = false;
AgentSettings::override_global(new_settings, cx);
// Trigger update
update_command_palette_filter(cx);
});
// Assert hidden
cx.update(|cx| {
let filter = CommandPaletteFilter::try_global(cx).unwrap();
assert!(
filter.is_hidden(&NewThread),
"NewThread should be hidden when agent is disabled"
);
});
// Test EditPredictionProvider
// Enable EditPredictionProvider::Copilot
cx.update(|cx| {
cx.update_global::<SettingsStore, _>(|store, cx| {
store.update_user_settings(cx, |s| {
s.project
.all_languages
.features
.get_or_insert(Default::default())
.edit_prediction_provider = Some(EditPredictionProvider::Copilot);
});
});
update_command_palette_filter(cx);
});
cx.update(|cx| {
let filter = CommandPaletteFilter::try_global(cx).unwrap();
assert!(
!filter.is_hidden(&AcceptEditPrediction),
"EditPrediction should be visible when provider is Copilot"
);
});
// Disable EditPredictionProvider (None)
cx.update(|cx| {
cx.update_global::<SettingsStore, _>(|store, cx| {
store.update_user_settings(cx, |s| {
s.project
.all_languages
.features
.get_or_insert(Default::default())
.edit_prediction_provider = Some(EditPredictionProvider::None);
});
});
update_command_palette_filter(cx);
});
cx.update(|cx| {
let filter = CommandPaletteFilter::try_global(cx).unwrap();
assert!(
filter.is_hidden(&AcceptEditPrediction),
"EditPrediction should be hidden when provider is None"
);
});
}
}

View File

@@ -429,12 +429,7 @@ impl CodegenAlternative {
let prompt = self let prompt = self
.builder .builder
.generate_inline_transformation_prompt( .generate_inline_transformation_prompt(user_prompt, language_name, buffer, range)
user_prompt,
language_name,
buffer,
range.start.0..range.end.0,
)
.context("generating content prompt")?; .context("generating content prompt")?;
let context_task = self.context_store.as_ref().and_then(|context_store| { let context_task = self.context_store.as_ref().and_then(|context_store| {
@@ -1087,7 +1082,10 @@ mod tests {
}; };
use gpui::TestAppContext; use gpui::TestAppContext;
use indoc::indoc; use indoc::indoc;
use language::{Buffer, Language, LanguageConfig, LanguageMatcher, Point, tree_sitter_rust}; use language::{
Buffer, Language, LanguageConfig, LanguageMatcher, Point, language_settings,
tree_sitter_rust,
};
use language_model::{LanguageModelRegistry, TokenUsage}; use language_model::{LanguageModelRegistry, TokenUsage};
use rand::prelude::*; use rand::prelude::*;
use settings::SettingsStore; use settings::SettingsStore;
@@ -1467,6 +1465,8 @@ mod tests {
fn init_test(cx: &mut TestAppContext) { fn init_test(cx: &mut TestAppContext) {
cx.update(LanguageModelRegistry::test); cx.update(LanguageModelRegistry::test);
cx.set_global(cx.update(SettingsStore::test)); cx.set_global(cx.update(SettingsStore::test));
cx.update(Project::init_settings);
cx.update(language_settings::init);
} }
fn simulate_response_stream( fn simulate_response_stream(

View File

@@ -1075,6 +1075,8 @@ mod tests {
cx.update(|cx| { cx.update(|cx| {
let settings_store = SettingsStore::test(cx); let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store); cx.set_global(settings_store);
language::init(cx);
Project::init_settings(cx);
}); });
} }
@@ -1089,7 +1091,7 @@ mod tests {
} }
#[gpui::test] #[gpui::test]
async fn test_large_file_uses_fallback(cx: &mut TestAppContext) { async fn test_large_file_uses_outline(cx: &mut TestAppContext) {
init_test_settings(cx); init_test_settings(cx);
// Create a large file that exceeds AUTO_OUTLINE_SIZE // Create a large file that exceeds AUTO_OUTLINE_SIZE
@@ -1101,16 +1103,16 @@ mod tests {
let file_context = load_context_for("file.txt", large_content, cx).await; let file_context = load_context_for("file.txt", large_content, cx).await;
// Should contain some of the actual file content
assert!( assert!(
file_context.text.contains(LINE), file_context
"Should contain some of the file content" .text
.contains(&format!("# File outline for {}", path!("test/file.txt"))),
"Large files should not get an outline"
); );
// Should be much smaller than original
assert!( assert!(
file_context.text.len() < content_len / 10, file_context.text.len() < content_len,
"Should be significantly smaller than original content" "Outline should be smaller than original content"
); );
} }

View File

@@ -42,7 +42,7 @@ use super::{
ContextPickerAction, ContextPickerEntry, ContextPickerMode, MentionLink, RecentEntry, ContextPickerAction, ContextPickerEntry, ContextPickerMode, MentionLink, RecentEntry,
available_context_picker_entries, recent_context_picker_entries_with_store, selection_ranges, available_context_picker_entries, recent_context_picker_entries_with_store, selection_ranges,
}; };
use crate::inline_prompt_editor::ContextCreasesAddon; use crate::message_editor::ContextCreasesAddon;
pub(crate) enum Match { pub(crate) enum Match {
File(FileMatch), File(FileMatch),
@@ -278,8 +278,6 @@ impl ContextPickerCompletionProvider {
icon_path: Some(mode.icon().path().into()), icon_path: Some(mode.icon().path().into()),
documentation: None, documentation: None,
source: project::CompletionSource::Custom, source: project::CompletionSource::Custom,
match_start: None,
snippet_deduplication_key: None,
insert_text_mode: None, insert_text_mode: None,
// This ensures that when a user accepts this completion, the // This ensures that when a user accepts this completion, the
// completion menu will still be shown after "@category " is // completion menu will still be shown after "@category " is
@@ -388,8 +386,6 @@ impl ContextPickerCompletionProvider {
icon_path: Some(action.icon().path().into()), icon_path: Some(action.icon().path().into()),
documentation: None, documentation: None,
source: project::CompletionSource::Custom, source: project::CompletionSource::Custom,
match_start: None,
snippet_deduplication_key: None,
insert_text_mode: None, insert_text_mode: None,
// This ensures that when a user accepts this completion, the // This ensures that when a user accepts this completion, the
// completion menu will still be shown after "@category " is // completion menu will still be shown after "@category " is
@@ -421,8 +417,6 @@ impl ContextPickerCompletionProvider {
replace_range: source_range.clone(), replace_range: source_range.clone(),
new_text, new_text,
label: CodeLabel::plain(thread_entry.title().to_string(), None), label: CodeLabel::plain(thread_entry.title().to_string(), None),
match_start: None,
snippet_deduplication_key: None,
documentation: None, documentation: None,
insert_text_mode: None, insert_text_mode: None,
source: project::CompletionSource::Custom, source: project::CompletionSource::Custom,
@@ -490,8 +484,6 @@ impl ContextPickerCompletionProvider {
replace_range: source_range.clone(), replace_range: source_range.clone(),
new_text, new_text,
label: CodeLabel::plain(rules.title.to_string(), None), label: CodeLabel::plain(rules.title.to_string(), None),
match_start: None,
snippet_deduplication_key: None,
documentation: None, documentation: None,
insert_text_mode: None, insert_text_mode: None,
source: project::CompletionSource::Custom, source: project::CompletionSource::Custom,
@@ -532,8 +524,6 @@ impl ContextPickerCompletionProvider {
documentation: None, documentation: None,
source: project::CompletionSource::Custom, source: project::CompletionSource::Custom,
icon_path: Some(IconName::ToolWeb.path().into()), icon_path: Some(IconName::ToolWeb.path().into()),
match_start: None,
snippet_deduplication_key: None,
insert_text_mode: None, insert_text_mode: None,
confirm: Some(confirm_completion_callback( confirm: Some(confirm_completion_callback(
IconName::ToolWeb.path().into(), IconName::ToolWeb.path().into(),
@@ -622,8 +612,6 @@ impl ContextPickerCompletionProvider {
documentation: None, documentation: None,
source: project::CompletionSource::Custom, source: project::CompletionSource::Custom,
icon_path: Some(completion_icon_path), icon_path: Some(completion_icon_path),
match_start: None,
snippet_deduplication_key: None,
insert_text_mode: None, insert_text_mode: None,
confirm: Some(confirm_completion_callback( confirm: Some(confirm_completion_callback(
crease_icon_path, crease_icon_path,
@@ -701,8 +689,6 @@ impl ContextPickerCompletionProvider {
documentation: None, documentation: None,
source: project::CompletionSource::Custom, source: project::CompletionSource::Custom,
icon_path: Some(IconName::Code.path().into()), icon_path: Some(IconName::Code.path().into()),
match_start: None,
snippet_deduplication_key: None,
insert_text_mode: None, insert_text_mode: None,
confirm: Some(confirm_completion_callback( confirm: Some(confirm_completion_callback(
IconName::Code.path().into(), IconName::Code.path().into(),
@@ -1082,7 +1068,7 @@ impl MentionCompletion {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use editor::{AnchorRangeExt, MultiBufferOffset}; use editor::AnchorRangeExt;
use gpui::{EventEmitter, FocusHandle, Focusable, TestAppContext, VisualTestContext}; use gpui::{EventEmitter, FocusHandle, Focusable, TestAppContext, VisualTestContext};
use project::{Project, ProjectPath}; use project::{Project, ProjectPath};
use serde_json::json; use serde_json::json;
@@ -1196,8 +1182,10 @@ mod tests {
let app_state = cx.update(AppState::test); let app_state = cx.update(AppState::test);
cx.update(|cx| { cx.update(|cx| {
language::init(cx);
editor::init(cx); editor::init(cx);
workspace::init(app_state.clone(), cx); workspace::init(app_state.clone(), cx);
Project::init_settings(cx);
}); });
app_state app_state
@@ -1498,8 +1486,10 @@ mod tests {
let app_state = cx.update(AppState::test); let app_state = cx.update(AppState::test);
cx.update(|cx| { cx.update(|cx| {
language::init(cx);
editor::init(cx); editor::init(cx);
workspace::init(app_state.clone(), cx); workspace::init(app_state.clone(), cx);
Project::init_settings(cx);
}); });
app_state app_state
@@ -1677,7 +1667,7 @@ mod tests {
editor.display_map.update(cx, |display_map, cx| { editor.display_map.update(cx, |display_map, cx| {
display_map display_map
.snapshot(cx) .snapshot(cx)
.folds_in_range(MultiBufferOffset(0)..snapshot.len()) .folds_in_range(0..snapshot.len())
.map(|fold| fold.range.to_point(&snapshot)) .map(|fold| fold.range.to_point(&snapshot))
.collect() .collect()
}) })
@@ -1696,6 +1686,11 @@ mod tests {
let store = SettingsStore::test(cx); let store = SettingsStore::test(cx);
cx.set_global(store); cx.set_global(store);
theme::init(theme::LoadThemes::JustBase, cx); theme::init(theme::LoadThemes::JustBase, cx);
client::init_settings(cx);
language::init(cx);
Project::init_settings(cx);
workspace::init_settings(cx);
editor::init_settings(cx);
}); });
} }
} }

View File

@@ -16,7 +16,6 @@ use agent_settings::AgentSettings;
use anyhow::{Context as _, Result}; use anyhow::{Context as _, Result};
use client::telemetry::Telemetry; use client::telemetry::Telemetry;
use collections::{HashMap, HashSet, VecDeque, hash_map}; use collections::{HashMap, HashSet, VecDeque, hash_map};
use editor::MultiBufferOffset;
use editor::RowExt; use editor::RowExt;
use editor::SelectionEffects; use editor::SelectionEffects;
use editor::scroll::ScrollOffset; use editor::scroll::ScrollOffset;
@@ -804,7 +803,7 @@ impl InlineAssistant {
( (
editor editor
.selections .selections
.newest::<MultiBufferOffset>(&editor.display_snapshot(cx)), .newest::<usize>(&editor.display_snapshot(cx)),
editor.buffer().read(cx).snapshot(cx), editor.buffer().read(cx).snapshot(cx),
) )
}); });
@@ -837,7 +836,7 @@ impl InlineAssistant {
( (
editor editor
.selections .selections
.newest::<MultiBufferOffset>(&editor.display_snapshot(cx)), .newest::<usize>(&editor.display_snapshot(cx)),
editor.buffer().read(cx).snapshot(cx), editor.buffer().read(cx).snapshot(cx),
) )
}); });
@@ -854,14 +853,12 @@ impl InlineAssistant {
} else { } else {
let distance_from_selection = assist_range let distance_from_selection = assist_range
.start .start
.0 .abs_diff(selection.start)
.abs_diff(selection.start.0) .min(assist_range.start.abs_diff(selection.end))
.min(assist_range.start.0.abs_diff(selection.end.0))
+ assist_range + assist_range
.end .end
.0 .abs_diff(selection.start)
.abs_diff(selection.start.0) .min(assist_range.end.abs_diff(selection.end));
.min(assist_range.end.0.abs_diff(selection.end.0));
match closest_assist_fallback { match closest_assist_fallback {
Some((_, old_distance)) => { Some((_, old_distance)) => {
if distance_from_selection < old_distance { if distance_from_selection < old_distance {
@@ -938,7 +935,7 @@ impl InlineAssistant {
EditorEvent::Edited { transaction_id } => { EditorEvent::Edited { transaction_id } => {
let buffer = editor.read(cx).buffer().read(cx); let buffer = editor.read(cx).buffer().read(cx);
let edited_ranges = let edited_ranges =
buffer.edited_ranges_for_transaction::<MultiBufferOffset>(*transaction_id, cx); buffer.edited_ranges_for_transaction::<usize>(*transaction_id, cx);
let snapshot = buffer.snapshot(cx); let snapshot = buffer.snapshot(cx);
for assist_id in editor_assists.assist_ids.clone() { for assist_id in editor_assists.assist_ids.clone() {

View File

@@ -1,8 +1,8 @@
use crate::context_store::ContextStore;
use agent::HistoryStore; use agent::HistoryStore;
use collections::{HashMap, VecDeque}; use collections::VecDeque;
use editor::actions::Paste; use editor::actions::Paste;
use editor::display_map::{CreaseId, EditorMargins}; use editor::display_map::EditorMargins;
use editor::{Addon, AnchorRangeExt as _, MultiBufferOffset};
use editor::{ use editor::{
ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer, ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
actions::{MoveDown, MoveUp}, actions::{MoveDown, MoveUp},
@@ -17,7 +17,6 @@ use parking_lot::Mutex;
use prompt_store::PromptStore; use prompt_store::PromptStore;
use settings::Settings; use settings::Settings;
use std::cmp; use std::cmp;
use std::ops::Range;
use std::rc::Rc; use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
use theme::ThemeSettings; use theme::ThemeSettings;
@@ -28,15 +27,12 @@ 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;
use crate::context::{AgentContextHandle, AgentContextKey}; use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider};
use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention};
use crate::context_store::{ContextStore, ContextStoreEvent};
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind}; use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
use crate::message_editor::{ContextCreasesAddon, extract_message_creases, insert_message_creases};
use crate::terminal_codegen::TerminalCodegen; use crate::terminal_codegen::TerminalCodegen;
use crate::{ use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext};
CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext, RemoveAllContext, use crate::{RemoveAllContext, ToggleContextPicker};
ToggleContextPicker,
};
pub struct PromptEditor<T> { pub struct PromptEditor<T> {
pub editor: Entity<Editor>, pub editor: Entity<Editor>,
@@ -1161,156 +1157,3 @@ impl GenerationMode {
} }
} }
} }
/// Stored information that can be used to resurrect a context crease when creating an editor for a past message.
#[derive(Clone, Debug)]
pub struct MessageCrease {
pub range: Range<MultiBufferOffset>,
pub icon_path: SharedString,
pub label: SharedString,
/// None for a deserialized message, Some otherwise.
pub context: Option<AgentContextHandle>,
}
#[derive(Default)]
pub struct ContextCreasesAddon {
creases: HashMap<AgentContextKey, Vec<(CreaseId, SharedString)>>,
_subscription: Option<Subscription>,
}
impl Addon for ContextCreasesAddon {
fn to_any(&self) -> &dyn std::any::Any {
self
}
fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
Some(self)
}
}
impl ContextCreasesAddon {
pub fn new() -> Self {
Self {
creases: HashMap::default(),
_subscription: None,
}
}
pub fn add_creases(
&mut self,
context_store: &Entity<ContextStore>,
key: AgentContextKey,
creases: impl IntoIterator<Item = (CreaseId, SharedString)>,
cx: &mut Context<Editor>,
) {
self.creases.entry(key).or_default().extend(creases);
self._subscription = Some(
cx.subscribe(context_store, |editor, _, event, cx| match event {
ContextStoreEvent::ContextRemoved(key) => {
let Some(this) = editor.addon_mut::<Self>() else {
return;
};
let (crease_ids, replacement_texts): (Vec<_>, Vec<_>) = this
.creases
.remove(key)
.unwrap_or_default()
.into_iter()
.unzip();
let ranges = editor
.remove_creases(crease_ids, cx)
.into_iter()
.map(|(_, range)| range)
.collect::<Vec<_>>();
editor.unfold_ranges(&ranges, false, false, cx);
editor.edit(ranges.into_iter().zip(replacement_texts), cx);
cx.notify();
}
}),
)
}
pub fn into_inner(self) -> HashMap<AgentContextKey, Vec<(CreaseId, SharedString)>> {
self.creases
}
}
pub fn extract_message_creases(
editor: &mut Editor,
cx: &mut Context<'_, Editor>,
) -> Vec<MessageCrease> {
let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
let mut contexts_by_crease_id = editor
.addon_mut::<ContextCreasesAddon>()
.map(std::mem::take)
.unwrap_or_default()
.into_inner()
.into_iter()
.flat_map(|(key, creases)| {
let context = key.0;
creases
.into_iter()
.map(move |(id, _)| (id, context.clone()))
})
.collect::<HashMap<_, _>>();
// Filter the addon's list of creases based on what the editor reports,
// since the addon might have removed creases in it.
editor.display_map.update(cx, |display_map, cx| {
display_map
.snapshot(cx)
.crease_snapshot
.creases()
.filter_map(|(id, crease)| {
Some((
id,
(
crease.range().to_offset(&buffer_snapshot),
crease.metadata()?.clone(),
),
))
})
.map(|(id, (range, metadata))| {
let context = contexts_by_crease_id.remove(&id);
MessageCrease {
range,
context,
label: metadata.label,
icon_path: metadata.icon_path,
}
})
.collect()
})
}
pub fn insert_message_creases(
editor: &mut Editor,
message_creases: &[MessageCrease],
context_store: &Entity<ContextStore>,
window: &mut Window,
cx: &mut Context<'_, Editor>,
) {
let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
let creases = message_creases
.iter()
.map(|crease| {
let start = buffer_snapshot.anchor_after(crease.range.start);
let end = buffer_snapshot.anchor_before(crease.range.end);
crease_for_mention(
crease.label.clone(),
crease.icon_path.clone(),
start..end,
cx.weak_entity(),
)
})
.collect::<Vec<_>>();
let ids = editor.insert_creases(creases.clone(), cx);
editor.fold_creases(creases, false, window, cx);
if let Some(addon) = editor.addon_mut::<ContextCreasesAddon>() {
for (crease, id) in message_creases.iter().zip(ids) {
if let Some(context) = crease.context.as_ref() {
let key = AgentContextKey(context.clone());
addon.add_creases(context_store, key, vec![(id, crease.label.clone())], cx);
}
}
}
}

View File

@@ -1,6 +1,6 @@
use std::{cmp::Reverse, sync::Arc}; use std::{cmp::Reverse, sync::Arc};
use collections::IndexMap; use collections::{HashSet, IndexMap};
use fuzzy::{StringMatch, StringMatchCandidate, match_strings}; use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
use gpui::{Action, AnyElement, App, BackgroundExecutor, DismissEvent, Subscription, Task}; use gpui::{Action, AnyElement, App, BackgroundExecutor, DismissEvent, Subscription, Task};
use language_model::{ use language_model::{
@@ -19,26 +19,14 @@ pub type LanguageModelSelector = Picker<LanguageModelPickerDelegate>;
pub fn language_model_selector( pub fn language_model_selector(
get_active_model: impl Fn(&App) -> Option<ConfiguredModel> + 'static, get_active_model: impl Fn(&App) -> Option<ConfiguredModel> + 'static,
on_model_changed: impl Fn(Arc<dyn LanguageModel>, &mut App) + 'static, on_model_changed: impl Fn(Arc<dyn LanguageModel>, &mut App) + 'static,
popover_styles: bool,
window: &mut Window, window: &mut Window,
cx: &mut Context<LanguageModelSelector>, cx: &mut Context<LanguageModelSelector>,
) -> LanguageModelSelector { ) -> LanguageModelSelector {
let delegate = LanguageModelPickerDelegate::new( let delegate = LanguageModelPickerDelegate::new(get_active_model, on_model_changed, window, cx);
get_active_model, Picker::list(delegate, window, cx)
on_model_changed, .show_scrollbar(true)
popover_styles, .width(rems(20.))
window, .max_height(Some(rems(20.).into()))
cx,
);
if popover_styles {
Picker::list(delegate, window, cx)
.show_scrollbar(true)
.width(rems(20.))
.max_height(Some(rems(20.).into()))
} else {
Picker::list(delegate, window, cx).show_scrollbar(true)
}
} }
fn all_models(cx: &App) -> GroupedModels { fn all_models(cx: &App) -> GroupedModels {
@@ -57,7 +45,7 @@ fn all_models(cx: &App) -> GroupedModels {
}) })
.collect(); .collect();
let all = providers let other = providers
.iter() .iter()
.flat_map(|provider| { .flat_map(|provider| {
provider provider
@@ -70,7 +58,7 @@ fn all_models(cx: &App) -> GroupedModels {
}) })
.collect(); .collect();
GroupedModels::new(all, recommended) GroupedModels::new(other, recommended)
} }
#[derive(Clone)] #[derive(Clone)]
@@ -87,14 +75,12 @@ pub struct LanguageModelPickerDelegate {
selected_index: usize, selected_index: usize,
_authenticate_all_providers_task: Task<()>, _authenticate_all_providers_task: Task<()>,
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
popover_styles: bool,
} }
impl LanguageModelPickerDelegate { impl LanguageModelPickerDelegate {
fn new( fn new(
get_active_model: impl Fn(&App) -> Option<ConfiguredModel> + 'static, get_active_model: impl Fn(&App) -> Option<ConfiguredModel> + 'static,
on_model_changed: impl Fn(Arc<dyn LanguageModel>, &mut App) + 'static, on_model_changed: impl Fn(Arc<dyn LanguageModel>, &mut App) + 'static,
popover_styles: bool,
window: &mut Window, window: &mut Window,
cx: &mut Context<Picker<Self>>, cx: &mut Context<Picker<Self>>,
) -> Self { ) -> Self {
@@ -127,7 +113,6 @@ impl LanguageModelPickerDelegate {
} }
}, },
)], )],
popover_styles,
} }
} }
@@ -192,7 +177,7 @@ impl LanguageModelPickerDelegate {
} }
_ => { _ => {
log::error!( log::error!(
"Failed to authenticate provider: {}: {err:#}", "Failed to authenticate provider: {}: {err}",
provider_name.0 provider_name.0
); );
} }
@@ -210,24 +195,33 @@ impl LanguageModelPickerDelegate {
struct GroupedModels { struct GroupedModels {
recommended: Vec<ModelInfo>, recommended: Vec<ModelInfo>,
all: IndexMap<LanguageModelProviderId, Vec<ModelInfo>>, other: IndexMap<LanguageModelProviderId, Vec<ModelInfo>>,
} }
impl GroupedModels { impl GroupedModels {
pub fn new(all: Vec<ModelInfo>, recommended: Vec<ModelInfo>) -> Self { pub fn new(other: Vec<ModelInfo>, recommended: Vec<ModelInfo>) -> Self {
let mut all_by_provider: IndexMap<_, Vec<ModelInfo>> = IndexMap::default(); let recommended_ids = recommended
for model in all { .iter()
.map(|info| (info.model.provider_id(), info.model.id()))
.collect::<HashSet<_>>();
let mut other_by_provider: IndexMap<_, Vec<ModelInfo>> = IndexMap::default();
for model in other {
if recommended_ids.contains(&(model.model.provider_id(), model.model.id())) {
continue;
}
let provider = model.model.provider_id(); let provider = model.model.provider_id();
if let Some(models) = all_by_provider.get_mut(&provider) { if let Some(models) = other_by_provider.get_mut(&provider) {
models.push(model); models.push(model);
} else { } else {
all_by_provider.insert(provider, vec![model]); other_by_provider.insert(provider, vec![model]);
} }
} }
Self { Self {
recommended, recommended,
all: all_by_provider, other: other_by_provider,
} }
} }
@@ -243,7 +237,7 @@ impl GroupedModels {
); );
} }
for models in self.all.values() { for models in self.other.values() {
if models.is_empty() { if models.is_empty() {
continue; continue;
} }
@@ -258,6 +252,20 @@ impl GroupedModels {
} }
entries entries
} }
fn model_infos(&self) -> Vec<ModelInfo> {
let other = self
.other
.values()
.flat_map(|model| model.iter())
.cloned()
.collect::<Vec<_>>();
self.recommended
.iter()
.chain(&other)
.cloned()
.collect::<Vec<_>>()
}
} }
enum LanguageModelPickerEntry { enum LanguageModelPickerEntry {
@@ -402,9 +410,8 @@ impl PickerDelegate for LanguageModelPickerDelegate {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let available_models = all_models let available_models = all_models
.all .model_infos()
.values() .iter()
.flat_map(|models| models.iter())
.filter(|m| configured_provider_ids.contains(&m.model.provider_id())) .filter(|m| configured_provider_ids.contains(&m.model.provider_id()))
.cloned() .cloned()
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@@ -492,15 +499,17 @@ impl PickerDelegate for LanguageModelPickerDelegate {
.inset(true) .inset(true)
.spacing(ListItemSpacing::Sparse) .spacing(ListItemSpacing::Sparse)
.toggle_state(selected) .toggle_state(selected)
.start_slot(
Icon::new(model_info.icon)
.color(model_icon_color)
.size(IconSize::Small),
)
.child( .child(
h_flex() h_flex()
.w_full() .w_full()
.pl_0p5()
.gap_1p5() .gap_1p5()
.child( .w(px(240.))
Icon::new(model_info.icon)
.color(model_icon_color)
.size(IconSize::Small),
)
.child(Label::new(model_info.model.name().0).truncate()), .child(Label::new(model_info.model.name().0).truncate()),
) )
.end_slot(div().pr_3().when(is_selected, |this| { .end_slot(div().pr_3().when(is_selected, |this| {
@@ -521,10 +530,6 @@ impl PickerDelegate for LanguageModelPickerDelegate {
_window: &mut Window, _window: &mut Window,
cx: &mut Context<Picker<Self>>, cx: &mut Context<Picker<Self>>,
) -> Option<gpui::AnyElement> { ) -> Option<gpui::AnyElement> {
if !self.popover_styles {
return None;
}
Some( Some(
h_flex() h_flex()
.w_full() .w_full()
@@ -740,52 +745,46 @@ mod tests {
} }
#[gpui::test] #[gpui::test]
fn test_recommended_models_also_appear_in_other(_cx: &mut TestAppContext) { fn test_exclude_recommended_models(_cx: &mut TestAppContext) {
let recommended_models = create_models(vec![("zed", "claude")]); let recommended_models = create_models(vec![("zed", "claude")]);
let all_models = create_models(vec![ let all_models = create_models(vec![
("zed", "claude"), // Should also appear in "other" ("zed", "claude"), // Should be filtered out from "other"
("zed", "gemini"), ("zed", "gemini"),
("copilot", "o3"), ("copilot", "o3"),
]); ]);
let grouped_models = GroupedModels::new(all_models, recommended_models); let grouped_models = GroupedModels::new(all_models, recommended_models);
let actual_all_models = grouped_models let actual_other_models = grouped_models
.all .other
.values() .values()
.flatten() .flatten()
.cloned() .cloned()
.collect::<Vec<_>>(); .collect::<Vec<_>>();
// Recommended models should also appear in "all" // Recommended models should not appear in "other"
assert_models_eq( assert_models_eq(actual_other_models, vec!["zed/gemini", "copilot/o3"]);
actual_all_models,
vec!["zed/claude", "zed/gemini", "copilot/o3"],
);
} }
#[gpui::test] #[gpui::test]
fn test_models_from_different_providers(_cx: &mut TestAppContext) { fn test_dont_exclude_models_from_other_providers(_cx: &mut TestAppContext) {
let recommended_models = create_models(vec![("zed", "claude")]); let recommended_models = create_models(vec![("zed", "claude")]);
let all_models = create_models(vec![ let all_models = create_models(vec![
("zed", "claude"), // Should also appear in "other" ("zed", "claude"), // Should be filtered out from "other"
("zed", "gemini"), ("zed", "gemini"),
("copilot", "claude"), // Different provider, should appear in "other" ("copilot", "claude"), // Should not be filtered out from "other"
]); ]);
let grouped_models = GroupedModels::new(all_models, recommended_models); let grouped_models = GroupedModels::new(all_models, recommended_models);
let actual_all_models = grouped_models let actual_other_models = grouped_models
.all .other
.values() .values()
.flatten() .flatten()
.cloned() .cloned()
.collect::<Vec<_>>(); .collect::<Vec<_>>();
// All models should appear in "all" regardless of recommended status // Recommended models should not appear in "other"
assert_models_eq( assert_models_eq(actual_other_models, vec!["zed/gemini", "copilot/claude"]);
actual_all_models,
vec!["zed/claude", "zed/gemini", "copilot/claude"],
);
} }
} }

View File

@@ -0,0 +1,166 @@
use std::ops::Range;
use collections::HashMap;
use editor::display_map::CreaseId;
use editor::{Addon, AnchorRangeExt, Editor};
use gpui::{Entity, Subscription};
use ui::prelude::*;
use crate::{
context::{AgentContextHandle, AgentContextKey},
context_picker::crease_for_mention,
context_store::{ContextStore, ContextStoreEvent},
};
/// Stored information that can be used to resurrect a context crease when creating an editor for a past message.
#[derive(Clone, Debug)]
pub struct MessageCrease {
pub range: Range<usize>,
pub icon_path: SharedString,
pub label: SharedString,
/// None for a deserialized message, Some otherwise.
pub context: Option<AgentContextHandle>,
}
#[derive(Default)]
pub struct ContextCreasesAddon {
creases: HashMap<AgentContextKey, Vec<(CreaseId, SharedString)>>,
_subscription: Option<Subscription>,
}
impl Addon for ContextCreasesAddon {
fn to_any(&self) -> &dyn std::any::Any {
self
}
fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
Some(self)
}
}
impl ContextCreasesAddon {
pub fn new() -> Self {
Self {
creases: HashMap::default(),
_subscription: None,
}
}
pub fn add_creases(
&mut self,
context_store: &Entity<ContextStore>,
key: AgentContextKey,
creases: impl IntoIterator<Item = (CreaseId, SharedString)>,
cx: &mut Context<Editor>,
) {
self.creases.entry(key).or_default().extend(creases);
self._subscription = Some(
cx.subscribe(context_store, |editor, _, event, cx| match event {
ContextStoreEvent::ContextRemoved(key) => {
let Some(this) = editor.addon_mut::<Self>() else {
return;
};
let (crease_ids, replacement_texts): (Vec<_>, Vec<_>) = this
.creases
.remove(key)
.unwrap_or_default()
.into_iter()
.unzip();
let ranges = editor
.remove_creases(crease_ids, cx)
.into_iter()
.map(|(_, range)| range)
.collect::<Vec<_>>();
editor.unfold_ranges(&ranges, false, false, cx);
editor.edit(ranges.into_iter().zip(replacement_texts), cx);
cx.notify();
}
}),
)
}
pub fn into_inner(self) -> HashMap<AgentContextKey, Vec<(CreaseId, SharedString)>> {
self.creases
}
}
pub fn extract_message_creases(
editor: &mut Editor,
cx: &mut Context<'_, Editor>,
) -> Vec<MessageCrease> {
let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
let mut contexts_by_crease_id = editor
.addon_mut::<ContextCreasesAddon>()
.map(std::mem::take)
.unwrap_or_default()
.into_inner()
.into_iter()
.flat_map(|(key, creases)| {
let context = key.0;
creases
.into_iter()
.map(move |(id, _)| (id, context.clone()))
})
.collect::<HashMap<_, _>>();
// Filter the addon's list of creases based on what the editor reports,
// since the addon might have removed creases in it.
editor.display_map.update(cx, |display_map, cx| {
display_map
.snapshot(cx)
.crease_snapshot
.creases()
.filter_map(|(id, crease)| {
Some((
id,
(
crease.range().to_offset(&buffer_snapshot),
crease.metadata()?.clone(),
),
))
})
.map(|(id, (range, metadata))| {
let context = contexts_by_crease_id.remove(&id);
MessageCrease {
range,
context,
label: metadata.label,
icon_path: metadata.icon_path,
}
})
.collect()
})
}
pub fn insert_message_creases(
editor: &mut Editor,
message_creases: &[MessageCrease],
context_store: &Entity<ContextStore>,
window: &mut Window,
cx: &mut Context<'_, Editor>,
) {
let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
let creases = message_creases
.iter()
.map(|crease| {
let start = buffer_snapshot.anchor_after(crease.range.start);
let end = buffer_snapshot.anchor_before(crease.range.end);
crease_for_mention(
crease.label.clone(),
crease.icon_path.clone(),
start..end,
cx.weak_entity(),
)
})
.collect::<Vec<_>>();
let ids = editor.insert_creases(creases.clone(), cx);
editor.fold_creases(creases, false, window, cx);
if let Some(addon) = editor.addon_mut::<ContextCreasesAddon>() {
for (crease, id) in message_creases.iter().zip(ids) {
if let Some(context) = crease.context.as_ref() {
let key = AgentContextKey(context.clone());
addon.add_creases(context_store, key, vec![(id, crease.label.clone())], cx);
}
}
}
}

View File

@@ -15,8 +15,8 @@ use std::{
sync::{Arc, atomic::AtomicBool}, sync::{Arc, atomic::AtomicBool},
}; };
use ui::{ use ui::{
DocumentationAside, DocumentationEdge, DocumentationSide, HighlightedLabel, KeyBinding, DocumentationAside, DocumentationEdge, DocumentationSide, HighlightedLabel, LabelSize,
LabelSize, ListItem, ListItemSpacing, PopoverMenuHandle, TintColor, Tooltip, prelude::*, ListItem, ListItemSpacing, PopoverMenuHandle, TintColor, Tooltip, prelude::*,
}; };
/// Trait for types that can provide and manage agent profiles /// Trait for types that can provide and manage agent profiles
@@ -81,7 +81,6 @@ impl ProfileSelector {
self.provider.clone(), self.provider.clone(),
self.profiles.clone(), self.profiles.clone(),
cx.background_executor().clone(), cx.background_executor().clone(),
self.focus_handle.clone(),
cx, cx,
); );
@@ -208,7 +207,6 @@ pub(crate) struct ProfilePickerDelegate {
selected_index: usize, selected_index: usize,
query: String, query: String,
cancel: Option<Arc<AtomicBool>>, cancel: Option<Arc<AtomicBool>>,
focus_handle: FocusHandle,
} }
impl ProfilePickerDelegate { impl ProfilePickerDelegate {
@@ -217,7 +215,6 @@ impl ProfilePickerDelegate {
provider: Arc<dyn ProfileProvider>, provider: Arc<dyn ProfileProvider>,
profiles: AvailableProfiles, profiles: AvailableProfiles,
background: BackgroundExecutor, background: BackgroundExecutor,
focus_handle: FocusHandle,
cx: &mut Context<ProfileSelector>, cx: &mut Context<ProfileSelector>,
) -> Self { ) -> Self {
let candidates = Self::candidates_from(profiles); let candidates = Self::candidates_from(profiles);
@@ -234,7 +231,6 @@ impl ProfilePickerDelegate {
selected_index: 0, selected_index: 0,
query: String::new(), query: String::new(),
cancel: None, cancel: None,
focus_handle,
}; };
this.selected_index = this this.selected_index = this
@@ -598,26 +594,20 @@ impl PickerDelegate for ProfilePickerDelegate {
_: &mut 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();
Some( Some(
h_flex() h_flex()
.w_full() .w_full()
.border_t_1() .border_t_1()
.border_color(cx.theme().colors().border_variant) .border_color(cx.theme().colors().border_variant)
.p_1p5() .p_1()
.gap_4()
.justify_between()
.child( .child(
Button::new("configure", "Configure") Button::new("configure", "Configure")
.full_width() .icon(IconName::Settings)
.style(ButtonStyle::Outlined) .icon_size(IconSize::Small)
.key_binding( .icon_color(Color::Muted)
KeyBinding::for_action_in( .icon_position(IconPosition::Start)
&ManageProfiles::default(),
&focus_handle,
cx,
)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(|_, window, cx| { .on_click(|_, window, cx| {
window.dispatch_action(ManageProfiles::default().boxed_clone(), cx); window.dispatch_action(ManageProfiles::default().boxed_clone(), cx);
}), }),
@@ -669,25 +659,20 @@ mod tests {
is_builtin: true, is_builtin: true,
}]; }];
cx.update(|cx| { let delegate = ProfilePickerDelegate {
let focus_handle = cx.focus_handle(); fs: FakeFs::new(cx.executor()),
provider: Arc::new(TestProfileProvider::new(AgentProfileId("write".into()))),
background: cx.executor(),
candidates,
string_candidates: Arc::new(Vec::new()),
filtered_entries: Vec::new(),
selected_index: 0,
query: String::new(),
cancel: None,
};
let delegate = ProfilePickerDelegate { let matches = Vec::new(); // No matches
fs: FakeFs::new(cx.background_executor().clone()), let _entries = delegate.entries_from_matches(matches);
provider: Arc::new(TestProfileProvider::new(AgentProfileId("write".into()))),
background: cx.background_executor().clone(),
candidates,
string_candidates: Arc::new(Vec::new()),
filtered_entries: Vec::new(),
selected_index: 0,
query: String::new(),
cancel: None,
focus_handle,
};
let matches = Vec::new(); // No matches
let _entries = delegate.entries_from_matches(matches);
});
} }
#[gpui::test] #[gpui::test]
@@ -705,35 +690,30 @@ mod tests {
}, },
]; ];
cx.update(|cx| { let delegate = ProfilePickerDelegate {
let focus_handle = cx.focus_handle(); fs: FakeFs::new(cx.executor()),
provider: Arc::new(TestProfileProvider::new(AgentProfileId("write".into()))),
background: cx.executor(),
candidates,
string_candidates: Arc::new(Vec::new()),
filtered_entries: vec![
ProfilePickerEntry::Profile(ProfileMatchEntry {
candidate_index: 0,
positions: Vec::new(),
}),
ProfilePickerEntry::Profile(ProfileMatchEntry {
candidate_index: 1,
positions: Vec::new(),
}),
],
selected_index: 0,
query: String::new(),
cancel: None,
};
let delegate = ProfilePickerDelegate { // Active profile should be found at index 0
fs: FakeFs::new(cx.background_executor().clone()), let active_index = delegate.index_of_profile(&AgentProfileId("write".into()));
provider: Arc::new(TestProfileProvider::new(AgentProfileId("write".into()))), assert_eq!(active_index, Some(0));
background: cx.background_executor().clone(),
candidates,
string_candidates: Arc::new(Vec::new()),
filtered_entries: vec![
ProfilePickerEntry::Profile(ProfileMatchEntry {
candidate_index: 0,
positions: Vec::new(),
}),
ProfilePickerEntry::Profile(ProfileMatchEntry {
candidate_index: 1,
positions: Vec::new(),
}),
],
selected_index: 0,
query: String::new(),
cancel: None,
focus_handle,
};
// Active profile should be found at index 0
let active_index = delegate.index_of_profile(&AgentProfileId("write".into()));
assert_eq!(active_index, Some(0));
});
} }
struct TestProfileProvider { struct TestProfileProvider {

View File

@@ -127,8 +127,6 @@ impl SlashCommandCompletionProvider {
new_text, new_text,
label: command.label(cx), label: command.label(cx),
icon_path: None, icon_path: None,
match_start: None,
snippet_deduplication_key: None,
insert_text_mode: None, insert_text_mode: None,
confirm, confirm,
source: CompletionSource::Custom, source: CompletionSource::Custom,
@@ -234,8 +232,6 @@ impl SlashCommandCompletionProvider {
icon_path: None, icon_path: None,
new_text, new_text,
documentation: None, documentation: None,
match_start: None,
snippet_deduplication_key: None,
confirm, confirm,
insert_text_mode: None, insert_text_mode: None,
source: CompletionSource::Custom, source: CompletionSource::Custom,

View File

@@ -9,8 +9,8 @@ use assistant_slash_commands::{DefaultSlashCommand, FileSlashCommand, selections
use client::{proto, zed_urls}; use client::{proto, zed_urls};
use collections::{BTreeSet, HashMap, HashSet, hash_map}; use collections::{BTreeSet, HashMap, HashSet, hash_map};
use editor::{ use editor::{
Anchor, Editor, EditorEvent, MenuEditPredictionsPolicy, MultiBuffer, MultiBufferOffset, Anchor, Editor, EditorEvent, MenuEditPredictionsPolicy, MultiBuffer, MultiBufferSnapshot,
MultiBufferSnapshot, RowExt, ToOffset as _, ToPoint, RowExt, ToOffset as _, ToPoint,
actions::{MoveToEndOfLine, Newline, ShowCompletions}, actions::{MoveToEndOfLine, Newline, ShowCompletions},
display_map::{ display_map::{
BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata, CustomBlockId, FoldId, BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata, CustomBlockId, FoldId,
@@ -22,11 +22,11 @@ use editor::{FoldPlaceholder, display_map::CreaseId};
use fs::Fs; use fs::Fs;
use futures::FutureExt; use futures::FutureExt;
use gpui::{ use gpui::{
Action, Animation, AnimationExt, AnyElement, App, ClipboardEntry, ClipboardItem, Empty, Entity, Action, Animation, AnimationExt, AnyElement, AnyView, App, ClipboardEntry, ClipboardItem,
EventEmitter, FocusHandle, Focusable, FontWeight, Global, InteractiveElement, IntoElement, Empty, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, Global, InteractiveElement,
ParentElement, Pixels, Render, RenderImage, SharedString, Size, StatefulInteractiveElement, IntoElement, ParentElement, Pixels, Render, RenderImage, SharedString, Size,
Styled, Subscription, Task, WeakEntity, actions, div, img, point, prelude::*, StatefulInteractiveElement, Styled, Subscription, Task, WeakEntity, actions, div, img, point,
pulsating_between, size, prelude::*, pulsating_between, size,
}; };
use language::{ use language::{
BufferSnapshot, LspAdapterDelegate, ToOffset, BufferSnapshot, LspAdapterDelegate, ToOffset,
@@ -66,7 +66,7 @@ use workspace::{
}; };
use workspace::{ use workspace::{
Save, Toast, Workspace, Save, Toast, Workspace,
item::{self, FollowableItem, Item}, item::{self, FollowableItem, Item, ItemHandle},
notifications::NotificationId, notifications::NotificationId,
pane, pane,
searchable::{SearchEvent, SearchableItem}, searchable::{SearchEvent, SearchableItem},
@@ -314,7 +314,6 @@ impl TextThreadEditor {
) )
}); });
}, },
true, // Use popover styles for picker
window, window,
cx, cx,
) )
@@ -390,7 +389,7 @@ impl TextThreadEditor {
let cursor = user_message let cursor = user_message
.start .start
.to_offset(self.text_thread.read(cx).buffer().read(cx)); .to_offset(self.text_thread.read(cx).buffer().read(cx));
MultiBufferOffset(cursor)..MultiBufferOffset(cursor) cursor..cursor
}; };
self.editor.update(cx, |editor, cx| { self.editor.update(cx, |editor, cx| {
editor.change_selections(Default::default(), window, cx, |selections| { editor.change_selections(Default::default(), window, cx, |selections| {
@@ -431,7 +430,7 @@ impl TextThreadEditor {
let cursors = self.cursors(cx); let cursors = self.cursors(cx);
self.text_thread.update(cx, |text_thread, cx| { self.text_thread.update(cx, |text_thread, cx| {
let messages = text_thread let messages = text_thread
.messages_for_offsets(cursors.into_iter().map(|cursor| cursor.0), cx) .messages_for_offsets(cursors, cx)
.into_iter() .into_iter()
.map(|message| message.id) .map(|message| message.id)
.collect(); .collect();
@@ -439,11 +438,9 @@ impl TextThreadEditor {
}); });
} }
fn cursors(&self, cx: &mut App) -> Vec<MultiBufferOffset> { fn cursors(&self, cx: &mut App) -> Vec<usize> {
let selections = self.editor.update(cx, |editor, cx| { let selections = self.editor.update(cx, |editor, cx| {
editor editor.selections.all::<usize>(&editor.display_snapshot(cx))
.selections
.all::<MultiBufferOffset>(&editor.display_snapshot(cx))
}); });
selections selections
.into_iter() .into_iter()
@@ -1582,11 +1579,7 @@ impl TextThreadEditor {
fn get_clipboard_contents( fn get_clipboard_contents(
&mut self, &mut self,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> ( ) -> (String, CopyMetadata, Vec<text::Selection<usize>>) {
String,
CopyMetadata,
Vec<text::Selection<MultiBufferOffset>>,
) {
let (mut selection, creases) = self.editor.update(cx, |editor, cx| { let (mut selection, creases) = self.editor.update(cx, |editor, cx| {
let mut selection = editor let mut selection = editor
.selections .selections
@@ -1644,26 +1637,30 @@ impl TextThreadEditor {
// If selection is empty, we want to copy the entire line // If selection is empty, we want to copy the entire line
if selection.range().is_empty() { if selection.range().is_empty() {
let snapshot = self.editor.read(cx).buffer().read(cx).snapshot(cx); let snapshot = text_thread.buffer().read(cx).snapshot();
let point = snapshot.offset_to_point(selection.range().start); let point = snapshot.offset_to_point(selection.range().start);
selection.start = snapshot.point_to_offset(Point::new(point.row, 0)); selection.start = snapshot.point_to_offset(Point::new(point.row, 0));
selection.end = snapshot selection.end = snapshot
.point_to_offset(cmp::min(Point::new(point.row + 1, 0), snapshot.max_point())); .point_to_offset(cmp::min(Point::new(point.row + 1, 0), snapshot.max_point()));
for chunk in snapshot.text_for_range(selection.range()) { for chunk in text_thread
.buffer()
.read(cx)
.text_for_range(selection.range())
{
text.push_str(chunk); text.push_str(chunk);
} }
} else { } else {
for message in text_thread.messages(cx) { for message in text_thread.messages(cx) {
if message.offset_range.start >= selection.range().end.0 { if message.offset_range.start >= selection.range().end {
break; break;
} else if message.offset_range.end >= selection.range().start.0 { } else if message.offset_range.end >= selection.range().start {
let range = cmp::max(message.offset_range.start, selection.range().start.0) let range = cmp::max(message.offset_range.start, selection.range().start)
..cmp::min(message.offset_range.end, selection.range().end.0); ..cmp::min(message.offset_range.end, selection.range().end);
if !range.is_empty() { if !range.is_empty() {
for chunk in text_thread.buffer().read(cx).text_for_range(range) { for chunk in text_thread.buffer().read(cx).text_for_range(range) {
text.push_str(chunk); text.push_str(chunk);
} }
if message.offset_range.end < selection.range().end.0 { if message.offset_range.end < selection.range().end {
text.push('\n'); text.push('\n');
} }
} }
@@ -1681,7 +1678,7 @@ impl TextThreadEditor {
) { ) {
cx.stop_propagation(); cx.stop_propagation();
let mut images = if let Some(item) = cx.read_from_clipboard() { let images = if let Some(item) = cx.read_from_clipboard() {
item.into_entries() item.into_entries()
.filter_map(|entry| { .filter_map(|entry| {
if let ClipboardEntry::Image(image) = entry { if let ClipboardEntry::Image(image) = entry {
@@ -1695,40 +1692,6 @@ impl TextThreadEditor {
Vec::new() Vec::new()
}; };
if let Some(paths) = cx.read_from_clipboard() {
for path in paths
.into_entries()
.filter_map(|entry| {
if let ClipboardEntry::ExternalPaths(paths) = entry {
Some(paths.paths().to_owned())
} else {
None
}
})
.flatten()
{
let Ok(content) = std::fs::read(path) else {
continue;
};
let Ok(format) = image::guess_format(&content) else {
continue;
};
images.push(gpui::Image::from_bytes(
match format {
image::ImageFormat::Png => gpui::ImageFormat::Png,
image::ImageFormat::Jpeg => gpui::ImageFormat::Jpeg,
image::ImageFormat::WebP => gpui::ImageFormat::Webp,
image::ImageFormat::Gif => gpui::ImageFormat::Gif,
image::ImageFormat::Bmp => gpui::ImageFormat::Bmp,
image::ImageFormat::Tiff => gpui::ImageFormat::Tiff,
image::ImageFormat::Ico => gpui::ImageFormat::Ico,
_ => continue,
},
content,
));
}
}
let metadata = if let Some(item) = cx.read_from_clipboard() { let metadata = if let Some(item) = cx.read_from_clipboard() {
item.entries().first().and_then(|entry| { item.entries().first().and_then(|entry| {
if let ClipboardEntry::String(text) = entry { if let ClipboardEntry::String(text) = entry {
@@ -1745,7 +1708,7 @@ impl TextThreadEditor {
self.editor.update(cx, |editor, cx| { self.editor.update(cx, |editor, cx| {
let paste_position = editor let paste_position = editor
.selections .selections
.newest::<MultiBufferOffset>(&editor.display_snapshot(cx)) .newest::<usize>(&editor.display_snapshot(cx))
.head(); .head();
editor.paste(action, window, cx); editor.paste(action, window, cx);
@@ -1793,16 +1756,13 @@ impl TextThreadEditor {
editor.transact(window, cx, |editor, _window, cx| { editor.transact(window, cx, |editor, _window, cx| {
let edits = editor let edits = editor
.selections .selections
.all::<MultiBufferOffset>(&editor.display_snapshot(cx)) .all::<usize>(&editor.display_snapshot(cx))
.into_iter() .into_iter()
.map(|selection| (selection.start..selection.end, "\n")); .map(|selection| (selection.start..selection.end, "\n"));
editor.edit(edits, cx); editor.edit(edits, cx);
let snapshot = editor.buffer().read(cx).snapshot(cx); let snapshot = editor.buffer().read(cx).snapshot(cx);
for selection in editor for selection in editor.selections.all::<usize>(&editor.display_snapshot(cx)) {
.selections
.all::<MultiBufferOffset>(&editor.display_snapshot(cx))
{
image_positions.push(snapshot.anchor_before(selection.end)); image_positions.push(snapshot.anchor_before(selection.end));
} }
}); });
@@ -1894,7 +1854,7 @@ impl TextThreadEditor {
let range = selection let range = selection
.map(|endpoint| endpoint.to_offset(&buffer)) .map(|endpoint| endpoint.to_offset(&buffer))
.range(); .range();
text_thread.split_message(range.start.0..range.end.0, cx); text_thread.split_message(range, cx);
} }
}); });
} }
@@ -2588,11 +2548,11 @@ impl Item for TextThreadEditor {
type_id: TypeId, type_id: TypeId,
self_handle: &'a Entity<Self>, self_handle: &'a Entity<Self>,
_: &'a App, _: &'a App,
) -> Option<gpui::AnyEntity> { ) -> Option<AnyView> {
if type_id == TypeId::of::<Self>() { if type_id == TypeId::of::<Self>() {
Some(self_handle.clone().into()) Some(self_handle.to_any())
} else if type_id == TypeId::of::<Editor>() { } else if type_id == TypeId::of::<Editor>() {
Some(self.editor.clone().into()) Some(self.editor.to_any())
} else { } else {
None None
} }
@@ -2631,11 +2591,12 @@ impl SearchableItem for TextThreadEditor {
&mut self, &mut self,
index: usize, index: usize,
matches: &[Self::Match], matches: &[Self::Match],
collapse: bool,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
self.editor.update(cx, |editor, cx| { self.editor.update(cx, |editor, cx| {
editor.activate_match(index, matches, window, cx); editor.activate_match(index, matches, collapse, window, cx);
}); });
} }
@@ -2968,7 +2929,7 @@ pub fn make_lsp_adapter_delegate(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use editor::{MultiBufferOffset, SelectionEffects}; use editor::SelectionEffects;
use fs::FakeFs; use fs::FakeFs;
use gpui::{App, TestAppContext, VisualTestContext}; use gpui::{App, TestAppContext, VisualTestContext};
use indoc::indoc; use indoc::indoc;
@@ -3174,16 +3135,15 @@ mod tests {
text_thread: &Entity<TextThread>, text_thread: &Entity<TextThread>,
message_ix: usize, message_ix: usize,
cx: &mut TestAppContext, cx: &mut TestAppContext,
) -> Range<MultiBufferOffset> { ) -> Range<usize> {
let range = text_thread.update(cx, |text_thread, cx| { text_thread.update(cx, |text_thread, cx| {
text_thread text_thread
.messages(cx) .messages(cx)
.nth(message_ix) .nth(message_ix)
.unwrap() .unwrap()
.anchor_range .anchor_range
.to_offset(&text_thread.buffer().read(cx).snapshot()) .to_offset(&text_thread.buffer().read(cx).snapshot())
}); })
MultiBufferOffset(range.start)..MultiBufferOffset(range.end)
} }
fn assert_copy_paste_text_thread_editor<T: editor::ToOffset>( fn assert_copy_paste_text_thread_editor<T: editor::ToOffset>(
@@ -3263,7 +3223,11 @@ mod tests {
prompt_store::init(cx); prompt_store::init(cx);
LanguageModelRegistry::test(cx); LanguageModelRegistry::test(cx);
cx.set_global(settings_store); cx.set_global(settings_store);
language::init(cx);
agent_settings::init(cx);
Project::init_settings(cx);
theme::init(theme::LoadThemes::JustBase, cx); theme::init(theme::LoadThemes::JustBase, cx);
workspace::init_settings(cx);
editor::init_settings(cx);
} }
} }

View File

@@ -4,7 +4,6 @@ mod burn_mode_tooltip;
mod claude_code_onboarding_modal; mod claude_code_onboarding_modal;
mod context_pill; mod context_pill;
mod end_trial_upsell; mod end_trial_upsell;
mod hold_for_default;
mod onboarding_modal; mod onboarding_modal;
mod unavailable_editing_tooltip; mod unavailable_editing_tooltip;
mod usage_callout; mod usage_callout;
@@ -15,7 +14,6 @@ pub use burn_mode_tooltip::*;
pub use claude_code_onboarding_modal::*; pub use claude_code_onboarding_modal::*;
pub use context_pill::*; pub use context_pill::*;
pub use end_trial_upsell::*; pub use end_trial_upsell::*;
pub use hold_for_default::*;
pub use onboarding_modal::*; pub use onboarding_modal::*;
pub use unavailable_editing_tooltip::*; pub use unavailable_editing_tooltip::*;
pub use usage_callout::*; pub use usage_callout::*;

Some files were not shown because too many files have changed in this diff Show More