Compare commits

..

1 Commits

Author SHA1 Message Date
Antonio Scandurra
55e4589739 WIP
We can parse `actions` but we need to enable them in rust-analyzer via

`rust-analyzer.hoverActions.enable`
2025-11-11 16:39:44 +00:00
685 changed files with 27413 additions and 38717 deletions

59
.github/ISSUE_TEMPLATE/01_bug_ai.yml vendored Normal file
View File

@@ -0,0 +1,59 @@
name: Bug Report (AI)
description: Zed Agent Panel Bugs
type: "Bug"
labels: ["ai"]
title: "AI: <a short description of the AI Related bug>"
body:
- type: textarea
attributes:
label: Summary
description: Describe the bug with a one line summary, and provide detailed reproduction steps
value: |
<!-- Please insert a one line summary of the issue below -->
SUMMARY_SENTENCE_HERE
### Description
<!-- Describe with sufficient detail to reproduce from a clean Zed install. -->
Steps to trigger the problem:
1.
2.
3.
**Expected Behavior**:
**Actual Behavior**:
### Model Provider Details
- Provider: (Anthropic via ZedPro, Anthropic via API key, Copilot Chat, Mistral, OpenAI, etc)
- Model Name:
- Mode: (Agent Panel, Inline Assistant, Terminal Assistant or Text Threads)
- Other Details (MCPs, other settings, etc):
validations:
required: true
- type: textarea
id: environment
attributes:
label: Zed Version and System Specs
description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"'
placeholder: |
Output of "zed: copy system specs into clipboard"
validations:
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

@@ -0,0 +1,53 @@
name: Bug Report (Debugger)
description: Zed Debugger-Related Bugs
type: "Bug"
labels: ["debugger"]
title: "Debugger: <a short description of the Debugger bug>"
body:
- type: textarea
attributes:
label: Summary
description: Describe the bug with a one line summary, and provide detailed reproduction steps
value: |
<!-- Please insert a one line summary of the issue below -->
SUMMARY_SENTENCE_HERE
### Description
<!-- Describe with sufficient detail to reproduce from a clean Zed install. -->
Steps to trigger the problem:
1.
2.
3.
**Expected Behavior**:
**Actual Behavior**:
validations:
required: true
- type: textarea
id: environment
attributes:
label: Zed Version and System Specs
description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"'
placeholder: |
Output of "zed: copy system specs into clipboard"
validations:
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

53
.github/ISSUE_TEMPLATE/06_bug_git.yml vendored Normal file
View File

@@ -0,0 +1,53 @@
name: Bug Report (Git)
description: Zed Git Related Bugs
type: "Bug"
labels: ["git"]
title: "Git: <a short description of the Git bug>"
body:
- type: textarea
attributes:
label: Summary
description: Describe the bug with a one-line summary, and provide detailed reproduction steps
value: |
<!-- Please insert a one-line summary of the issue below -->
SUMMARY_SENTENCE_HERE
### Description
<!-- Describe with sufficient detail to reproduce from a clean Zed install. -->
Steps to trigger the problem:
1.
2.
3.
**Expected Behavior**:
**Actual Behavior**:
validations:
required: true
- type: textarea
id: environment
attributes:
label: Zed Version and System Specs
description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"'
placeholder: |
Output of "zed: copy system specs into clipboard"
validations:
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

@@ -0,0 +1,53 @@
name: Bug Report (Windows)
description: Zed Windows Related Bugs
type: "Bug"
labels: ["windows"]
title: "Windows: <a short description of the Windows bug>"
body:
- type: textarea
attributes:
label: Summary
description: Describe the bug with a one-line summary, and provide detailed reproduction steps
value: |
<!-- Please insert a one-line summary of the issue below -->
SUMMARY_SENTENCE_HERE
### Description
<!-- Describe with sufficient detail to reproduce from a clean Zed install. -->
Steps to trigger the problem:
1.
2.
3.
**Expected Behavior**:
**Actual Behavior**:
validations:
required: true
- type: textarea
id: environment
attributes:
label: Zed Version and System Specs
description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"'
placeholder: |
Output of "zed: copy system specs into clipboard"
validations:
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,70 +0,0 @@
name: Report an issue
description: Report an issue with Zed.
type: Bug
body:
- type: markdown
attributes:
value: |
Feature requests should be opened in [discussions](https://github.com/zed-industries/zed/discussions/new/choose).
Before opening a new issue, please do a [search](https://github.com/zed-industries/zed/issues) of existing issues and :+1: upvote the existing issue instead. This will help us maintain a proper signal-to-noise ratio.
If you need help with your own project, you can ask a question in our [Discord Support Forums](https://discord.com/invite/zedindustries).
- type: textarea
attributes:
label: Reproduction steps
description: A step-by-step description of how to reproduce the issue from a **clean Zed install**. Any code must be sufficient to reproduce (make sure to include context!). Include code as text, not just as a screenshot. **Issues with insufficient detail may be summarily closed**.
placeholder: |
1. Start Zed
2. Click X
3. Y will happen
validations:
required: true
- type: textarea
attributes:
label: Current vs. Expected behavior
description: |
A clear and concise description of what is the current behavior (screenshots, videos), vs. what you expected the behavior to be.
**Skipping this/failure to provide complete information will result in the issue being closed.**
placeholder: "Based on my reproduction steps above, when I click X, I expect this to happen, but instead Y happens."
validations:
required: true
- type: textarea
attributes:
label: If applicable, attach your Zed log file to this issue.
description: |
Open the command palette in Zed, then type `zed: open log` to see the last 1000 lines. Or type `zed: reveal log in file manager` in the command palette to reveal the log file itself.
value: |
<details><summary>Zed.log</summary>
<!-- Paste your log inside the code block. -->
```log
```
</details>
validations:
required: false
- type: textarea
attributes:
label: If applicable, provide details about your model provider
placeholder: |
- Provider: (Anthropic via ZedPro, Anthropic via API key, Copilot Chat, Mistral, OpenAI, etc.)
- Model Name: (Claude Sonnet 4.5, Gemini 3 Pro, GPT-5)
- Mode: (Agent Panel, Inline Assistant, Terminal Assistant or Text Threads)
- Other details (ACPs, MCPs, other settings, etc.):
validations:
required: false
- type: textarea
attributes:
label: Zed version and system specs
description: |
Open the command palette in Zed, then type “zed: copy system specs into clipboard”. **Skipping this/failure to provide complete information will result in the issue being closed**.
placeholder: |
Zed: v0.215.0 (Zed Nightly bfe141ea79aa4984028934067ba75c48d99136ae)
OS: macOS 15.1
Memory: 36 GiB
Architecture: aarch64
validations:
required: true

View File

@@ -0,0 +1,75 @@
name: Bug Report (Other)
description: |
Something else is broken in Zed (exclude crashing).
type: "Bug"
body:
- type: textarea
attributes:
label: Summary
description: Provide a one sentence summary and detailed reproduction steps
value: |
<!-- Begin your issue with a one sentence summary -->
SUMMARY_SENTENCE_HERE
### Description
<!-- Describe with sufficient detail to reproduce from a clean Zed install.
- Any code must be sufficient to reproduce (include context!)
- Include code as text, not just as a screenshot.
- Issues with insufficient detail may be summarily closed.
-->
DESCRIPTION_HERE
Steps to reproduce:
1.
2.
3.
4.
**Expected Behavior**:
**Actual Behavior**:
<!-- Before Submitting, did you:
1. Include settings.json, keymap.json, .editorconfig if relevant?
2. Check your Zed.log for relevant errors? (please include!)
3. Click Preview to ensure everything looks right?
4. Hide videos, large images and logs in ``` inside collapsible blocks:
<details><summary>click to expand</summary>
```json
```
</details>
-->
validations:
required: true
- type: textarea
id: environment
attributes:
label: Zed Version and System Specs
description: |
Open Zed, from the command palette select "zed: copy system specs into clipboard"
placeholder: |
Output of "zed: copy system specs into clipboard"
validations:
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

@@ -0,0 +1,50 @@
name: Crash Report
description: Zed is Crashing or Hanging
type: "Crash"
body:
- type: textarea
attributes:
label: Summary
description: Summarize the issue with detailed reproduction steps
value: |
<!-- Begin your issue with a one sentence summary -->
SUMMARY_SENTENCE_HERE
### Description
<!-- Include all steps necessary to reproduce from a clean Zed installation. Be verbose -->
Steps to trigger the problem:
1.
2.
3.
Actual Behavior:
Expected Behavior:
validations:
required: true
- type: textarea
id: environment
attributes:
label: Zed Version and System Specs
description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"'
placeholder: |
Output of "zed: copy system specs into clipboard"
validations:
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,52 +0,0 @@
name: Report a crash
description: Zed is crashing or freezing or hanging.
type: Crash
body:
- type: textarea
attributes:
label: Reproduction steps
description: A step-by-step description of how to reproduce the crash from a **clean Zed install**. **Be verbose**. **Issues with insufficient detail may be summarily closed**.
placeholder: |
1. Start Zed
2. Perform an action
3. Zed crashes
validations:
required: true
- type: textarea
attributes:
label: Current vs. Expected behavior
description: |
Go into depth about what actions youre performing in Zed to trigger the crash. If Zed crashes before it loads any windows, make sure to mention that. Again, **be verbose**.
**Skipping this/failure to provide complete information will result in the issue being closed.**
placeholder: "Based on my reproduction steps above, when I perform said action, I expect this to happen, but instead Zed crashes."
validations:
required: true
- type: textarea
attributes:
label: Zed version and system specs
description: |
Open the command palette in Zed, then type “zed: copy system specs into clipboard”. **Skipping this/failure to provide complete information will result in the issue being closed**.
placeholder: |
Zed: v0.215.0 (Zed Nightly bfe141ea79aa4984028934067ba75c48d99136ae)
OS: macOS 15.1
Memory: 36 GiB
Architecture: aarch64
validations:
required: true
- type: textarea
attributes:
label: If applicable, attach your Zed log file to this issue
description: |
Open the command palette in Zed, then type `zed: open log` to see the last 1000 lines. Or type `zed: reveal log in file manager` in the command palette 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,9 +1,9 @@
# yaml-language-server: $schema=https://www.schemastore.org/github-issue-config.json # yaml-language-server: $schema=https://www.schemastore.org/github-issue-config.json
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: Feature request - name: Feature Request
url: https://github.com/zed-industries/zed/discussions/new/choose url: https://github.com/zed-industries/zed/discussions/new/choose
about: To request a feature, open a new discussion under one of the appropriate categories. about: To request a feature, open a new Discussion in one of the appropriate Discussion categories
- name: Our Discord community - name: "Zed Discord"
url: https://discord.com/invite/zedindustries url: https://zed.dev/community-links
about: Join our Discord server for real-time discussion and user support. about: Real-time discussion and user support

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

@@ -7,7 +7,7 @@ on:
- published - published
jobs: jobs:
rebuild_releases_page: rebuild_releases_page:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') if: github.repository_owner == 'zed-industries'
runs-on: namespace-profile-2x4-ubuntu-2404 runs-on: namespace-profile-2x4-ubuntu-2404
steps: steps:
- name: after_release::rebuild_releases_page::refresh_cloud_releases - name: after_release::rebuild_releases_page::refresh_cloud_releases
@@ -21,7 +21,7 @@ jobs:
post_to_discord: post_to_discord:
needs: needs:
- rebuild_releases_page - rebuild_releases_page
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:
- id: get-release-url - id: get-release-url
@@ -56,14 +56,14 @@ jobs:
- id: set-package-name - id: set-package-name
name: after_release::publish_winget::set_package_name name: after_release::publish_winget::set_package_name
run: | run: |
if ("${{ github.event.release.prerelease }}" -eq "true") { if [ "${{ github.event.release.prerelease }}" == "true" ]; then
$PACKAGE_NAME = "ZedIndustries.Zed.Preview" PACKAGE_NAME=ZedIndustries.Zed.Preview
} else { else
$PACKAGE_NAME = "ZedIndustries.Zed" PACKAGE_NAME=ZedIndustries.Zed
} fi
echo "PACKAGE_NAME=$PACKAGE_NAME" >> $env:GITHUB_OUTPUT echo "PACKAGE_NAME=$PACKAGE_NAME" >> "$GITHUB_OUTPUT"
shell: pwsh shell: bash -euxo pipefail {0}
- name: after_release::publish_winget::winget_releaser - name: after_release::publish_winget::winget_releaser
uses: vedantmgoyal9/winget-releaser@19e706d4c9121098010096f9c495a70a7518b30f uses: vedantmgoyal9/winget-releaser@19e706d4c9121098010096f9c495a70a7518b30f
with: with:
@@ -71,7 +71,7 @@ jobs:
max-versions-to-keep: 5 max-versions-to-keep: 5
token: ${{ secrets.WINGET_TOKEN }} token: ${{ secrets.WINGET_TOKEN }}
create_sentry_release: create_sentry_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
@@ -86,19 +86,3 @@ jobs:
SENTRY_ORG: zed-dev SENTRY_ORG: zed-dev
SENTRY_PROJECT: zed SENTRY_PROJECT: zed
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} 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

@@ -13,65 +13,13 @@ jobs:
steps: steps:
- name: Check if author is a community champion and apply label - name: Check if author is a community champion and apply label
uses: actions/github-script@v7 uses: actions/github-script@v7
env:
COMMUNITY_CHAMPIONS: |
0x2CA
5brian
5herlocked
abdelq
afgomez
AidanV
akbxr
AlvaroParker
artemevsevev
bajrangCoder
bcomnes
Be-ing
blopker
bobbymannino
CharlesChen0823
chbk
cppcoffee
davewa
ddoemonn
djsauble
fantacell
findrakecil
gko
huacnlee
imumesh18
jacobtread
jansol
jeffreyguenther
jenslys
jongretar
lemorage
lnay
marcocondrache
marius851000
mikebronner
ognevny
RemcoSmitsDev
romaninsh
Simek
someone13574
sourcefrog
suxiaoshao
Takk8IS
tidely
timvermeulen
valentinegb
versecafe
vitallium
warrenjokinen
ya7010
Zertsov
with: with:
script: | script: |
const communityChampions = process.env.COMMUNITY_CHAMPIONS const communityChampionBody = `${{ secrets.COMMUNITY_CHAMPIONS }}`;
const communityChampions = communityChampionBody
.split('\n') .split('\n')
.map(handle => handle.trim().toLowerCase()) .map(handle => handle.trim().toLowerCase());
.filter(handle => handle.length > 0);
let author; let author;
if (context.eventName === 'issues') { if (context.eventName === 'issues') {

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

@@ -39,7 +39,8 @@ jobs:
run: ./script/download-wasi-sdk run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0} 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
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,11 +29,14 @@ 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}
- name: steps::cargo_nextest - name: steps::cargo_nextest
run: cargo nextest run --workspace --no-fail-fast run: cargo nextest run --workspace --no-fail-fast --failure-output immediate-final
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
- name: steps::cleanup_cargo_config - name: steps::cleanup_cargo_config
if: always() if: always()
@@ -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
@@ -75,12 +78,13 @@ 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}
- name: steps::cargo_nextest - name: steps::cargo_nextest
run: cargo nextest run --workspace --no-fail-fast run: cargo nextest run --workspace --no-fail-fast --failure-output immediate-final
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
- name: steps::cleanup_cargo_config - name: steps::cleanup_cargo_config
if: always() if: always()
@@ -89,7 +93,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,11 +112,14 @@ 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
- name: steps::cargo_nextest - name: steps::cargo_nextest
run: cargo nextest run --workspace --no-fail-fast run: cargo nextest run --workspace --no-fail-fast --failure-output immediate-final
shell: pwsh shell: pwsh
- name: steps::cleanup_cargo_config - name: steps::cleanup_cargo_config
if: always() if: always()
@@ -121,7 +128,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 +157,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
@@ -477,20 +484,6 @@ 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:
needs:
- upload_release_assets
- auto_release_preview
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 }}
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,11 +47,14 @@ 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
- name: steps::cargo_nextest - name: steps::cargo_nextest
run: cargo nextest run --workspace --no-fail-fast run: cargo nextest run --workspace --no-fail-fast --failure-output immediate-final
shell: pwsh shell: pwsh
- name: steps::cleanup_cargo_config - name: steps::cleanup_cargo_config
if: always() if: always()
@@ -361,7 +364,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 +395,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 +437,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 +493,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,9 +6,6 @@ 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 }} MODEL_NAME: ${{ inputs.model_name }}
@@ -51,11 +48,6 @@ jobs:
- 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 --model "${MODEL_NAME}"
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: |

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
@@ -47,7 +47,7 @@ jobs:
} }
check_pattern "run_action_checks" '^\.github/(workflows/|actions/|actionlint.yml)|tooling/xtask|script/' -qP check_pattern "run_action_checks" '^\.github/(workflows/|actions/|actionlint.yml)|tooling/xtask|script/' -qP
check_pattern "run_docs" '^(docs/|crates/.*\.rs)' -qP check_pattern "run_docs" '^docs/' -qP
check_pattern "run_licenses" '^(Cargo.lock|script/.*licenses)' -qP check_pattern "run_licenses" '^(Cargo.lock|script/.*licenses)' -qP
check_pattern "run_nix" '^(nix/|flake\.|Cargo\.|rust-toolchain.toml|\.cargo/config.toml)' -qP check_pattern "run_nix" '^(nix/|flake\.|Cargo\.|rust-toolchain.toml|\.cargo/config.toml)' -qP
check_pattern "run_tests" '^(docs/|script/update_top_ranking_issues/|\.github/(ISSUE_TEMPLATE|workflows/(?!run_tests)))' -qvP check_pattern "run_tests" '^(docs/|script/update_top_ranking_issues/|\.github/(ISSUE_TEMPLATE|workflows/(?!run_tests)))' -qvP
@@ -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,11 +113,14 @@ 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
- name: steps::cargo_nextest - name: steps::cargo_nextest
run: cargo nextest run --workspace --no-fail-fast run: cargo nextest run --workspace --no-fail-fast --failure-output immediate-final
shell: pwsh shell: pwsh
- name: steps::cleanup_cargo_config - name: steps::cleanup_cargo_config
if: always() if: always()
@@ -161,12 +164,13 @@ 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}
- name: steps::cargo_nextest - name: steps::cargo_nextest
run: cargo nextest run --workspace --no-fail-fast run: cargo nextest run --workspace --no-fail-fast --failure-output immediate-final
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
- name: steps::cleanup_cargo_config - name: steps::cleanup_cargo_config
if: always() if: always()
@@ -196,11 +200,14 @@ 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}
- name: steps::cargo_nextest - name: steps::cargo_nextest
run: cargo nextest run --workspace --no-fail-fast run: cargo nextest run --workspace --no-fail-fast --failure-output immediate-final
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
- name: steps::cleanup_cargo_config - name: steps::cleanup_cargo_config
if: always() if: always()
@@ -493,10 +500,7 @@ jobs:
needs: needs:
- orchestrate - orchestrate
if: needs.orchestrate.outputs.run_tests == 'true' if: needs.orchestrate.outputs.run_tests == 'true'
runs-on: namespace-profile-16x32-ubuntu-2204 runs-on: self-mini-macos
env:
GIT_AUTHOR_NAME: Protobuf Action
GIT_AUTHOR_EMAIL: ci@zed.dev
steps: steps:
- name: steps::checkout_repo - name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
@@ -541,7 +545,7 @@ jobs:
- 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

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
@@ -46,7 +37,8 @@ jobs:
run: ./script/download-wasi-sdk run: ./script/download-wasi-sdk
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}
@@ -55,15 +47,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

587
Cargo.lock generated

File diff suppressed because it is too large Load Diff

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",
@@ -147,6 +145,7 @@ members = [
"crates/rules_library", "crates/rules_library",
"crates/schema_generator", "crates/schema_generator",
"crates/search", "crates/search",
"crates/semantic_version",
"crates/session", "crates/session",
"crates/settings", "crates/settings",
"crates/settings_json", "crates/settings_json",
@@ -201,6 +200,7 @@ members = [
"crates/zed_actions", "crates/zed_actions",
"crates/zed_env_vars", "crates/zed_env_vars",
"crates/zeta", "crates/zeta",
"crates/zeta2",
"crates/zeta_cli", "crates/zeta_cli",
"crates/zlog", "crates/zlog",
"crates/zlog_settings", "crates/zlog_settings",
@@ -341,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" }
@@ -379,6 +378,7 @@ rope = { path = "crates/rope" }
rpc = { path = "crates/rpc" } rpc = { path = "crates/rpc" }
rules_library = { path = "crates/rules_library" } rules_library = { path = "crates/rules_library" }
search = { path = "crates/search" } search = { path = "crates/search" }
semantic_version = { path = "crates/semantic_version" }
session = { path = "crates/session" } session = { path = "crates/session" }
settings = { path = "crates/settings" } settings = { path = "crates/settings" }
settings_json = { path = "crates/settings_json" } settings_json = { path = "crates/settings_json" }
@@ -432,6 +432,7 @@ zed = { path = "crates/zed" }
zed_actions = { path = "crates/zed_actions" } zed_actions = { path = "crates/zed_actions" }
zed_env_vars = { path = "crates/zed_env_vars" } zed_env_vars = { path = "crates/zed_env_vars" }
zeta = { path = "crates/zeta" } zeta = { path = "crates/zeta" }
zeta2 = { path = "crates/zeta2" }
zlog = { path = "crates/zlog" } zlog = { path = "crates/zlog" }
zlog_settings = { path = "crates/zlog_settings" } zlog_settings = { path = "crates/zlog_settings" }
@@ -457,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",
@@ -474,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"
@@ -482,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"
@@ -504,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"
@@ -531,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"
@@ -545,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"
@@ -603,6 +603,7 @@ pulldown-cmark = { version = "0.12.0", default-features = false }
quote = "1.0.9" quote = "1.0.9"
rand = "0.9" rand = "0.9"
rayon = "1.8" rayon = "1.8"
ref-cast = "1.0.24"
regex = "1.5" regex = "1.5"
# WARNING: If you change this, you must also publish a new version of zed-reqwest to crates.io # WARNING: If you change this, you must also publish a new version of zed-reqwest to crates.io
reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "c15662463bda39148ba154100dd44d3fba5873a4", default-features = false, features = [ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "c15662463bda39148ba154100dd44d3fba5873a4", default-features = false, features = [
@@ -615,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"
@@ -625,9 +626,8 @@ rustls-platform-verifier = "0.5.0"
# WARNING: If you change this, you must also publish a new version of zed-scap to crates.io # WARNING: If you change this, you must also publish a new version of zed-scap to crates.io
scap = { git = "https://github.com/zed-industries/scap", rev = "4afea48c3b002197176fb19cd0f9b180dd36eaac", default-features = false, package = "zed-scap", version = "0.0.8-zed" } scap = { git = "https://github.com/zed-industries/scap", rev = "4afea48c3b002197176fb19cd0f9b180dd36eaac", default-features = false, package = "zed-scap", version = "0.0.8-zed" }
schemars = { version = "1.0", features = ["indexmap2"] } schemars = { version = "1.0", features = ["indexmap2"] }
semver = { version = "1.0", features = ["serde"] } 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",
@@ -636,6 +636,7 @@ serde_json_lenient = { version = "0.2", features = [
serde_path_to_error = "0.1.17" serde_path_to_error = "0.1.17"
serde_repr = "0.1" serde_repr = "0.1"
serde_urlencoded = "0.7" serde_urlencoded = "0.7"
serde_with = "3.4.0"
sha2 = "0.10" sha2 = "0.10"
shellexpand = "2.1.0" shellexpand = "2.1.0"
shlex = "1.3.0" shlex = "1.3.0"
@@ -713,7 +714,6 @@ wasmtime = { version = "29", default-features = false, features = [
"parallel-compilation", "parallel-compilation",
] } ] }
wasmtime-wasi = "29" wasmtime-wasi = "29"
wax = "0.6"
which = "6.0.0" which = "6.0.0"
windows-core = "0.61" windows-core = "0.61"
wit-component = "0.221" wit-component = "0.221"
@@ -721,7 +721,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 = [
@@ -777,7 +776,6 @@ features = [
notify = { git = "https://github.com/zed-industries/notify.git", rev = "b4588b2e5aee68f4c0e100f140e808cbce7b1419" } notify = { git = "https://github.com/zed-industries/notify.git", rev = "b4588b2e5aee68f4c0e100f140e808cbce7b1419" }
notify-types = { git = "https://github.com/zed-industries/notify.git", rev = "b4588b2e5aee68f4c0e100f140e808cbce7b1419" } notify-types = { git = "https://github.com/zed-industries/notify.git", rev = "b4588b2e5aee68f4c0e100f140e808cbce7b1419" }
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"
@@ -791,19 +789,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 }
@@ -841,9 +826,11 @@ refineable = { codegen-units = 1 }
release_channel = { codegen-units = 1 } release_channel = { codegen-units = 1 }
reqwest_client = { codegen-units = 1 } reqwest_client = { codegen-units = 1 }
rich_text = { codegen-units = 1 } rich_text = { codegen-units = 1 }
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 }
@@ -866,10 +853,6 @@ debug = "full"
lto = false lto = false
codegen-units = 16 codegen-units = 16
[profile.profiling]
inherits = "release"
debug = "full"
[workspace.lints.rust] [workspace.lints.rust]
unexpected_cfgs = { level = "allow" } unexpected_cfgs = { level = "allow" }

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

@@ -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,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"
} }
}, },
{ {
@@ -239,11 +240,13 @@
"ctrl-alt-l": "agent::OpenRulesLibrary", "ctrl-alt-l": "agent::OpenRulesLibrary",
"ctrl-i": "agent::ToggleProfileSelector", "ctrl-i": "agent::ToggleProfileSelector",
"ctrl-alt-/": "agent::ToggleModelSelector", "ctrl-alt-/": "agent::ToggleModelSelector",
"ctrl-shift-a": "agent::ToggleContextPicker",
"ctrl-shift-j": "agent::ToggleNavigationMenu", "ctrl-shift-j": "agent::ToggleNavigationMenu",
"ctrl-alt-i": "agent::ToggleOptionsMenu", "ctrl-alt-i": "agent::ToggleOptionsMenu",
"ctrl-alt-shift-n": "agent::ToggleNewThreadMenu", "ctrl-alt-shift-n": "agent::ToggleNewThreadMenu",
"shift-alt-escape": "agent::ExpandMessageEditor", "shift-alt-escape": "agent::ExpandMessageEditor",
"ctrl->": "agent::AddSelectionToThread", "ctrl->": "agent::AddSelectionToThread",
"ctrl-alt-e": "agent::RemoveAllContext",
"ctrl-shift-e": "project_panel::ToggleFocus", "ctrl-shift-e": "project_panel::ToggleFocus",
"ctrl-shift-enter": "agent::ContinueThread", "ctrl-shift-enter": "agent::ContinueThread",
"super-ctrl-b": "agent::ToggleBurnMode", "super-ctrl-b": "agent::ToggleBurnMode",
@@ -320,6 +323,17 @@
"alt-enter": "editor::Newline" "alt-enter": "editor::Newline"
} }
}, },
{
"context": "ContextStrip",
"bindings": {
"up": "agent::FocusUp",
"right": "agent::FocusRight",
"left": "agent::FocusLeft",
"down": "agent::FocusDown",
"backspace": "agent::RemoveFocusedContext",
"enter": "agent::AcceptSuggestedContext"
}
},
{ {
"context": "AcpThread > ModeSelector", "context": "AcpThread > ModeSelector",
"bindings": { "bindings": {
@@ -811,7 +825,8 @@
"context": "PromptEditor", "context": "PromptEditor",
"bindings": { "bindings": {
"ctrl-[": "agent::CyclePreviousInlineAssist", "ctrl-[": "agent::CyclePreviousInlineAssist",
"ctrl-]": "agent::CycleNextInlineAssist" "ctrl-]": "agent::CycleNextInlineAssist",
"ctrl-alt-e": "agent::RemoveAllContext"
} }
}, },
{ {
@@ -851,7 +866,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",
@@ -1237,25 +1251,11 @@
"context": "Onboarding", "context": "Onboarding",
"use_key_equivalents": true, "use_key_equivalents": true,
"bindings": { "bindings": {
"ctrl-=": ["zed::IncreaseUiFontSize", { "persist": false }],
"ctrl-+": ["zed::IncreaseUiFontSize", { "persist": false }],
"ctrl--": ["zed::DecreaseUiFontSize", { "persist": false }],
"ctrl-0": ["zed::ResetUiFontSize", { "persist": false }],
"ctrl-enter": "onboarding::Finish", "ctrl-enter": "onboarding::Finish",
"alt-shift-l": "onboarding::SignIn", "alt-shift-l": "onboarding::SignIn",
"alt-shift-a": "onboarding::OpenAccount" "alt-shift-a": "onboarding::OpenAccount"
} }
}, },
{
"context": "Welcome",
"use_key_equivalents": true,
"bindings": {
"ctrl-=": ["zed::IncreaseUiFontSize", { "persist": false }],
"ctrl-+": ["zed::IncreaseUiFontSize", { "persist": false }],
"ctrl--": ["zed::DecreaseUiFontSize", { "persist": false }],
"ctrl-0": ["zed::ResetUiFontSize", { "persist": false }]
}
},
{ {
"context": "InvalidBuffer", "context": "InvalidBuffer",
"use_key_equivalents": true, "use_key_equivalents": true,

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"
} }
}, },
{ {
@@ -278,11 +279,13 @@
"cmd-alt-p": "agent::ManageProfiles", "cmd-alt-p": "agent::ManageProfiles",
"cmd-i": "agent::ToggleProfileSelector", "cmd-i": "agent::ToggleProfileSelector",
"cmd-alt-/": "agent::ToggleModelSelector", "cmd-alt-/": "agent::ToggleModelSelector",
"cmd-shift-a": "agent::ToggleContextPicker",
"cmd-shift-j": "agent::ToggleNavigationMenu", "cmd-shift-j": "agent::ToggleNavigationMenu",
"cmd-alt-m": "agent::ToggleOptionsMenu", "cmd-alt-m": "agent::ToggleOptionsMenu",
"cmd-alt-shift-n": "agent::ToggleNewThreadMenu", "cmd-alt-shift-n": "agent::ToggleNewThreadMenu",
"shift-alt-escape": "agent::ExpandMessageEditor", "shift-alt-escape": "agent::ExpandMessageEditor",
"cmd->": "agent::AddSelectionToThread", "cmd->": "agent::AddSelectionToThread",
"cmd-alt-e": "agent::RemoveAllContext",
"cmd-shift-e": "project_panel::ToggleFocus", "cmd-shift-e": "project_panel::ToggleFocus",
"cmd-ctrl-b": "agent::ToggleBurnMode", "cmd-ctrl-b": "agent::ToggleBurnMode",
"cmd-shift-enter": "agent::ContinueThread", "cmd-shift-enter": "agent::ContinueThread",
@@ -310,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"
} }
}, },
{ {
@@ -363,6 +366,18 @@
"alt-enter": "editor::Newline" "alt-enter": "editor::Newline"
} }
}, },
{
"context": "ContextStrip",
"use_key_equivalents": true,
"bindings": {
"up": "agent::FocusUp",
"right": "agent::FocusRight",
"left": "agent::FocusLeft",
"down": "agent::FocusDown",
"backspace": "agent::RemoveFocusedContext",
"enter": "agent::AcceptSuggestedContext"
}
},
{ {
"context": "AgentConfiguration", "context": "AgentConfiguration",
"bindings": { "bindings": {
@@ -875,7 +890,9 @@
"context": "PromptEditor", "context": "PromptEditor",
"use_key_equivalents": true, "use_key_equivalents": true,
"bindings": { "bindings": {
"cmd-shift-a": "agent::ToggleContextPicker",
"cmd-alt-/": "agent::ToggleModelSelector", "cmd-alt-/": "agent::ToggleModelSelector",
"cmd-alt-e": "agent::RemoveAllContext",
"ctrl-[": "agent::CyclePreviousInlineAssist", "ctrl-[": "agent::CyclePreviousInlineAssist",
"ctrl-]": "agent::CycleNextInlineAssist" "ctrl-]": "agent::CycleNextInlineAssist"
} }
@@ -919,7 +936,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",
@@ -1218,23 +1234,23 @@
} }
}, },
{ {
"context": "RatePredictionsModal", "context": "RateCompletionModal",
"use_key_equivalents": true, "use_key_equivalents": true,
"bindings": { "bindings": {
"cmd-shift-enter": "zeta::ThumbsUpActivePrediction", "cmd-shift-enter": "zeta::ThumbsUpActiveCompletion",
"cmd-shift-backspace": "zeta::ThumbsDownActivePrediction", "cmd-shift-backspace": "zeta::ThumbsDownActiveCompletion",
"shift-down": "zeta::NextEdit", "shift-down": "zeta::NextEdit",
"shift-up": "zeta::PreviousEdit", "shift-up": "zeta::PreviousEdit",
"right": "zeta::PreviewPrediction" "right": "zeta::PreviewCompletion"
} }
}, },
{ {
"context": "RatePredictionsModal > Editor", "context": "RateCompletionModal > Editor",
"use_key_equivalents": true, "use_key_equivalents": true,
"bindings": { "bindings": {
"escape": "zeta::FocusPredictions", "escape": "zeta::FocusCompletions",
"cmd-shift-enter": "zeta::ThumbsUpActivePrediction", "cmd-shift-enter": "zeta::ThumbsUpActiveCompletion",
"cmd-shift-backspace": "zeta::ThumbsDownActivePrediction" "cmd-shift-backspace": "zeta::ThumbsDownActiveCompletion"
} }
}, },
{ {
@@ -1340,25 +1356,11 @@
"context": "Onboarding", "context": "Onboarding",
"use_key_equivalents": true, "use_key_equivalents": true,
"bindings": { "bindings": {
"cmd-=": ["zed::IncreaseUiFontSize", { "persist": false }],
"cmd-+": ["zed::IncreaseUiFontSize", { "persist": false }],
"cmd--": ["zed::DecreaseUiFontSize", { "persist": false }],
"cmd-0": ["zed::ResetUiFontSize", { "persist": false }],
"cmd-enter": "onboarding::Finish", "cmd-enter": "onboarding::Finish",
"alt-tab": "onboarding::SignIn", "alt-tab": "onboarding::SignIn",
"alt-shift-a": "onboarding::OpenAccount" "alt-shift-a": "onboarding::OpenAccount"
} }
}, },
{
"context": "Welcome",
"use_key_equivalents": true,
"bindings": {
"cmd-=": ["zed::IncreaseUiFontSize", { "persist": false }],
"cmd-+": ["zed::IncreaseUiFontSize", { "persist": false }],
"cmd--": ["zed::DecreaseUiFontSize", { "persist": false }],
"cmd-0": ["zed::ResetUiFontSize", { "persist": false }]
}
},
{ {
"context": "InvalidBuffer", "context": "InvalidBuffer",
"use_key_equivalents": true, "use_key_equivalents": true,

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"
} }
}, },
{ {
@@ -240,11 +241,13 @@
"shift-alt-p": "agent::ManageProfiles", "shift-alt-p": "agent::ManageProfiles",
"ctrl-i": "agent::ToggleProfileSelector", "ctrl-i": "agent::ToggleProfileSelector",
"shift-alt-/": "agent::ToggleModelSelector", "shift-alt-/": "agent::ToggleModelSelector",
"ctrl-shift-a": "agent::ToggleContextPicker",
"ctrl-shift-j": "agent::ToggleNavigationMenu", "ctrl-shift-j": "agent::ToggleNavigationMenu",
"ctrl-alt-i": "agent::ToggleOptionsMenu", "ctrl-alt-i": "agent::ToggleOptionsMenu",
// "ctrl-shift-alt-n": "agent::ToggleNewThreadMenu", // "ctrl-shift-alt-n": "agent::ToggleNewThreadMenu",
"shift-alt-escape": "agent::ExpandMessageEditor", "shift-alt-escape": "agent::ExpandMessageEditor",
"ctrl-shift-.": "agent::AddSelectionToThread", "ctrl-shift-.": "agent::AddSelectionToThread",
"shift-alt-e": "agent::RemoveAllContext",
"ctrl-shift-e": "project_panel::ToggleFocus", "ctrl-shift-e": "project_panel::ToggleFocus",
"ctrl-shift-enter": "agent::ContinueThread", "ctrl-shift-enter": "agent::ContinueThread",
"super-ctrl-b": "agent::ToggleBurnMode", "super-ctrl-b": "agent::ToggleBurnMode",
@@ -326,6 +329,18 @@
"alt-enter": "editor::Newline" "alt-enter": "editor::Newline"
} }
}, },
{
"context": "ContextStrip",
"use_key_equivalents": true,
"bindings": {
"up": "agent::FocusUp",
"right": "agent::FocusRight",
"left": "agent::FocusLeft",
"down": "agent::FocusDown",
"backspace": "agent::RemoveFocusedContext",
"enter": "agent::AcceptSuggestedContext"
}
},
{ {
"context": "AcpThread > ModeSelector", "context": "AcpThread > ModeSelector",
"bindings": { "bindings": {
@@ -823,7 +838,8 @@
"use_key_equivalents": true, "use_key_equivalents": true,
"bindings": { "bindings": {
"ctrl-[": "agent::CyclePreviousInlineAssist", "ctrl-[": "agent::CyclePreviousInlineAssist",
"ctrl-]": "agent::CycleNextInlineAssist" "ctrl-]": "agent::CycleNextInlineAssist",
"shift-alt-e": "agent::RemoveAllContext"
} }
}, },
{ {
@@ -864,7 +880,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",
@@ -1270,25 +1285,11 @@
"context": "Onboarding", "context": "Onboarding",
"use_key_equivalents": true, "use_key_equivalents": true,
"bindings": { "bindings": {
"ctrl-=": ["zed::IncreaseUiFontSize", { "persist": false }],
"ctrl-+": ["zed::IncreaseUiFontSize", { "persist": false }],
"ctrl--": ["zed::DecreaseUiFontSize", { "persist": false }],
"ctrl-0": ["zed::ResetUiFontSize", { "persist": false }],
"ctrl-enter": "onboarding::Finish", "ctrl-enter": "onboarding::Finish",
"alt-shift-l": "onboarding::SignIn", "alt-shift-l": "onboarding::SignIn",
"shift-alt-a": "onboarding::OpenAccount" "shift-alt-a": "onboarding::OpenAccount"
} }
}, },
{
"context": "Welcome",
"use_key_equivalents": true,
"bindings": {
"ctrl-=": ["zed::IncreaseUiFontSize", { "persist": false }],
"ctrl-+": ["zed::IncreaseUiFontSize", { "persist": false }],
"ctrl--": ["zed::DecreaseUiFontSize", { "persist": false }],
"ctrl-0": ["zed::ResetUiFontSize", { "persist": false }]
}
},
{ {
"context": "GitWorktreeSelector || (GitWorktreeSelector > Picker > Editor)", "context": "GitWorktreeSelector || (GitWorktreeSelector > Picker > Editor)",
"use_key_equivalents": true, "use_key_equivalents": true,

View File

@@ -1,18 +1,16 @@
[ [
{ {
"bindings": { "bindings": {
"ctrl-alt-s": "zed::OpenSettings", "ctrl-alt-s": "zed::OpenSettingsFile",
"ctrl-{": "pane::ActivatePreviousItem", "ctrl-{": "pane::ActivatePreviousItem",
"ctrl-}": "pane::ActivateNextItem", "ctrl-}": "pane::ActivateNextItem",
"shift-escape": null, // Unmap workspace::zoom "shift-escape": null, // Unmap workspace::zoom
"ctrl-~": "git::Branch",
"ctrl-f2": "debugger::Stop", "ctrl-f2": "debugger::Stop",
"f6": "debugger::Pause", "f6": "debugger::Pause",
"f7": "debugger::StepInto", "f7": "debugger::StepInto",
"f8": "debugger::StepOver", "f8": "debugger::StepOver",
"shift-f8": "debugger::StepOut", "shift-f8": "debugger::StepOut",
"f9": "debugger::Continue", "f9": "debugger::Continue",
"shift-f9": "debugger::Start",
"alt-shift-f9": "debugger::Start" "alt-shift-f9": "debugger::Start"
} }
}, },
@@ -48,7 +46,7 @@
"alt-f7": "editor::FindAllReferences", "alt-f7": "editor::FindAllReferences",
"ctrl-alt-f7": "editor::FindAllReferences", "ctrl-alt-f7": "editor::FindAllReferences",
"ctrl-b": "editor::GoToDefinition", // Conflicts with workspace::ToggleLeftDock "ctrl-b": "editor::GoToDefinition", // Conflicts with workspace::ToggleLeftDock
"ctrl-alt-b": "editor::GoToImplementation", // Conflicts with workspace::ToggleRightDock "ctrl-alt-b": "editor::GoToDefinitionSplit", // Conflicts with workspace::ToggleRightDock
"ctrl-shift-b": "editor::GoToTypeDefinition", "ctrl-shift-b": "editor::GoToTypeDefinition",
"ctrl-alt-shift-b": "editor::GoToTypeDefinitionSplit", "ctrl-alt-shift-b": "editor::GoToTypeDefinitionSplit",
"f2": "editor::GoToDiagnostic", "f2": "editor::GoToDiagnostic",
@@ -72,11 +70,7 @@
"ctrl-r": ["buffer_search::Deploy", { "replace_enabled": true }], "ctrl-r": ["buffer_search::Deploy", { "replace_enabled": true }],
"ctrl-shift-n": "file_finder::Toggle", "ctrl-shift-n": "file_finder::Toggle",
"ctrl-g": "go_to_line::Toggle", "ctrl-g": "go_to_line::Toggle",
"alt-enter": "editor::ToggleCodeActions", "alt-enter": "editor::ToggleCodeActions"
"ctrl-space": "editor::ShowCompletions",
"ctrl-q": "editor::Hover",
"ctrl-p": "editor::ShowSignatureHelp",
"ctrl-\\": "assistant::InlineAssist"
} }
}, },
{ {
@@ -100,13 +94,9 @@
"ctrl-shift-f12": "workspace::ToggleAllDocks", "ctrl-shift-f12": "workspace::ToggleAllDocks",
"ctrl-shift-r": ["pane::DeploySearch", { "replace_enabled": true }], "ctrl-shift-r": ["pane::DeploySearch", { "replace_enabled": true }],
"alt-shift-f10": "task::Spawn", "alt-shift-f10": "task::Spawn",
"shift-f10": "task::Spawn",
"ctrl-f5": "task::Rerun",
"ctrl-e": "file_finder::Toggle", "ctrl-e": "file_finder::Toggle",
"ctrl-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor // "ctrl-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor
"ctrl-shift-n": "file_finder::Toggle", "ctrl-shift-n": "file_finder::Toggle",
"ctrl-n": "project_symbols::Toggle",
"ctrl-alt-n": "file_finder::Toggle",
"ctrl-shift-a": "command_palette::Toggle", "ctrl-shift-a": "command_palette::Toggle",
"shift shift": "command_palette::Toggle", "shift shift": "command_palette::Toggle",
"ctrl-alt-shift-n": "project_symbols::Toggle", "ctrl-alt-shift-n": "project_symbols::Toggle",
@@ -143,9 +133,7 @@
"context": "Pane", "context": "Pane",
"bindings": { "bindings": {
"ctrl-alt-left": "pane::GoBack", "ctrl-alt-left": "pane::GoBack",
"ctrl-alt-right": "pane::GoForward", "ctrl-alt-right": "pane::GoForward"
"alt-left": "pane::ActivatePreviousItem",
"alt-right": "pane::ActivateNextItem"
} }
}, },
{ {
@@ -164,6 +152,8 @@
"bindings": { "bindings": {
"ctrl-shift-t": "workspace::NewTerminal", "ctrl-shift-t": "workspace::NewTerminal",
"alt-f12": "workspace::CloseActiveDock", "alt-f12": "workspace::CloseActiveDock",
"alt-left": "pane::ActivatePreviousItem",
"alt-right": "pane::ActivateNextItem",
"ctrl-up": "terminal::ScrollLineUp", "ctrl-up": "terminal::ScrollLineUp",
"ctrl-down": "terminal::ScrollLineDown", "ctrl-down": "terminal::ScrollLineDown",
"shift-pageup": "terminal::ScrollPageUp", "shift-pageup": "terminal::ScrollPageUp",

View File

@@ -5,14 +5,12 @@
"cmd-}": "pane::ActivateNextItem", "cmd-}": "pane::ActivateNextItem",
"cmd-0": "git_panel::ToggleFocus", // overrides `cmd-0` zoom reset "cmd-0": "git_panel::ToggleFocus", // overrides `cmd-0` zoom reset
"shift-escape": null, // Unmap workspace::zoom "shift-escape": null, // Unmap workspace::zoom
"cmd-~": "git::Branch",
"ctrl-f2": "debugger::Stop", "ctrl-f2": "debugger::Stop",
"f6": "debugger::Pause", "f6": "debugger::Pause",
"f7": "debugger::StepInto", "f7": "debugger::StepInto",
"f8": "debugger::StepOver", "f8": "debugger::StepOver",
"shift-f8": "debugger::StepOut", "shift-f8": "debugger::StepOut",
"f9": "debugger::Continue", "f9": "debugger::Continue",
"shift-f9": "debugger::Start",
"alt-shift-f9": "debugger::Start" "alt-shift-f9": "debugger::Start"
} }
}, },
@@ -47,7 +45,7 @@
"alt-f7": "editor::FindAllReferences", "alt-f7": "editor::FindAllReferences",
"cmd-alt-f7": "editor::FindAllReferences", "cmd-alt-f7": "editor::FindAllReferences",
"cmd-b": "editor::GoToDefinition", // Conflicts with workspace::ToggleLeftDock "cmd-b": "editor::GoToDefinition", // Conflicts with workspace::ToggleLeftDock
"cmd-alt-b": "editor::GoToImplementation", "cmd-alt-b": "editor::GoToDefinitionSplit",
"cmd-shift-b": "editor::GoToTypeDefinition", "cmd-shift-b": "editor::GoToTypeDefinition",
"cmd-alt-shift-b": "editor::GoToTypeDefinitionSplit", "cmd-alt-shift-b": "editor::GoToTypeDefinitionSplit",
"f2": "editor::GoToDiagnostic", "f2": "editor::GoToDiagnostic",
@@ -70,11 +68,7 @@
"cmd-r": ["buffer_search::Deploy", { "replace_enabled": true }], "cmd-r": ["buffer_search::Deploy", { "replace_enabled": true }],
"cmd-shift-o": "file_finder::Toggle", "cmd-shift-o": "file_finder::Toggle",
"cmd-l": "go_to_line::Toggle", "cmd-l": "go_to_line::Toggle",
"alt-enter": "editor::ToggleCodeActions", "alt-enter": "editor::ToggleCodeActions"
"ctrl-space": "editor::ShowCompletions",
"cmd-j": "editor::Hover",
"cmd-p": "editor::ShowSignatureHelp",
"cmd-\\": "assistant::InlineAssist"
} }
}, },
{ {
@@ -102,13 +96,9 @@
"cmd-shift-f12": "workspace::ToggleAllDocks", "cmd-shift-f12": "workspace::ToggleAllDocks",
"cmd-shift-r": ["pane::DeploySearch", { "replace_enabled": true }], "cmd-shift-r": ["pane::DeploySearch", { "replace_enabled": true }],
"ctrl-alt-r": "task::Spawn", "ctrl-alt-r": "task::Spawn",
"shift-f10": "task::Spawn",
"cmd-f5": "task::Rerun",
"cmd-e": "file_finder::Toggle", "cmd-e": "file_finder::Toggle",
"cmd-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor // "cmd-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor
"cmd-shift-o": "file_finder::Toggle", "cmd-shift-o": "file_finder::Toggle",
"cmd-shift-n": "file_finder::Toggle",
"cmd-n": "project_symbols::Toggle",
"cmd-shift-a": "command_palette::Toggle", "cmd-shift-a": "command_palette::Toggle",
"shift shift": "command_palette::Toggle", "shift shift": "command_palette::Toggle",
"cmd-alt-o": "project_symbols::Toggle", // JetBrains: Go to Symbol "cmd-alt-o": "project_symbols::Toggle", // JetBrains: Go to Symbol
@@ -145,9 +135,7 @@
"context": "Pane", "context": "Pane",
"bindings": { "bindings": {
"cmd-alt-left": "pane::GoBack", "cmd-alt-left": "pane::GoBack",
"cmd-alt-right": "pane::GoForward", "cmd-alt-right": "pane::GoForward"
"alt-left": "pane::ActivatePreviousItem",
"alt-right": "pane::ActivateNextItem"
} }
}, },
{ {

View File

@@ -414,9 +414,8 @@
} }
}, },
{ {
"context": "VimControl && vim_mode == helix_normal && !menu", "context": "vim_mode == helix_normal && !menu",
"bindings": { "bindings": {
"escape": "vim::SwitchToHelixNormalMode",
"i": "vim::HelixInsert", "i": "vim::HelixInsert",
"a": "vim::HelixAppend", "a": "vim::HelixAppend",
"ctrl-[": "editor::Cancel" "ctrl-[": "editor::Cancel"
@@ -477,9 +476,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

@@ -175,16 +175,6 @@
// //
// Default: true // Default: true
"zoomed_padding": true, "zoomed_padding": true,
// What draws Zed's window decorations (titlebar):
// 1. Client application (Zed) draws its own window decorations
// "client"
// 2. Display server draws the window decorations. Not supported by GNOME Wayland.
// "server"
//
// This requires restarting Zed for changes to take effect.
//
// Default: "client"
"window_decorations": "client",
// Whether to use the system provided dialogs for Open and Save As. // Whether to use the system provided dialogs for Open and Save As.
// When set to false, Zed will use the built-in keyboard-first pickers. // When set to false, Zed will use the built-in keyboard-first pickers.
"use_system_path_prompts": true, "use_system_path_prompts": true,
@@ -265,12 +255,6 @@
// Whether to display inline and alongside documentation for items in the // Whether to display inline and alongside documentation for items in the
// completions menu // completions menu
"show_completion_documentation": true, "show_completion_documentation": true,
// Whether to colorize brackets in the editor.
// (also known as "rainbow brackets")
//
// The colors that are used for different indentation levels are defined in the theme (theme key: `accents`).
// They can be customized by using theme overrides.
"colorize_brackets": false,
// When to show the scrollbar in the completion menu. // When to show the scrollbar in the completion menu.
// This setting can take four values: // This setting can take four values:
// //
@@ -758,31 +742,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
@@ -1332,10 +1299,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": [
@@ -1441,7 +1405,7 @@
"default_height": 320, "default_height": 320,
// What working directory to use when launching the terminal. // What working directory to use when launching the terminal.
// May take 4 values: // May take 4 values:
// 1. Use the current file's project directory. Fallback to the // 1. Use the current file's project directory. Will Fallback to the
// first project directory strategy if unsuccessful // first project directory strategy if unsuccessful
// "working_directory": "current_project_directory" // "working_directory": "current_project_directory"
// 2. Use the first project in this workspace's directory // 2. Use the first project in this workspace's directory
@@ -1579,8 +1543,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.
@@ -1595,59 +1557,7 @@
// //
// Most terminal themes have APCA values of 40-70. // Most terminal themes have APCA values of 40-70.
// A value of 45 preserves colorful themes while ensuring legibility. // A value of 45 preserves colorful themes while ensuring legibility.
"minimum_contrast": 45, "minimum_contrast": 45
// Regexes used to identify paths for hyperlink navigation. Supports optional named capture
// groups `path`, `line`, `column`, and `link`. If none of these are present, the entire match
// is the hyperlink target. If `path` is present, it is the hyperlink target, along with `line`
// and `column` if present. `link` may be used to customize what text in terminal is part of the
// hyperlink. If `link` is not present, the text of the entire match is used. If `line` and
// `column` are not present, the default built-in line and column suffix processing is used
// which parses `line:column` and `(line,column)` variants. The default value handles Python
// diagnostics and common path, line, column syntaxes. This can be extended or replaced to
// handle specific scenarios. For example, to enable support for hyperlinking paths which
// contain spaces in rust output,
//
// [
// "\\s+(-->|:::|at) (?<link>(?<path>.+?))(:$|$)",
// "\\s+(Compiling|Checking|Documenting) [^(]+\\((?<link>(?<path>.+))\\)"
// ],
//
// could be used. Processing stops at the first regex with a match, even if no link is
// produced which is the case when the cursor is not over the hyperlinked text. For best
// performance it is recommended to order regexes from most common to least common. For
// readability and documentation, each regex may be an array of strings which are collected
// into one multi-line regex string for use in terminal path hyperlink detection.
"path_hyperlink_regexes": [
// Python-style diagnostics
"File \"(?<path>[^\"]+)\", line (?<line>[0-9]+)",
// Common path syntax with optional line, column, description, trailing punctuation, or
// surrounding symbols or quotes
[
"(?x)",
"# optionally starts with 0-2 opening prefix symbols",
"[({\\[<]{0,2}",
"# which may be followed by an opening quote",
"(?<quote>[\"'`])?",
"# `path` is the shortest sequence of any non-space character",
"(?<link>(?<path>[^ ]+?",
" # which may end with a line and optionally a column,",
" (?<line_column>:+[0-9]+(:[0-9]+)?|:?\\([0-9]+([,:][0-9]+)?\\))?",
"))",
"# which must be followed by a matching quote",
"(?(<quote>)\\k<quote>)",
"# and optionally a single closing symbol",
"[)}\\]>]?",
"# if line/column matched, may be followed by a description",
"(?(<line_column>):[^ 0-9][^ ]*)?",
"# which may be followed by trailing punctuation",
"[.,:)}\\]>]*",
"# and always includes trailing whitespace or end of line",
"([ ]+|$)"
]
],
// Timeout for hover and Cmd-click path hyperlink discovery in milliseconds. Specifying a
// timeout of `0` will disable path hyperlinking in terminal.
"path_hyperlink_timeout_ms": 1
}, },
"code_actions_on_format": {}, "code_actions_on_format": {},
// Settings related to running tasks. // Settings related to running tasks.
@@ -1889,7 +1799,7 @@
} }
}, },
"PHP": { "PHP": {
"language_servers": ["phpactor", "!intelephense", "!phptools", "..."], "language_servers": ["phpactor", "!intelephense", "..."],
"prettier": { "prettier": {
"allowed": true, "allowed": true,
"plugins": ["@prettier/plugin-php"], "plugins": ["@prettier/plugin-php"],
@@ -2130,18 +2040,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", "!phptools", "..."]
}
}
},
// 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

@@ -1866,14 +1866,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
@@ -1898,25 +1894,9 @@ impl AcpThread {
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().update(cx, |action_log, cx| {
action_log.reject_all_edits(Some(telemetry), cx) action_log.reject_all_edits(Some(telemetry), cx)
@@ -3823,314 +3803,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

@@ -197,11 +197,6 @@ pub trait AgentModelSelector: 'static {
fn watch(&self, _cx: &mut App) -> Option<watch::Receiver<()>> { fn watch(&self, _cx: &mut App) -> Option<watch::Receiver<()>> {
None None
} }
/// Returns whether the model picker should render a footer.
fn should_render_footer(&self) -> bool {
false
}
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]

View File

@@ -23,7 +23,6 @@ gpui.workspace = true
language.workspace = true language.workspace = true
project.workspace = true project.workspace = true
proto.workspace = true proto.workspace = true
semver.workspace = true
smallvec.workspace = true smallvec.workspace = true
ui.workspace = true ui.workspace = true
util.workspace = true util.workspace = true

View File

@@ -925,15 +925,15 @@ impl StatusItemView for ActivityIndicator {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use gpui::SemanticVersion;
use release_channel::AppCommitSha; use release_channel::AppCommitSha;
use semver::Version;
use super::*; use super::*;
#[test] #[test]
fn test_version_tooltip_message() { fn test_version_tooltip_message() {
let message = ActivityIndicator::version_tooltip_message(&VersionCheckType::Semantic( let message = ActivityIndicator::version_tooltip_message(&VersionCheckType::Semantic(
Version::new(1, 0, 0), SemanticVersion::new(1, 0, 0),
)); ));
assert_eq!(message, "Version: 1.0.0"); assert_eq!(message, "Version: 1.0.0");

View File

@@ -133,7 +133,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() {
@@ -961,10 +963,6 @@ impl acp_thread::AgentModelSelector for NativeAgentModelSelector {
fn watch(&self, cx: &mut App) -> Option<watch::Receiver<()>> { fn watch(&self, cx: &mut App) -> Option<watch::Receiver<()>> {
Some(self.connection.0.read(cx).models.watch()) Some(self.connection.0.read(cx).models.watch())
} }
fn should_render_footer(&self) -> bool {
true
}
} }
impl acp_thread::AgentConnection for NativeAgentConnection { impl acp_thread::AgentConnection for NativeAgentConnection {

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,
}, },
)); ));
} }
@@ -182,7 +181,6 @@ impl DbThread {
crate::Message::Agent(AgentMessage { crate::Message::Agent(AgentMessage {
content, content,
tool_results, tool_results,
reasoning_details: None,
}) })
} }
language_model::Role::System => { language_model::Role::System => {

View File

@@ -703,7 +703,6 @@ impl EditAgent {
role: Role::User, role: Role::User,
content: vec![MessageContent::Text(prompt)], content: vec![MessageContent::Text(prompt)],
cache: false, cache: false,
reasoning_details: None,
}); });
// Include tools in the request so that we can take advantage of // Include tools in the request so that we can take advantage of

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

@@ -1081,7 +1081,6 @@ fn message(
role, role,
content: contents.into_iter().collect(), content: contents.into_iter().collect(),
cache: false, cache: false,
reasoning_details: None,
} }
} }
@@ -1109,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,
}) })
} }
@@ -1269,7 +1267,6 @@ impl EvalAssertion {
role: Role::User, role: Role::User,
content: vec![prompt.into()], content: vec![prompt.into()],
cache: false, cache: false,
reasoning_details: None,
}], }],
thinking_allowed: true, thinking_allowed: true,
..Default::default() ..Default::default()
@@ -1596,7 +1593,6 @@ impl EditAgentTest {
role: Role::System, role: Role::System,
content: vec![MessageContent::Text(system_prompt)], content: vec![MessageContent::Text(system_prompt)],
cache: true, cache: true,
reasoning_details: None,
}] }]
.into_iter() .into_iter()
.chain(eval.conversation) .chain(eval.conversation)

View File

@@ -44,25 +44,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(snapshot.as_rope().floor_char_boundary(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 +140,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 = "".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("⚡⚡⚡⚡⚡⚡⚡"),
"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

@@ -215,8 +215,7 @@ async fn test_prompt_caching(cx: &mut TestAppContext) {
vec![LanguageModelRequestMessage { vec![LanguageModelRequestMessage {
role: Role::User, role: Role::User,
content: vec!["Message 1".into()], content: vec!["Message 1".into()],
cache: true, cache: true
reasoning_details: None,
}] }]
); );
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::Text( fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::Text(
@@ -240,20 +239,17 @@ async fn test_prompt_caching(cx: &mut TestAppContext) {
LanguageModelRequestMessage { LanguageModelRequestMessage {
role: Role::User, role: Role::User,
content: vec!["Message 1".into()], content: vec!["Message 1".into()],
cache: false, cache: false
reasoning_details: None,
}, },
LanguageModelRequestMessage { LanguageModelRequestMessage {
role: Role::Assistant, role: Role::Assistant,
content: vec!["Response to Message 1".into()], content: vec!["Response to Message 1".into()],
cache: false, cache: false
reasoning_details: None,
}, },
LanguageModelRequestMessage { LanguageModelRequestMessage {
role: Role::User, role: Role::User,
content: vec!["Message 2".into()], content: vec!["Message 2".into()],
cache: true, cache: true
reasoning_details: None,
} }
] ]
); );
@@ -278,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()));
@@ -299,44 +294,37 @@ async fn test_prompt_caching(cx: &mut TestAppContext) {
LanguageModelRequestMessage { LanguageModelRequestMessage {
role: Role::User, role: Role::User,
content: vec!["Message 1".into()], content: vec!["Message 1".into()],
cache: false, cache: false
reasoning_details: None,
}, },
LanguageModelRequestMessage { LanguageModelRequestMessage {
role: Role::Assistant, role: Role::Assistant,
content: vec!["Response to Message 1".into()], content: vec!["Response to Message 1".into()],
cache: false, cache: false
reasoning_details: None,
}, },
LanguageModelRequestMessage { LanguageModelRequestMessage {
role: Role::User, role: Role::User,
content: vec!["Message 2".into()], content: vec!["Message 2".into()],
cache: false, cache: false
reasoning_details: None,
}, },
LanguageModelRequestMessage { LanguageModelRequestMessage {
role: Role::Assistant, role: Role::Assistant,
content: vec!["Response to Message 2".into()], content: vec!["Response to Message 2".into()],
cache: false, cache: false
reasoning_details: None,
}, },
LanguageModelRequestMessage { LanguageModelRequestMessage {
role: Role::User, role: Role::User,
content: vec!["Use the echo tool".into()], content: vec!["Use the echo tool".into()],
cache: false, cache: false
reasoning_details: None,
}, },
LanguageModelRequestMessage { LanguageModelRequestMessage {
role: Role::Assistant, role: Role::Assistant,
content: vec![MessageContent::ToolUse(tool_use)], content: vec![MessageContent::ToolUse(tool_use)],
cache: false, cache: false
reasoning_details: None,
}, },
LanguageModelRequestMessage { LanguageModelRequestMessage {
role: Role::User, role: Role::User,
content: vec![MessageContent::ToolResult(tool_result)], content: vec![MessageContent::ToolResult(tool_result)],
cache: true, cache: true
reasoning_details: None,
} }
] ]
); );
@@ -473,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(
@@ -483,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();
@@ -534,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();
@@ -569,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();
@@ -608,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();
@@ -638,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()));
@@ -659,26 +641,25 @@ async fn test_resume_after_tool_use_limit(cx: &mut TestAppContext) {
LanguageModelRequestMessage { LanguageModelRequestMessage {
role: Role::User, role: Role::User,
content: vec!["abc".into()], content: vec!["abc".into()],
cache: false, cache: false
reasoning_details: None,
}, },
LanguageModelRequestMessage { LanguageModelRequestMessage {
role: Role::Assistant, role: Role::Assistant,
content: vec![MessageContent::ToolUse(tool_use.clone())], content: vec![MessageContent::ToolUse(tool_use.clone())],
cache: false, cache: false
reasoning_details: None,
}, },
LanguageModelRequestMessage { LanguageModelRequestMessage {
role: Role::User, role: Role::User,
content: vec![MessageContent::ToolResult(tool_result.clone())], content: vec![MessageContent::ToolResult(tool_result.clone())],
cache: true, cache: true
reasoning_details: None,
}, },
] ]
); );
// Simulate reaching tool use limit. // Simulate reaching tool use limit.
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUseLimitReached); fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::StatusUpdate(
cloud_llm_client::CompletionRequestStatus::ToolUseLimitReached,
));
fake_model.end_last_completion_stream(); fake_model.end_last_completion_stream();
let last_event = events.collect::<Vec<_>>().await.pop().unwrap(); let last_event = events.collect::<Vec<_>>().await.pop().unwrap();
assert!( assert!(
@@ -696,26 +677,22 @@ async fn test_resume_after_tool_use_limit(cx: &mut TestAppContext) {
LanguageModelRequestMessage { LanguageModelRequestMessage {
role: Role::User, role: Role::User,
content: vec!["abc".into()], content: vec!["abc".into()],
cache: false, cache: false
reasoning_details: None,
}, },
LanguageModelRequestMessage { LanguageModelRequestMessage {
role: Role::Assistant, role: Role::Assistant,
content: vec![MessageContent::ToolUse(tool_use)], content: vec![MessageContent::ToolUse(tool_use)],
cache: false, cache: false
reasoning_details: None,
}, },
LanguageModelRequestMessage { LanguageModelRequestMessage {
role: Role::User, role: Role::User,
content: vec![MessageContent::ToolResult(tool_result)], content: vec![MessageContent::ToolResult(tool_result)],
cache: false, cache: false
reasoning_details: None,
}, },
LanguageModelRequestMessage { LanguageModelRequestMessage {
role: Role::User, role: Role::User,
content: vec!["Continue where you left off".into()], content: vec!["Continue where you left off".into()],
cache: true, cache: true
reasoning_details: None,
} }
] ]
); );
@@ -754,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(),
@@ -765,7 +741,9 @@ async fn test_send_after_tool_use_limit(cx: &mut TestAppContext) {
}; };
fake_model fake_model
.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(tool_use.clone())); .send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(tool_use.clone()));
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUseLimitReached); fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::StatusUpdate(
cloud_llm_client::CompletionRequestStatus::ToolUseLimitReached,
));
fake_model.end_last_completion_stream(); fake_model.end_last_completion_stream();
let last_event = events.collect::<Vec<_>>().await.pop().unwrap(); let last_event = events.collect::<Vec<_>>().await.pop().unwrap();
assert!( assert!(
@@ -787,26 +765,22 @@ async fn test_send_after_tool_use_limit(cx: &mut TestAppContext) {
LanguageModelRequestMessage { LanguageModelRequestMessage {
role: Role::User, role: Role::User,
content: vec!["abc".into()], content: vec!["abc".into()],
cache: false, cache: false
reasoning_details: None,
}, },
LanguageModelRequestMessage { LanguageModelRequestMessage {
role: Role::Assistant, role: Role::Assistant,
content: vec![MessageContent::ToolUse(tool_use)], content: vec![MessageContent::ToolUse(tool_use)],
cache: false, cache: false
reasoning_details: None,
}, },
LanguageModelRequestMessage { LanguageModelRequestMessage {
role: Role::User, role: Role::User,
content: vec![MessageContent::ToolResult(tool_result)], content: vec![MessageContent::ToolResult(tool_result)],
cache: false, cache: false
reasoning_details: None,
}, },
LanguageModelRequestMessage { LanguageModelRequestMessage {
role: Role::User, role: Role::User,
content: vec!["ghi".into()], content: vec!["ghi".into()],
cache: true, cache: true
reasoning_details: None,
} }
] ]
); );
@@ -1063,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();
@@ -1107,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(
@@ -1117,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();
@@ -1817,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(),
@@ -1825,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(
@@ -1849,8 +1818,7 @@ async fn test_building_request_with_pending_tools(cx: &mut TestAppContext) {
LanguageModelRequestMessage { LanguageModelRequestMessage {
role: Role::User, role: Role::User,
content: vec!["Hey!".into()], content: vec!["Hey!".into()],
cache: true, cache: true
reasoning_details: None,
}, },
LanguageModelRequestMessage { LanguageModelRequestMessage {
role: Role::Assistant, role: Role::Assistant,
@@ -1858,8 +1826,7 @@ async fn test_building_request_with_pending_tools(cx: &mut TestAppContext) {
MessageContent::Text("Hi!".into()), MessageContent::Text("Hi!".into()),
MessageContent::ToolUse(echo_tool_use.clone()) MessageContent::ToolUse(echo_tool_use.clone())
], ],
cache: false, cache: false
reasoning_details: None,
}, },
LanguageModelRequestMessage { LanguageModelRequestMessage {
role: Role::User, role: Role::User,
@@ -1870,8 +1837,7 @@ async fn test_building_request_with_pending_tools(cx: &mut TestAppContext) {
content: "test".into(), content: "test".into(),
output: Some("test".into()) output: Some("test".into())
})], })],
cache: false, cache: false
reasoning_details: None,
}, },
], ],
); );
@@ -2034,7 +2000,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,
}, },
)); ));
@@ -2047,7 +2012,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();
@@ -2250,7 +2214,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(),
@@ -2269,14 +2232,12 @@ async fn test_send_retry_finishes_tool_calls_on_error(cx: &mut TestAppContext) {
LanguageModelRequestMessage { LanguageModelRequestMessage {
role: Role::User, role: Role::User,
content: vec!["Call the echo tool!".into()], content: vec!["Call the echo tool!".into()],
cache: false, cache: false
reasoning_details: None,
}, },
LanguageModelRequestMessage { LanguageModelRequestMessage {
role: Role::Assistant, role: Role::Assistant,
content: vec![language_model::MessageContent::ToolUse(tool_use_1.clone())], content: vec![language_model::MessageContent::ToolUse(tool_use_1.clone())],
cache: false, cache: false
reasoning_details: None,
}, },
LanguageModelRequestMessage { LanguageModelRequestMessage {
role: Role::User, role: Role::User,
@@ -2289,8 +2250,7 @@ async fn test_send_retry_finishes_tool_calls_on_error(cx: &mut TestAppContext) {
output: Some("test".into()) output: Some("test".into())
} }
)], )],
cache: true, cache: true
reasoning_details: None,
}, },
] ]
); );
@@ -2304,8 +2264,7 @@ async fn test_send_retry_finishes_tool_calls_on_error(cx: &mut TestAppContext) {
thread.last_message(), thread.last_message(),
Some(Message::Agent(AgentMessage { Some(Message::Agent(AgentMessage {
content: vec![AgentMessageContent::Text("Done".into())], content: vec![AgentMessageContent::Text("Done".into())],
tool_results: IndexMap::default(), tool_results: IndexMap::default()
reasoning_details: None,
})) }))
); );
}) })

View File

@@ -15,7 +15,7 @@ use agent_settings::{
use anyhow::{Context as _, Result, anyhow}; use anyhow::{Context as _, Result, anyhow};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use client::{ModelRequestUsage, RequestUsage, UserStore}; use client::{ModelRequestUsage, RequestUsage, UserStore};
use cloud_llm_client::{CompletionIntent, Plan, UsageLimit}; use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, Plan, UsageLimit};
use collections::{HashMap, HashSet, IndexMap}; use collections::{HashMap, HashSet, IndexMap};
use fs::Fs; use fs::Fs;
use futures::stream; use futures::stream;
@@ -113,7 +113,6 @@ impl Message {
role: Role::User, role: Role::User,
content: vec!["Continue where you left off".into()], content: vec!["Continue where you left off".into()],
cache: false, cache: false,
reasoning_details: None,
}], }],
} }
} }
@@ -178,7 +177,6 @@ impl UserMessage {
role: Role::User, role: Role::User,
content: Vec::with_capacity(self.content.len()), content: Vec::with_capacity(self.content.len()),
cache: false, cache: false,
reasoning_details: None,
}; };
const OPEN_CONTEXT: &str = "<context>\n\ const OPEN_CONTEXT: &str = "<context>\n\
@@ -446,7 +444,6 @@ impl AgentMessage {
role: Role::Assistant, role: Role::Assistant,
content: Vec::with_capacity(self.content.len()), content: Vec::with_capacity(self.content.len()),
cache: false, cache: false,
reasoning_details: self.reasoning_details.clone(),
}; };
for chunk in &self.content { for chunk in &self.content {
match chunk { match chunk {
@@ -482,7 +479,6 @@ impl AgentMessage {
role: Role::User, role: Role::User,
content: Vec::new(), content: Vec::new(),
cache: false, cache: false,
reasoning_details: None,
}; };
for tool_result in self.tool_results.values() { for tool_result in self.tool_results.values() {
@@ -512,7 +508,6 @@ impl AgentMessage {
pub struct AgentMessage { pub struct AgentMessage {
pub content: Vec<AgentMessageContent>, pub content: Vec<AgentMessageContent>,
pub tool_results: IndexMap<LanguageModelToolUseId, LanguageModelToolResult>, pub tool_results: IndexMap<LanguageModelToolUseId, LanguageModelToolResult>,
pub reasoning_details: Option<serde_json::Value>,
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@@ -612,8 +607,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 {
@@ -672,7 +665,6 @@ impl Thread {
prompt_capabilities_rx, prompt_capabilities_rx,
project, project,
action_log, action_log,
file_read_times: HashMap::default(),
} }
} }
@@ -868,7 +860,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(),
} }
} }
@@ -1008,7 +999,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(),
)); ));
@@ -1403,18 +1393,6 @@ impl Thread {
self.handle_thinking_event(text, signature, event_stream, cx) self.handle_thinking_event(text, signature, event_stream, cx)
} }
RedactedThinking { data } => self.handle_redacted_thinking_event(data, cx), RedactedThinking { data } => self.handle_redacted_thinking_event(data, cx),
ReasoningDetails(details) => {
let last_message = self.pending_message();
// Store the last non-empty reasoning_details (overwrites earlier ones)
// This ensures we keep the encrypted reasoning with signatures, not the early text reasoning
if let serde_json::Value::Array(ref arr) = details {
if !arr.is_empty() {
last_message.reasoning_details = Some(details);
}
} else {
last_message.reasoning_details = Some(details);
}
}
ToolUse(tool_use) => { ToolUse(tool_use) => {
return Ok(self.handle_tool_use_event(tool_use, event_stream, cx)); return Ok(self.handle_tool_use_event(tool_use, event_stream, cx));
} }
@@ -1447,16 +1425,20 @@ impl Thread {
); );
self.update_token_usage(usage, cx); self.update_token_usage(usage, cx);
} }
UsageUpdated { amount, limit } => { StatusUpdate(CompletionRequestStatus::UsageUpdated { amount, limit }) => {
self.update_model_request_usage(amount, limit, cx); self.update_model_request_usage(amount, limit, cx);
} }
ToolUseLimitReached => { StatusUpdate(
CompletionRequestStatus::Started
| CompletionRequestStatus::Queued { .. }
| CompletionRequestStatus::Failed { .. },
) => {}
StatusUpdate(CompletionRequestStatus::ToolUseLimitReached) => {
self.tool_use_limit_reached = true; self.tool_use_limit_reached = true;
} }
Stop(StopReason::Refusal) => return Err(CompletionError::Refusal.into()), Stop(StopReason::Refusal) => return Err(CompletionError::Refusal.into()),
Stop(StopReason::MaxTokens) => return Err(CompletionError::MaxTokens.into()), Stop(StopReason::MaxTokens) => return Err(CompletionError::MaxTokens.into()),
Stop(StopReason::ToolUse | StopReason::EndTurn) => {} Stop(StopReason::ToolUse | StopReason::EndTurn) => {}
Started | Queued { .. } => {}
} }
Ok(None) Ok(None)
@@ -1690,7 +1672,6 @@ impl Thread {
role: Role::User, role: Role::User,
content: vec![SUMMARIZE_THREAD_DETAILED_PROMPT.into()], content: vec![SUMMARIZE_THREAD_DETAILED_PROMPT.into()],
cache: false, cache: false,
reasoning_details: None,
}); });
let task = cx let task = cx
@@ -1701,7 +1682,9 @@ impl Thread {
let event = event.log_err()?; let event = event.log_err()?;
let text = match event { let text = match event {
LanguageModelCompletionEvent::Text(text) => text, LanguageModelCompletionEvent::Text(text) => text,
LanguageModelCompletionEvent::UsageUpdated { amount, limit } => { LanguageModelCompletionEvent::StatusUpdate(
CompletionRequestStatus::UsageUpdated { amount, limit },
) => {
this.update(cx, |thread, cx| { this.update(cx, |thread, cx| {
thread.update_model_request_usage(amount, limit, cx); thread.update_model_request_usage(amount, limit, cx);
}) })
@@ -1755,7 +1738,6 @@ impl Thread {
role: Role::User, role: Role::User,
content: vec![SUMMARIZE_THREAD_PROMPT.into()], content: vec![SUMMARIZE_THREAD_PROMPT.into()],
cache: false, cache: false,
reasoning_details: None,
}); });
self.pending_title_generation = Some(cx.spawn(async move |this, cx| { self.pending_title_generation = Some(cx.spawn(async move |this, cx| {
let mut title = String::new(); let mut title = String::new();
@@ -1766,7 +1748,9 @@ impl Thread {
let event = event?; let event = event?;
let text = match event { let text = match event {
LanguageModelCompletionEvent::Text(text) => text, LanguageModelCompletionEvent::Text(text) => text,
LanguageModelCompletionEvent::UsageUpdated { amount, limit } => { LanguageModelCompletionEvent::StatusUpdate(
CompletionRequestStatus::UsageUpdated { amount, limit },
) => {
this.update(cx, |thread, cx| { this.update(cx, |thread, cx| {
thread.update_model_request_usage(amount, limit, cx); thread.update_model_request_usage(amount, limit, cx);
})?; })?;
@@ -2003,7 +1987,6 @@ impl Thread {
role: Role::System, role: Role::System,
content: vec![system_prompt.into()], content: vec![system_prompt.into()],
cache: false, cache: false,
reasoning_details: None,
}]; }];
for message in &self.messages { for message in &self.messages {
messages.extend(message.to_request()); messages.extend(message.to_request());

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({
@@ -1793,426 +1748,10 @@ 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);
}); });
} }
#[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

@@ -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
@@ -693,20 +607,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 +821,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

@@ -35,7 +35,6 @@ pub struct AcpConnection {
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).
@@ -58,7 +57,6 @@ pub async fn connect(
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>> {
@@ -68,7 +66,6 @@ pub async fn connect(
command.clone(), command.clone(),
root_dir, root_dir,
default_mode, default_mode,
default_model,
is_remote, is_remote,
cx, cx,
) )
@@ -85,7 +82,6 @@ impl AcpConnection {
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> {
@@ -211,7 +207,6 @@ impl AcpConnection {
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,
@@ -250,61 +245,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 +312,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 +346,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>,
@@ -89,7 +68,6 @@ impl AgentServer for ClaudeCode {
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
@@ -112,7 +90,6 @@ impl AgentServer for ClaudeCode {
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>,
@@ -90,7 +69,6 @@ impl AgentServer for Codex {
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
@@ -114,7 +92,6 @@ impl AgentServer for Codex {
command, command,
root_dir.as_ref(), root_dir.as_ref(),
default_mode, default_mode,
default_model,
is_remote, is_remote,
cx, cx,
) )

View File

@@ -44,63 +44,19 @@ impl crate::AgentServer for CustomAgentServer {
settings settings
.as_ref() .as_ref()
.and_then(|s| s.default_mode().map(|m| acp::SessionModeId(m.into()))) .and_then(|s| s.default_mode.clone().map(|m| acp::SessionModeId(m.into())))
} }
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, _| {
let settings = settings if let Some(settings) = settings
.agent_servers .agent_servers
.get_or_insert_default() .get_or_insert_default()
.custom .custom
.entry(name.clone()) .get_mut(&name)
.or_insert_with(|| settings::CustomAgentServerSettings::Extension { {
default_model: None, settings.default_mode = mode_id.map(|m| m.to_string())
default_mode: None,
});
match settings {
settings::CustomAgentServerSettings::Custom { default_mode, .. }
| settings::CustomAgentServerSettings::Extension { default_mode, .. } => {
*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
.get::<AllAgentServersSettings>(None)
.custom
.get(&self.name())
.cloned()
});
settings
.as_ref()
.and_then(|s| s.default_model().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, _| {
let settings = settings
.agent_servers
.get_or_insert_default()
.custom
.entry(name.clone())
.or_insert_with(|| settings::CustomAgentServerSettings::Extension {
default_model: None,
default_mode: None,
});
match settings {
settings::CustomAgentServerSettings::Custom { default_model, .. }
| settings::CustomAgentServerSettings::Extension { default_model, .. } => {
*default_model = model_id.map(|m| m.to_string());
}
} }
}); });
} }
@@ -116,7 +72,6 @@ impl crate::AgentServer for CustomAgentServer {
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);
@@ -143,7 +98,6 @@ impl crate::AgentServer for CustomAgentServer {
command, command,
root_dir.as_ref(), root_dir.as_ref(),
default_mode, default_mode,
default_model,
is_remote, is_remote,
cx, cx,
) )

View File

@@ -476,7 +476,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,
}), }),
gemini: Some(crate::gemini::tests::local_command().into()), gemini: Some(crate::gemini::tests::local_command().into()),
codex: Some(BuiltinAgentServerSettings { codex: Some(BuiltinAgentServerSettings {
@@ -485,7 +484,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

@@ -37,7 +37,6 @@ impl AgentServer for Gemini {
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());
@@ -70,7 +69,6 @@ impl AgentServer for Gemini {
command, command,
root_dir.as_ref(), root_dir.as_ref(),
default_mode, default_mode,
default_model,
is_remote, is_remote,
cx, cx,
) )

View File

@@ -69,6 +69,7 @@ postage.workspace = true
project.workspace = true project.workspace = true
prompt_store.workspace = true prompt_store.workspace = true
proto.workspace = true proto.workspace = true
ref-cast.workspace = true
release_channel.workspace = true release_channel.workspace = true
rope.workspace = true rope.workspace = true
rules_library.workspace = true rules_library.workspace = true
@@ -92,12 +93,11 @@ time_format.workspace = true
ui.workspace = true ui.workspace = true
ui_input.workspace = true ui_input.workspace = true
url.workspace = true url.workspace = true
urlencoding.workspace = true
util.workspace = true 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"] }
@@ -113,7 +113,6 @@ languages = { workspace = true, features = ["test-support"] }
language_model = { workspace = true, "features" = ["test-support"] } language_model = { workspace = true, "features" = ["test-support"] }
pretty_assertions.workspace = true pretty_assertions.workspace = true
project = { workspace = true, features = ["test-support"] } project = { workspace = true, features = ["test-support"] }
semver.workspace = true
rand.workspace = true rand.workspace = true
tree-sitter-md.workspace = true tree-sitter-md.workspace = true
unindent.workspace = true unindent.workspace = true

View File

@@ -1,3 +1,4 @@
mod completion_provider;
mod entry_view_state; mod entry_view_state;
mod message_editor; mod message_editor;
mod mode_selector; mod mode_selector;

View File

@@ -405,7 +405,7 @@ mod tests {
use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind}; use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
use editor::RowInfo; use editor::RowInfo;
use fs::FakeFs; use fs::FakeFs;
use gpui::{AppContext as _, TestAppContext}; use gpui::{AppContext as _, SemanticVersion, TestAppContext};
use crate::acp::entry_view_state::EntryViewState; use crate::acp::entry_view_state::EntryViewState;
use multi_buffer::MultiBufferRow; use multi_buffer::MultiBufferRow;
@@ -539,7 +539,7 @@ mod tests {
let settings_store = SettingsStore::test(cx); let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store); cx.set_global(settings_store);
theme::init(theme::LoadThemes::JustBase, cx); theme::init(theme::LoadThemes::JustBase, cx);
release_channel::init(semver::Version::new(0, 0, 0), cx); release_channel::init(SemanticVersion::default(), cx);
}); });
} }
} }

File diff suppressed because it is too large Load Diff

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,38 +1,27 @@
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::{ use gpui::{AsyncWindowContext, BackgroundExecutor, DismissEvent, Task, WeakEntity};
Action, AsyncWindowContext, BackgroundExecutor, DismissEvent, FocusHandle, Task, WeakEntity,
};
use ordered_float::OrderedFloat; use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate}; use picker::{Picker, PickerDelegate};
use ui::{ use ui::{
DocumentationAside, DocumentationEdge, DocumentationSide, IntoElement, KeyBinding, ListItem, DocumentationAside, DocumentationEdge, DocumentationSide, IntoElement, ListItem,
ListItemSpacing, prelude::*, ListItemSpacing, prelude::*,
}; };
use util::ResultExt; use util::ResultExt;
use zed_actions::agent::OpenSettings;
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>,
focus_handle: FocusHandle,
window: &mut Window, window: &mut Window,
cx: &mut Context<AcpModelSelector>, cx: &mut Context<AcpModelSelector>,
) -> AcpModelSelector { ) -> AcpModelSelector {
let delegate = let delegate = AcpModelPickerDelegate::new(selector, window, cx);
AcpModelPickerDelegate::new(selector, agent_server, fs, focus_handle, window, cx);
Picker::list(delegate, window, cx) Picker::list(delegate, window, cx)
.show_scrollbar(true) .show_scrollbar(true)
.width(rems(20.)) .width(rems(20.))
@@ -46,23 +35,17 @@ 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<()>,
focus_handle: FocusHandle,
} }
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>,
focus_handle: FocusHandle,
window: &mut Window, window: &mut Window,
cx: &mut Context<AcpModelSelector>, cx: &mut Context<AcpModelSelector>,
) -> Self { ) -> Self {
@@ -103,15 +86,12 @@ 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,
selected_index: 0, selected_index: 0,
selected_description: None, selected_description: None,
_refresh_models_task: refresh_models_task, _refresh_models_task: refresh_models_task,
focus_handle,
} }
} }
@@ -201,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);
@@ -260,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
@@ -276,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();
@@ -288,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| {
@@ -320,57 +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()
}),
)
})
}
fn render_footer(
&self,
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<AnyElement> {
let focus_handle = self.focus_handle.clone();
if !self.selector.should_render_footer() {
return None;
}
Some(
h_flex()
.w_full()
.p_1p5()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.child(
Button::new("configure", "Configure")
.full_width()
.style(ButtonStyle::Outlined)
.key_binding(
KeyBinding::for_action_in(&OpenSettings, &focus_handle, cx)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(|_, window, cx| {
window.dispatch_action(OpenSettings.boxed_clone(), cx);
}),
)
.into_any(),
)
} }
} }

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::{AgentModelInfo, 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,25 +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 {
let focus_handle_clone = focus_handle.clone();
Self { Self {
selector: cx.new(move |cx| { selector: cx.new(move |cx| acp_model_selector(selector, window, cx)),
acp_model_selector(
selector,
agent_server,
fs,
focus_handle_clone.clone(),
window,
cx,
)
}),
menu_handle, menu_handle,
focus_handle, focus_handle,
} }

View File

@@ -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)]
@@ -278,7 +278,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<()>>,
@@ -297,7 +296,6 @@ pub struct AcpThreadView {
_cancel_task: Option<Task<()>>, _cancel_task: Option<Task<()>>,
_subscriptions: [Subscription; 5], _subscriptions: [Subscription; 5],
show_codex_windows_warning: bool, show_codex_windows_warning: bool,
in_flight_prompt: Option<Vec<acp::ContentBlock>>,
} }
enum ThreadState { enum ThreadState {
@@ -417,7 +415,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(),
@@ -438,7 +435,6 @@ impl AcpThreadView {
new_server_version_available: None, new_server_version_available: None,
resume_thread_metadata: resume_thread, resume_thread_metadata: resume_thread,
show_codex_windows_warning, show_codex_windows_warning,
in_flight_prompt: None,
} }
} }
@@ -593,13 +589,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,
@@ -653,6 +645,7 @@ impl AcpThreadView {
mode_selector, mode_selector,
_subscriptions: subscriptions, _subscriptions: subscriptions,
}; };
this.message_editor.focus_handle(cx).focus(window);
this.profile_selector = this.as_native_thread(cx).map(|thread| { this.profile_selector = this.as_native_thread(cx).map(|thread| {
cx.new(|cx| { cx.new(|cx| {
@@ -805,7 +798,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);
} }
} }
@@ -1140,7 +1132,6 @@ impl AcpThreadView {
self.is_loading_contents = true; self.is_loading_contents = true;
let model_id = self.current_model_id(cx); 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;
@@ -1156,7 +1147,6 @@ impl AcpThreadView {
} }
this.update_in(cx, |this, window, cx| { this.update_in(cx, |this, window, cx| {
this.in_flight_prompt = Some(contents.clone());
this.set_editor_is_expanded(false, cx); this.set_editor_is_expanded(false, cx);
this.scroll_to_bottom(cx); this.scroll_to_bottom(cx);
this.message_editor.update(cx, |message_editor, cx| { this.message_editor.update(cx, |message_editor, cx| {
@@ -1176,26 +1166,19 @@ impl AcpThreadView {
"Agent Message Sent", "Agent Message Sent",
agent = agent_telemetry_id, agent = agent_telemetry_id,
session = session_id, session = session_id,
model = model_id, model = model_id
mode = mode_id
); );
thread.send(contents, cx) thread.send(contents, cx)
})?; })?;
let res = send.await; let res = send.await;
let turn_time_ms = turn_start_time.elapsed().as_millis(); let turn_time_ms = turn_start_time.elapsed().as_millis();
let status = if res.is_ok() { let status = if res.is_ok() { "success" } else { "failure" };
this.update(cx, |this, _| this.in_flight_prompt.take()).ok();
"success"
} else {
"failure"
};
telemetry::event!( telemetry::event!(
"Agent Turn Completed", "Agent Turn Completed",
agent = agent_telemetry_id, agent = agent_telemetry_id,
session = session_id, session = session_id,
model = model_id, model = model_id,
mode = mode_id,
status, status,
turn_time_ms, turn_time_ms,
); );
@@ -1272,28 +1255,6 @@ impl AcpThreadView {
}; };
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
// Check if there are any edits from prompts before the one being regenerated.
//
// If there are, we keep/accept them since we're not regenerating the prompt that created them.
//
// If editing the prompt that generated the edits, they are auto-rejected
// through the `rewind` function in the `acp_thread`.
let has_earlier_edits = thread.read_with(cx, |thread, _| {
thread
.entries()
.iter()
.take(entry_ix)
.any(|entry| entry.diffs().next().is_some())
})?;
if has_earlier_edits {
thread.update(cx, |thread, cx| {
thread.action_log().update(cx, |action_log, cx| {
action_log.keep_all_edits(None, cx);
});
})?;
}
thread thread
.update(cx, |thread, cx| thread.rewind(user_message_id, cx))? .update(cx, |thread, cx| thread.rewind(user_message_id, cx))?
.await?; .await?;
@@ -1366,7 +1327,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();
} }
@@ -3180,7 +3140,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()
@@ -3188,15 +3148,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()
})), })),
) )
}) })
@@ -4796,7 +4748,7 @@ impl AcpThreadView {
buffer.update(cx, |buffer, cx| { buffer.update(cx, |buffer, cx| {
buffer.set_text(markdown, cx); buffer.set_text(markdown, cx);
buffer.set_language(Some(markdown_language), cx); buffer.set_language(Some(markdown_language), cx);
buffer.set_capability(language::Capability::ReadWrite, cx); buffer.set_capability(language::Capability::ReadOnly, cx);
})?; })?;
workspace.update_in(cx, |workspace, window, cx| { workspace.update_in(cx, |workspace, window, cx| {
@@ -5053,12 +5005,15 @@ impl AcpThreadView {
})); }));
let mut container = h_flex() let mut container = h_flex()
.id("thread-controls-container")
.group("thread-controls-container")
.w_full() .w_full()
.py_2() .py_2()
.px_5() .px_5()
.gap_px() .gap_px()
.opacity(0.6) .opacity(0.6)
.hover(|s| s.opacity(1.)) .hover(|style| style.opacity(1.))
.flex_wrap()
.justify_end(); .justify_end();
if AgentSettings::get_global(cx).enable_feedback if AgentSettings::get_global(cx).enable_feedback
@@ -5068,13 +5023,23 @@ impl AcpThreadView {
{ {
let feedback = self.thread_feedback.feedback; let feedback = self.thread_feedback.feedback;
let tooltip_meta = || {
SharedString::new(
"Rating the thread sends all of your current conversation to the Zed team.",
)
};
container = container container = container
.child(
div().visible_on_hover("thread-controls-container").child(
Label::new(match feedback {
Some(ThreadFeedback::Positive) => "Thanks for your feedback!",
Some(ThreadFeedback::Negative) => {
"We appreciate your feedback and will use it to improve."
}
None => {
"Rating the thread sends all of your current conversation to the Zed team."
}
})
.color(Color::Muted)
.size(LabelSize::XSmall)
.truncate(),
),
)
.child( .child(
IconButton::new("feedback-thumbs-up", IconName::ThumbsUp) IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
.shape(ui::IconButtonShape::Square) .shape(ui::IconButtonShape::Square)
@@ -5083,12 +5048,7 @@ impl AcpThreadView {
Some(ThreadFeedback::Positive) => Color::Accent, Some(ThreadFeedback::Positive) => Color::Accent,
_ => Color::Ignored, _ => Color::Ignored,
}) })
.tooltip(move |window, cx| match feedback { .tooltip(Tooltip::text("Helpful Response"))
Some(ThreadFeedback::Positive) => {
Tooltip::text("Thanks for your feedback!")(window, cx)
}
_ => Tooltip::with_meta("Helpful Response", None, tooltip_meta(), cx),
})
.on_click(cx.listener(move |this, _, window, cx| { .on_click(cx.listener(move |this, _, window, cx| {
this.handle_feedback_click(ThreadFeedback::Positive, window, cx); this.handle_feedback_click(ThreadFeedback::Positive, window, cx);
})), })),
@@ -5101,16 +5061,7 @@ impl AcpThreadView {
Some(ThreadFeedback::Negative) => Color::Accent, Some(ThreadFeedback::Negative) => Color::Accent,
_ => Color::Ignored, _ => Color::Ignored,
}) })
.tooltip(move |window, cx| match feedback { .tooltip(Tooltip::text("Not Helpful"))
Some(ThreadFeedback::Negative) => {
Tooltip::text(
"We appreciate your feedback and will use it to improve in the future.",
)(window, cx)
}
_ => {
Tooltip::with_meta("Not Helpful Response", None, tooltip_meta(), cx)
}
})
.on_click(cx.listener(move |this, _, window, cx| { .on_click(cx.listener(move |this, _, window, cx| {
this.handle_feedback_click(ThreadFeedback::Negative, window, cx); this.handle_feedback_click(ThreadFeedback::Negative, window, cx);
})), })),
@@ -5393,9 +5344,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)
@@ -5442,16 +5393,6 @@ impl AcpThreadView {
) )
} }
fn current_mode_id(&self, cx: &App) -> Option<Arc<str>> {
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> { fn current_model_id(&self, cx: &App) -> Option<String> {
self.model_selector self.model_selector
.as_ref() .as_ref()
@@ -5490,12 +5431,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));
@@ -5508,24 +5444,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()
@@ -5544,9 +5467,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);
})), })),
@@ -5688,6 +5613,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()))
@@ -5697,6 +5623,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| {
@@ -5723,11 +5650,6 @@ impl AcpThreadView {
provider_id: None, provider_id: None,
}; };
this.clear_thread_error(cx); this.clear_thread_error(cx);
if let Some(message) = this.in_flight_prompt.take() {
this.message_editor.update(cx, |editor, cx| {
editor.set_message(message, window, cx);
});
}
let this = cx.weak_entity(); let this = cx.weak_entity();
window.defer(cx, |window, cx| { window.defer(cx, |window, cx| {
Self::handle_auth_required(this, err, agent, connection, window, cx); Self::handle_auth_required(this, err, agent, connection, window, cx);
@@ -5919,7 +5841,7 @@ impl Render for AcpThreadView {
None None
} }
}) })
.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 { .. })
@@ -5985,6 +5907,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()),
@@ -6052,7 +5975,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.),
@@ -6105,9 +6027,8 @@ 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 fs::FakeFs; use fs::FakeFs;
use gpui::{EventEmitter, TestAppContext, VisualTestContext}; use gpui::{EventEmitter, SemanticVersion, TestAppContext, VisualTestContext};
use project::Project; use project::Project;
use serde_json::json; use serde_json::json;
use settings::SettingsStore; use settings::SettingsStore;
@@ -6624,7 +6545,7 @@ pub(crate) mod tests {
let settings_store = SettingsStore::test(cx); let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store); cx.set_global(settings_store);
theme::init(theme::LoadThemes::JustBase, cx); theme::init(theme::LoadThemes::JustBase, cx);
release_channel::init(semver::Version::new(0, 0, 0), cx); release_channel::init(SemanticVersion::default(), cx);
prompt_store::init(cx) prompt_store::init(cx)
}); });
} }
@@ -7274,7 +7195,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
@@ -7336,7 +7257,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;
@@ -12,7 +12,7 @@ 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;
@@ -46,8 +46,9 @@ 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 {
@@ -552,9 +553,7 @@ impl AgentConfiguration {
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| {
@@ -652,7 +651,7 @@ 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. // Servers without a configuration can only be provided by extensions.
let provided_by_extension = server_configuration.as_ref().is_none_or(|config| { let provided_by_extension = server_configuration.is_none_or(|config| {
matches!( matches!(
config.as_ref(), config.as_ref(),
ContextServerConfiguration::Extension { .. } ContextServerConfiguration::Extension { .. }
@@ -708,10 +707,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)
@@ -734,25 +730,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();
@@ -1343,12 +1328,11 @@ async fn open_new_agent_servers_entry_in_settings_editor(
.custom .custom
.insert( .insert(
server_name, server_name,
settings::CustomAgentServerSettings::Custom { settings::CustomAgentServerSettings {
path: "path_to_executable".into(), path: "path_to_executable".into(),
args: vec![], args: vec![],
env: Some(HashMap::default()), env: Some(HashMap::default()),
default_mode: None, default_mode: None,
default_model: None,
}, },
); );
} }
@@ -1363,15 +1347,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 +1360,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(
@@ -702,7 +642,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 +678,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 +703,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);
@@ -827,7 +767,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

@@ -253,7 +253,6 @@ impl ManageProfilesModal {
}); });
}, },
false, // Do not use popover styles for the model picker false, // Do not use popover styles for the model picker
self.focus_handle.clone(),
window, window,
cx, cx,
) )

View File

@@ -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};
@@ -580,11 +580,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
} }

View File

@@ -25,8 +25,6 @@ impl AgentModelSelector {
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Self { ) -> Self {
let focus_handle_clone = focus_handle.clone();
Self { Self {
selector: cx.new(move |cx| { selector: cx.new(move |cx| {
let fs = fs.clone(); let fs = fs.clone();
@@ -50,7 +48,6 @@ impl AgentModelSelector {
} }
}, },
true, // Use popover styles for picker true, // Use popover styles for picker
focus_handle_clone,
window, window,
cx, cx,
) )

View File

@@ -8,7 +8,9 @@ use agent::{ContextServerRegistry, DbThreadMetadata, HistoryEntry, HistoryStore}
use db::kvp::{Dismissable, KEY_VALUE_STORE}; use db::kvp::{Dismissable, KEY_VALUE_STORE};
use project::{ use project::{
ExternalAgentServerName, ExternalAgentServerName,
agent_server_store::{CLAUDE_CODE_NAME, CODEX_NAME, GEMINI_NAME}, agent_server_store::{
AgentServerCommand, AllAgentServersSettings, CLAUDE_CODE_NAME, CODEX_NAME, GEMINI_NAME,
},
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use settings::{ use settings::{
@@ -17,7 +19,6 @@ use settings::{
use zed_actions::agent::{OpenClaudeCodeOnboardingModal, ReauthenticateAgent}; use zed_actions::agent::{OpenClaudeCodeOnboardingModal, ReauthenticateAgent};
use crate::ManageProfiles;
use crate::ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal}; use crate::ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal};
use crate::{ use crate::{
AddContextServer, AgentDiffPane, DeleteRecentlyOpenThread, Follow, InlineAssistant, AddContextServer, AgentDiffPane, DeleteRecentlyOpenThread, Follow, InlineAssistant,
@@ -34,7 +35,10 @@ use crate::{
ExpandMessageEditor, ExpandMessageEditor,
acp::{AcpThreadHistory, ThreadHistoryEvent}, acp::{AcpThreadHistory, ThreadHistoryEvent},
}; };
use crate::{ExternalAgent, NewExternalAgentThread, NewNativeAgentThreadFromSummary}; use crate::{
ExternalAgent, NewExternalAgentThread, NewNativeAgentThreadFromSummary, placeholder_command,
};
use crate::{ManageProfiles, context_store::ContextStore};
use agent_settings::AgentSettings; use agent_settings::AgentSettings;
use ai_onboarding::AgentPanelOnboarding; use ai_onboarding::AgentPanelOnboarding;
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
@@ -57,7 +61,7 @@ use project::{Project, ProjectPath, Worktree};
use prompt_store::{PromptBuilder, PromptStore, UserPromptId}; use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
use rules_library::{RulesLibrary, open_rules_library}; use rules_library::{RulesLibrary, open_rules_library};
use search::{BufferSearchBar, buffer_search}; use search::{BufferSearchBar, buffer_search};
use settings::{Settings, update_settings_file}; use settings::{Settings, SettingsStore, update_settings_file};
use theme::ThemeSettings; use theme::ThemeSettings;
use ui::utils::WithRemSize; use ui::utils::WithRemSize;
use ui::{ use ui::{
@@ -244,6 +248,7 @@ pub enum AgentType {
Codex, Codex,
Custom { Custom {
name: SharedString, name: SharedString,
command: AgentServerCommand,
}, },
} }
@@ -275,7 +280,7 @@ impl From<ExternalAgent> for AgentType {
ExternalAgent::Gemini => Self::Gemini, ExternalAgent::Gemini => Self::Gemini,
ExternalAgent::ClaudeCode => Self::ClaudeCode, ExternalAgent::ClaudeCode => Self::ClaudeCode,
ExternalAgent::Codex => Self::Codex, ExternalAgent::Codex => Self::Codex,
ExternalAgent::Custom { name } => Self::Custom { name }, ExternalAgent::Custom { name, command } => Self::Custom { name, command },
ExternalAgent::NativeAgent => Self::NativeAgent, ExternalAgent::NativeAgent => Self::NativeAgent,
} }
} }
@@ -431,6 +436,7 @@ pub struct AgentPanel {
text_thread_store: Entity<assistant_text_thread::TextThreadStore>, text_thread_store: Entity<assistant_text_thread::TextThreadStore>,
prompt_store: Option<Entity<PromptStore>>, prompt_store: Option<Entity<PromptStore>>,
context_server_registry: Entity<ContextServerRegistry>, context_server_registry: Entity<ContextServerRegistry>,
inline_assist_context_store: Entity<ContextStore>,
configuration: Option<Entity<AgentConfiguration>>, configuration: Option<Entity<AgentConfiguration>>,
configuration_subscription: Option<Subscription>, configuration_subscription: Option<Subscription>,
active_view: ActiveView, active_view: ActiveView,
@@ -542,6 +548,7 @@ impl AgentPanel {
let client = workspace.client().clone(); let client = workspace.client().clone();
let workspace = workspace.weak_handle(); let workspace = workspace.weak_handle();
let inline_assist_context_store = cx.new(|_cx| ContextStore::new(project.downgrade()));
let context_server_registry = let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
@@ -678,6 +685,7 @@ impl AgentPanel {
configuration: None, configuration: None,
configuration_subscription: None, configuration_subscription: None,
context_server_registry, context_server_registry,
inline_assist_context_store,
previous_view: None, previous_view: None,
new_thread_menu_handle: PopoverMenuHandle::default(), new_thread_menu_handle: PopoverMenuHandle::default(),
agent_panel_menu_handle: PopoverMenuHandle::default(), agent_panel_menu_handle: PopoverMenuHandle::default(),
@@ -718,6 +726,10 @@ impl AgentPanel {
&self.prompt_store &self.prompt_store
} }
pub(crate) fn inline_assist_context_store(&self) -> &Entity<ContextStore> {
&self.inline_assist_context_store
}
pub(crate) fn thread_store(&self) -> &Entity<HistoryStore> { pub(crate) fn thread_store(&self) -> &Entity<HistoryStore> {
&self.history_store &self.history_store
} }
@@ -816,7 +828,6 @@ impl AgentPanel {
window, window,
cx, cx,
), ),
true,
window, window,
cx, cx,
); );
@@ -912,12 +923,7 @@ impl AgentPanel {
) )
}); });
this.set_active_view( this.set_active_view(ActiveView::ExternalAgentThread { thread_view }, window, cx);
ActiveView::ExternalAgentThread { thread_view },
!loading,
window,
cx,
);
}) })
}) })
.detach_and_log_err(cx); .detach_and_log_err(cx);
@@ -959,10 +965,10 @@ impl AgentPanel {
fn open_history(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn open_history(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if matches!(self.active_view, ActiveView::History) { if matches!(self.active_view, ActiveView::History) {
if let Some(previous_view) = self.previous_view.take() { if let Some(previous_view) = self.previous_view.take() {
self.set_active_view(previous_view, true, window, cx); self.set_active_view(previous_view, window, cx);
} }
} else { } else {
self.set_active_view(ActiveView::History, true, window, cx); self.set_active_view(ActiveView::History, window, cx);
} }
cx.notify(); cx.notify();
} }
@@ -1018,7 +1024,6 @@ impl AgentPanel {
window, window,
cx, cx,
), ),
true,
window, window,
cx, cx,
); );
@@ -1164,7 +1169,7 @@ impl AgentPanel {
let context_server_store = self.project.read(cx).context_server_store(); let context_server_store = self.project.read(cx).context_server_store();
let fs = self.fs.clone(); let fs = self.fs.clone();
self.set_active_view(ActiveView::Configuration, true, window, cx); self.set_active_view(ActiveView::Configuration, window, cx);
self.configuration = Some(cx.new(|cx| { self.configuration = Some(cx.new(|cx| {
AgentConfiguration::new( AgentConfiguration::new(
fs, fs,
@@ -1281,7 +1286,6 @@ impl AgentPanel {
fn set_active_view( fn set_active_view(
&mut self, &mut self,
new_view: ActiveView, new_view: ActiveView,
focus: bool,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
@@ -1320,9 +1324,7 @@ impl AgentPanel {
self.active_view = new_view; self.active_view = new_view;
} }
if focus { self.focus_handle(cx).focus(window);
self.focus_handle(cx).focus(window);
}
} }
fn populate_recently_opened_menu_section( fn populate_recently_opened_menu_section(
@@ -1457,8 +1459,8 @@ impl AgentPanel {
self.serialize(cx); self.serialize(cx);
self.external_thread(Some(crate::ExternalAgent::Codex), None, None, window, cx) self.external_thread(Some(crate::ExternalAgent::Codex), None, None, window, cx)
} }
AgentType::Custom { name } => self.external_thread( AgentType::Custom { name, command } => self.external_thread(
Some(crate::ExternalAgent::Custom { name }), Some(crate::ExternalAgent::Custom { name, command }),
None, None,
None, None,
window, window,
@@ -1890,9 +1892,6 @@ impl AgentPanel {
.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| {
@@ -1906,6 +1905,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);
@@ -1929,11 +1929,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();
@@ -1957,10 +1955,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| {
@@ -1985,10 +1983,7 @@ impl AgentPanel {
.separator() .separator()
.header("External Agents") .header("External Agents")
.item( .item(
ContextMenuEntry::new("Claude Code") ContextMenuEntry::new("New Claude Code")
.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)
@@ -2014,10 +2009,7 @@ impl AgentPanel {
}), }),
) )
.item( .item(
ContextMenuEntry::new("Codex CLI") ContextMenuEntry::new("New Codex CLI")
.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)
@@ -2043,10 +2035,7 @@ impl AgentPanel {
}), }),
) )
.item( .item(
ContextMenuEntry::new("Gemini CLI") ContextMenuEntry::new("New Gemini CLI")
.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)
@@ -2072,8 +2061,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
@@ -2082,31 +2071,27 @@ impl AgentPanel {
}) })
.cloned() .cloned()
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let custom_settings = cx
.global::<SettingsStore>()
.get::<AllAgentServersSettings>(None)
.custom
.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 {}", agent_name));
if let Some(icon_path) = icon_path { if let Some(icon_path) = icon_path {
entry = entry.custom_icon_svg(icon_path); entry = entry.custom_icon_svg(icon_path);
} else { } else {
entry = entry.icon(IconName::Terminal); entry = entry.icon(IconName::Terminal);
} }
entry = entry entry = entry
.when(
is_agent_selected(AgentType::Custom {
name: agent_name.0.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({
let workspace = workspace.clone(); let workspace = workspace.clone();
let agent_name = agent_name.clone(); let agent_name = agent_name.clone();
let custom_settings = custom_settings.clone();
move |window, cx| { move |window, cx| {
if let Some(workspace) = workspace.upgrade() { if let Some(workspace) = workspace.upgrade() {
workspace.update(cx, |workspace, cx| { workspace.update(cx, |workspace, cx| {
@@ -2119,6 +2104,17 @@ impl AgentPanel {
name: agent_name name: agent_name
.clone() .clone()
.into(), .into(),
command: custom_settings
.get(&agent_name.0)
.map(|settings| {
settings
.command
.clone()
})
.unwrap_or(
placeholder_command(
),
),
}, },
window, window,
cx, cx,
@@ -2129,7 +2125,6 @@ impl AgentPanel {
} }
} }
}); });
menu = menu.item(entry); menu = menu.item(entry);
} }
@@ -2162,7 +2157,7 @@ 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_external_svg(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)
@@ -2171,7 +2166,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)
@@ -2667,19 +2662,23 @@ impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
cx: &mut Context<RulesLibrary>, cx: &mut Context<RulesLibrary>,
) { ) {
InlineAssistant::update_global(cx, |assistant, cx| { InlineAssistant::update_global(cx, |assistant, cx| {
let Some(workspace) = self.workspace.upgrade() else { let Some(project) = self
.workspace
.upgrade()
.map(|workspace| workspace.read(cx).project().downgrade())
else {
return; return;
}; };
let Some(panel) = workspace.read(cx).panel::<AgentPanel>(cx) else { let prompt_store = None;
return; let thread_store = None;
}; let context_store = cx.new(|_| ContextStore::new(project.clone()));
let project = workspace.read(cx).project().downgrade();
assistant.assist( assistant.assist(
prompt_editor, prompt_editor,
self.workspace.clone(), self.workspace.clone(),
context_store,
project, project,
panel.read(cx).thread_store().clone(), prompt_store,
None, thread_store,
initial_prompt, initial_prompt,
window, window,
cx, cx,

View File

@@ -4,13 +4,14 @@ mod agent_diff;
mod agent_model_selector; mod agent_model_selector;
mod agent_panel; mod agent_panel;
mod buffer_codegen; mod buffer_codegen;
mod completion_provider;
mod context; mod context;
mod context_picker;
mod context_server_configuration; mod context_server_configuration;
mod context_store;
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 mention_set;
mod profile_selector; mod profile_selector;
mod slash_command; mod slash_command;
mod slash_command_picker; mod slash_command_picker;
@@ -29,14 +30,12 @@ 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, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry,
}; };
use project::DisableAiSettings; use project::DisableAiSettings;
use project::agent_server_store::AgentServerCommand;
use prompt_store::PromptBuilder; use prompt_store::PromptBuilder;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -55,6 +54,8 @@ actions!(
[ [
/// Creates a new text-based conversation thread. /// Creates a new text-based conversation thread.
NewTextThread, NewTextThread,
/// Toggles the context picker interface for adding files, symbols, or other context.
ToggleContextPicker,
/// Toggles the menu to create new agent threads. /// Toggles the menu to create new agent threads.
ToggleNewThreadMenu, ToggleNewThreadMenu,
/// Toggles the navigation menu for switching between threads and views. /// Toggles the navigation menu for switching between threads and views.
@@ -67,6 +68,8 @@ actions!(
ToggleProfileSelector, ToggleProfileSelector,
/// Cycles through available session modes. /// Cycles through available session modes.
CycleModeSelector, CycleModeSelector,
/// Removes all added context from the current conversation.
RemoveAllContext,
/// Expands the message editor to full size. /// Expands the message editor to full size.
ExpandMessageEditor, ExpandMessageEditor,
/// Opens the conversation history view. /// Opens the conversation history view.
@@ -89,6 +92,10 @@ actions!(
FocusLeft, FocusLeft,
/// Moves focus right in the interface. /// Moves focus right in the interface.
FocusRight, FocusRight,
/// Removes the currently focused context item.
RemoveFocusedContext,
/// Accepts the suggested context item.
AcceptSuggestedContext,
/// Opens the active thread as a markdown file. /// Opens the active thread as a markdown file.
OpenActiveThreadAsMarkdown, OpenActiveThreadAsMarkdown,
/// Opens the agent diff view to review changes. /// Opens the agent diff view to review changes.
@@ -152,7 +159,18 @@ pub enum ExternalAgent {
ClaudeCode, ClaudeCode,
Codex, Codex,
NativeAgent, NativeAgent,
Custom { name: SharedString }, Custom {
name: SharedString,
command: AgentServerCommand,
},
}
fn placeholder_command() -> AgentServerCommand {
AgentServerCommand {
path: "/placeholder".into(),
args: vec![],
env: None,
}
} }
impl ExternalAgent { impl ExternalAgent {
@@ -176,7 +194,9 @@ impl ExternalAgent {
Self::ClaudeCode => Rc::new(agent_servers::ClaudeCode), Self::ClaudeCode => Rc::new(agent_servers::ClaudeCode),
Self::Codex => Rc::new(agent_servers::Codex), Self::Codex => Rc::new(agent_servers::Codex),
Self::NativeAgent => Rc::new(agent::NativeAgentServer::new(fs, history)), Self::NativeAgent => Rc::new(agent::NativeAgentServer::new(fs, history)),
Self::Custom { name } => Rc::new(agent_servers::CustomAgentServer::new(name.clone())), Self::Custom { name, command: _ } => {
Rc::new(agent_servers::CustomAgentServer::new(name.clone()))
}
} }
} }
} }
@@ -211,6 +231,11 @@ impl ModelUsageContext {
} }
} }
} }
pub fn language_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {
self.configured_model(cx)
.map(|configured_model| configured_model.model)
}
} }
/// Initializes the `agent` crate. /// Initializes the `agent` crate.
@@ -261,25 +286,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");
@@ -288,47 +295,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>()]);
} }
}); });
@@ -418,137 +420,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

@@ -1,4 +1,6 @@
use crate::{context::LoadedContext, inline_prompt_editor::CodegenStatus}; use crate::{
context::load_context, context_store::ContextStore, inline_prompt_editor::CodegenStatus,
};
use agent_settings::AgentSettings; use agent_settings::AgentSettings;
use anyhow::{Context as _, Result}; use anyhow::{Context as _, Result};
use client::telemetry::Telemetry; use client::telemetry::Telemetry;
@@ -6,12 +8,9 @@ use cloud_llm_client::CompletionIntent;
use collections::HashSet; use collections::HashSet;
use editor::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint}; use editor::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint};
use futures::{ use futures::{
SinkExt, Stream, StreamExt, TryStreamExt as _, SinkExt, Stream, StreamExt, TryStreamExt as _, channel::mpsc, future::LocalBoxFuture, join,
channel::mpsc,
future::{LocalBoxFuture, Shared},
join,
}; };
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Subscription, Task}; use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Subscription, Task, WeakEntity};
use language::{Buffer, IndentKind, Point, TransactionId, line_diff}; use language::{Buffer, IndentKind, Point, TransactionId, line_diff};
use language_model::{ use language_model::{
LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
@@ -19,7 +18,8 @@ use language_model::{
}; };
use multi_buffer::MultiBufferRow; use multi_buffer::MultiBufferRow;
use parking_lot::Mutex; use parking_lot::Mutex;
use prompt_store::PromptBuilder; use project::Project;
use prompt_store::{PromptBuilder, PromptStore};
use rope::Rope; use rope::Rope;
use smol::future::FutureExt; use smol::future::FutureExt;
use std::{ use std::{
@@ -43,6 +43,9 @@ pub struct BufferCodegen {
buffer: Entity<MultiBuffer>, buffer: Entity<MultiBuffer>,
range: Range<Anchor>, range: Range<Anchor>,
initial_transaction_id: Option<TransactionId>, initial_transaction_id: Option<TransactionId>,
context_store: Entity<ContextStore>,
project: WeakEntity<Project>,
prompt_store: Option<Entity<PromptStore>>,
telemetry: Arc<Telemetry>, telemetry: Arc<Telemetry>,
builder: Arc<PromptBuilder>, builder: Arc<PromptBuilder>,
pub is_insertion: bool, pub is_insertion: bool,
@@ -53,6 +56,9 @@ impl BufferCodegen {
buffer: Entity<MultiBuffer>, buffer: Entity<MultiBuffer>,
range: Range<Anchor>, range: Range<Anchor>,
initial_transaction_id: Option<TransactionId>, initial_transaction_id: Option<TransactionId>,
context_store: Entity<ContextStore>,
project: WeakEntity<Project>,
prompt_store: Option<Entity<PromptStore>>,
telemetry: Arc<Telemetry>, telemetry: Arc<Telemetry>,
builder: Arc<PromptBuilder>, builder: Arc<PromptBuilder>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
@@ -62,6 +68,9 @@ impl BufferCodegen {
buffer.clone(), buffer.clone(),
range.clone(), range.clone(),
false, false,
Some(context_store.clone()),
project.clone(),
prompt_store.clone(),
Some(telemetry.clone()), Some(telemetry.clone()),
builder.clone(), builder.clone(),
cx, cx,
@@ -76,6 +85,9 @@ impl BufferCodegen {
buffer, buffer,
range, range,
initial_transaction_id, initial_transaction_id,
context_store,
project,
prompt_store,
telemetry, telemetry,
builder, builder,
}; };
@@ -136,7 +148,6 @@ impl BufferCodegen {
&mut self, &mut self,
primary_model: Arc<dyn LanguageModel>, primary_model: Arc<dyn LanguageModel>,
user_prompt: String, user_prompt: String,
context_task: Shared<Task<Option<LoadedContext>>>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Result<()> { ) -> Result<()> {
let alternative_models = LanguageModelRegistry::read_global(cx) let alternative_models = LanguageModelRegistry::read_global(cx)
@@ -154,6 +165,9 @@ impl BufferCodegen {
self.buffer.clone(), self.buffer.clone(),
self.range.clone(), self.range.clone(),
false, false,
Some(self.context_store.clone()),
self.project.clone(),
self.prompt_store.clone(),
Some(self.telemetry.clone()), Some(self.telemetry.clone()),
self.builder.clone(), self.builder.clone(),
cx, cx,
@@ -166,7 +180,7 @@ impl BufferCodegen {
.zip(&self.alternatives) .zip(&self.alternatives)
{ {
alternative.update(cx, |alternative, cx| { alternative.update(cx, |alternative, cx| {
alternative.start(user_prompt.clone(), context_task.clone(), model.clone(), cx) alternative.start(user_prompt.clone(), model.clone(), cx)
})?; })?;
} }
@@ -229,6 +243,9 @@ pub struct CodegenAlternative {
status: CodegenStatus, status: CodegenStatus,
generation: Task<()>, generation: Task<()>,
diff: Diff, diff: Diff,
context_store: Option<Entity<ContextStore>>,
project: WeakEntity<Project>,
prompt_store: Option<Entity<PromptStore>>,
telemetry: Option<Arc<Telemetry>>, telemetry: Option<Arc<Telemetry>>,
_subscription: gpui::Subscription, _subscription: gpui::Subscription,
builder: Arc<PromptBuilder>, builder: Arc<PromptBuilder>,
@@ -247,6 +264,9 @@ impl CodegenAlternative {
buffer: Entity<MultiBuffer>, buffer: Entity<MultiBuffer>,
range: Range<Anchor>, range: Range<Anchor>,
active: bool, active: bool,
context_store: Option<Entity<ContextStore>>,
project: WeakEntity<Project>,
prompt_store: Option<Entity<PromptStore>>,
telemetry: Option<Arc<Telemetry>>, telemetry: Option<Arc<Telemetry>>,
builder: Arc<PromptBuilder>, builder: Arc<PromptBuilder>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
@@ -287,6 +307,9 @@ impl CodegenAlternative {
status: CodegenStatus::Idle, status: CodegenStatus::Idle,
generation: Task::ready(()), generation: Task::ready(()),
diff: Diff::default(), diff: Diff::default(),
context_store,
project,
prompt_store,
telemetry, telemetry,
_subscription: cx.subscribe(&buffer, Self::handle_buffer_event), _subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
builder, builder,
@@ -343,7 +366,6 @@ impl CodegenAlternative {
pub fn start( pub fn start(
&mut self, &mut self,
user_prompt: String, user_prompt: String,
context_task: Shared<Task<Option<LoadedContext>>>,
model: Arc<dyn LanguageModel>, model: Arc<dyn LanguageModel>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Result<()> { ) -> Result<()> {
@@ -362,7 +384,7 @@ impl CodegenAlternative {
if user_prompt.trim().to_lowercase() == "delete" { if user_prompt.trim().to_lowercase() == "delete" {
async { Ok(LanguageModelTextStream::default()) }.boxed_local() async { Ok(LanguageModelTextStream::default()) }.boxed_local()
} else { } else {
let request = self.build_request(&model, user_prompt, context_task, cx)?; let request = self.build_request(&model, user_prompt, cx)?;
cx.spawn(async move |_, cx| { cx.spawn(async move |_, cx| {
Ok(model.stream_completion_text(request.await, cx).await?) Ok(model.stream_completion_text(request.await, cx).await?)
}) })
@@ -376,7 +398,6 @@ impl CodegenAlternative {
&self, &self,
model: &Arc<dyn LanguageModel>, model: &Arc<dyn LanguageModel>,
user_prompt: String, user_prompt: String,
context_task: Shared<Task<Option<LoadedContext>>>,
cx: &mut App, cx: &mut App,
) -> Result<Task<LanguageModelRequest>> { ) -> Result<Task<LanguageModelRequest>> {
let buffer = self.buffer.read(cx).snapshot(cx); let buffer = self.buffer.read(cx).snapshot(cx);
@@ -408,14 +429,22 @@ 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| {
if let Some(project) = self.project.upgrade() {
let context = context_store
.read(cx)
.context()
.cloned()
.collect::<Vec<_>>();
Some(load_context(context, &project, &self.prompt_store, cx))
} else {
None
}
});
let temperature = AgentSettings::temperature_for_model(model, cx); let temperature = AgentSettings::temperature_for_model(model, cx);
Ok(cx.spawn(async move |_cx| { Ok(cx.spawn(async move |_cx| {
@@ -423,11 +452,12 @@ impl CodegenAlternative {
role: Role::User, role: Role::User,
content: Vec::new(), content: Vec::new(),
cache: false, cache: false,
reasoning_details: None,
}; };
if let Some(context) = context_task.await { if let Some(context_task) = context_task {
context.add_to_request_message(&mut request_message); context_task
.await
.add_to_request_message(&mut request_message);
} }
request_message.content.push(prompt.into()); request_message.content.push(prompt.into());
@@ -456,14 +486,6 @@ impl CodegenAlternative {
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let start_time = Instant::now(); let start_time = Instant::now();
// Make a new snapshot and re-resolve anchor in case the document was modified.
// This can happen often if the editor loses focus and is saved + reformatted,
// as in https://github.com/zed-industries/zed/issues/39088
self.snapshot = self.buffer.read(cx).snapshot(cx);
self.range = self.snapshot.anchor_after(self.range.start)
..self.snapshot.anchor_after(self.range.end);
let snapshot = self.snapshot.clone(); let snapshot = self.snapshot.clone();
let selected_text = snapshot let selected_text = snapshot
.text_for_range(self.range.start..self.range.end) .text_for_range(self.range.start..self.range.end)
@@ -1053,6 +1075,7 @@ impl Diff {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use fs::FakeFs;
use futures::{ use futures::{
Stream, Stream,
stream::{self}, stream::{self},
@@ -1084,12 +1107,17 @@ mod tests {
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5)) snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5))
}); });
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap()); let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, vec![], cx).await;
let codegen = cx.new(|cx| { let codegen = cx.new(|cx| {
CodegenAlternative::new( CodegenAlternative::new(
buffer.clone(), buffer.clone(),
range.clone(), range.clone(),
true, true,
None, None,
project.downgrade(),
None,
None,
prompt_builder, prompt_builder,
cx, cx,
) )
@@ -1146,12 +1174,17 @@ mod tests {
snapshot.anchor_before(Point::new(1, 6))..snapshot.anchor_after(Point::new(1, 6)) snapshot.anchor_before(Point::new(1, 6))..snapshot.anchor_after(Point::new(1, 6))
}); });
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap()); let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, vec![], cx).await;
let codegen = cx.new(|cx| { let codegen = cx.new(|cx| {
CodegenAlternative::new( CodegenAlternative::new(
buffer.clone(), buffer.clone(),
range.clone(), range.clone(),
true, true,
None, None,
project.downgrade(),
None,
None,
prompt_builder, prompt_builder,
cx, cx,
) )
@@ -1210,12 +1243,17 @@ mod tests {
snapshot.anchor_before(Point::new(1, 2))..snapshot.anchor_after(Point::new(1, 2)) snapshot.anchor_before(Point::new(1, 2))..snapshot.anchor_after(Point::new(1, 2))
}); });
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap()); let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, vec![], cx).await;
let codegen = cx.new(|cx| { let codegen = cx.new(|cx| {
CodegenAlternative::new( CodegenAlternative::new(
buffer.clone(), buffer.clone(),
range.clone(), range.clone(),
true, true,
None, None,
project.downgrade(),
None,
None,
prompt_builder, prompt_builder,
cx, cx,
) )
@@ -1274,12 +1312,17 @@ mod tests {
snapshot.anchor_before(Point::new(0, 0))..snapshot.anchor_after(Point::new(4, 2)) snapshot.anchor_before(Point::new(0, 0))..snapshot.anchor_after(Point::new(4, 2))
}); });
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap()); let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, vec![], cx).await;
let codegen = cx.new(|cx| { let codegen = cx.new(|cx| {
CodegenAlternative::new( CodegenAlternative::new(
buffer.clone(), buffer.clone(),
range.clone(), range.clone(),
true, true,
None, None,
project.downgrade(),
None,
None,
prompt_builder, prompt_builder,
cx, cx,
) )
@@ -1326,12 +1369,17 @@ mod tests {
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(1, 14)) snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(1, 14))
}); });
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap()); let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, vec![], cx).await;
let codegen = cx.new(|cx| { let codegen = cx.new(|cx| {
CodegenAlternative::new( CodegenAlternative::new(
buffer.clone(), buffer.clone(),
range.clone(), range.clone(),
false, false,
None, None,
project.downgrade(),
None,
None,
prompt_builder, prompt_builder,
cx, cx,
) )

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,931 @@
mod completion_provider;
pub(crate) mod fetch_context_picker;
pub(crate) mod file_context_picker;
pub(crate) mod rules_context_picker;
pub(crate) mod symbol_context_picker;
pub(crate) mod thread_context_picker;
use std::ops::Range;
use std::path::PathBuf;
use std::sync::Arc;
use agent::{HistoryEntry, HistoryEntryId, HistoryStore};
use agent_client_protocol as acp;
use anyhow::{Result, anyhow};
use collections::HashSet;
pub use completion_provider::ContextPickerCompletionProvider;
use editor::display_map::{Crease, CreaseId, CreaseMetadata, FoldId};
use editor::{Anchor, Editor, ExcerptId, FoldPlaceholder, ToOffset};
use fetch_context_picker::FetchContextPicker;
use file_context_picker::FileContextPicker;
use file_context_picker::render_file_context_entry;
use gpui::{
App, DismissEvent, Empty, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task,
WeakEntity,
};
use language::Buffer;
use multi_buffer::MultiBufferRow;
use project::ProjectPath;
use prompt_store::PromptStore;
use rules_context_picker::{RulesContextEntry, RulesContextPicker};
use symbol_context_picker::SymbolContextPicker;
use thread_context_picker::render_thread_context_entry;
use ui::{
ButtonLike, ContextMenu, ContextMenuEntry, ContextMenuItem, Disclosure, TintColor, prelude::*,
};
use util::paths::PathStyle;
use util::rel_path::RelPath;
use workspace::{Workspace, notifications::NotifyResultExt};
use crate::context_picker::thread_context_picker::ThreadContextPicker;
use crate::{context::RULES_ICON, context_store::ContextStore};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ContextPickerEntry {
Mode(ContextPickerMode),
Action(ContextPickerAction),
}
impl ContextPickerEntry {
pub fn keyword(&self) -> &'static str {
match self {
Self::Mode(mode) => mode.keyword(),
Self::Action(action) => action.keyword(),
}
}
pub fn label(&self) -> &'static str {
match self {
Self::Mode(mode) => mode.label(),
Self::Action(action) => action.label(),
}
}
pub fn icon(&self) -> IconName {
match self {
Self::Mode(mode) => mode.icon(),
Self::Action(action) => action.icon(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ContextPickerMode {
File,
Symbol,
Fetch,
Thread,
Rules,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ContextPickerAction {
AddSelections,
}
impl ContextPickerAction {
pub fn keyword(&self) -> &'static str {
match self {
Self::AddSelections => "selection",
}
}
pub fn label(&self) -> &'static str {
match self {
Self::AddSelections => "Selection",
}
}
pub fn icon(&self) -> IconName {
match self {
Self::AddSelections => IconName::Reader,
}
}
}
impl TryFrom<&str> for ContextPickerMode {
type Error = String;
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value {
"file" => Ok(Self::File),
"symbol" => Ok(Self::Symbol),
"fetch" => Ok(Self::Fetch),
"thread" => Ok(Self::Thread),
"rule" => Ok(Self::Rules),
_ => Err(format!("Invalid context picker mode: {}", value)),
}
}
}
impl ContextPickerMode {
pub fn keyword(&self) -> &'static str {
match self {
Self::File => "file",
Self::Symbol => "symbol",
Self::Fetch => "fetch",
Self::Thread => "thread",
Self::Rules => "rule",
}
}
pub fn label(&self) -> &'static str {
match self {
Self::File => "Files & Directories",
Self::Symbol => "Symbols",
Self::Fetch => "Fetch",
Self::Thread => "Threads",
Self::Rules => "Rules",
}
}
pub fn icon(&self) -> IconName {
match self {
Self::File => IconName::File,
Self::Symbol => IconName::Code,
Self::Fetch => IconName::ToolWeb,
Self::Thread => IconName::Thread,
Self::Rules => RULES_ICON,
}
}
}
#[derive(Debug, Clone)]
enum ContextPickerState {
Default(Entity<ContextMenu>),
File(Entity<FileContextPicker>),
Symbol(Entity<SymbolContextPicker>),
Fetch(Entity<FetchContextPicker>),
Thread(Entity<ThreadContextPicker>),
Rules(Entity<RulesContextPicker>),
}
pub(super) struct ContextPicker {
mode: ContextPickerState,
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
thread_store: Option<WeakEntity<HistoryStore>>,
prompt_store: Option<WeakEntity<PromptStore>>,
_subscriptions: Vec<Subscription>,
}
impl ContextPicker {
pub fn new(
workspace: WeakEntity<Workspace>,
thread_store: Option<WeakEntity<HistoryStore>>,
prompt_store: Option<WeakEntity<PromptStore>>,
context_store: WeakEntity<ContextStore>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let subscriptions = context_store
.upgrade()
.map(|context_store| {
cx.observe(&context_store, |this, _, cx| this.notify_current_picker(cx))
})
.into_iter()
.chain(
thread_store
.as_ref()
.and_then(|thread_store| thread_store.upgrade())
.map(|thread_store| {
cx.observe(&thread_store, |this, _, cx| this.notify_current_picker(cx))
}),
)
.collect::<Vec<Subscription>>();
ContextPicker {
mode: ContextPickerState::Default(ContextMenu::build(
window,
cx,
|menu, _window, _cx| menu,
)),
workspace,
context_store,
thread_store,
prompt_store,
_subscriptions: subscriptions,
}
}
pub fn init(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.mode = ContextPickerState::Default(self.build_menu(window, cx));
cx.notify();
}
fn build_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Entity<ContextMenu> {
let context_picker = cx.entity();
let menu = ContextMenu::build(window, cx, move |menu, _window, cx| {
let Some(workspace) = self.workspace.upgrade() else {
return menu;
};
let path_style = workspace.read(cx).path_style(cx);
let recent = self.recent_entries(cx);
let has_recent = !recent.is_empty();
let recent_entries = recent
.into_iter()
.enumerate()
.map(|(ix, entry)| {
self.recent_menu_item(context_picker.clone(), ix, entry, path_style)
})
.collect::<Vec<_>>();
let entries = self
.workspace
.upgrade()
.map(|workspace| {
available_context_picker_entries(
&self.prompt_store,
&self.thread_store,
&workspace,
cx,
)
})
.unwrap_or_default();
menu.when(has_recent, |menu| {
menu.custom_row(|_, _| {
div()
.mb_1()
.child(
Label::new("Recent")
.color(Color::Muted)
.size(LabelSize::Small),
)
.into_any_element()
})
})
.extend(recent_entries)
.when(has_recent, |menu| menu.separator())
.extend(entries.into_iter().map(|entry| {
let context_picker = context_picker.clone();
ContextMenuEntry::new(entry.label())
.icon(entry.icon())
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.handler(move |window, cx| {
context_picker.update(cx, |this, cx| this.select_entry(entry, window, cx))
})
}))
.keep_open_on_confirm(true)
});
cx.subscribe(&menu, move |_, _, _: &DismissEvent, cx| {
cx.emit(DismissEvent);
})
.detach();
menu
}
/// Whether threads are allowed as context.
pub fn allow_threads(&self) -> bool {
self.thread_store.is_some()
}
fn select_entry(
&mut self,
entry: ContextPickerEntry,
window: &mut Window,
cx: &mut Context<Self>,
) {
let context_picker = cx.entity().downgrade();
match entry {
ContextPickerEntry::Mode(mode) => match mode {
ContextPickerMode::File => {
self.mode = ContextPickerState::File(cx.new(|cx| {
FileContextPicker::new(
context_picker.clone(),
self.workspace.clone(),
self.context_store.clone(),
window,
cx,
)
}));
}
ContextPickerMode::Symbol => {
self.mode = ContextPickerState::Symbol(cx.new(|cx| {
SymbolContextPicker::new(
context_picker.clone(),
self.workspace.clone(),
self.context_store.clone(),
window,
cx,
)
}));
}
ContextPickerMode::Rules => {
if let Some(prompt_store) = self.prompt_store.as_ref() {
self.mode = ContextPickerState::Rules(cx.new(|cx| {
RulesContextPicker::new(
prompt_store.clone(),
context_picker.clone(),
self.context_store.clone(),
window,
cx,
)
}));
}
}
ContextPickerMode::Fetch => {
self.mode = ContextPickerState::Fetch(cx.new(|cx| {
FetchContextPicker::new(
context_picker.clone(),
self.workspace.clone(),
self.context_store.clone(),
window,
cx,
)
}));
}
ContextPickerMode::Thread => {
if let Some(thread_store) = self.thread_store.clone() {
self.mode = ContextPickerState::Thread(cx.new(|cx| {
ThreadContextPicker::new(
thread_store,
context_picker.clone(),
self.context_store.clone(),
self.workspace.clone(),
window,
cx,
)
}));
}
}
},
ContextPickerEntry::Action(action) => match action {
ContextPickerAction::AddSelections => {
if let Some((context_store, workspace)) =
self.context_store.upgrade().zip(self.workspace.upgrade())
{
add_selections_as_context(&context_store, &workspace, cx);
}
cx.emit(DismissEvent);
}
},
}
cx.notify();
cx.focus_self(window);
}
pub fn select_first(&mut self, window: &mut Window, cx: &mut Context<Self>) {
// Other variants already select their first entry on open automatically
if let ContextPickerState::Default(entity) = &self.mode {
entity.update(cx, |entity, cx| {
entity.select_first(&Default::default(), window, cx)
})
}
}
fn recent_menu_item(
&self,
context_picker: Entity<ContextPicker>,
ix: usize,
entry: RecentEntry,
path_style: PathStyle,
) -> ContextMenuItem {
match entry {
RecentEntry::File {
project_path,
path_prefix,
} => {
let context_store = self.context_store.clone();
let worktree_id = project_path.worktree_id;
let path = project_path.path.clone();
ContextMenuItem::custom_entry(
move |_window, cx| {
render_file_context_entry(
ElementId::named_usize("ctx-recent", ix),
worktree_id,
&path,
&path_prefix,
false,
path_style,
context_store.clone(),
cx,
)
.into_any()
},
move |window, cx| {
context_picker.update(cx, |this, cx| {
this.add_recent_file(project_path.clone(), window, cx);
})
},
None,
)
}
RecentEntry::Thread(thread) => {
let context_store = self.context_store.clone();
let view_thread = thread.clone();
ContextMenuItem::custom_entry(
move |_window, cx| {
render_thread_context_entry(&view_thread, context_store.clone(), cx)
.into_any()
},
move |window, cx| {
context_picker.update(cx, |this, cx| {
this.add_recent_thread(thread.clone(), window, cx)
.detach_and_log_err(cx);
})
},
None,
)
}
}
}
fn add_recent_file(
&self,
project_path: ProjectPath,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(context_store) = self.context_store.upgrade() else {
return;
};
let task = context_store.update(cx, |context_store, cx| {
context_store.add_file_from_path(project_path.clone(), true, cx)
});
cx.spawn_in(window, async move |_, cx| task.await.notify_async_err(cx))
.detach();
cx.notify();
}
fn add_recent_thread(
&self,
entry: HistoryEntry,
_window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let Some(context_store) = self.context_store.upgrade() else {
return Task::ready(Err(anyhow!("context store not available")));
};
let Some(project) = self
.workspace
.upgrade()
.map(|workspace| workspace.read(cx).project().clone())
else {
return Task::ready(Err(anyhow!("project not available")));
};
match entry {
HistoryEntry::AcpThread(thread) => {
let Some(thread_store) = self
.thread_store
.as_ref()
.and_then(|thread_store| thread_store.upgrade())
else {
return Task::ready(Err(anyhow!("thread store not available")));
};
let load_thread_task =
agent::load_agent_thread(thread.id, thread_store, project, cx);
cx.spawn(async move |this, cx| {
let thread = load_thread_task.await?;
context_store.update(cx, |context_store, cx| {
context_store.add_thread(thread, true, cx);
})?;
this.update(cx, |_this, cx| cx.notify())
})
}
HistoryEntry::TextThread(thread) => {
let Some(thread_store) = self
.thread_store
.as_ref()
.and_then(|thread_store| thread_store.upgrade())
else {
return Task::ready(Err(anyhow!("text thread store not available")));
};
let task = thread_store.update(cx, |this, cx| {
this.load_text_thread(thread.path.clone(), cx)
});
cx.spawn(async move |this, cx| {
let thread = task.await?;
context_store.update(cx, |context_store, cx| {
context_store.add_text_thread(thread, true, cx);
})?;
this.update(cx, |_this, cx| cx.notify())
})
}
}
}
fn recent_entries(&self, cx: &mut App) -> Vec<RecentEntry> {
let Some(workspace) = self.workspace.upgrade() else {
return vec![];
};
let Some(context_store) = self.context_store.upgrade() else {
return vec![];
};
recent_context_picker_entries_with_store(
context_store,
self.thread_store.clone(),
workspace,
None,
cx,
)
}
fn notify_current_picker(&mut self, cx: &mut Context<Self>) {
match &self.mode {
ContextPickerState::Default(entity) => entity.update(cx, |_, cx| cx.notify()),
ContextPickerState::File(entity) => entity.update(cx, |_, cx| cx.notify()),
ContextPickerState::Symbol(entity) => entity.update(cx, |_, cx| cx.notify()),
ContextPickerState::Fetch(entity) => entity.update(cx, |_, cx| cx.notify()),
ContextPickerState::Thread(entity) => entity.update(cx, |_, cx| cx.notify()),
ContextPickerState::Rules(entity) => entity.update(cx, |_, cx| cx.notify()),
}
}
}
impl EventEmitter<DismissEvent> for ContextPicker {}
impl Focusable for ContextPicker {
fn focus_handle(&self, cx: &App) -> FocusHandle {
match &self.mode {
ContextPickerState::Default(menu) => menu.focus_handle(cx),
ContextPickerState::File(file_picker) => file_picker.focus_handle(cx),
ContextPickerState::Symbol(symbol_picker) => symbol_picker.focus_handle(cx),
ContextPickerState::Fetch(fetch_picker) => fetch_picker.focus_handle(cx),
ContextPickerState::Thread(thread_picker) => thread_picker.focus_handle(cx),
ContextPickerState::Rules(user_rules_picker) => user_rules_picker.focus_handle(cx),
}
}
}
impl Render for ContextPicker {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.w(px(400.))
.min_w(px(400.))
.map(|parent| match &self.mode {
ContextPickerState::Default(menu) => parent.child(menu.clone()),
ContextPickerState::File(file_picker) => parent.child(file_picker.clone()),
ContextPickerState::Symbol(symbol_picker) => parent.child(symbol_picker.clone()),
ContextPickerState::Fetch(fetch_picker) => parent.child(fetch_picker.clone()),
ContextPickerState::Thread(thread_picker) => parent.child(thread_picker.clone()),
ContextPickerState::Rules(user_rules_picker) => {
parent.child(user_rules_picker.clone())
}
})
}
}
pub(crate) enum RecentEntry {
File {
project_path: ProjectPath,
path_prefix: Arc<RelPath>,
},
Thread(HistoryEntry),
}
pub(crate) fn available_context_picker_entries(
prompt_store: &Option<WeakEntity<PromptStore>>,
thread_store: &Option<WeakEntity<HistoryStore>>,
workspace: &Entity<Workspace>,
cx: &mut App,
) -> Vec<ContextPickerEntry> {
let mut entries = vec![
ContextPickerEntry::Mode(ContextPickerMode::File),
ContextPickerEntry::Mode(ContextPickerMode::Symbol),
];
let has_selection = workspace
.read(cx)
.active_item(cx)
.and_then(|item| item.downcast::<Editor>())
.is_some_and(|editor| {
editor.update(cx, |editor, cx| {
editor.has_non_empty_selection(&editor.display_snapshot(cx))
})
});
if has_selection {
entries.push(ContextPickerEntry::Action(
ContextPickerAction::AddSelections,
));
}
if thread_store.is_some() {
entries.push(ContextPickerEntry::Mode(ContextPickerMode::Thread));
}
if prompt_store.is_some() {
entries.push(ContextPickerEntry::Mode(ContextPickerMode::Rules));
}
entries.push(ContextPickerEntry::Mode(ContextPickerMode::Fetch));
entries
}
fn recent_context_picker_entries_with_store(
context_store: Entity<ContextStore>,
thread_store: Option<WeakEntity<HistoryStore>>,
workspace: Entity<Workspace>,
exclude_path: Option<ProjectPath>,
cx: &App,
) -> Vec<RecentEntry> {
let project = workspace.read(cx).project();
let mut exclude_paths = context_store.read(cx).file_paths(cx);
exclude_paths.extend(exclude_path);
let exclude_paths = exclude_paths
.into_iter()
.filter_map(|project_path| project.read(cx).absolute_path(&project_path, cx))
.collect();
let exclude_threads = context_store.read(cx).thread_ids();
recent_context_picker_entries(thread_store, workspace, &exclude_paths, exclude_threads, cx)
}
pub(crate) fn recent_context_picker_entries(
thread_store: Option<WeakEntity<HistoryStore>>,
workspace: Entity<Workspace>,
exclude_paths: &HashSet<PathBuf>,
exclude_threads: &HashSet<acp::SessionId>,
cx: &App,
) -> Vec<RecentEntry> {
let mut recent = Vec::with_capacity(6);
let workspace = workspace.read(cx);
let project = workspace.project().read(cx);
let include_root_name = workspace.visible_worktrees(cx).count() > 1;
recent.extend(
workspace
.recent_navigation_history_iter(cx)
.filter(|(_, abs_path)| {
abs_path
.as_ref()
.is_none_or(|path| !exclude_paths.contains(path.as_path()))
})
.take(4)
.filter_map(|(project_path, _)| {
project
.worktree_for_id(project_path.worktree_id, cx)
.map(|worktree| {
let path_prefix = if include_root_name {
worktree.read(cx).root_name().into()
} else {
RelPath::empty().into()
};
RecentEntry::File {
project_path,
path_prefix,
}
})
}),
);
if let Some(thread_store) = thread_store.and_then(|store| store.upgrade()) {
const RECENT_THREADS_COUNT: usize = 2;
recent.extend(
thread_store
.read(cx)
.recently_opened_entries(cx)
.iter()
.filter(|e| match e.id() {
HistoryEntryId::AcpThread(session_id) => !exclude_threads.contains(&session_id),
HistoryEntryId::TextThread(path) => {
!exclude_paths.contains(&path.to_path_buf())
}
})
.take(RECENT_THREADS_COUNT)
.map(|thread| RecentEntry::Thread(thread.clone())),
);
}
recent
}
fn add_selections_as_context(
context_store: &Entity<ContextStore>,
workspace: &Entity<Workspace>,
cx: &mut App,
) {
let selection_ranges = selection_ranges(workspace, cx);
context_store.update(cx, |context_store, cx| {
for (buffer, range) in selection_ranges {
context_store.add_selection(buffer, range, cx);
}
})
}
pub(crate) fn selection_ranges(
workspace: &Entity<Workspace>,
cx: &mut App,
) -> Vec<(Entity<Buffer>, Range<text::Anchor>)> {
let Some(editor) = workspace
.read(cx)
.active_item(cx)
.and_then(|item| item.act_as::<Editor>(cx))
else {
return Vec::new();
};
editor.update(cx, |editor, cx| {
let selections = editor.selections.all_adjusted(&editor.display_snapshot(cx));
let buffer = editor.buffer().clone().read(cx);
let snapshot = buffer.snapshot(cx);
selections
.into_iter()
.map(|s| snapshot.anchor_after(s.start)..snapshot.anchor_before(s.end))
.flat_map(|range| {
let (start_buffer, start) = buffer.text_anchor_for_position(range.start, cx)?;
let (end_buffer, end) = buffer.text_anchor_for_position(range.end, cx)?;
if start_buffer != end_buffer {
return None;
}
Some((start_buffer, start..end))
})
.collect::<Vec<_>>()
})
}
pub(crate) fn insert_crease_for_mention(
excerpt_id: ExcerptId,
crease_start: text::Anchor,
content_len: usize,
crease_label: SharedString,
crease_icon_path: SharedString,
editor_entity: Entity<Editor>,
window: &mut Window,
cx: &mut App,
) -> Option<CreaseId> {
editor_entity.update(cx, |editor, cx| {
let snapshot = editor.buffer().read(cx).snapshot(cx);
let start = snapshot.anchor_in_excerpt(excerpt_id, crease_start)?;
let start = start.bias_right(&snapshot);
let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
let crease = crease_for_mention(
crease_label,
crease_icon_path,
start..end,
editor_entity.downgrade(),
);
let ids = editor.insert_creases(vec![crease.clone()], cx);
editor.fold_creases(vec![crease], false, window, cx);
Some(ids[0])
})
}
pub fn crease_for_mention(
label: SharedString,
icon_path: SharedString,
range: Range<Anchor>,
editor_entity: WeakEntity<Editor>,
) -> Crease<Anchor> {
let placeholder = FoldPlaceholder {
render: render_fold_icon_button(icon_path.clone(), label.clone(), editor_entity),
merge_adjacent: false,
..Default::default()
};
let render_trailer = move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any();
Crease::inline(range, placeholder, fold_toggle("mention"), render_trailer)
.with_metadata(CreaseMetadata { icon_path, label })
}
fn render_fold_icon_button(
icon_path: SharedString,
label: SharedString,
editor: WeakEntity<Editor>,
) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
Arc::new({
move |fold_id, fold_range, cx| {
let is_in_text_selection = editor
.update(cx, |editor, cx| editor.is_range_selected(&fold_range, cx))
.unwrap_or_default();
ButtonLike::new(fold_id)
.style(ButtonStyle::Filled)
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
.toggle_state(is_in_text_selection)
.child(
h_flex()
.gap_1()
.child(
Icon::from_path(icon_path.clone())
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(
Label::new(label.clone())
.size(LabelSize::Small)
.buffer_font(cx)
.single_line(),
),
)
.into_any_element()
}
})
}
fn fold_toggle(
name: &'static str,
) -> impl Fn(
MultiBufferRow,
bool,
Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
&mut Window,
&mut App,
) -> AnyElement {
move |row, is_folded, fold, _window, _cx| {
Disclosure::new((name, row.0 as u64), !is_folded)
.toggle_state(is_folded)
.on_click(move |_e, window, cx| fold(!is_folded, window, cx))
.into_any_element()
}
}
pub struct MentionLink;
impl MentionLink {
const FILE: &str = "@file";
const SYMBOL: &str = "@symbol";
const SELECTION: &str = "@selection";
const THREAD: &str = "@thread";
const FETCH: &str = "@fetch";
const RULE: &str = "@rule";
const TEXT_THREAD_URL_PREFIX: &str = "text-thread://";
pub fn for_file(file_name: &str, full_path: &str) -> String {
format!("[@{}]({}:{})", file_name, Self::FILE, full_path)
}
pub fn for_symbol(symbol_name: &str, full_path: &str) -> String {
format!(
"[@{}]({}:{}:{})",
symbol_name,
Self::SYMBOL,
full_path,
symbol_name
)
}
pub fn for_selection(file_name: &str, full_path: &str, line_range: Range<usize>) -> String {
format!(
"[@{} ({}-{})]({}:{}:{}-{})",
file_name,
line_range.start + 1,
line_range.end + 1,
Self::SELECTION,
full_path,
line_range.start,
line_range.end
)
}
pub fn for_thread(thread: &HistoryEntry) -> String {
match thread {
HistoryEntry::AcpThread(thread) => {
format!("[@{}]({}:{})", thread.title, Self::THREAD, thread.id)
}
HistoryEntry::TextThread(thread) => {
let filename = thread
.path
.file_name()
.unwrap_or_default()
.to_string_lossy();
let escaped_filename = urlencoding::encode(&filename);
format!(
"[@{}]({}:{}{})",
thread.title,
Self::THREAD,
Self::TEXT_THREAD_URL_PREFIX,
escaped_filename
)
}
}
}
pub fn for_fetch(url: &str) -> String {
format!("[@{}]({}:{})", url, Self::FETCH, url)
}
pub fn for_rule(rule: &RulesContextEntry) -> String {
format!("[@{}]({}:{})", rule.title, Self::RULE, rule.prompt_id.0)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,252 @@
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;
use anyhow::{Context as _, Result, bail};
use futures::AsyncReadExt as _;
use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown};
use http_client::{AsyncBody, HttpClientWithUrl};
use picker::{Picker, PickerDelegate};
use ui::{Context, ListItem, Window, prelude::*};
use workspace::Workspace;
use crate::{context_picker::ContextPicker, context_store::ContextStore};
pub struct FetchContextPicker {
picker: Entity<Picker<FetchContextPickerDelegate>>,
}
impl FetchContextPicker {
pub fn new(
context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let delegate = FetchContextPickerDelegate::new(context_picker, workspace, context_store);
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
Self { picker }
}
}
impl Focusable for FetchContextPicker {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for FetchContextPicker {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
self.picker.clone()
}
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
enum ContentType {
Html,
Plaintext,
Json,
}
pub struct FetchContextPickerDelegate {
context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
url: String,
}
impl FetchContextPickerDelegate {
pub fn new(
context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
) -> Self {
FetchContextPickerDelegate {
context_picker,
workspace,
context_store,
url: String::new(),
}
}
}
pub(crate) async fn fetch_url_content(
http_client: Arc<HttpClientWithUrl>,
url: String,
) -> Result<String> {
let url = if !url.starts_with("https://") && !url.starts_with("http://") {
format!("https://{url}")
} else {
url
};
let mut response = http_client.get(&url, AsyncBody::default(), true).await?;
let mut body = Vec::new();
response
.body_mut()
.read_to_end(&mut body)
.await
.context("error reading response body")?;
if response.status().is_client_error() {
let text = String::from_utf8_lossy(body.as_slice());
bail!(
"status error {}, response: {text:?}",
response.status().as_u16()
);
}
let Some(content_type) = response.headers().get("content-type") else {
bail!("missing Content-Type header");
};
let content_type = content_type
.to_str()
.context("invalid Content-Type header")?;
let content_type = match content_type {
"text/html" => ContentType::Html,
"text/plain" => ContentType::Plaintext,
"application/json" => ContentType::Json,
_ => ContentType::Html,
};
match content_type {
ContentType::Html => {
let mut handlers: Vec<TagHandler> = vec![
Rc::new(RefCell::new(markdown::WebpageChromeRemover)),
Rc::new(RefCell::new(markdown::ParagraphHandler)),
Rc::new(RefCell::new(markdown::HeadingHandler)),
Rc::new(RefCell::new(markdown::ListHandler)),
Rc::new(RefCell::new(markdown::TableHandler::new())),
Rc::new(RefCell::new(markdown::StyledTextHandler)),
];
if url.contains("wikipedia.org") {
use html_to_markdown::structure::wikipedia;
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaChromeRemover)));
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaInfoboxHandler)));
handlers.push(Rc::new(
RefCell::new(wikipedia::WikipediaCodeHandler::new()),
));
} else {
handlers.push(Rc::new(RefCell::new(markdown::CodeHandler)));
}
convert_html_to_markdown(&body[..], &mut handlers)
}
ContentType::Plaintext => Ok(std::str::from_utf8(&body)?.to_owned()),
ContentType::Json => {
let json: serde_json::Value = serde_json::from_slice(&body)?;
Ok(format!(
"```json\n{}\n```",
serde_json::to_string_pretty(&json)?
))
}
}
}
impl PickerDelegate for FetchContextPickerDelegate {
type ListItem = ListItem;
fn match_count(&self) -> usize {
if self.url.is_empty() { 0 } else { 1 }
}
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
Some("Enter the URL that you would like to fetch".into())
}
fn selected_index(&self) -> usize {
0
}
fn set_selected_index(
&mut self,
_ix: usize,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) {
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
"Enter a URL…".into()
}
fn update_matches(
&mut self,
query: String,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) -> Task<()> {
self.url = query;
Task::ready(())
}
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
let Some(workspace) = self.workspace.upgrade() else {
return;
};
let http_client = workspace.read(cx).client().http_client();
let url = self.url.clone();
cx.spawn_in(window, async move |this, cx| {
let text = cx
.background_spawn(fetch_url_content(http_client, url.clone()))
.await?;
this.update(cx, |this, cx| {
this.delegate.context_store.update(cx, |context_store, cx| {
context_store.add_fetched_url(url, text, cx)
})
})??;
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
self.context_picker
.update(cx, |_, cx| {
cx.emit(DismissEvent);
})
.ok();
}
fn render_match(
&self,
ix: usize,
selected: bool,
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let added = self
.context_store
.upgrade()
.is_some_and(|context_store| context_store.read(cx).includes_url(&self.url));
Some(
ListItem::new(ix)
.inset(true)
.toggle_state(selected)
.child(Label::new(self.url.clone()))
.when(added, |child| {
child.disabled(true).end_slot(
h_flex()
.gap_1()
.child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success),
)
.child(Label::new("Added").size(LabelSize::Small)),
)
}),
)
}
}

View File

@@ -0,0 +1,392 @@
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use file_icons::FileIcons;
use fuzzy::PathMatch;
use gpui::{
App, AppContext, DismissEvent, Entity, FocusHandle, Focusable, Stateful, Task, WeakEntity,
};
use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, ProjectPath, WorktreeId};
use ui::{ListItem, Tooltip, prelude::*};
use util::{ResultExt as _, paths::PathStyle, rel_path::RelPath};
use workspace::Workspace;
use crate::{
context_picker::ContextPicker,
context_store::{ContextStore, FileInclusion},
};
pub struct FileContextPicker {
picker: Entity<Picker<FileContextPickerDelegate>>,
}
impl FileContextPicker {
pub fn new(
context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let delegate = FileContextPickerDelegate::new(context_picker, workspace, context_store);
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
Self { picker }
}
}
impl Focusable for FileContextPicker {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for FileContextPicker {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
self.picker.clone()
}
}
pub struct FileContextPickerDelegate {
context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
matches: Vec<FileMatch>,
selected_index: usize,
}
impl FileContextPickerDelegate {
pub fn new(
context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
) -> Self {
Self {
context_picker,
workspace,
context_store,
matches: Vec::new(),
selected_index: 0,
}
}
}
impl PickerDelegate for FileContextPickerDelegate {
type ListItem = ListItem;
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(
&mut self,
ix: usize,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) {
self.selected_index = ix;
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
"Search files & directories…".into()
}
fn update_matches(
&mut self,
query: String,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
let Some(workspace) = self.workspace.upgrade() else {
return Task::ready(());
};
let search_task = search_files(query, Arc::<AtomicBool>::default(), &workspace, cx);
cx.spawn_in(window, async move |this, cx| {
// TODO: This should be probably be run in the background.
let paths = search_task.await;
this.update(cx, |this, _cx| {
this.delegate.matches = paths;
})
.log_err();
})
}
fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
let Some(FileMatch { mat, .. }) = self.matches.get(self.selected_index) else {
return;
};
let project_path = ProjectPath {
worktree_id: WorktreeId::from_usize(mat.worktree_id),
path: mat.path.clone(),
};
let is_directory = mat.is_dir;
self.context_store
.update(cx, |context_store, cx| {
if is_directory {
context_store
.add_directory(&project_path, true, cx)
.log_err();
} else {
context_store
.add_file_from_path(project_path.clone(), true, cx)
.detach_and_log_err(cx);
}
})
.ok();
}
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
self.context_picker
.update(cx, |_, cx| {
cx.emit(DismissEvent);
})
.ok();
}
fn render_match(
&self,
ix: usize,
selected: bool,
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let FileMatch { mat, .. } = &self.matches.get(ix)?;
let workspace = self.workspace.upgrade()?;
let path_style = workspace.read(cx).path_style(cx);
Some(
ListItem::new(ix)
.inset(true)
.toggle_state(selected)
.child(render_file_context_entry(
ElementId::named_usize("file-ctx-picker", ix),
WorktreeId::from_usize(mat.worktree_id),
&mat.path,
&mat.path_prefix,
mat.is_dir,
path_style,
self.context_store.clone(),
cx,
)),
)
}
}
pub struct FileMatch {
pub mat: PathMatch,
pub is_recent: bool,
}
pub(crate) fn search_files(
query: String,
cancellation_flag: Arc<AtomicBool>,
workspace: &Entity<Workspace>,
cx: &App,
) -> Task<Vec<FileMatch>> {
if query.is_empty() {
let workspace = workspace.read(cx);
let project = workspace.project().read(cx);
let visible_worktrees = workspace.visible_worktrees(cx).collect::<Vec<_>>();
let include_root_name = visible_worktrees.len() > 1;
let recent_matches = workspace
.recent_navigation_history(Some(10), cx)
.into_iter()
.map(|(project_path, _)| {
let path_prefix = if include_root_name {
project
.worktree_for_id(project_path.worktree_id, cx)
.map(|wt| wt.read(cx).root_name().into())
.unwrap_or_else(|| RelPath::empty().into())
} else {
RelPath::empty().into()
};
FileMatch {
mat: PathMatch {
score: 0.,
positions: Vec::new(),
worktree_id: project_path.worktree_id.to_usize(),
path: project_path.path,
path_prefix,
distance_to_relative_ancestor: 0,
is_dir: false,
},
is_recent: true,
}
});
let file_matches = visible_worktrees.into_iter().flat_map(|worktree| {
let worktree = worktree.read(cx);
let path_prefix: Arc<RelPath> = if include_root_name {
worktree.root_name().into()
} else {
RelPath::empty().into()
};
worktree.entries(false, 0).map(move |entry| FileMatch {
mat: PathMatch {
score: 0.,
positions: Vec::new(),
worktree_id: worktree.id().to_usize(),
path: entry.path.clone(),
path_prefix: path_prefix.clone(),
distance_to_relative_ancestor: 0,
is_dir: entry.is_dir(),
},
is_recent: false,
})
});
Task::ready(recent_matches.chain(file_matches).collect())
} else {
let worktrees = workspace.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
let include_root_name = worktrees.len() > 1;
let candidate_sets = worktrees
.into_iter()
.map(|worktree| {
let worktree = worktree.read(cx);
PathMatchCandidateSet {
snapshot: worktree.snapshot(),
include_ignored: worktree.root_entry().is_some_and(|entry| entry.is_ignored),
include_root_name,
candidates: project::Candidates::Entries,
}
})
.collect::<Vec<_>>();
let executor = cx.background_executor().clone();
cx.foreground_executor().spawn(async move {
fuzzy::match_path_sets(
candidate_sets.as_slice(),
query.as_str(),
&None,
false,
100,
&cancellation_flag,
executor,
)
.await
.into_iter()
.map(|mat| FileMatch {
mat,
is_recent: false,
})
.collect::<Vec<_>>()
})
}
}
pub fn extract_file_name_and_directory(
path: &RelPath,
path_prefix: &RelPath,
path_style: PathStyle,
) -> (SharedString, Option<SharedString>) {
// If path is empty, this means we're matching with the root directory itself
// so we use the path_prefix as the name
if path.is_empty() && !path_prefix.is_empty() {
return (path_prefix.display(path_style).to_string().into(), None);
}
let full_path = path_prefix.join(path);
let file_name = full_path.file_name().unwrap_or_default();
let display_path = full_path.display(path_style);
let (directory, file_name) = display_path.split_at(display_path.len() - file_name.len());
(
file_name.to_string().into(),
Some(SharedString::new(directory)).filter(|dir| !dir.is_empty()),
)
}
pub fn render_file_context_entry(
id: ElementId,
worktree_id: WorktreeId,
path: &Arc<RelPath>,
path_prefix: &Arc<RelPath>,
is_directory: bool,
path_style: PathStyle,
context_store: WeakEntity<ContextStore>,
cx: &App,
) -> Stateful<Div> {
let (file_name, directory) = extract_file_name_and_directory(path, path_prefix, path_style);
let added = context_store.upgrade().and_then(|context_store| {
let project_path = ProjectPath {
worktree_id,
path: path.clone(),
};
if is_directory {
context_store
.read(cx)
.path_included_in_directory(&project_path, cx)
} else {
context_store.read(cx).file_path_included(&project_path, cx)
}
});
let file_icon = if is_directory {
FileIcons::get_folder_icon(false, path.as_std_path(), cx)
} else {
FileIcons::get_icon(path.as_std_path(), cx)
}
.map(Icon::from_path)
.unwrap_or_else(|| Icon::new(IconName::File));
h_flex()
.id(id)
.gap_1p5()
.w_full()
.child(file_icon.size(IconSize::Small).color(Color::Muted))
.child(
h_flex()
.gap_1()
.child(Label::new(file_name))
.children(directory.map(|directory| {
Label::new(directory)
.size(LabelSize::Small)
.color(Color::Muted)
})),
)
.when_some(added, |el, added| match added {
FileInclusion::Direct => el.child(
h_flex()
.w_full()
.justify_end()
.gap_0p5()
.child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success),
)
.child(Label::new("Added").size(LabelSize::Small)),
),
FileInclusion::InDirectory { full_path } => {
let directory_full_path = full_path.to_string_lossy().into_owned();
el.child(
h_flex()
.w_full()
.justify_end()
.gap_0p5()
.child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success),
)
.child(Label::new("Included").size(LabelSize::Small)),
)
.tooltip(Tooltip::text(format!("in {directory_full_path}")))
}
})
}

View File

@@ -0,0 +1,224 @@
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
use picker::{Picker, PickerDelegate};
use prompt_store::{PromptId, PromptStore, UserPromptId};
use ui::{ListItem, prelude::*};
use util::ResultExt as _;
use crate::{
context::RULES_ICON,
context_picker::ContextPicker,
context_store::{self, ContextStore},
};
pub struct RulesContextPicker {
picker: Entity<Picker<RulesContextPickerDelegate>>,
}
impl RulesContextPicker {
pub fn new(
prompt_store: WeakEntity<PromptStore>,
context_picker: WeakEntity<ContextPicker>,
context_store: WeakEntity<context_store::ContextStore>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let delegate = RulesContextPickerDelegate::new(prompt_store, context_picker, context_store);
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
RulesContextPicker { picker }
}
}
impl Focusable for RulesContextPicker {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for RulesContextPicker {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
self.picker.clone()
}
}
#[derive(Debug, Clone)]
pub struct RulesContextEntry {
pub prompt_id: UserPromptId,
pub title: SharedString,
}
pub struct RulesContextPickerDelegate {
prompt_store: WeakEntity<PromptStore>,
context_picker: WeakEntity<ContextPicker>,
context_store: WeakEntity<context_store::ContextStore>,
matches: Vec<RulesContextEntry>,
selected_index: usize,
}
impl RulesContextPickerDelegate {
pub fn new(
prompt_store: WeakEntity<PromptStore>,
context_picker: WeakEntity<ContextPicker>,
context_store: WeakEntity<context_store::ContextStore>,
) -> Self {
RulesContextPickerDelegate {
prompt_store,
context_picker,
context_store,
matches: Vec::new(),
selected_index: 0,
}
}
}
impl PickerDelegate for RulesContextPickerDelegate {
type ListItem = ListItem;
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(
&mut self,
ix: usize,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) {
self.selected_index = ix;
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
"Search available rules…".into()
}
fn update_matches(
&mut self,
query: String,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
let Some(prompt_store) = self.prompt_store.upgrade() else {
return Task::ready(());
};
let search_task = search_rules(query, Arc::new(AtomicBool::default()), &prompt_store, cx);
cx.spawn_in(window, async move |this, cx| {
let matches = search_task.await;
this.update(cx, |this, cx| {
this.delegate.matches = matches;
this.delegate.selected_index = 0;
cx.notify();
})
.ok();
})
}
fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
let Some(entry) = self.matches.get(self.selected_index) else {
return;
};
self.context_store
.update(cx, |context_store, cx| {
context_store.add_rules(entry.prompt_id, true, cx)
})
.log_err();
}
fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
self.context_picker
.update(cx, |_, cx| {
cx.emit(DismissEvent);
})
.ok();
}
fn render_match(
&self,
ix: usize,
selected: bool,
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let thread = &self.matches.get(ix)?;
Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
render_thread_context_entry(thread, self.context_store.clone(), cx),
))
}
}
pub fn render_thread_context_entry(
user_rules: &RulesContextEntry,
context_store: WeakEntity<ContextStore>,
cx: &mut App,
) -> Div {
let added = context_store.upgrade().is_some_and(|context_store| {
context_store
.read(cx)
.includes_user_rules(user_rules.prompt_id)
});
h_flex()
.gap_1p5()
.w_full()
.justify_between()
.child(
h_flex()
.gap_1p5()
.max_w_72()
.child(
Icon::new(RULES_ICON)
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(Label::new(user_rules.title.clone()).truncate()),
)
.when(added, |el| {
el.child(
h_flex()
.gap_1()
.child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success),
)
.child(Label::new("Added").size(LabelSize::Small)),
)
})
}
pub(crate) fn search_rules(
query: String,
cancellation_flag: Arc<AtomicBool>,
prompt_store: &Entity<PromptStore>,
cx: &mut App,
) -> Task<Vec<RulesContextEntry>> {
let search_task = prompt_store.read(cx).search(query, cancellation_flag, cx);
cx.background_spawn(async move {
search_task
.await
.into_iter()
.flat_map(|metadata| {
// Default prompts are filtered out as they are automatically included.
if metadata.default {
None
} else {
match metadata.id {
PromptId::EditWorkflow => None,
PromptId::User { uuid } => Some(RulesContextEntry {
prompt_id: uuid,
title: metadata.title?,
}),
}
}
})
.collect::<Vec<_>>()
})
}

View File

@@ -0,0 +1,415 @@
use std::cmp::Reverse;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use anyhow::{Result, anyhow};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
App, AppContext, DismissEvent, Entity, FocusHandle, Focusable, Stateful, Task, WeakEntity,
};
use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate};
use project::lsp_store::SymbolLocation;
use project::{DocumentSymbol, Symbol};
use ui::{ListItem, prelude::*};
use util::ResultExt as _;
use workspace::Workspace;
use crate::{
context::AgentContextHandle, context_picker::ContextPicker, context_store::ContextStore,
};
pub struct SymbolContextPicker {
picker: Entity<Picker<SymbolContextPickerDelegate>>,
}
impl SymbolContextPicker {
pub fn new(
context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let delegate = SymbolContextPickerDelegate::new(context_picker, workspace, context_store);
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
Self { picker }
}
}
impl Focusable for SymbolContextPicker {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for SymbolContextPicker {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
self.picker.clone()
}
}
pub struct SymbolContextPickerDelegate {
context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
matches: Vec<SymbolEntry>,
selected_index: usize,
}
impl SymbolContextPickerDelegate {
pub fn new(
context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
) -> Self {
Self {
context_picker,
workspace,
context_store,
matches: Vec::new(),
selected_index: 0,
}
}
}
impl PickerDelegate for SymbolContextPickerDelegate {
type ListItem = ListItem;
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(
&mut self,
ix: usize,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) {
self.selected_index = ix;
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
"Search symbols…".into()
}
fn update_matches(
&mut self,
query: String,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
let Some(workspace) = self.workspace.upgrade() else {
return Task::ready(());
};
let search_task = search_symbols(query, Arc::<AtomicBool>::default(), &workspace, cx);
let context_store = self.context_store.clone();
cx.spawn_in(window, async move |this, cx| {
let symbols = search_task.await;
let symbol_entries = context_store
.read_with(cx, |context_store, cx| {
compute_symbol_entries(symbols, context_store, cx)
})
.log_err()
.unwrap_or_default();
this.update(cx, |this, _cx| {
this.delegate.matches = symbol_entries;
})
.log_err();
})
}
fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
let Some(mat) = self.matches.get(self.selected_index) else {
return;
};
let Some(workspace) = self.workspace.upgrade() else {
return;
};
let add_symbol_task = add_symbol(
mat.symbol.clone(),
true,
workspace,
self.context_store.clone(),
cx,
);
let selected_index = self.selected_index;
cx.spawn(async move |this, cx| {
let (_, included) = add_symbol_task.await?;
this.update(cx, |this, _| {
if let Some(mat) = this.delegate.matches.get_mut(selected_index) {
mat.is_included = included;
}
})
})
.detach_and_log_err(cx);
}
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
self.context_picker
.update(cx, |_, cx| {
cx.emit(DismissEvent);
})
.ok();
}
fn render_match(
&self,
ix: usize,
selected: bool,
_window: &mut Window,
_: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let mat = &self.matches.get(ix)?;
Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
render_symbol_context_entry(ElementId::named_usize("symbol-ctx-picker", ix), mat),
))
}
}
pub(crate) struct SymbolEntry {
pub symbol: Symbol,
pub is_included: bool,
}
pub(crate) fn add_symbol(
symbol: Symbol,
remove_if_exists: bool,
workspace: Entity<Workspace>,
context_store: WeakEntity<ContextStore>,
cx: &mut App,
) -> Task<Result<(Option<AgentContextHandle>, bool)>> {
let project = workspace.read(cx).project().clone();
let open_buffer_task = project.update(cx, |project, cx| {
let SymbolLocation::InProject(symbol_path) = &symbol.path else {
return Task::ready(Err(anyhow!("can't add symbol from outside of project")));
};
project.open_buffer(symbol_path.clone(), cx)
});
cx.spawn(async move |cx| {
let buffer = open_buffer_task.await?;
let document_symbols = project
.update(cx, |project, cx| project.document_symbols(&buffer, cx))?
.await?;
// Try to find a matching document symbol. Document symbols include
// not only the symbol itself (e.g. function name), but they also
// include the context that they contain (e.g. function body).
let (name, range, enclosing_range) = if let Some(DocumentSymbol {
name,
range,
selection_range,
..
}) =
find_matching_symbol(&symbol, document_symbols.as_slice())
{
(name, selection_range, range)
} else {
// If we do not find a matching document symbol, fall back to
// just the symbol itself
(symbol.name, symbol.range.clone(), symbol.range)
};
let (range, enclosing_range) = buffer.read_with(cx, |buffer, _| {
(
buffer.anchor_after(range.start)..buffer.anchor_before(range.end),
buffer.anchor_after(enclosing_range.start)
..buffer.anchor_before(enclosing_range.end),
)
})?;
context_store.update(cx, move |context_store, cx| {
context_store.add_symbol(
buffer,
name.into(),
range,
enclosing_range,
remove_if_exists,
cx,
)
})
})
}
fn find_matching_symbol(symbol: &Symbol, candidates: &[DocumentSymbol]) -> Option<DocumentSymbol> {
let mut candidates = candidates.iter();
let mut candidate = candidates.next()?;
loop {
if candidate.range.start > symbol.range.end {
return None;
}
if candidate.range.end < symbol.range.start {
candidate = candidates.next()?;
continue;
}
if candidate.selection_range == symbol.range {
return Some(candidate.clone());
}
if candidate.range.start <= symbol.range.start && symbol.range.end <= candidate.range.end {
candidates = candidate.children.iter();
candidate = candidates.next()?;
continue;
}
return None;
}
}
pub struct SymbolMatch {
pub symbol: Symbol,
}
pub(crate) fn search_symbols(
query: String,
cancellation_flag: Arc<AtomicBool>,
workspace: &Entity<Workspace>,
cx: &mut App,
) -> Task<Vec<SymbolMatch>> {
let symbols_task = workspace.update(cx, |workspace, cx| {
workspace
.project()
.update(cx, |project, cx| project.symbols(&query, cx))
});
let project = workspace.read(cx).project().clone();
cx.spawn(async move |cx| {
let Some(symbols) = symbols_task.await.log_err() else {
return Vec::new();
};
let Some((visible_match_candidates, external_match_candidates)): Option<(Vec<_>, Vec<_>)> =
project
.update(cx, |project, cx| {
symbols
.iter()
.enumerate()
.map(|(id, symbol)| {
StringMatchCandidate::new(id, symbol.label.filter_text())
})
.partition(|candidate| match &symbols[candidate.id].path {
SymbolLocation::InProject(project_path) => project
.entry_for_path(project_path, cx)
.is_some_and(|e| !e.is_ignored),
SymbolLocation::OutsideProject { .. } => false,
})
})
.log_err()
else {
return Vec::new();
};
const MAX_MATCHES: usize = 100;
let mut visible_matches = cx.background_executor().block(fuzzy::match_strings(
&visible_match_candidates,
&query,
false,
true,
MAX_MATCHES,
&cancellation_flag,
cx.background_executor().clone(),
));
let mut external_matches = cx.background_executor().block(fuzzy::match_strings(
&external_match_candidates,
&query,
false,
true,
MAX_MATCHES - visible_matches.len().min(MAX_MATCHES),
&cancellation_flag,
cx.background_executor().clone(),
));
let sort_key_for_match = |mat: &StringMatch| {
let symbol = &symbols[mat.candidate_id];
(Reverse(OrderedFloat(mat.score)), symbol.label.filter_text())
};
visible_matches.sort_unstable_by_key(sort_key_for_match);
external_matches.sort_unstable_by_key(sort_key_for_match);
let mut matches = visible_matches;
matches.append(&mut external_matches);
matches
.into_iter()
.map(|mut mat| {
let symbol = symbols[mat.candidate_id].clone();
let filter_start = symbol.label.filter_range.start;
for position in &mut mat.positions {
*position += filter_start;
}
SymbolMatch { symbol }
})
.collect()
})
}
fn compute_symbol_entries(
symbols: Vec<SymbolMatch>,
context_store: &ContextStore,
cx: &App,
) -> Vec<SymbolEntry> {
symbols
.into_iter()
.map(|SymbolMatch { symbol, .. }| SymbolEntry {
is_included: context_store.includes_symbol(&symbol, cx),
symbol,
})
.collect::<Vec<_>>()
}
pub fn render_symbol_context_entry(id: ElementId, entry: &SymbolEntry) -> Stateful<Div> {
let path = match &entry.symbol.path {
SymbolLocation::InProject(project_path) => {
project_path.path.file_name().unwrap_or_default().into()
}
SymbolLocation::OutsideProject {
abs_path,
signature: _,
} => abs_path
.file_name()
.map(|f| f.to_string_lossy())
.unwrap_or_default(),
};
let symbol_location = format!("{} L{}", path, entry.symbol.range.start.0.row + 1);
h_flex()
.id(id)
.gap_1p5()
.w_full()
.child(
Icon::new(IconName::Code)
.size(IconSize::Small)
.color(Color::Muted),
)
.child(
h_flex()
.gap_1()
.child(Label::new(&entry.symbol.name))
.child(
Label::new(symbol_location)
.size(LabelSize::Small)
.color(Color::Muted),
),
)
.when(entry.is_included, |el| {
el.child(
h_flex()
.w_full()
.justify_end()
.gap_0p5()
.child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success),
)
.child(Label::new("Added").size(LabelSize::Small)),
)
})
}

View File

@@ -0,0 +1,280 @@
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use crate::{
context_picker::ContextPicker,
context_store::{self, ContextStore},
};
use agent::{HistoryEntry, HistoryStore};
use fuzzy::StringMatchCandidate;
use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
use picker::{Picker, PickerDelegate};
use ui::{ListItem, prelude::*};
use workspace::Workspace;
pub struct ThreadContextPicker {
picker: Entity<Picker<ThreadContextPickerDelegate>>,
}
impl ThreadContextPicker {
pub fn new(
thread_store: WeakEntity<HistoryStore>,
context_picker: WeakEntity<ContextPicker>,
context_store: WeakEntity<context_store::ContextStore>,
workspace: WeakEntity<Workspace>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let delegate = ThreadContextPickerDelegate::new(
thread_store,
context_picker,
context_store,
workspace,
);
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
ThreadContextPicker { picker }
}
}
impl Focusable for ThreadContextPicker {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for ThreadContextPicker {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
self.picker.clone()
}
}
pub struct ThreadContextPickerDelegate {
thread_store: WeakEntity<HistoryStore>,
context_picker: WeakEntity<ContextPicker>,
context_store: WeakEntity<context_store::ContextStore>,
workspace: WeakEntity<Workspace>,
matches: Vec<HistoryEntry>,
selected_index: usize,
}
impl ThreadContextPickerDelegate {
pub fn new(
thread_store: WeakEntity<HistoryStore>,
context_picker: WeakEntity<ContextPicker>,
context_store: WeakEntity<context_store::ContextStore>,
workspace: WeakEntity<Workspace>,
) -> Self {
ThreadContextPickerDelegate {
thread_store,
context_picker,
context_store,
workspace,
matches: Vec::new(),
selected_index: 0,
}
}
}
impl PickerDelegate for ThreadContextPickerDelegate {
type ListItem = ListItem;
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(
&mut self,
ix: usize,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) {
self.selected_index = ix;
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
"Search threads…".into()
}
fn update_matches(
&mut self,
query: String,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
let Some(thread_store) = self.thread_store.upgrade() else {
return Task::ready(());
};
let search_task = search_threads(query, Arc::new(AtomicBool::default()), &thread_store, cx);
cx.spawn_in(window, async move |this, cx| {
let matches = search_task.await;
this.update(cx, |this, cx| {
this.delegate.matches = matches;
this.delegate.selected_index = 0;
cx.notify();
})
.ok();
})
}
fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
let Some(project) = self
.workspace
.upgrade()
.map(|w| w.read(cx).project().clone())
else {
return;
};
let Some((entry, thread_store)) = self
.matches
.get(self.selected_index)
.zip(self.thread_store.upgrade())
else {
return;
};
match entry {
HistoryEntry::AcpThread(thread) => {
let load_thread_task =
agent::load_agent_thread(thread.id.clone(), thread_store, project, cx);
cx.spawn(async move |this, cx| {
let thread = load_thread_task.await?;
this.update(cx, |this, cx| {
this.delegate
.context_store
.update(cx, |context_store, cx| {
context_store.add_thread(thread, true, cx)
})
.ok();
})
})
.detach_and_log_err(cx);
}
HistoryEntry::TextThread(thread) => {
let task = thread_store.update(cx, |this, cx| {
this.load_text_thread(thread.path.clone(), cx)
});
cx.spawn(async move |this, cx| {
let thread = task.await?;
this.update(cx, |this, cx| {
this.delegate
.context_store
.update(cx, |context_store, cx| {
context_store.add_text_thread(thread, true, cx)
})
.ok();
})
})
.detach_and_log_err(cx);
}
}
}
fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
self.context_picker
.update(cx, |_, cx| {
cx.emit(DismissEvent);
})
.ok();
}
fn render_match(
&self,
ix: usize,
selected: bool,
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let thread = &self.matches.get(ix)?;
Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
render_thread_context_entry(thread, self.context_store.clone(), cx),
))
}
}
pub fn render_thread_context_entry(
entry: &HistoryEntry,
context_store: WeakEntity<ContextStore>,
cx: &mut App,
) -> Div {
let is_added = match entry {
HistoryEntry::AcpThread(thread) => context_store
.upgrade()
.is_some_and(|ctx_store| ctx_store.read(cx).includes_thread(&thread.id)),
HistoryEntry::TextThread(thread) => context_store
.upgrade()
.is_some_and(|ctx_store| ctx_store.read(cx).includes_text_thread(&thread.path)),
};
h_flex()
.gap_1p5()
.w_full()
.justify_between()
.child(
h_flex()
.gap_1p5()
.max_w_72()
.child(
Icon::new(IconName::Thread)
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(Label::new(entry.title().clone()).truncate()),
)
.when(is_added, |el| {
el.child(
h_flex()
.gap_1()
.child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success),
)
.child(Label::new("Added").size(LabelSize::Small)),
)
})
}
pub(crate) fn search_threads(
query: String,
cancellation_flag: Arc<AtomicBool>,
thread_store: &Entity<HistoryStore>,
cx: &mut App,
) -> Task<Vec<HistoryEntry>> {
let threads = thread_store.read(cx).entries().collect();
if query.is_empty() {
return Task::ready(threads);
}
let executor = cx.background_executor().clone();
cx.background_spawn(async move {
let candidates = threads
.iter()
.enumerate()
.map(|(id, thread)| StringMatchCandidate::new(id, thread.title()))
.collect::<Vec<_>>();
let matches = fuzzy::match_strings(
&candidates,
&query,
false,
true,
100,
&cancellation_flag,
executor,
)
.await;
matches
.into_iter()
.map(|mat| threads[mat.candidate_id].clone())
.collect()
})
}

View File

@@ -0,0 +1,614 @@
use crate::context::{
AgentContextHandle, AgentContextKey, ContextId, ContextKind, DirectoryContextHandle,
FetchedUrlContext, FileContextHandle, ImageContext, RulesContextHandle, SelectionContextHandle,
SymbolContextHandle, TextThreadContextHandle, ThreadContextHandle,
};
use agent_client_protocol as acp;
use anyhow::{Context as _, Result, anyhow};
use assistant_text_thread::TextThread;
use collections::{HashSet, IndexSet};
use futures::{self, FutureExt};
use gpui::{App, Context, Entity, EventEmitter, Image, SharedString, Task, WeakEntity};
use language::{Buffer, File as _};
use language_model::LanguageModelImage;
use project::{
Project, ProjectItem, ProjectPath, Symbol, image_store::is_image_file,
lsp_store::SymbolLocation,
};
use prompt_store::UserPromptId;
use ref_cast::RefCast as _;
use std::{
ops::Range,
path::{Path, PathBuf},
sync::Arc,
};
use text::{Anchor, OffsetRangeExt};
pub struct ContextStore {
project: WeakEntity<Project>,
next_context_id: ContextId,
context_set: IndexSet<AgentContextKey>,
context_thread_ids: HashSet<acp::SessionId>,
context_text_thread_paths: HashSet<Arc<Path>>,
}
pub enum ContextStoreEvent {
ContextRemoved(AgentContextKey),
}
impl EventEmitter<ContextStoreEvent> for ContextStore {}
impl ContextStore {
pub fn new(project: WeakEntity<Project>) -> Self {
Self {
project,
next_context_id: ContextId::zero(),
context_set: IndexSet::default(),
context_thread_ids: HashSet::default(),
context_text_thread_paths: HashSet::default(),
}
}
pub fn context(&self) -> impl Iterator<Item = &AgentContextHandle> {
self.context_set.iter().map(|entry| entry.as_ref())
}
pub fn clear(&mut self, cx: &mut Context<Self>) {
self.context_set.clear();
self.context_thread_ids.clear();
cx.notify();
}
pub fn add_file_from_path(
&mut self,
project_path: ProjectPath,
remove_if_exists: bool,
cx: &mut Context<Self>,
) -> Task<Result<Option<AgentContextHandle>>> {
let Some(project) = self.project.upgrade() else {
return Task::ready(Err(anyhow!("failed to read project")));
};
if is_image_file(&project, &project_path, cx) {
self.add_image_from_path(project_path, remove_if_exists, cx)
} else {
cx.spawn(async move |this, cx| {
let open_buffer_task = project.update(cx, |project, cx| {
project.open_buffer(project_path.clone(), cx)
})?;
let buffer = open_buffer_task.await?;
this.update(cx, |this, cx| {
this.add_file_from_buffer(&project_path, buffer, remove_if_exists, cx)
})
})
}
}
pub fn add_file_from_buffer(
&mut self,
project_path: &ProjectPath,
buffer: Entity<Buffer>,
remove_if_exists: bool,
cx: &mut Context<Self>,
) -> Option<AgentContextHandle> {
let context_id = self.next_context_id.post_inc();
let context = AgentContextHandle::File(FileContextHandle { buffer, context_id });
if let Some(key) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
if remove_if_exists {
self.remove_context(&context, cx);
None
} else {
Some(key.as_ref().clone())
}
} else if self.path_included_in_directory(project_path, cx).is_some() {
None
} else {
self.insert_context(context.clone(), cx);
Some(context)
}
}
pub fn add_directory(
&mut self,
project_path: &ProjectPath,
remove_if_exists: bool,
cx: &mut Context<Self>,
) -> Result<Option<AgentContextHandle>> {
let project = self.project.upgrade().context("failed to read project")?;
let entry_id = project
.read(cx)
.entry_for_path(project_path, cx)
.map(|entry| entry.id)
.context("no entry found for directory context")?;
let context_id = self.next_context_id.post_inc();
let context = AgentContextHandle::Directory(DirectoryContextHandle {
entry_id,
context_id,
});
let context =
if let Some(existing) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
if remove_if_exists {
self.remove_context(&context, cx);
None
} else {
Some(existing.as_ref().clone())
}
} else {
self.insert_context(context.clone(), cx);
Some(context)
};
anyhow::Ok(context)
}
pub fn add_symbol(
&mut self,
buffer: Entity<Buffer>,
symbol: SharedString,
range: Range<Anchor>,
enclosing_range: Range<Anchor>,
remove_if_exists: bool,
cx: &mut Context<Self>,
) -> (Option<AgentContextHandle>, bool) {
let context_id = self.next_context_id.post_inc();
let context = AgentContextHandle::Symbol(SymbolContextHandle {
buffer,
symbol,
range,
enclosing_range,
context_id,
});
if let Some(key) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
let handle = if remove_if_exists {
self.remove_context(&context, cx);
None
} else {
Some(key.as_ref().clone())
};
return (handle, false);
}
let included = self.insert_context(context.clone(), cx);
(Some(context), included)
}
pub fn add_thread(
&mut self,
thread: Entity<agent::Thread>,
remove_if_exists: bool,
cx: &mut Context<Self>,
) -> Option<AgentContextHandle> {
let context_id = self.next_context_id.post_inc();
let context = AgentContextHandle::Thread(ThreadContextHandle { thread, context_id });
if let Some(existing) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
if remove_if_exists {
self.remove_context(&context, cx);
None
} else {
Some(existing.as_ref().clone())
}
} else {
self.insert_context(context.clone(), cx);
Some(context)
}
}
pub fn add_text_thread(
&mut self,
text_thread: Entity<TextThread>,
remove_if_exists: bool,
cx: &mut Context<Self>,
) -> Option<AgentContextHandle> {
let context_id = self.next_context_id.post_inc();
let context = AgentContextHandle::TextThread(TextThreadContextHandle {
text_thread,
context_id,
});
if let Some(existing) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
if remove_if_exists {
self.remove_context(&context, cx);
None
} else {
Some(existing.as_ref().clone())
}
} else {
self.insert_context(context.clone(), cx);
Some(context)
}
}
pub fn add_rules(
&mut self,
prompt_id: UserPromptId,
remove_if_exists: bool,
cx: &mut Context<ContextStore>,
) -> Option<AgentContextHandle> {
let context_id = self.next_context_id.post_inc();
let context = AgentContextHandle::Rules(RulesContextHandle {
prompt_id,
context_id,
});
if let Some(existing) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
if remove_if_exists {
self.remove_context(&context, cx);
None
} else {
Some(existing.as_ref().clone())
}
} else {
self.insert_context(context.clone(), cx);
Some(context)
}
}
pub fn add_fetched_url(
&mut self,
url: String,
text: impl Into<SharedString>,
cx: &mut Context<ContextStore>,
) -> AgentContextHandle {
let context = AgentContextHandle::FetchedUrl(FetchedUrlContext {
url: url.into(),
text: text.into(),
context_id: self.next_context_id.post_inc(),
});
self.insert_context(context.clone(), cx);
context
}
pub fn add_image_from_path(
&mut self,
project_path: ProjectPath,
remove_if_exists: bool,
cx: &mut Context<ContextStore>,
) -> Task<Result<Option<AgentContextHandle>>> {
let project = self.project.clone();
cx.spawn(async move |this, cx| {
let open_image_task = project.update(cx, |project, cx| {
project.open_image(project_path.clone(), cx)
})?;
let image_item = open_image_task.await?;
this.update(cx, |this, cx| {
let item = image_item.read(cx);
this.insert_image(
Some(item.project_path(cx)),
Some(item.file.full_path(cx).to_string_lossy().into_owned()),
item.image.clone(),
remove_if_exists,
cx,
)
})
})
}
pub fn add_image_instance(&mut self, image: Arc<Image>, cx: &mut Context<ContextStore>) {
self.insert_image(None, None, image, false, cx);
}
fn insert_image(
&mut self,
project_path: Option<ProjectPath>,
full_path: Option<String>,
image: Arc<Image>,
remove_if_exists: bool,
cx: &mut Context<ContextStore>,
) -> Option<AgentContextHandle> {
let image_task = LanguageModelImage::from_image(image.clone(), cx).shared();
let context = AgentContextHandle::Image(ImageContext {
project_path,
full_path,
original_image: image,
image_task,
context_id: self.next_context_id.post_inc(),
});
if self.has_context(&context) && remove_if_exists {
self.remove_context(&context, cx);
return None;
}
self.insert_context(context.clone(), cx);
Some(context)
}
pub fn add_selection(
&mut self,
buffer: Entity<Buffer>,
range: Range<Anchor>,
cx: &mut Context<ContextStore>,
) {
let context_id = self.next_context_id.post_inc();
let context = AgentContextHandle::Selection(SelectionContextHandle {
buffer,
range,
context_id,
});
self.insert_context(context, cx);
}
pub fn add_suggested_context(
&mut self,
suggested: &SuggestedContext,
cx: &mut Context<ContextStore>,
) {
match suggested {
SuggestedContext::File {
buffer,
icon_path: _,
name: _,
} => {
if let Some(buffer) = buffer.upgrade() {
let context_id = self.next_context_id.post_inc();
self.insert_context(
AgentContextHandle::File(FileContextHandle { buffer, context_id }),
cx,
);
};
}
SuggestedContext::TextThread {
text_thread,
name: _,
} => {
if let Some(text_thread) = text_thread.upgrade() {
let context_id = self.next_context_id.post_inc();
self.insert_context(
AgentContextHandle::TextThread(TextThreadContextHandle {
text_thread,
context_id,
}),
cx,
);
}
}
}
}
fn insert_context(&mut self, context: AgentContextHandle, cx: &mut Context<Self>) -> bool {
match &context {
// AgentContextHandle::Thread(thread_context) => {
// if let Some(thread_store) = self.thread_store.clone() {
// thread_context.thread.update(cx, |thread, cx| {
// thread.start_generating_detailed_summary_if_needed(thread_store, cx);
// });
// self.context_thread_ids
// .insert(thread_context.thread.read(cx).id().clone());
// } else {
// return false;
// }
// }
AgentContextHandle::TextThread(text_thread_context) => {
self.context_text_thread_paths
.extend(text_thread_context.text_thread.read(cx).path().cloned());
}
_ => {}
}
let inserted = self.context_set.insert(AgentContextKey(context));
if inserted {
cx.notify();
}
inserted
}
pub fn remove_context(&mut self, context: &AgentContextHandle, cx: &mut Context<Self>) {
if let Some((_, key)) = self
.context_set
.shift_remove_full(AgentContextKey::ref_cast(context))
{
match context {
AgentContextHandle::Thread(thread_context) => {
self.context_thread_ids
.remove(thread_context.thread.read(cx).id());
}
AgentContextHandle::TextThread(text_thread_context) => {
if let Some(path) = text_thread_context.text_thread.read(cx).path() {
self.context_text_thread_paths.remove(path);
}
}
_ => {}
}
cx.emit(ContextStoreEvent::ContextRemoved(key));
cx.notify();
}
}
pub fn has_context(&mut self, context: &AgentContextHandle) -> bool {
self.context_set
.contains(AgentContextKey::ref_cast(context))
}
/// Returns whether this file path is already included directly in the context, or if it will be
/// included in the context via a directory.
pub fn file_path_included(&self, path: &ProjectPath, cx: &App) -> Option<FileInclusion> {
let project = self.project.upgrade()?.read(cx);
self.context().find_map(|context| match context {
AgentContextHandle::File(file_context) => {
FileInclusion::check_file(file_context, path, cx)
}
AgentContextHandle::Image(image_context) => {
FileInclusion::check_image(image_context, path)
}
AgentContextHandle::Directory(directory_context) => {
FileInclusion::check_directory(directory_context, path, project, cx)
}
_ => None,
})
}
pub fn path_included_in_directory(
&self,
path: &ProjectPath,
cx: &App,
) -> Option<FileInclusion> {
let project = self.project.upgrade()?.read(cx);
self.context().find_map(|context| match context {
AgentContextHandle::Directory(directory_context) => {
FileInclusion::check_directory(directory_context, path, project, cx)
}
_ => None,
})
}
pub fn includes_symbol(&self, symbol: &Symbol, cx: &App) -> bool {
self.context().any(|context| match context {
AgentContextHandle::Symbol(context) => {
if context.symbol != symbol.name {
return false;
}
let buffer = context.buffer.read(cx);
let Some(context_path) = buffer.project_path(cx) else {
return false;
};
if symbol.path != SymbolLocation::InProject(context_path) {
return false;
}
let context_range = context.range.to_point_utf16(&buffer.snapshot());
context_range.start == symbol.range.start.0
&& context_range.end == symbol.range.end.0
}
_ => false,
})
}
pub fn includes_thread(&self, thread_id: &acp::SessionId) -> bool {
self.context_thread_ids.contains(thread_id)
}
pub fn includes_text_thread(&self, path: &Arc<Path>) -> bool {
self.context_text_thread_paths.contains(path)
}
pub fn includes_user_rules(&self, prompt_id: UserPromptId) -> bool {
self.context_set
.contains(&RulesContextHandle::lookup_key(prompt_id))
}
pub fn includes_url(&self, url: impl Into<SharedString>) -> bool {
self.context_set
.contains(&FetchedUrlContext::lookup_key(url.into()))
}
pub fn get_url_context(&self, url: SharedString) -> Option<AgentContextHandle> {
self.context_set
.get(&FetchedUrlContext::lookup_key(url))
.map(|key| key.as_ref().clone())
}
pub fn file_paths(&self, cx: &App) -> HashSet<ProjectPath> {
self.context()
.filter_map(|context| match context {
AgentContextHandle::File(file) => {
let buffer = file.buffer.read(cx);
buffer.project_path(cx)
}
AgentContextHandle::Directory(_)
| AgentContextHandle::Symbol(_)
| AgentContextHandle::Thread(_)
| AgentContextHandle::Selection(_)
| AgentContextHandle::FetchedUrl(_)
| AgentContextHandle::TextThread(_)
| AgentContextHandle::Rules(_)
| AgentContextHandle::Image(_) => None,
})
.collect()
}
pub fn thread_ids(&self) -> &HashSet<acp::SessionId> {
&self.context_thread_ids
}
}
#[derive(Clone)]
pub enum SuggestedContext {
File {
name: SharedString,
icon_path: Option<SharedString>,
buffer: WeakEntity<Buffer>,
},
TextThread {
name: SharedString,
text_thread: WeakEntity<TextThread>,
},
}
impl SuggestedContext {
pub fn name(&self) -> &SharedString {
match self {
Self::File { name, .. } => name,
Self::TextThread { name, .. } => name,
}
}
pub fn icon_path(&self) -> Option<SharedString> {
match self {
Self::File { icon_path, .. } => icon_path.clone(),
Self::TextThread { .. } => None,
}
}
pub fn kind(&self) -> ContextKind {
match self {
Self::File { .. } => ContextKind::File,
Self::TextThread { .. } => ContextKind::TextThread,
}
}
}
pub enum FileInclusion {
Direct,
InDirectory { full_path: PathBuf },
}
impl FileInclusion {
fn check_file(file_context: &FileContextHandle, path: &ProjectPath, cx: &App) -> Option<Self> {
let file_path = file_context.buffer.read(cx).project_path(cx)?;
if path == &file_path {
Some(FileInclusion::Direct)
} else {
None
}
}
fn check_image(image_context: &ImageContext, path: &ProjectPath) -> Option<Self> {
let image_path = image_context.project_path.as_ref()?;
if path == image_path {
Some(FileInclusion::Direct)
} else {
None
}
}
fn check_directory(
directory_context: &DirectoryContextHandle,
path: &ProjectPath,
project: &Project,
cx: &App,
) -> Option<Self> {
let worktree = project
.worktree_for_entry(directory_context.entry_id, cx)?
.read(cx);
let entry = worktree.entry_for_id(directory_context.entry_id)?;
let directory_path = ProjectPath {
worktree_id: worktree.id(),
path: entry.path.clone(),
};
if path.starts_with(&directory_path) {
if path == &directory_path {
Some(FileInclusion::Direct)
} else {
Some(FileInclusion::InDirectory {
full_path: worktree.full_path(&entry.path),
})
}
} else {
None
}
}
}

View File

@@ -0,0 +1,619 @@
use crate::{
AcceptSuggestedContext, AgentPanel, FocusDown, FocusLeft, FocusRight, FocusUp,
ModelUsageContext, RemoveAllContext, RemoveFocusedContext, ToggleContextPicker,
context_picker::ContextPicker,
ui::{AddedContext, ContextPill},
};
use crate::{
context::AgentContextHandle,
context_store::{ContextStore, SuggestedContext},
};
use agent::HistoryStore;
use collections::HashSet;
use editor::Editor;
use gpui::{
App, Bounds, ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
Subscription, Task, WeakEntity,
};
use itertools::Itertools;
use project::ProjectItem;
use prompt_store::PromptStore;
use rope::Point;
use std::rc::Rc;
use text::ToPoint as _;
use ui::{PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
use util::ResultExt as _;
use workspace::Workspace;
use zed_actions::assistant::OpenRulesLibrary;
pub struct ContextStrip {
context_store: Entity<ContextStore>,
context_picker: Entity<ContextPicker>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
focus_handle: FocusHandle,
suggest_context_kind: SuggestContextKind,
workspace: WeakEntity<Workspace>,
prompt_store: Option<WeakEntity<PromptStore>>,
_subscriptions: Vec<Subscription>,
focused_index: Option<usize>,
children_bounds: Option<Vec<Bounds<Pixels>>>,
model_usage_context: ModelUsageContext,
}
impl ContextStrip {
pub fn new(
context_store: Entity<ContextStore>,
workspace: WeakEntity<Workspace>,
thread_store: Option<WeakEntity<HistoryStore>>,
prompt_store: Option<WeakEntity<PromptStore>>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
suggest_context_kind: SuggestContextKind,
model_usage_context: ModelUsageContext,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let context_picker = cx.new(|cx| {
ContextPicker::new(
workspace.clone(),
thread_store.clone(),
prompt_store.clone(),
context_store.downgrade(),
window,
cx,
)
});
let focus_handle = cx.focus_handle();
let subscriptions = vec![
cx.observe(&context_store, |_, _, cx| cx.notify()),
cx.subscribe_in(&context_picker, window, Self::handle_context_picker_event),
cx.on_focus(&focus_handle, window, Self::handle_focus),
cx.on_blur(&focus_handle, window, Self::handle_blur),
];
Self {
context_store: context_store.clone(),
context_picker,
context_picker_menu_handle,
focus_handle,
suggest_context_kind,
workspace,
prompt_store,
_subscriptions: subscriptions,
focused_index: None,
children_bounds: None,
model_usage_context,
}
}
/// Whether or not the context strip has items to display
pub fn has_context_items(&self, cx: &App) -> bool {
self.context_store.read(cx).context().next().is_some()
|| self.suggested_context(cx).is_some()
}
fn added_contexts(&self, cx: &App) -> Vec<AddedContext> {
if let Some(workspace) = self.workspace.upgrade() {
let project = workspace.read(cx).project().read(cx);
let prompt_store = self.prompt_store.as_ref().and_then(|p| p.upgrade());
let current_model = self.model_usage_context.language_model(cx);
self.context_store
.read(cx)
.context()
.flat_map(|context| {
AddedContext::new_pending(
context.clone(),
prompt_store.as_ref(),
project,
current_model.as_ref(),
cx,
)
})
.collect::<Vec<_>>()
} else {
Vec::new()
}
}
fn suggested_context(&self, cx: &App) -> Option<SuggestedContext> {
match self.suggest_context_kind {
SuggestContextKind::Thread => self.suggested_thread(cx),
}
}
fn suggested_thread(&self, cx: &App) -> Option<SuggestedContext> {
if !self.context_picker.read(cx).allow_threads() {
return None;
}
let workspace = self.workspace.upgrade()?;
let panel = workspace.read(cx).panel::<AgentPanel>(cx)?.read(cx);
if let Some(active_text_thread_editor) = panel.active_text_thread_editor() {
let text_thread = active_text_thread_editor.read(cx).text_thread();
let weak_text_thread = text_thread.downgrade();
let text_thread = text_thread.read(cx);
let path = text_thread.path()?;
if self.context_store.read(cx).includes_text_thread(path) {
return None;
}
Some(SuggestedContext::TextThread {
name: text_thread.summary().or_default(),
text_thread: weak_text_thread,
})
} else {
None
}
}
fn handle_context_picker_event(
&mut self,
_picker: &Entity<ContextPicker>,
_event: &DismissEvent,
_window: &mut Window,
cx: &mut Context<Self>,
) {
cx.emit(ContextStripEvent::PickerDismissed);
}
fn handle_focus(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
self.focused_index = self.last_pill_index();
cx.notify();
}
fn handle_blur(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
self.focused_index = None;
cx.notify();
}
fn focus_left(&mut self, _: &FocusLeft, _window: &mut Window, cx: &mut Context<Self>) {
self.focused_index = match self.focused_index {
Some(index) if index > 0 => Some(index - 1),
_ => self.last_pill_index(),
};
cx.notify();
}
fn focus_right(&mut self, _: &FocusRight, _window: &mut Window, cx: &mut Context<Self>) {
let Some(last_index) = self.last_pill_index() else {
return;
};
self.focused_index = match self.focused_index {
Some(index) if index < last_index => Some(index + 1),
_ => Some(0),
};
cx.notify();
}
fn focus_up(&mut self, _: &FocusUp, _window: &mut Window, cx: &mut Context<Self>) {
let Some(focused_index) = self.focused_index else {
return;
};
if focused_index == 0 {
return cx.emit(ContextStripEvent::BlurredUp);
}
let Some((focused, pills)) = self.focused_bounds(focused_index) else {
return;
};
let iter = pills[..focused_index].iter().enumerate().rev();
self.focused_index = Self::find_best_horizontal_match(focused, iter).or(Some(0));
cx.notify();
}
fn focus_down(&mut self, _: &FocusDown, _window: &mut Window, cx: &mut Context<Self>) {
let Some(focused_index) = self.focused_index else {
return;
};
let last_index = self.last_pill_index();
if self.focused_index == last_index {
return cx.emit(ContextStripEvent::BlurredDown);
}
let Some((focused, pills)) = self.focused_bounds(focused_index) else {
return;
};
let iter = pills.iter().enumerate().skip(focused_index + 1);
self.focused_index = Self::find_best_horizontal_match(focused, iter).or(last_index);
cx.notify();
}
fn focused_bounds(&self, focused: usize) -> Option<(&Bounds<Pixels>, &[Bounds<Pixels>])> {
let pill_bounds = self.pill_bounds()?;
let focused = pill_bounds.get(focused)?;
Some((focused, pill_bounds))
}
fn pill_bounds(&self) -> Option<&[Bounds<Pixels>]> {
let bounds = self.children_bounds.as_ref()?;
let eraser = if bounds.len() < 3 { 0 } else { 1 };
let pills = &bounds[1..bounds.len() - eraser];
if pills.is_empty() { None } else { Some(pills) }
}
fn last_pill_index(&self) -> Option<usize> {
Some(self.pill_bounds()?.len() - 1)
}
fn find_best_horizontal_match<'a>(
focused: &'a Bounds<Pixels>,
iter: impl Iterator<Item = (usize, &'a Bounds<Pixels>)>,
) -> Option<usize> {
let mut best = None;
let focused_left = focused.left();
let focused_right = focused.right();
for (index, probe) in iter {
if probe.origin.y == focused.origin.y {
continue;
}
let overlap = probe.right().min(focused_right) - probe.left().max(focused_left);
best = match best {
Some((_, prev_overlap, y)) if probe.origin.y != y || prev_overlap > overlap => {
break;
}
Some(_) | None => Some((index, overlap, probe.origin.y)),
};
}
best.map(|(index, _, _)| index)
}
fn open_context(&mut self, context: &AgentContextHandle, window: &mut Window, cx: &mut App) {
let Some(workspace) = self.workspace.upgrade() else {
return;
};
match context {
AgentContextHandle::File(file_context) => {
if let Some(project_path) = file_context.project_path(cx) {
workspace.update(cx, |workspace, cx| {
workspace
.open_path(project_path, None, true, window, cx)
.detach_and_log_err(cx);
});
}
}
AgentContextHandle::Directory(directory_context) => {
let entry_id = directory_context.entry_id;
workspace.update(cx, |workspace, cx| {
workspace.project().update(cx, |_project, cx| {
cx.emit(project::Event::RevealInProjectPanel(entry_id));
})
})
}
AgentContextHandle::Symbol(symbol_context) => {
let buffer = symbol_context.buffer.read(cx);
if let Some(project_path) = buffer.project_path(cx) {
let snapshot = buffer.snapshot();
let target_position = symbol_context.range.start.to_point(&snapshot);
open_editor_at_position(project_path, target_position, &workspace, window, cx)
.detach();
}
}
AgentContextHandle::Selection(selection_context) => {
let buffer = selection_context.buffer.read(cx);
if let Some(project_path) = buffer.project_path(cx) {
let snapshot = buffer.snapshot();
let target_position = selection_context.range.start.to_point(&snapshot);
open_editor_at_position(project_path, target_position, &workspace, window, cx)
.detach();
}
}
AgentContextHandle::FetchedUrl(fetched_url_context) => {
cx.open_url(&fetched_url_context.url);
}
AgentContextHandle::Thread(_thread_context) => {}
AgentContextHandle::TextThread(text_thread_context) => {
workspace.update(cx, |workspace, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
let context = text_thread_context.text_thread.clone();
window.defer(cx, move |window, cx| {
panel.update(cx, |panel, cx| {
panel.open_text_thread(context, window, cx)
});
});
}
})
}
AgentContextHandle::Rules(rules_context) => window.dispatch_action(
Box::new(OpenRulesLibrary {
prompt_to_select: Some(rules_context.prompt_id.0),
}),
cx,
),
AgentContextHandle::Image(_) => {}
}
}
fn remove_focused_context(
&mut self,
_: &RemoveFocusedContext,
_window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(index) = self.focused_index {
let added_contexts = self.added_contexts(cx);
let Some(context) = added_contexts.get(index) else {
return;
};
self.context_store.update(cx, |this, cx| {
this.remove_context(&context.handle, cx);
});
let is_now_empty = added_contexts.len() == 1;
if is_now_empty {
cx.emit(ContextStripEvent::BlurredEmpty);
} else {
self.focused_index = Some(index.saturating_sub(1));
cx.notify();
}
}
}
fn is_suggested_focused(&self, added_contexts: &Vec<AddedContext>) -> bool {
// We only suggest one item after the actual context
self.focused_index == Some(added_contexts.len())
}
fn accept_suggested_context(
&mut self,
_: &AcceptSuggestedContext,
_window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(suggested) = self.suggested_context(cx)
&& self.is_suggested_focused(&self.added_contexts(cx))
{
self.add_suggested_context(&suggested, cx);
}
}
fn add_suggested_context(&mut self, suggested: &SuggestedContext, cx: &mut Context<Self>) {
self.context_store.update(cx, |context_store, cx| {
context_store.add_suggested_context(suggested, cx)
});
cx.notify();
}
}
impl Focusable for ContextStrip {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Render for ContextStrip {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let context_picker = self.context_picker.clone();
let focus_handle = self.focus_handle.clone();
let added_contexts = self.added_contexts(cx);
let dupe_names = added_contexts
.iter()
.map(|c| c.name.clone())
.sorted()
.tuple_windows()
.filter(|(a, b)| a == b)
.map(|(a, _)| a)
.collect::<HashSet<SharedString>>();
let no_added_context = added_contexts.is_empty();
let suggested_context = self.suggested_context(cx).map(|suggested_context| {
(
suggested_context,
self.is_suggested_focused(&added_contexts),
)
});
h_flex()
.flex_wrap()
.gap_1()
.track_focus(&focus_handle)
.key_context("ContextStrip")
.on_action(cx.listener(Self::focus_up))
.on_action(cx.listener(Self::focus_right))
.on_action(cx.listener(Self::focus_down))
.on_action(cx.listener(Self::focus_left))
.on_action(cx.listener(Self::remove_focused_context))
.on_action(cx.listener(Self::accept_suggested_context))
.on_children_prepainted({
let entity = cx.entity().downgrade();
move |children_bounds, _window, cx| {
entity
.update(cx, |this, _| {
this.children_bounds = Some(children_bounds);
})
.ok();
}
})
.child(
PopoverMenu::new("context-picker")
.menu({
let context_picker = context_picker.clone();
move |window, cx| {
context_picker.update(cx, |this, cx| {
this.init(window, cx);
});
Some(context_picker.clone())
}
})
.on_open({
let context_picker = context_picker.downgrade();
Rc::new(move |window, cx| {
context_picker
.update(cx, |context_picker, cx| {
context_picker.select_first(window, cx);
})
.ok();
})
})
.trigger_with_tooltip(
IconButton::new("add-context", IconName::Plus)
.icon_size(IconSize::Small)
.style(ui::ButtonStyle::Filled),
{
let focus_handle = focus_handle.clone();
move |_window, cx| {
Tooltip::for_action_in(
"Add Context",
&ToggleContextPicker,
&focus_handle,
cx,
)
}
},
)
.attach(gpui::Corner::TopLeft)
.anchor(gpui::Corner::BottomLeft)
.offset(gpui::Point {
x: px(0.0),
y: px(-2.0),
})
.with_handle(self.context_picker_menu_handle.clone()),
)
.children(
added_contexts
.into_iter()
.enumerate()
.map(|(i, added_context)| {
let name = added_context.name.clone();
let context = added_context.handle.clone();
ContextPill::added(
added_context,
dupe_names.contains(&name),
self.focused_index == Some(i),
Some({
let context = context.clone();
let context_store = self.context_store.clone();
Rc::new(cx.listener(move |_this, _event, _window, cx| {
context_store.update(cx, |this, cx| {
this.remove_context(&context, cx);
});
cx.notify();
}))
}),
)
.on_click({
Rc::new(cx.listener(move |this, event: &ClickEvent, window, cx| {
if event.click_count() > 1 {
this.open_context(&context, window, cx);
} else {
this.focused_index = Some(i);
}
cx.notify();
}))
})
}),
)
.when_some(suggested_context, |el, (suggested, focused)| {
el.child(
ContextPill::suggested(
suggested.name().clone(),
suggested.icon_path(),
suggested.kind(),
focused,
)
.on_click(Rc::new(cx.listener(
move |this, _event, _window, cx| {
this.add_suggested_context(&suggested, cx);
},
))),
)
})
.when(!no_added_context, {
move |parent| {
parent.child(
IconButton::new("remove-all-context", IconName::Eraser)
.icon_size(IconSize::Small)
.tooltip({
let focus_handle = focus_handle.clone();
move |_window, cx| {
Tooltip::for_action_in(
"Remove All Context",
&RemoveAllContext,
&focus_handle,
cx,
)
}
})
.on_click(cx.listener({
let focus_handle = focus_handle.clone();
move |_this, _event, window, cx| {
focus_handle.dispatch_action(&RemoveAllContext, window, cx);
}
})),
)
}
})
.into_any()
}
}
pub enum ContextStripEvent {
PickerDismissed,
BlurredEmpty,
BlurredDown,
BlurredUp,
}
impl EventEmitter<ContextStripEvent> for ContextStrip {}
pub enum SuggestContextKind {
Thread,
}
fn open_editor_at_position(
project_path: project::ProjectPath,
target_position: Point,
workspace: &Entity<Workspace>,
window: &mut Window,
cx: &mut App,
) -> Task<()> {
let open_task = workspace.update(cx, |workspace, cx| {
workspace.open_path(project_path, None, true, window, cx)
});
window.spawn(cx, async move |cx| {
if let Some(active_editor) = open_task
.await
.log_err()
.and_then(|item| item.downcast::<Editor>())
{
active_editor
.downgrade()
.update_in(cx, |editor, window, cx| {
editor.go_to_singleton_buffer_point(target_position, window, cx);
})
.log_err();
}
})
}

View File

@@ -4,11 +4,10 @@ use std::ops::Range;
use std::rc::Rc; use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
use crate::context::load_context;
use crate::mention_set::MentionSet;
use crate::{ use crate::{
AgentPanel, AgentPanel,
buffer_codegen::{BufferCodegen, CodegenAlternative, CodegenEvent}, buffer_codegen::{BufferCodegen, CodegenAlternative, CodegenEvent},
context_store::ContextStore,
inline_prompt_editor::{CodegenStatus, InlineAssistId, PromptEditor, PromptEditorEvent}, inline_prompt_editor::{CodegenStatus, InlineAssistId, PromptEditor, PromptEditorEvent},
terminal_inline_assistant::TerminalInlineAssistant, terminal_inline_assistant::TerminalInlineAssistant,
}; };
@@ -17,8 +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::EditorSnapshot;
use editor::MultiBufferOffset;
use editor::RowExt; use editor::RowExt;
use editor::SelectionEffects; use editor::SelectionEffects;
use editor::scroll::ScrollOffset; use editor::scroll::ScrollOffset;
@@ -32,7 +29,6 @@ use editor::{
}, },
}; };
use fs::Fs; use fs::Fs;
use futures::FutureExt;
use gpui::{ use gpui::{
App, Context, Entity, Focusable, Global, HighlightStyle, Subscription, Task, UpdateGlobal, App, Context, Entity, Focusable, Global, HighlightStyle, Subscription, Task, UpdateGlobal,
WeakEntity, Window, point, WeakEntity, Window, point,
@@ -216,10 +212,16 @@ impl InlineAssistant {
if let Some(editor) = item.act_as::<Editor>(cx) { if let Some(editor) = item.act_as::<Editor>(cx) {
editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
if is_ai_enabled { if is_ai_enabled {
let panel = workspace.read(cx).panel::<AgentPanel>(cx);
let thread_store = panel
.as_ref()
.map(|agent_panel| agent_panel.read(cx).thread_store().downgrade());
editor.add_code_action_provider( editor.add_code_action_provider(
Rc::new(AssistantCodeActionProvider { Rc::new(AssistantCodeActionProvider {
editor: cx.entity().downgrade(), editor: cx.entity().downgrade(),
workspace: workspace.downgrade(), workspace: workspace.downgrade(),
thread_store,
}), }),
window, window,
cx, cx,
@@ -231,6 +233,9 @@ impl InlineAssistant {
editor.cancel(&Default::default(), window, cx); editor.cancel(&Default::default(), window, cx);
} }
} }
// Remove the Assistant1 code action provider, as it still might be registered.
editor.remove_code_action_provider("assistant".into(), window, cx);
} else { } else {
editor.remove_code_action_provider( editor.remove_code_action_provider(
ASSISTANT_CODE_ACTION_PROVIDER_ID.into(), ASSISTANT_CODE_ACTION_PROVIDER_ID.into(),
@@ -272,7 +277,8 @@ impl InlineAssistant {
let agent_panel = agent_panel.read(cx); let agent_panel = agent_panel.read(cx);
let prompt_store = agent_panel.prompt_store().as_ref().cloned(); let prompt_store = agent_panel.prompt_store().as_ref().cloned();
let thread_store = agent_panel.thread_store().clone(); let thread_store = Some(agent_panel.thread_store().downgrade());
let context_store = agent_panel.inline_assist_context_store().clone();
let handle_assist = let handle_assist =
|window: &mut Window, cx: &mut Context<Workspace>| match inline_assist_target { |window: &mut Window, cx: &mut Context<Workspace>| match inline_assist_target {
@@ -281,9 +287,10 @@ impl InlineAssistant {
assistant.assist( assistant.assist(
&active_editor, &active_editor,
cx.entity().downgrade(), cx.entity().downgrade(),
context_store,
workspace.project().downgrade(), workspace.project().downgrade(),
thread_store,
prompt_store, prompt_store,
thread_store,
action.prompt.clone(), action.prompt.clone(),
window, window,
cx, cx,
@@ -296,8 +303,8 @@ impl InlineAssistant {
&active_terminal, &active_terminal,
cx.entity().downgrade(), cx.entity().downgrade(),
workspace.project().downgrade(), workspace.project().downgrade(),
thread_store,
prompt_store, prompt_store,
thread_store,
action.prompt.clone(), action.prompt.clone(),
window, window,
cx, cx,
@@ -343,20 +350,25 @@ impl InlineAssistant {
} }
} }
fn codegen_ranges( pub fn assist(
&mut self, &mut self,
editor: &Entity<Editor>, editor: &Entity<Editor>,
snapshot: &EditorSnapshot, workspace: WeakEntity<Workspace>,
context_store: Entity<ContextStore>,
project: WeakEntity<Project>,
prompt_store: Option<Entity<PromptStore>>,
thread_store: Option<WeakEntity<HistoryStore>>,
initial_prompt: Option<String>,
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) -> Option<(Vec<Range<Anchor>>, Selection<Point>)> { ) {
let (initial_selections, newest_selection) = editor.update(cx, |editor, _| { let (snapshot, initial_selections, newest_selection) = editor.update(cx, |editor, cx| {
( let snapshot = editor.snapshot(window, cx);
editor.selections.all::<Point>(&snapshot.display_snapshot), let selections = editor.selections.all::<Point>(&snapshot.display_snapshot);
editor let newest_selection = editor
.selections .selections
.newest::<Point>(&snapshot.display_snapshot), .newest::<Point>(&snapshot.display_snapshot);
) (snapshot, selections, newest_selection)
}); });
// Check if there is already an inline assistant that contains the // Check if there is already an inline assistant that contains the
@@ -369,7 +381,7 @@ impl InlineAssistant {
&& newest_selection.end.row <= range.end.row && newest_selection.end.row <= range.end.row
{ {
self.focus_assist(*assist_id, window, cx); self.focus_assist(*assist_id, window, cx);
return None; return;
} }
} }
} }
@@ -461,25 +473,6 @@ impl InlineAssistant {
} }
} }
Some((codegen_ranges, newest_selection))
}
fn batch_assist(
&mut self,
editor: &Entity<Editor>,
workspace: WeakEntity<Workspace>,
project: WeakEntity<Project>,
thread_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
initial_prompt: Option<String>,
window: &mut Window,
codegen_ranges: &[Range<Anchor>],
newest_selection: Option<Selection<Point>>,
initial_transaction_id: Option<TransactionId>,
cx: &mut App,
) -> Option<InlineAssistId> {
let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
let assist_group_id = self.next_assist_group_id.post_inc(); let assist_group_id = self.next_assist_group_id.post_inc();
let prompt_buffer = cx.new(|cx| { let prompt_buffer = cx.new(|cx| {
MultiBuffer::singleton( MultiBuffer::singleton(
@@ -490,14 +483,16 @@ impl InlineAssistant {
let mut assists = Vec::new(); let mut assists = Vec::new();
let mut assist_to_focus = None; let mut assist_to_focus = None;
for range in codegen_ranges { for range in codegen_ranges {
let assist_id = self.next_assist_id.post_inc(); let assist_id = self.next_assist_id.post_inc();
let codegen = cx.new(|cx| { let codegen = cx.new(|cx| {
BufferCodegen::new( BufferCodegen::new(
editor.read(cx).buffer().clone(), editor.read(cx).buffer().clone(),
range.clone(), range.clone(),
initial_transaction_id, None,
context_store.clone(),
project.clone(),
prompt_store.clone(),
self.telemetry.clone(), self.telemetry.clone(),
self.prompt_builder.clone(), self.prompt_builder.clone(),
cx, cx,
@@ -513,22 +508,20 @@ impl InlineAssistant {
prompt_buffer.clone(), prompt_buffer.clone(),
codegen.clone(), codegen.clone(),
self.fs.clone(), self.fs.clone(),
thread_store.clone(), context_store.clone(),
prompt_store.clone(),
project.clone(),
workspace.clone(), workspace.clone(),
thread_store.clone(),
prompt_store.as_ref().map(|s| s.downgrade()),
window, window,
cx, cx,
) )
}); });
if let Some(newest_selection) = newest_selection.as_ref() if assist_to_focus.is_none() {
&& assist_to_focus.is_none()
{
let focus_assist = if newest_selection.reversed { let focus_assist = if newest_selection.reversed {
range.start.to_point(&snapshot) == newest_selection.start range.start.to_point(snapshot) == newest_selection.start
} else { } else {
range.end.to_point(&snapshot) == newest_selection.end range.end.to_point(snapshot) == newest_selection.end
}; };
if focus_assist { if focus_assist {
assist_to_focus = Some(assist_id); assist_to_focus = Some(assist_id);
@@ -540,7 +533,7 @@ impl InlineAssistant {
assists.push(( assists.push((
assist_id, assist_id,
range.clone(), range,
prompt_editor, prompt_editor,
prompt_block_id, prompt_block_id,
end_block_id, end_block_id,
@@ -551,15 +544,6 @@ impl InlineAssistant {
.assists_by_editor .assists_by_editor
.entry(editor.downgrade()) .entry(editor.downgrade())
.or_insert_with(|| EditorInlineAssists::new(editor, window, cx)); .or_insert_with(|| EditorInlineAssists::new(editor, window, cx));
let assist_to_focus = if let Some(focus_id) = assist_to_focus {
Some(focus_id)
} else if assists.len() >= 1 {
Some(assists[0].0)
} else {
None
};
let mut assist_group = InlineAssistGroup::new(); let mut assist_group = InlineAssistGroup::new();
for (assist_id, range, prompt_editor, prompt_block_id, end_block_id) in assists { for (assist_id, range, prompt_editor, prompt_block_id, end_block_id) in assists {
let codegen = prompt_editor.read(cx).codegen().clone(); let codegen = prompt_editor.read(cx).codegen().clone();
@@ -583,45 +567,8 @@ impl InlineAssistant {
assist_group.assist_ids.push(assist_id); assist_group.assist_ids.push(assist_id);
editor_assists.assist_ids.push(assist_id); editor_assists.assist_ids.push(assist_id);
} }
self.assist_groups.insert(assist_group_id, assist_group); self.assist_groups.insert(assist_group_id, assist_group);
assist_to_focus
}
pub fn assist(
&mut self,
editor: &Entity<Editor>,
workspace: WeakEntity<Workspace>,
project: WeakEntity<Project>,
thread_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
initial_prompt: Option<String>,
window: &mut Window,
cx: &mut App,
) {
let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
let Some((codegen_ranges, newest_selection)) =
self.codegen_ranges(editor, &snapshot, window, cx)
else {
return;
};
let assist_to_focus = self.batch_assist(
editor,
workspace,
project,
thread_store,
prompt_store,
initial_prompt,
window,
&codegen_ranges,
Some(newest_selection),
None,
cx,
);
if let Some(assist_id) = assist_to_focus { if let Some(assist_id) = assist_to_focus {
self.focus_assist(assist_id, window, cx); self.focus_assist(assist_id, window, cx);
} }
@@ -635,11 +582,17 @@ impl InlineAssistant {
initial_transaction_id: Option<TransactionId>, initial_transaction_id: Option<TransactionId>,
focus: bool, focus: bool,
workspace: Entity<Workspace>, workspace: Entity<Workspace>,
thread_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>, prompt_store: Option<Entity<PromptStore>>,
thread_store: Option<WeakEntity<HistoryStore>>,
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) -> InlineAssistId { ) -> InlineAssistId {
let assist_group_id = self.next_assist_group_id.post_inc();
let prompt_buffer = cx.new(|cx| Buffer::local(&initial_prompt, cx));
let prompt_buffer = cx.new(|cx| MultiBuffer::singleton(prompt_buffer, cx));
let assist_id = self.next_assist_id.post_inc();
let buffer = editor.read(cx).buffer().clone(); let buffer = editor.read(cx).buffer().clone();
{ {
let snapshot = buffer.read(cx).read(cx); let snapshot = buffer.read(cx).read(cx);
@@ -648,22 +601,68 @@ impl InlineAssistant {
} }
let project = workspace.read(cx).project().downgrade(); let project = workspace.read(cx).project().downgrade();
let context_store = cx.new(|_cx| ContextStore::new(project.clone()));
let assist_id = self let codegen = cx.new(|cx| {
.batch_assist( BufferCodegen::new(
editor, editor.read(cx).buffer().clone(),
workspace.downgrade(), range.clone(),
project,
thread_store,
prompt_store,
Some(initial_prompt),
window,
&[range],
None,
initial_transaction_id, initial_transaction_id,
context_store.clone(),
project,
prompt_store.clone(),
self.telemetry.clone(),
self.prompt_builder.clone(),
cx, cx,
) )
.expect("batch_assist returns an id if there's only one range"); });
let editor_margins = Arc::new(Mutex::new(EditorMargins::default()));
let prompt_editor = cx.new(|cx| {
PromptEditor::new_buffer(
assist_id,
editor_margins,
self.prompt_history.clone(),
prompt_buffer.clone(),
codegen.clone(),
self.fs.clone(),
context_store,
workspace.downgrade(),
thread_store,
prompt_store.map(|s| s.downgrade()),
window,
cx,
)
});
let [prompt_block_id, end_block_id] =
self.insert_assist_blocks(editor, &range, &prompt_editor, cx);
let editor_assists = self
.assists_by_editor
.entry(editor.downgrade())
.or_insert_with(|| EditorInlineAssists::new(editor, window, cx));
let mut assist_group = InlineAssistGroup::new();
self.assists.insert(
assist_id,
InlineAssist::new(
assist_id,
assist_group_id,
editor,
&prompt_editor,
prompt_block_id,
end_block_id,
range,
codegen.clone(),
workspace.downgrade(),
window,
cx,
),
);
assist_group.assist_ids.push(assist_id);
editor_assists.assist_ids.push(assist_id);
self.assist_groups.insert(assist_group_id, assist_group);
if focus { if focus {
self.focus_assist(assist_id, window, cx); self.focus_assist(assist_id, window, cx);
@@ -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() {
@@ -1277,8 +1274,7 @@ impl InlineAssistant {
return; return;
} }
let Some((user_prompt, mention_set)) = assist.user_prompt(cx).zip(assist.mention_set(cx)) let Some(user_prompt) = assist.user_prompt(cx) else {
else {
return; return;
}; };
@@ -1294,12 +1290,9 @@ impl InlineAssistant {
return; return;
}; };
let context_task = load_context(&mention_set, cx).shared();
assist assist
.codegen .codegen
.update(cx, |codegen, cx| { .update(cx, |codegen, cx| codegen.start(model, user_prompt, cx))
codegen.start(model, user_prompt, context_task, cx)
})
.log_err(); .log_err();
} }
@@ -1445,7 +1438,6 @@ impl InlineAssistant {
multi_buffer.update(cx, |multi_buffer, cx| { multi_buffer.update(cx, |multi_buffer, cx| {
multi_buffer.push_excerpts( multi_buffer.push_excerpts(
old_buffer.clone(), old_buffer.clone(),
// todo(lw): buffer_start and buffer_end might come from different snapshots!
Some(ExcerptRange::new(buffer_start..buffer_end)), Some(ExcerptRange::new(buffer_start..buffer_end)),
cx, cx,
); );
@@ -1766,11 +1758,6 @@ impl InlineAssist {
let decorations = self.decorations.as_ref()?; let decorations = self.decorations.as_ref()?;
Some(decorations.prompt_editor.read(cx).prompt(cx)) Some(decorations.prompt_editor.read(cx).prompt(cx))
} }
fn mention_set(&self, cx: &App) -> Option<Entity<MentionSet>> {
let decorations = self.decorations.as_ref()?;
Some(decorations.prompt_editor.read(cx).mention_set().clone())
}
} }
struct InlineAssistDecorations { struct InlineAssistDecorations {
@@ -1783,9 +1770,10 @@ struct InlineAssistDecorations {
struct AssistantCodeActionProvider { struct AssistantCodeActionProvider {
editor: WeakEntity<Editor>, editor: WeakEntity<Editor>,
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
thread_store: Option<WeakEntity<HistoryStore>>,
} }
const ASSISTANT_CODE_ACTION_PROVIDER_ID: &str = "assistant"; const ASSISTANT_CODE_ACTION_PROVIDER_ID: &str = "assistant2";
impl CodeActionProvider for AssistantCodeActionProvider { impl CodeActionProvider for AssistantCodeActionProvider {
fn id(&self) -> Arc<str> { fn id(&self) -> Arc<str> {
@@ -1853,20 +1841,10 @@ impl CodeActionProvider for AssistantCodeActionProvider {
) -> Task<Result<ProjectTransaction>> { ) -> Task<Result<ProjectTransaction>> {
let editor = self.editor.clone(); let editor = self.editor.clone();
let workspace = self.workspace.clone(); let workspace = self.workspace.clone();
let thread_store = self.thread_store.clone();
let prompt_store = PromptStore::global(cx); let prompt_store = PromptStore::global(cx);
window.spawn(cx, async move |cx| { window.spawn(cx, async move |cx| {
let workspace = workspace.upgrade().context("workspace was released")?; let workspace = workspace.upgrade().context("workspace was released")?;
let thread_store = cx.update(|_window, cx| {
anyhow::Ok(
workspace
.read(cx)
.panel::<AgentPanel>(cx)
.context("missing agent panel")?
.read(cx)
.thread_store()
.clone(),
)
})??;
let editor = editor.upgrade().context("editor was released")?; let editor = editor.upgrade().context("editor was released")?;
let range = editor let range = editor
.update(cx, |editor, cx| { .update(cx, |editor, cx| {
@@ -1909,8 +1887,8 @@ impl CodeActionProvider for AssistantCodeActionProvider {
None, None,
true, true,
workspace, workspace,
thread_store,
prompt_store, prompt_store,
thread_store,
window, window,
cx, cx,
); );

View File

@@ -1,21 +1,19 @@
use agent::HistoryStore; use agent::HistoryStore;
use collections::{HashMap, VecDeque}; use collections::{HashMap, VecDeque};
use editor::actions::Paste; use editor::actions::Paste;
use editor::code_context_menus::CodeContextMenu;
use editor::display_map::{CreaseId, EditorMargins}; use editor::display_map::{CreaseId, EditorMargins};
use editor::{AnchorRangeExt as _, MultiBufferOffset, ToOffset as _}; use editor::{Addon, AnchorRangeExt as _};
use editor::{ use editor::{
ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer, ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
actions::{MoveDown, MoveUp}, actions::{MoveDown, MoveUp},
}; };
use fs::Fs; use fs::Fs;
use gpui::{ use gpui::{
AnyElement, App, Context, CursorStyle, Entity, EventEmitter, FocusHandle, Focusable, AnyElement, App, ClipboardEntry, Context, CursorStyle, Entity, EventEmitter, FocusHandle,
Subscription, TextStyle, WeakEntity, Window, Focusable, Subscription, TextStyle, WeakEntity, Window,
}; };
use language_model::{LanguageModel, LanguageModelRegistry}; use language_model::{LanguageModel, LanguageModelRegistry};
use parking_lot::Mutex; use parking_lot::Mutex;
use project::Project;
use prompt_store::PromptStore; use prompt_store::PromptStore;
use settings::Settings; use settings::Settings;
use std::cmp; use std::cmp;
@@ -30,21 +28,22 @@ 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::completion_provider::{ use crate::context::{AgentContextHandle, AgentContextKey};
PromptCompletionProvider, PromptCompletionProviderDelegate, PromptContextType, use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention};
}; use crate::context_store::{ContextStore, ContextStoreEvent};
use crate::mention_set::paste_images_as_context; use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
use crate::mention_set::{MentionSet, crease_for_mention};
use crate::terminal_codegen::TerminalCodegen; use crate::terminal_codegen::TerminalCodegen;
use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext}; use crate::{
CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext, RemoveAllContext,
ToggleContextPicker,
};
pub struct PromptEditor<T> { pub struct PromptEditor<T> {
pub editor: Entity<Editor>, pub editor: Entity<Editor>,
mode: PromptEditorMode, mode: PromptEditorMode,
mention_set: Entity<MentionSet>, context_store: Entity<ContextStore>,
history_store: Entity<HistoryStore>, context_strip: Entity<ContextStrip>,
prompt_store: Option<Entity<PromptStore>>, context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
workspace: WeakEntity<Workspace>,
model_selector: Entity<AgentModelSelector>, model_selector: Entity<AgentModelSelector>,
edited_since_done: bool, edited_since_done: bool,
prompt_history: VecDeque<String>, prompt_history: VecDeque<String>,
@@ -52,6 +51,7 @@ pub struct PromptEditor<T> {
pending_prompt: String, pending_prompt: String,
_codegen_subscription: Subscription, _codegen_subscription: Subscription,
editor_subscriptions: Vec<Subscription>, editor_subscriptions: Vec<Subscription>,
_context_strip_subscription: Subscription,
show_rate_limit_notice: bool, show_rate_limit_notice: bool,
_phantom: std::marker::PhantomData<T>, _phantom: std::marker::PhantomData<T>,
} }
@@ -98,19 +98,6 @@ impl<T: 'static> Render for PromptEditor<T> {
buttons.extend(self.render_buttons(window, cx)); buttons.extend(self.render_buttons(window, cx));
let menu_visible = self.is_completions_menu_visible(cx);
let add_context_button = 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| {
this.trigger_completion_menu(window, cx);
}));
v_flex() v_flex()
.key_context("PromptEditor") .key_context("PromptEditor")
.capture_action(cx.listener(Self::paste)) .capture_action(cx.listener(Self::paste))
@@ -127,6 +114,7 @@ impl<T: 'static> Render for PromptEditor<T> {
h_flex() h_flex()
.items_start() .items_start()
.cursor(CursorStyle::Arrow) .cursor(CursorStyle::Arrow)
.on_action(cx.listener(Self::toggle_context_picker))
.on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| { .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
this.model_selector this.model_selector
.update(cx, |model_selector, cx| model_selector.toggle(window, cx)); .update(cx, |model_selector, cx| model_selector.toggle(window, cx));
@@ -135,6 +123,7 @@ impl<T: 'static> Render for PromptEditor<T> {
.on_action(cx.listener(Self::cancel)) .on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::move_up)) .on_action(cx.listener(Self::move_up))
.on_action(cx.listener(Self::move_down)) .on_action(cx.listener(Self::move_down))
.on_action(cx.listener(Self::remove_all_context))
.capture_action(cx.listener(Self::cycle_prev)) .capture_action(cx.listener(Self::cycle_prev))
.capture_action(cx.listener(Self::cycle_next)) .capture_action(cx.listener(Self::cycle_next))
.child( .child(
@@ -193,7 +182,7 @@ impl<T: 'static> Render for PromptEditor<T> {
.pl_1() .pl_1()
.items_start() .items_start()
.justify_between() .justify_between()
.child(add_context_button) .child(self.context_strip.clone())
.child(self.model_selector.clone()), .child(self.model_selector.clone()),
), ),
) )
@@ -225,19 +214,6 @@ impl<T: 'static> PromptEditor<T> {
)); ));
} }
fn assign_completion_provider(&mut self, cx: &mut Context<Self>) {
self.editor.update(cx, |editor, cx| {
editor.set_completion_provider(Some(Rc::new(PromptCompletionProvider::new(
PromptEditorCompletionProviderDelegate,
cx.weak_entity(),
self.mention_set.clone(),
self.history_store.clone(),
self.prompt_store.clone(),
self.workspace.clone(),
))));
});
}
pub fn set_show_cursor_when_unfocused( pub fn set_show_cursor_when_unfocused(
&mut self, &mut self,
show_cursor_when_unfocused: bool, show_cursor_when_unfocused: bool,
@@ -250,40 +226,27 @@ impl<T: 'static> PromptEditor<T> {
pub fn unlink(&mut self, window: &mut Window, cx: &mut Context<Self>) { pub fn unlink(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let prompt = self.prompt(cx); let prompt = self.prompt(cx);
let existing_creases = self.editor.update(cx, |editor, cx| { let existing_creases = self.editor.update(cx, extract_message_creases);
extract_message_creases(editor, &self.mention_set, window, cx)
});
let focus = self.editor.focus_handle(cx).contains_focused(window, cx); let focus = self.editor.focus_handle(cx).contains_focused(window, cx);
let mut creases = vec![];
self.editor = cx.new(|cx| { self.editor = cx.new(|cx| {
let mut editor = Editor::auto_height(1, Self::MAX_LINES as usize, window, cx); let mut editor = Editor::auto_height(1, Self::MAX_LINES as usize, window, cx);
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx); editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
editor.set_placeholder_text("Add a prompt…", window, cx); editor.set_placeholder_text("Add a prompt…", window, cx);
editor.set_text(prompt, window, cx); editor.set_text(prompt, window, cx);
creases = insert_message_creases(&mut editor, &existing_creases, window, cx); insert_message_creases(
&mut editor,
&existing_creases,
&self.context_store,
window,
cx,
);
if focus { if focus {
window.focus(&editor.focus_handle(cx)); window.focus(&editor.focus_handle(cx));
} }
editor editor
}); });
self.mention_set.update(cx, |mention_set, _cx| {
debug_assert_eq!(
creases.len(),
mention_set.creases().len(),
"Missing creases"
);
let mentions = mention_set
.clear()
.zip(creases)
.map(|((_, value), id)| (id, value))
.collect::<HashMap<_, _>>();
mention_set.set_mentions(mentions);
});
self.assign_completion_provider(cx);
self.subscribe_to_editor(window, cx); self.subscribe_to_editor(window, cx);
} }
@@ -311,29 +274,43 @@ impl<T: 'static> PromptEditor<T> {
self.editor.read(cx).text(cx) self.editor.read(cx).text(cx)
} }
fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) { fn paste(&mut self, _: &Paste, _window: &mut Window, cx: &mut Context<Self>) {
if inline_assistant_model_supports_images(cx) let images = cx
&& let Some(task) = .read_from_clipboard()
paste_images_as_context(self.editor.clone(), self.mention_set.clone(), window, cx) .map(|item| {
{ item.into_entries()
task.detach(); .filter_map(|entry| {
if let ClipboardEntry::Image(image) = entry {
Some(image)
} else {
None
}
})
.collect::<Vec<_>>()
})
.unwrap_or_default();
if images.is_empty() {
return;
} }
cx.stop_propagation();
self.context_store.update(cx, |store, cx| {
for image in images {
store.add_image_instance(Arc::new(image), cx);
}
});
} }
fn handle_prompt_editor_events( fn handle_prompt_editor_events(
&mut self, &mut self,
editor: &Entity<Editor>, _: &Entity<Editor>,
event: &EditorEvent, event: &EditorEvent,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
match event { match event {
EditorEvent::Edited { .. } => { EditorEvent::Edited { .. } => {
let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
self.mention_set
.update(cx, |mention_set, _cx| mention_set.remove_invalid(&snapshot));
if let Some(workspace) = window.root::<Workspace>().flatten() { if let Some(workspace) = window.root::<Workspace>().flatten() {
workspace.update(cx, |workspace, cx| { workspace.update(cx, |workspace, cx| {
let is_via_ssh = workspace.project().read(cx).is_via_remote_server(); let is_via_ssh = workspace.project().read(cx).is_via_remote_server();
@@ -344,7 +321,7 @@ impl<T: 'static> PromptEditor<T> {
.log_edit_event("inline assist", is_via_ssh); .log_edit_event("inline assist", is_via_ssh);
}); });
} }
let prompt = snapshot.text(); let prompt = self.editor.read(cx).text(cx);
if self if self
.prompt_history_ix .prompt_history_ix
.is_none_or(|ix| self.prompt_history[ix] != prompt) .is_none_or(|ix| self.prompt_history[ix] != prompt)
@@ -366,44 +343,23 @@ impl<T: 'static> PromptEditor<T> {
} }
} }
pub fn is_completions_menu_visible(&self, cx: &App) -> bool { fn toggle_context_picker(
self.editor &mut self,
.read(cx) _: &ToggleContextPicker,
.context_menu() window: &mut Window,
.borrow() cx: &mut Context<Self>,
.as_ref() ) {
.is_some_and(|menu| matches!(menu, CodeContextMenu::Completions(_)) && menu.visible()) self.context_picker_menu_handle.toggle(window, cx);
} }
pub fn trigger_completion_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) { pub fn remove_all_context(
self.editor.update(cx, |editor, cx| { &mut self,
let menu_is_open = editor.context_menu().borrow().as_ref().is_some_and(|menu| { _: &RemoveAllContext,
matches!(menu, CodeContextMenu::Completions(_)) && menu.visible() _window: &mut Window,
}); cx: &mut Context<Self>,
) {
let has_at_sign = { self.context_store.update(cx, |store, cx| store.clear(cx));
let snapshot = editor.display_snapshot(cx); cx.notify();
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);
});
} }
fn cancel( fn cancel(
@@ -478,6 +434,8 @@ impl<T: 'static> PromptEditor<T> {
editor.move_to_end(&Default::default(), window, cx) editor.move_to_end(&Default::default(), window, cx)
}); });
} }
} else if self.context_strip.read(cx).has_context_items(cx) {
self.context_strip.focus_handle(cx).focus(window);
} }
} }
@@ -751,7 +709,6 @@ impl<T: 'static> PromptEditor<T> {
EditorStyle { EditorStyle {
background: colors.editor_background, background: colors.editor_background,
local_player: cx.theme().players().local(), local_player: cx.theme().players().local(),
syntax: cx.theme().syntax().clone(),
text: text_style, text: text_style,
..Default::default() ..Default::default()
}, },
@@ -759,6 +716,21 @@ impl<T: 'static> PromptEditor<T> {
}) })
.into_any_element() .into_any_element()
} }
fn handle_context_strip_event(
&mut self,
_context_strip: &Entity<ContextStrip>,
event: &ContextStripEvent,
window: &mut Window,
cx: &mut Context<Self>,
) {
match event {
ContextStripEvent::PickerDismissed
| ContextStripEvent::BlurredEmpty
| ContextStripEvent::BlurredUp => self.editor.focus_handle(cx).focus(window),
ContextStripEvent::BlurredDown => {}
}
}
} }
pub enum PromptEditorMode { pub enum PromptEditorMode {
@@ -793,36 +765,6 @@ impl InlineAssistId {
} }
} }
struct PromptEditorCompletionProviderDelegate;
fn inline_assistant_model_supports_images(cx: &App) -> bool {
LanguageModelRegistry::read_global(cx)
.inline_assistant_model()
.map_or(false, |m| m.model.supports_images())
}
impl PromptCompletionProviderDelegate for PromptEditorCompletionProviderDelegate {
fn supported_modes(&self, _cx: &App) -> Vec<PromptContextType> {
vec![
PromptContextType::File,
PromptContextType::Symbol,
PromptContextType::Thread,
PromptContextType::Fetch,
PromptContextType::Rules,
]
}
fn supports_images(&self, cx: &App) -> bool {
inline_assistant_model_supports_images(cx)
}
fn available_commands(&self, _cx: &App) -> Vec<crate::completion_provider::AvailableCommand> {
Vec::new()
}
fn confirm_command(&self, _cx: &mut App) {}
}
impl PromptEditor<BufferCodegen> { impl PromptEditor<BufferCodegen> {
pub fn new_buffer( pub fn new_buffer(
id: InlineAssistId, id: InlineAssistId,
@@ -831,14 +773,15 @@ impl PromptEditor<BufferCodegen> {
prompt_buffer: Entity<MultiBuffer>, prompt_buffer: Entity<MultiBuffer>,
codegen: Entity<BufferCodegen>, codegen: Entity<BufferCodegen>,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
history_store: Entity<HistoryStore>, context_store: Entity<ContextStore>,
prompt_store: Option<Entity<PromptStore>>,
project: WeakEntity<Project>,
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
thread_store: Option<WeakEntity<HistoryStore>>,
prompt_store: Option<WeakEntity<PromptStore>>,
window: &mut Window, window: &mut Window,
cx: &mut Context<PromptEditor<BufferCodegen>>, cx: &mut Context<PromptEditor<BufferCodegen>>,
) -> PromptEditor<BufferCodegen> { ) -> PromptEditor<BufferCodegen> {
let codegen_subscription = cx.observe(&codegen, Self::handle_codegen_changed); let codegen_subscription = cx.observe(&codegen, Self::handle_codegen_changed);
let codegen_buffer = codegen.read(cx).buffer(cx).read(cx).as_singleton();
let mode = PromptEditorMode::Buffer { let mode = PromptEditorMode::Buffer {
id, id,
codegen, codegen,
@@ -862,6 +805,7 @@ impl PromptEditor<BufferCodegen> {
// typing in one will make what you typed appear in all of them. // typing in one will make what you typed appear in all of them.
editor.set_show_cursor_when_unfocused(true, cx); editor.set_show_cursor_when_unfocused(true, cx);
editor.set_placeholder_text(&Self::placeholder_text(&mode, window, cx), window, cx); editor.set_placeholder_text(&Self::placeholder_text(&mode, window, cx), window, cx);
editor.register_addon(ContextCreasesAddon::new());
editor.set_context_menu_options(ContextMenuOptions { editor.set_context_menu_options(ContextMenuOptions {
min_entries_visible: 12, min_entries_visible: 12,
max_entries_visible: 12, max_entries_visible: 12,
@@ -871,17 +815,43 @@ impl PromptEditor<BufferCodegen> {
editor editor
}); });
let mention_set = let prompt_editor_entity = prompt_editor.downgrade();
cx.new(|_cx| MentionSet::new(project, history_store.clone(), prompt_store.clone())); prompt_editor.update(cx, |editor, _| {
editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
workspace.clone(),
context_store.downgrade(),
thread_store.clone(),
prompt_store.clone(),
prompt_editor_entity,
codegen_buffer.as_ref().map(Entity::downgrade),
))));
});
let context_picker_menu_handle = PopoverMenuHandle::default();
let model_selector_menu_handle = PopoverMenuHandle::default(); let model_selector_menu_handle = PopoverMenuHandle::default();
let context_strip = cx.new(|cx| {
ContextStrip::new(
context_store.clone(),
workspace.clone(),
thread_store.clone(),
prompt_store,
context_picker_menu_handle.clone(),
SuggestContextKind::Thread,
ModelUsageContext::InlineAssistant,
window,
cx,
)
});
let context_strip_subscription =
cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event);
let mut this: PromptEditor<BufferCodegen> = PromptEditor { let mut this: PromptEditor<BufferCodegen> = PromptEditor {
editor: prompt_editor.clone(), editor: prompt_editor.clone(),
mention_set, context_store,
history_store, context_strip,
prompt_store, context_picker_menu_handle,
workspace,
model_selector: cx.new(|cx| { model_selector: cx.new(|cx| {
AgentModelSelector::new( AgentModelSelector::new(
fs, fs,
@@ -898,12 +868,12 @@ impl PromptEditor<BufferCodegen> {
pending_prompt: String::new(), pending_prompt: String::new(),
_codegen_subscription: codegen_subscription, _codegen_subscription: codegen_subscription,
editor_subscriptions: Vec::new(), editor_subscriptions: Vec::new(),
_context_strip_subscription: context_strip_subscription,
show_rate_limit_notice: false, show_rate_limit_notice: false,
mode, mode,
_phantom: Default::default(), _phantom: Default::default(),
}; };
this.assign_completion_provider(cx);
this.subscribe_to_editor(window, cx); this.subscribe_to_editor(window, cx);
this this
} }
@@ -949,10 +919,6 @@ impl PromptEditor<BufferCodegen> {
} }
} }
pub fn mention_set(&self) -> &Entity<MentionSet> {
&self.mention_set
}
pub fn editor_margins(&self) -> &Arc<Mutex<EditorMargins>> { pub fn editor_margins(&self) -> &Arc<Mutex<EditorMargins>> {
match &self.mode { match &self.mode {
PromptEditorMode::Buffer { editor_margins, .. } => editor_margins, PromptEditorMode::Buffer { editor_margins, .. } => editor_margins,
@@ -979,10 +945,10 @@ impl PromptEditor<TerminalCodegen> {
prompt_buffer: Entity<MultiBuffer>, prompt_buffer: Entity<MultiBuffer>,
codegen: Entity<TerminalCodegen>, codegen: Entity<TerminalCodegen>,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
history_store: Entity<HistoryStore>, context_store: Entity<ContextStore>,
prompt_store: Option<Entity<PromptStore>>,
project: WeakEntity<Project>,
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
thread_store: Option<WeakEntity<HistoryStore>>,
prompt_store: Option<WeakEntity<PromptStore>>,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Self { ) -> Self {
@@ -1014,17 +980,43 @@ impl PromptEditor<TerminalCodegen> {
editor editor
}); });
let mention_set = let prompt_editor_entity = prompt_editor.downgrade();
cx.new(|_cx| MentionSet::new(project, history_store.clone(), prompt_store.clone())); prompt_editor.update(cx, |editor, _| {
editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
workspace.clone(),
context_store.downgrade(),
thread_store.clone(),
prompt_store.clone(),
prompt_editor_entity,
None,
))));
});
let context_picker_menu_handle = PopoverMenuHandle::default();
let model_selector_menu_handle = PopoverMenuHandle::default(); let model_selector_menu_handle = PopoverMenuHandle::default();
let context_strip = cx.new(|cx| {
ContextStrip::new(
context_store.clone(),
workspace.clone(),
thread_store.clone(),
prompt_store.clone(),
context_picker_menu_handle.clone(),
SuggestContextKind::Thread,
ModelUsageContext::InlineAssistant,
window,
cx,
)
});
let context_strip_subscription =
cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event);
let mut this = Self { let mut this = Self {
editor: prompt_editor.clone(), editor: prompt_editor.clone(),
mention_set, context_store,
history_store, context_strip,
prompt_store, context_picker_menu_handle,
workspace,
model_selector: cx.new(|cx| { model_selector: cx.new(|cx| {
AgentModelSelector::new( AgentModelSelector::new(
fs, fs,
@@ -1041,12 +1033,12 @@ impl PromptEditor<TerminalCodegen> {
pending_prompt: String::new(), pending_prompt: String::new(),
_codegen_subscription: codegen_subscription, _codegen_subscription: codegen_subscription,
editor_subscriptions: Vec::new(), editor_subscriptions: Vec::new(),
_context_strip_subscription: context_strip_subscription,
mode, mode,
show_rate_limit_notice: false, show_rate_limit_notice: false,
_phantom: Default::default(), _phantom: Default::default(),
}; };
this.count_lines(cx); this.count_lines(cx);
this.assign_completion_provider(cx);
this.subscribe_to_editor(window, cx); this.subscribe_to_editor(window, cx);
this this
} }
@@ -1093,10 +1085,6 @@ impl PromptEditor<TerminalCodegen> {
} }
} }
pub fn mention_set(&self) -> &Entity<MentionSet> {
&self.mention_set
}
pub fn codegen(&self) -> &Entity<TerminalCodegen> { pub fn codegen(&self) -> &Entity<TerminalCodegen> {
match &self.mode { match &self.mode {
PromptEditorMode::Buffer { .. } => unreachable!(), PromptEditorMode::Buffer { .. } => unreachable!(),
@@ -1176,41 +1164,131 @@ impl GenerationMode {
/// Stored information that can be used to resurrect a context crease when creating an editor for a past message. /// Stored information that can be used to resurrect a context crease when creating an editor for a past message.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
struct MessageCrease { pub struct MessageCrease {
range: Range<MultiBufferOffset>, pub range: Range<usize>,
icon_path: SharedString, pub icon_path: SharedString,
label: SharedString, pub label: SharedString,
/// None for a deserialized message, Some otherwise.
pub context: Option<AgentContextHandle>,
} }
fn extract_message_creases( #[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, editor: &mut Editor,
mention_set: &Entity<MentionSet>,
window: &mut Window,
cx: &mut Context<'_, Editor>, cx: &mut Context<'_, Editor>,
) -> Vec<MessageCrease> { ) -> Vec<MessageCrease> {
let creases = mention_set.read(cx).creases(); let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
let snapshot = editor.snapshot(window, cx); let mut contexts_by_crease_id = editor
snapshot .addon_mut::<ContextCreasesAddon>()
.crease_snapshot .map(std::mem::take)
.creases() .unwrap_or_default()
.filter(|(id, _)| creases.contains(id)) .into_inner()
.filter_map(|(_, crease)| { .into_iter()
let metadata = crease.metadata()?.clone(); .flat_map(|(key, creases)| {
Some(MessageCrease { let context = key.0;
range: crease.range().to_offset(snapshot.buffer()), creases
label: metadata.label, .into_iter()
icon_path: metadata.icon_path, .map(move |(id, _)| (id, context.clone()))
})
}) })
.collect() .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()
})
} }
fn insert_message_creases( pub fn insert_message_creases(
editor: &mut Editor, editor: &mut Editor,
message_creases: &[MessageCrease], message_creases: &[MessageCrease],
context_store: &Entity<ContextStore>,
window: &mut Window, window: &mut Window,
cx: &mut Context<'_, Editor>, cx: &mut Context<'_, Editor>,
) -> Vec<CreaseId> { ) {
let buffer_snapshot = editor.buffer().read(cx).snapshot(cx); let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
let creases = message_creases let creases = message_creases
.iter() .iter()
@@ -1227,5 +1305,12 @@ fn insert_message_creases(
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let ids = editor.insert_creases(creases.clone(), cx); let ids = editor.insert_creases(creases.clone(), cx);
editor.fold_creases(creases, false, window, cx); editor.fold_creases(creases, false, window, cx);
ids 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,18 +1,15 @@
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::{ use gpui::{Action, AnyElement, App, BackgroundExecutor, DismissEvent, Subscription, Task};
Action, AnyElement, App, BackgroundExecutor, DismissEvent, FocusHandle, Subscription, Task,
};
use language_model::{ use language_model::{
AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelProviderId, AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelProviderId,
LanguageModelRegistry, LanguageModelRegistry,
}; };
use ordered_float::OrderedFloat; use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate}; use picker::{Picker, PickerDelegate};
use ui::{KeyBinding, ListItem, ListItemSpacing, prelude::*}; use ui::{ListItem, ListItemSpacing, prelude::*};
use zed_actions::agent::OpenSettings;
type OnModelChanged = Arc<dyn Fn(Arc<dyn LanguageModel>, &mut App) + 'static>; type OnModelChanged = Arc<dyn Fn(Arc<dyn LanguageModel>, &mut App) + 'static>;
type GetActiveModel = Arc<dyn Fn(&App) -> Option<ConfiguredModel> + 'static>; type GetActiveModel = Arc<dyn Fn(&App) -> Option<ConfiguredModel> + 'static>;
@@ -23,7 +20,6 @@ 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, popover_styles: bool,
focus_handle: FocusHandle,
window: &mut Window, window: &mut Window,
cx: &mut Context<LanguageModelSelector>, cx: &mut Context<LanguageModelSelector>,
) -> LanguageModelSelector { ) -> LanguageModelSelector {
@@ -31,7 +27,6 @@ pub fn language_model_selector(
get_active_model, get_active_model,
on_model_changed, on_model_changed,
popover_styles, popover_styles,
focus_handle,
window, window,
cx, cx,
); );
@@ -62,7 +57,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
@@ -75,7 +70,7 @@ fn all_models(cx: &App) -> GroupedModels {
}) })
.collect(); .collect();
GroupedModels::new(all, recommended) GroupedModels::new(other, recommended)
} }
#[derive(Clone)] #[derive(Clone)]
@@ -93,7 +88,6 @@ pub struct LanguageModelPickerDelegate {
_authenticate_all_providers_task: Task<()>, _authenticate_all_providers_task: Task<()>,
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
popover_styles: bool, popover_styles: bool,
focus_handle: FocusHandle,
} }
impl LanguageModelPickerDelegate { impl LanguageModelPickerDelegate {
@@ -101,7 +95,6 @@ impl LanguageModelPickerDelegate {
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, popover_styles: bool,
focus_handle: FocusHandle,
window: &mut Window, window: &mut Window,
cx: &mut Context<Picker<Self>>, cx: &mut Context<Picker<Self>>,
) -> Self { ) -> Self {
@@ -135,7 +128,6 @@ impl LanguageModelPickerDelegate {
}, },
)], )],
popover_styles, popover_styles,
focus_handle,
} }
} }
@@ -218,24 +210,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,
} }
} }
@@ -251,7 +252,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;
} }
@@ -266,6 +267,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 {
@@ -410,9 +425,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<_>>();
@@ -500,15 +514,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| {
@@ -529,8 +545,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> {
let focus_handle = self.focus_handle.clone();
if !self.popover_styles { if !self.popover_styles {
return None; return None;
} }
@@ -538,19 +552,22 @@ impl PickerDelegate for LanguageModelPickerDelegate {
Some( Some(
h_flex() h_flex()
.w_full() .w_full()
.p_1p5()
.border_t_1() .border_t_1()
.border_color(cx.theme().colors().border_variant) .border_color(cx.theme().colors().border_variant)
.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(&OpenSettings, &focus_handle, cx) .icon_position(IconPosition::Start)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(|_, window, cx| { .on_click(|_, window, cx| {
window.dispatch_action(OpenSettings.boxed_clone(), cx); window.dispatch_action(
zed_actions::agent::OpenSettings.boxed_clone(),
cx,
);
}), }),
) )
.into_any(), .into_any(),
@@ -747,52 +764,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"],
);
} }
} }

File diff suppressed because it is too large Load Diff

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

@@ -1,5 +1,6 @@
use crate::{ use crate::{
context::load_context, context::load_context,
context_store::ContextStore,
inline_prompt_editor::{ inline_prompt_editor::{
CodegenStatus, PromptEditor, PromptEditorEvent, TerminalInlineAssistId, CodegenStatus, PromptEditor, PromptEditorEvent, TerminalInlineAssistId,
}, },
@@ -72,8 +73,8 @@ impl TerminalInlineAssistant {
terminal_view: &Entity<TerminalView>, terminal_view: &Entity<TerminalView>,
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
project: WeakEntity<Project>, project: WeakEntity<Project>,
thread_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>, prompt_store: Option<Entity<PromptStore>>,
thread_store: Option<WeakEntity<HistoryStore>>,
initial_prompt: Option<String>, initial_prompt: Option<String>,
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
@@ -86,6 +87,7 @@ impl TerminalInlineAssistant {
cx, cx,
) )
}); });
let context_store = cx.new(|_cx| ContextStore::new(project));
let codegen = cx.new(|_| TerminalCodegen::new(terminal, self.telemetry.clone())); let codegen = cx.new(|_| TerminalCodegen::new(terminal, self.telemetry.clone()));
let prompt_editor = cx.new(|cx| { let prompt_editor = cx.new(|cx| {
@@ -95,10 +97,10 @@ impl TerminalInlineAssistant {
prompt_buffer.clone(), prompt_buffer.clone(),
codegen, codegen,
self.fs.clone(), self.fs.clone(),
thread_store.clone(), context_store.clone(),
prompt_store.clone(),
project.clone(),
workspace.clone(), workspace.clone(),
thread_store.clone(),
prompt_store.as_ref().map(|s| s.downgrade()),
window, window,
cx, cx,
) )
@@ -117,6 +119,8 @@ impl TerminalInlineAssistant {
terminal_view, terminal_view,
prompt_editor, prompt_editor,
workspace.clone(), workspace.clone(),
context_store,
prompt_store,
window, window,
cx, cx,
); );
@@ -223,10 +227,6 @@ impl TerminalInlineAssistant {
assist_id: TerminalInlineAssistId, assist_id: TerminalInlineAssistId,
cx: &mut App, cx: &mut App,
) -> Result<Task<LanguageModelRequest>> { ) -> Result<Task<LanguageModelRequest>> {
let ConfiguredModel { model, .. } = LanguageModelRegistry::read_global(cx)
.inline_assistant_model()
.context("No inline assistant model")?;
let assist = self.assists.get(&assist_id).context("invalid assist")?; let assist = self.assists.get(&assist_id).context("invalid assist")?;
let shell = std::env::var("SHELL").ok(); let shell = std::env::var("SHELL").ok();
@@ -243,31 +243,45 @@ impl TerminalInlineAssistant {
.ok() .ok()
.unwrap_or_default(); .unwrap_or_default();
let prompt_editor = assist.prompt_editor.clone().context("invalid assist")?;
let prompt = self.prompt_builder.generate_terminal_assistant_prompt( let prompt = self.prompt_builder.generate_terminal_assistant_prompt(
&prompt_editor.read(cx).prompt(cx), &assist
.prompt_editor
.clone()
.context("invalid assist")?
.read(cx)
.prompt(cx),
shell.as_deref(), shell.as_deref(),
working_directory.as_deref(), working_directory.as_deref(),
&latest_output, &latest_output,
)?; )?;
let temperature = AgentSettings::temperature_for_model(&model, cx); let contexts = assist
.context_store
.read(cx)
.context()
.cloned()
.collect::<Vec<_>>();
let context_load_task = assist.workspace.update(cx, |workspace, cx| {
let project = workspace.project();
load_context(contexts, project, &assist.prompt_store, cx)
})?;
let mention_set = prompt_editor.read(cx).mention_set().clone(); let ConfiguredModel { model, .. } = LanguageModelRegistry::read_global(cx)
let load_context_task = load_context(&mention_set, cx); .inline_assistant_model()
.context("No inline assistant model")?;
let temperature = AgentSettings::temperature_for_model(&model, cx);
Ok(cx.background_spawn(async move { Ok(cx.background_spawn(async move {
let mut request_message = LanguageModelRequestMessage { let mut request_message = LanguageModelRequestMessage {
role: Role::User, role: Role::User,
content: vec![], content: vec![],
cache: false, cache: false,
reasoning_details: None,
}; };
if let Some(context) = load_context_task.await { context_load_task
context.add_to_request_message(&mut request_message); .await
} .add_to_request_message(&mut request_message);
request_message.content.push(prompt.into()); request_message.content.push(prompt.into());
@@ -395,6 +409,8 @@ struct TerminalInlineAssist {
prompt_editor: Option<Entity<PromptEditor<TerminalCodegen>>>, prompt_editor: Option<Entity<PromptEditor<TerminalCodegen>>>,
codegen: Entity<TerminalCodegen>, codegen: Entity<TerminalCodegen>,
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
context_store: Entity<ContextStore>,
prompt_store: Option<Entity<PromptStore>>,
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
} }
@@ -404,6 +420,8 @@ impl TerminalInlineAssist {
terminal: &Entity<TerminalView>, terminal: &Entity<TerminalView>,
prompt_editor: Entity<PromptEditor<TerminalCodegen>>, prompt_editor: Entity<PromptEditor<TerminalCodegen>>,
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
context_store: Entity<ContextStore>,
prompt_store: Option<Entity<PromptStore>>,
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) -> Self { ) -> Self {
@@ -413,6 +431,8 @@ impl TerminalInlineAssist {
prompt_editor: Some(prompt_editor.clone()), prompt_editor: Some(prompt_editor.clone()),
codegen: codegen.clone(), codegen: codegen.clone(),
workspace, workspace,
context_store,
prompt_store,
_subscriptions: vec![ _subscriptions: vec![
window.subscribe(&prompt_editor, cx, |prompt_editor, event, window, cx| { window.subscribe(&prompt_editor, cx, |prompt_editor, event, window, cx| {
TerminalInlineAssistant::update_global(cx, |this, cx| { TerminalInlineAssistant::update_global(cx, |this, cx| {

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},
@@ -280,8 +280,6 @@ impl TextThreadEditor {
.thought_process_output_sections() .thought_process_output_sections()
.to_vec(); .to_vec();
let slash_commands = text_thread.read(cx).slash_commands().clone(); let slash_commands = text_thread.read(cx).slash_commands().clone();
let focus_handle = editor.read(cx).focus_handle(cx);
let mut this = Self { let mut this = Self {
text_thread, text_thread,
slash_commands, slash_commands,
@@ -317,7 +315,6 @@ impl TextThreadEditor {
}); });
}, },
true, // Use popover styles for picker true, // Use popover styles for picker
focus_handle,
window, window,
cx, cx,
) )
@@ -393,7 +390,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| {
@@ -434,7 +431,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();
@@ -442,11 +439,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()
@@ -1585,11 +1580,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
@@ -1647,26 +1638,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');
} }
} }
@@ -1684,7 +1679,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 {
@@ -1698,40 +1693,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 {
@@ -1748,7 +1709,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);
@@ -1796,16 +1757,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));
} }
}); });
@@ -1897,7 +1855,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);
} }
}); });
} }
@@ -2591,11 +2549,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
} }
@@ -2634,11 +2592,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);
}); });
} }
@@ -2971,7 +2930,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;
@@ -3177,16 +3136,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>(

View File

@@ -2,8 +2,8 @@ mod acp_onboarding_modal;
mod agent_notification; mod agent_notification;
mod burn_mode_tooltip; mod burn_mode_tooltip;
mod claude_code_onboarding_modal; mod claude_code_onboarding_modal;
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;
@@ -12,8 +12,8 @@ pub use acp_onboarding_modal::*;
pub use agent_notification::*; pub use agent_notification::*;
pub use burn_mode_tooltip::*; pub use burn_mode_tooltip::*;
pub use claude_code_onboarding_modal::*; pub use claude_code_onboarding_modal::*;
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::*;

View File

@@ -0,0 +1,858 @@
use std::{ops::Range, path::Path, rc::Rc, sync::Arc, time::Duration};
use file_icons::FileIcons;
use futures::FutureExt as _;
use gpui::{
Animation, AnimationExt as _, AnyView, ClickEvent, Entity, Image, MouseButton, Task,
pulsating_between,
};
use language_model::LanguageModelImage;
use project::Project;
use prompt_store::PromptStore;
use rope::Point;
use ui::{IconButtonShape, Tooltip, prelude::*, tooltip_container};
use util::paths::PathStyle;
use crate::context::{
AgentContextHandle, ContextId, ContextKind, DirectoryContextHandle, FetchedUrlContext,
FileContextHandle, ImageContext, ImageStatus, RulesContextHandle, SelectionContextHandle,
SymbolContextHandle, TextThreadContextHandle, ThreadContextHandle,
};
#[derive(IntoElement)]
pub enum ContextPill {
Added {
context: AddedContext,
dupe_name: bool,
focused: bool,
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
},
Suggested {
name: SharedString,
icon_path: Option<SharedString>,
kind: ContextKind,
focused: bool,
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
},
}
impl ContextPill {
pub fn added(
context: AddedContext,
dupe_name: bool,
focused: bool,
on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
) -> Self {
Self::Added {
context,
dupe_name,
on_remove,
focused,
on_click: None,
}
}
pub fn suggested(
name: SharedString,
icon_path: Option<SharedString>,
kind: ContextKind,
focused: bool,
) -> Self {
Self::Suggested {
name,
icon_path,
kind,
focused,
on_click: None,
}
}
pub fn on_click(mut self, listener: Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>) -> Self {
match &mut self {
ContextPill::Added { on_click, .. } => {
*on_click = Some(listener);
}
ContextPill::Suggested { on_click, .. } => {
*on_click = Some(listener);
}
}
self
}
pub fn id(&self) -> ElementId {
match self {
Self::Added { context, .. } => context.handle.element_id("context-pill".into()),
Self::Suggested { .. } => "suggested-context-pill".into(),
}
}
pub fn icon(&self) -> Icon {
match self {
Self::Suggested {
icon_path: Some(icon_path),
..
} => Icon::from_path(icon_path),
Self::Suggested { kind, .. } => Icon::new(kind.icon()),
Self::Added { context, .. } => context.icon(),
}
}
}
impl RenderOnce for ContextPill {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let color = cx.theme().colors();
let base_pill = h_flex()
.id(self.id())
.pl_1()
.pb(px(1.))
.border_1()
.rounded_sm()
.gap_1()
.child(self.icon().size(IconSize::XSmall).color(Color::Muted));
match &self {
ContextPill::Added {
context,
dupe_name,
on_remove,
focused,
on_click,
} => {
let status_is_error = matches!(context.status, ContextStatus::Error { .. });
let status_is_warning = matches!(context.status, ContextStatus::Warning { .. });
base_pill
.pr(if on_remove.is_some() { px(2.) } else { px(4.) })
.map(|pill| {
if status_is_error {
pill.bg(cx.theme().status().error_background)
.border_color(cx.theme().status().error_border)
} else if status_is_warning {
pill.bg(cx.theme().status().warning_background)
.border_color(cx.theme().status().warning_border)
} else if *focused {
pill.bg(color.element_background)
.border_color(color.border_focused)
} else {
pill.bg(color.element_background)
.border_color(color.border.opacity(0.5))
}
})
.child(
h_flex()
.id("context-data")
.gap_1()
.child(
div().max_w_64().child(
Label::new(context.name.clone())
.size(LabelSize::Small)
.truncate(),
),
)
.when_some(context.parent.as_ref(), |element, parent_name| {
if *dupe_name {
element.child(
Label::new(parent_name.clone())
.size(LabelSize::XSmall)
.color(Color::Muted),
)
} else {
element
}
})
.when_some(context.tooltip.as_ref(), |element, tooltip| {
element.tooltip(Tooltip::text(tooltip.clone()))
})
.map(|element| match &context.status {
ContextStatus::Ready => element
.when_some(
context.render_hover.as_ref(),
|element, render_hover| {
let render_hover = render_hover.clone();
element.hoverable_tooltip(move |window, cx| {
render_hover(window, cx)
})
},
)
.into_any(),
ContextStatus::Loading { message } => element
.tooltip(ui::Tooltip::text(message.clone()))
.with_animation(
"pulsating-ctx-pill",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.4, 0.8)),
|label, delta| label.opacity(delta),
)
.into_any_element(),
ContextStatus::Warning { message }
| ContextStatus::Error { message } => element
.tooltip(ui::Tooltip::text(message.clone()))
.into_any_element(),
}),
)
.when_some(on_remove.as_ref(), |element, on_remove| {
element.child(
IconButton::new(
context.handle.element_id("remove".into()),
IconName::Close,
)
.shape(IconButtonShape::Square)
.icon_size(IconSize::XSmall)
.tooltip(Tooltip::text("Remove Context"))
.on_click({
let on_remove = on_remove.clone();
move |event, window, cx| on_remove(event, window, cx)
}),
)
})
.when_some(on_click.as_ref(), |element, on_click| {
let on_click = on_click.clone();
element.cursor_pointer().on_click(move |event, window, cx| {
on_click(event, window, cx);
cx.stop_propagation();
})
})
.into_any_element()
}
ContextPill::Suggested {
name,
icon_path: _,
kind: _,
focused,
on_click,
} => base_pill
.cursor_pointer()
.pr_1()
.border_dashed()
.map(|pill| {
if *focused {
pill.border_color(color.border_focused)
.bg(color.element_background.opacity(0.5))
} else {
pill.border_color(color.border)
}
})
.hover(|style| style.bg(color.element_hover.opacity(0.5)))
.child(
div().max_w_64().child(
Label::new(name.clone())
.size(LabelSize::Small)
.color(Color::Muted)
.truncate(),
),
)
.tooltip(|_window, cx| {
Tooltip::with_meta("Suggested Context", None, "Click to add it", cx)
})
.when_some(on_click.as_ref(), |element, on_click| {
let on_click = on_click.clone();
element.on_click(move |event, window, cx| {
on_click(event, window, cx);
cx.stop_propagation();
})
})
.into_any(),
}
}
}
pub enum ContextStatus {
Ready,
Loading { message: SharedString },
Error { message: SharedString },
Warning { message: SharedString },
}
#[derive(RegisterComponent)]
pub struct AddedContext {
pub handle: AgentContextHandle,
pub kind: ContextKind,
pub name: SharedString,
pub parent: Option<SharedString>,
pub tooltip: Option<SharedString>,
pub icon_path: Option<SharedString>,
pub status: ContextStatus,
pub render_hover: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyView + 'static>>,
}
impl AddedContext {
pub fn icon(&self) -> Icon {
match &self.status {
ContextStatus::Warning { .. } => Icon::new(IconName::Warning).color(Color::Warning),
ContextStatus::Error { .. } => Icon::new(IconName::XCircle).color(Color::Error),
_ => {
if let Some(icon_path) = &self.icon_path {
Icon::from_path(icon_path)
} else {
Icon::new(self.kind.icon())
}
}
}
}
/// Creates an `AddedContext` by retrieving relevant details of `AgentContext`. This returns a
/// `None` if `DirectoryContext` or `RulesContext` no longer exist.
///
/// TODO: `None` cases are unremovable from `ContextStore` and so are a very minor memory leak.
pub fn new_pending(
handle: AgentContextHandle,
prompt_store: Option<&Entity<PromptStore>>,
project: &Project,
model: Option<&Arc<dyn language_model::LanguageModel>>,
cx: &App,
) -> Option<AddedContext> {
match handle {
AgentContextHandle::File(handle) => {
Self::pending_file(handle, project.path_style(cx), cx)
}
AgentContextHandle::Directory(handle) => Self::pending_directory(handle, project, cx),
AgentContextHandle::Symbol(handle) => {
Self::pending_symbol(handle, project.path_style(cx), cx)
}
AgentContextHandle::Selection(handle) => {
Self::pending_selection(handle, project.path_style(cx), cx)
}
AgentContextHandle::FetchedUrl(handle) => Some(Self::fetched_url(handle)),
AgentContextHandle::Thread(handle) => Some(Self::pending_thread(handle, cx)),
AgentContextHandle::TextThread(handle) => Some(Self::pending_text_thread(handle, cx)),
AgentContextHandle::Rules(handle) => Self::pending_rules(handle, prompt_store, cx),
AgentContextHandle::Image(handle) => {
Some(Self::image(handle, model, project.path_style(cx), cx))
}
}
}
fn pending_file(
handle: FileContextHandle,
path_style: PathStyle,
cx: &App,
) -> Option<AddedContext> {
let full_path = handle
.buffer
.read(cx)
.file()?
.full_path(cx)
.to_string_lossy()
.to_string();
Some(Self::file(handle, &full_path, path_style, cx))
}
fn file(
handle: FileContextHandle,
full_path: &str,
path_style: PathStyle,
cx: &App,
) -> AddedContext {
let (name, parent) = extract_file_name_and_directory_from_full_path(full_path, path_style);
AddedContext {
kind: ContextKind::File,
name,
parent,
tooltip: Some(SharedString::new(full_path)),
icon_path: FileIcons::get_icon(Path::new(full_path), cx),
status: ContextStatus::Ready,
render_hover: None,
handle: AgentContextHandle::File(handle),
}
}
fn pending_directory(
handle: DirectoryContextHandle,
project: &Project,
cx: &App,
) -> Option<AddedContext> {
let worktree = project.worktree_for_entry(handle.entry_id, cx)?.read(cx);
let entry = worktree.entry_for_id(handle.entry_id)?;
let full_path = worktree
.full_path(&entry.path)
.to_string_lossy()
.to_string();
Some(Self::directory(handle, &full_path, project.path_style(cx)))
}
fn directory(
handle: DirectoryContextHandle,
full_path: &str,
path_style: PathStyle,
) -> AddedContext {
let (name, parent) = extract_file_name_and_directory_from_full_path(full_path, path_style);
AddedContext {
kind: ContextKind::Directory,
name,
parent,
tooltip: Some(SharedString::new(full_path)),
icon_path: None,
status: ContextStatus::Ready,
render_hover: None,
handle: AgentContextHandle::Directory(handle),
}
}
fn pending_symbol(
handle: SymbolContextHandle,
path_style: PathStyle,
cx: &App,
) -> Option<AddedContext> {
let excerpt = ContextFileExcerpt::new(
&handle.full_path(cx)?.to_string_lossy(),
handle.enclosing_line_range(cx),
path_style,
cx,
);
Some(AddedContext {
kind: ContextKind::Symbol,
name: handle.symbol.clone(),
parent: Some(excerpt.file_name_and_range.clone()),
tooltip: None,
icon_path: None,
status: ContextStatus::Ready,
render_hover: {
let handle = handle.clone();
Some(Rc::new(move |_, cx| {
excerpt.hover_view(handle.text(cx), cx).into()
}))
},
handle: AgentContextHandle::Symbol(handle),
})
}
fn pending_selection(
handle: SelectionContextHandle,
path_style: PathStyle,
cx: &App,
) -> Option<AddedContext> {
let excerpt = ContextFileExcerpt::new(
&handle.full_path(cx)?.to_string_lossy(),
handle.line_range(cx),
path_style,
cx,
);
Some(AddedContext {
kind: ContextKind::Selection,
name: excerpt.file_name_and_range.clone(),
parent: excerpt.parent_name.clone(),
tooltip: None,
icon_path: excerpt.icon_path.clone(),
status: ContextStatus::Ready,
render_hover: {
let handle = handle.clone();
Some(Rc::new(move |_, cx| {
excerpt.hover_view(handle.text(cx), cx).into()
}))
},
handle: AgentContextHandle::Selection(handle),
})
}
fn fetched_url(context: FetchedUrlContext) -> AddedContext {
AddedContext {
kind: ContextKind::FetchedUrl,
name: context.url.clone(),
parent: None,
tooltip: None,
icon_path: None,
status: ContextStatus::Ready,
render_hover: None,
handle: AgentContextHandle::FetchedUrl(context),
}
}
fn pending_thread(handle: ThreadContextHandle, cx: &App) -> AddedContext {
AddedContext {
kind: ContextKind::Thread,
name: handle.title(cx),
parent: None,
tooltip: None,
icon_path: None,
status: if handle.thread.read(cx).is_generating_summary() {
ContextStatus::Loading {
message: "Summarizing…".into(),
}
} else {
ContextStatus::Ready
},
render_hover: {
let thread = handle.thread.clone();
Some(Rc::new(move |_, cx| {
let text = thread
.update(cx, |thread, cx| thread.summary(cx))
.now_or_never()
.flatten()
.unwrap_or_else(|| SharedString::from(thread.read(cx).to_markdown()));
ContextPillHover::new_text(text, cx).into()
}))
},
handle: AgentContextHandle::Thread(handle),
}
}
fn pending_text_thread(handle: TextThreadContextHandle, cx: &App) -> AddedContext {
AddedContext {
kind: ContextKind::TextThread,
name: handle.title(cx),
parent: None,
tooltip: None,
icon_path: None,
status: ContextStatus::Ready,
render_hover: {
let text_thread = handle.text_thread.clone();
Some(Rc::new(move |_, cx| {
let text = text_thread.read(cx).to_xml(cx);
ContextPillHover::new_text(text.into(), cx).into()
}))
},
handle: AgentContextHandle::TextThread(handle),
}
}
fn pending_rules(
handle: RulesContextHandle,
prompt_store: Option<&Entity<PromptStore>>,
cx: &App,
) -> Option<AddedContext> {
let title = prompt_store
.as_ref()?
.read(cx)
.metadata(handle.prompt_id.into())?
.title
.unwrap_or_else(|| "Unnamed Rule".into());
Some(AddedContext {
kind: ContextKind::Rules,
name: title,
parent: None,
tooltip: None,
icon_path: None,
status: ContextStatus::Ready,
render_hover: None,
handle: AgentContextHandle::Rules(handle),
})
}
fn image(
context: ImageContext,
model: Option<&Arc<dyn language_model::LanguageModel>>,
path_style: PathStyle,
cx: &App,
) -> AddedContext {
let (name, parent, icon_path) = if let Some(full_path) = context.full_path.as_ref() {
let (name, parent) =
extract_file_name_and_directory_from_full_path(full_path, path_style);
let icon_path = FileIcons::get_icon(Path::new(full_path), cx);
(name, parent, icon_path)
} else {
("Image".into(), None, None)
};
let status = match context.status(model) {
ImageStatus::Loading => ContextStatus::Loading {
message: "Loading…".into(),
},
ImageStatus::Error => ContextStatus::Error {
message: "Failed to load Image".into(),
},
ImageStatus::Warning => ContextStatus::Warning {
message: format!(
"{} doesn't support attaching Images as Context",
model.map(|m| m.name().0).unwrap_or_else(|| "Model".into())
)
.into(),
},
ImageStatus::Ready => ContextStatus::Ready,
};
AddedContext {
kind: ContextKind::Image,
name,
parent,
tooltip: None,
icon_path,
status,
render_hover: Some(Rc::new({
let image = context.original_image.clone();
move |_, cx| {
let image = image.clone();
ContextPillHover::new(cx, move |_, _| {
gpui::img(image.clone())
.max_w_96()
.max_h_96()
.into_any_element()
})
.into()
}
})),
handle: AgentContextHandle::Image(context),
}
}
}
fn extract_file_name_and_directory_from_full_path(
path: &str,
path_style: PathStyle,
) -> (SharedString, Option<SharedString>) {
let (parent, file_name) = path_style.split(path);
let parent = parent.and_then(|parent| {
let parent = parent.trim_end_matches(path_style.separator());
let (_, parent) = path_style.split(parent);
if parent.is_empty() {
None
} else {
Some(SharedString::new(parent))
}
});
(SharedString::new(file_name), parent)
}
#[derive(Debug, Clone)]
struct ContextFileExcerpt {
pub file_name_and_range: SharedString,
pub full_path_and_range: SharedString,
pub parent_name: Option<SharedString>,
pub icon_path: Option<SharedString>,
}
impl ContextFileExcerpt {
pub fn new(full_path: &str, line_range: Range<Point>, path_style: PathStyle, cx: &App) -> Self {
let (parent, file_name) = path_style.split(full_path);
let line_range_text = format!(" ({}-{})", line_range.start.row + 1, line_range.end.row + 1);
let mut full_path_and_range = full_path.to_owned();
full_path_and_range.push_str(&line_range_text);
let mut file_name_and_range = file_name.to_owned();
file_name_and_range.push_str(&line_range_text);
let parent_name = parent.and_then(|parent| {
let parent = parent.trim_end_matches(path_style.separator());
let (_, parent) = path_style.split(parent);
if parent.is_empty() {
None
} else {
Some(SharedString::new(parent))
}
});
let icon_path = FileIcons::get_icon(Path::new(full_path), cx);
ContextFileExcerpt {
file_name_and_range: file_name_and_range.into(),
full_path_and_range: full_path_and_range.into(),
parent_name,
icon_path,
}
}
fn hover_view(&self, text: SharedString, cx: &mut App) -> Entity<ContextPillHover> {
let icon_path = self.icon_path.clone();
let full_path_and_range = self.full_path_and_range.clone();
ContextPillHover::new(cx, move |_, cx| {
v_flex()
.child(
h_flex()
.gap_0p5()
.w_full()
.max_w_full()
.border_b_1()
.border_color(cx.theme().colors().border.opacity(0.6))
.children(
icon_path
.clone()
.map(Icon::from_path)
.map(|icon| icon.color(Color::Muted).size(IconSize::XSmall)),
)
.child(
// TODO: make this truncate on the left.
Label::new(full_path_and_range.clone())
.size(LabelSize::Small)
.ml_1(),
),
)
.child(
div()
.id("context-pill-hover-contents")
.overflow_scroll()
.max_w_128()
.max_h_96()
.child(Label::new(text.clone()).buffer_font(cx)),
)
.into_any_element()
})
}
}
struct ContextPillHover {
render_hover: Box<dyn Fn(&mut Window, &mut App) -> AnyElement>,
}
impl ContextPillHover {
fn new(
cx: &mut App,
render_hover: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
) -> Entity<Self> {
cx.new(|_| Self {
render_hover: Box::new(render_hover),
})
}
fn new_text(content: SharedString, cx: &mut App) -> Entity<Self> {
Self::new(cx, move |_, _| {
div()
.id("context-pill-hover-contents")
.overflow_scroll()
.max_w_128()
.max_h_96()
.child(content.clone())
.into_any_element()
})
}
}
impl Render for ContextPillHover {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
tooltip_container(cx, move |this, cx| {
this.occlude()
.on_mouse_move(|_, _, cx| cx.stop_propagation())
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
.child((self.render_hover)(window, cx))
})
}
}
impl Component for AddedContext {
fn scope() -> ComponentScope {
ComponentScope::Agent
}
fn sort_name() -> &'static str {
"AddedContext"
}
fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
let mut next_context_id = ContextId::zero();
let image_ready = (
"Ready",
AddedContext::image(
ImageContext {
context_id: next_context_id.post_inc(),
project_path: None,
full_path: None,
original_image: Arc::new(Image::empty()),
image_task: Task::ready(Some(LanguageModelImage::empty())).shared(),
},
None,
PathStyle::local(),
cx,
),
);
let image_loading = (
"Loading",
AddedContext::image(
ImageContext {
context_id: next_context_id.post_inc(),
project_path: None,
full_path: None,
original_image: Arc::new(Image::empty()),
image_task: cx
.background_spawn(async move {
smol::Timer::after(Duration::from_secs(60 * 5)).await;
Some(LanguageModelImage::empty())
})
.shared(),
},
None,
PathStyle::local(),
cx,
),
);
let image_error = (
"Error",
AddedContext::image(
ImageContext {
context_id: next_context_id.post_inc(),
project_path: None,
full_path: None,
original_image: Arc::new(Image::empty()),
image_task: Task::ready(None).shared(),
},
None,
PathStyle::local(),
cx,
),
);
Some(
v_flex()
.gap_6()
.children(
vec![image_ready, image_loading, image_error]
.into_iter()
.map(|(text, context)| {
single_example(
text,
ContextPill::added(context, false, false, None).into_any_element(),
)
}),
)
.into_any(),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use gpui::App;
use language_model::{LanguageModel, fake_provider::FakeLanguageModel};
use std::sync::Arc;
#[gpui::test]
fn test_image_context_warning_for_unsupported_model(cx: &mut App) {
let model: Arc<dyn LanguageModel> = Arc::new(FakeLanguageModel::default());
assert!(!model.supports_images());
let image_context = ImageContext {
context_id: ContextId::zero(),
project_path: None,
original_image: Arc::new(Image::empty()),
image_task: Task::ready(Some(LanguageModelImage::empty())).shared(),
full_path: None,
};
let added_context =
AddedContext::image(image_context, Some(&model), PathStyle::local(), cx);
assert!(matches!(
added_context.status,
ContextStatus::Warning { .. }
));
assert!(matches!(added_context.kind, ContextKind::Image));
assert_eq!(added_context.name.as_ref(), "Image");
assert!(added_context.parent.is_none());
assert!(added_context.icon_path.is_none());
}
#[gpui::test]
fn test_image_context_ready_for_no_model(cx: &mut App) {
let image_context = ImageContext {
context_id: ContextId::zero(),
project_path: None,
original_image: Arc::new(Image::empty()),
image_task: Task::ready(Some(LanguageModelImage::empty())).shared(),
full_path: None,
};
let added_context = AddedContext::image(image_context, None, PathStyle::local(), cx);
assert!(
matches!(added_context.status, ContextStatus::Ready),
"Expected ready status when no model provided"
);
assert!(matches!(added_context.kind, ContextKind::Image));
assert_eq!(added_context.name.as_ref(), "Image");
assert!(added_context.parent.is_none());
assert!(added_context.icon_path.is_none());
}
}

View File

@@ -1,40 +0,0 @@
use gpui::{App, IntoElement, Modifiers, RenderOnce, Window};
use ui::{prelude::*, render_modifiers};
#[derive(IntoElement)]
pub struct HoldForDefault {
is_default: bool,
}
impl HoldForDefault {
pub fn new(is_default: bool) -> Self {
Self { is_default }
}
}
impl RenderOnce for HoldForDefault {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
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(render_modifiers(
&Modifiers::secondary_key(),
PlatformStyle::platform(),
None,
Some(TextSize::Default.rems(cx).into()),
true,
)))
.child(div().map(|this| {
if self.is_default {
this.child("to unset as default")
} else {
this.child("to set as default")
}
}))
}
}

View File

@@ -67,13 +67,6 @@ pub enum Model {
alias = "claude-opus-4-1-thinking-latest" alias = "claude-opus-4-1-thinking-latest"
)] )]
ClaudeOpus4_1Thinking, ClaudeOpus4_1Thinking,
#[serde(rename = "claude-opus-4-5", alias = "claude-opus-4-5-latest")]
ClaudeOpus4_5,
#[serde(
rename = "claude-opus-4-5-thinking",
alias = "claude-opus-4-5-thinking-latest"
)]
ClaudeOpus4_5Thinking,
#[serde(rename = "claude-sonnet-4", alias = "claude-sonnet-4-latest")] #[serde(rename = "claude-sonnet-4", alias = "claude-sonnet-4-latest")]
ClaudeSonnet4, ClaudeSonnet4,
#[serde( #[serde(
@@ -138,14 +131,6 @@ impl Model {
} }
pub fn from_id(id: &str) -> Result<Self> { pub fn from_id(id: &str) -> Result<Self> {
if id.starts_with("claude-opus-4-5-thinking") {
return Ok(Self::ClaudeOpus4_5Thinking);
}
if id.starts_with("claude-opus-4-5") {
return Ok(Self::ClaudeOpus4_5);
}
if id.starts_with("claude-opus-4-1-thinking") { if id.starts_with("claude-opus-4-1-thinking") {
return Ok(Self::ClaudeOpus4_1Thinking); return Ok(Self::ClaudeOpus4_1Thinking);
} }
@@ -223,8 +208,6 @@ impl Model {
Self::ClaudeOpus4_1 => "claude-opus-4-1-latest", Self::ClaudeOpus4_1 => "claude-opus-4-1-latest",
Self::ClaudeOpus4Thinking => "claude-opus-4-thinking-latest", Self::ClaudeOpus4Thinking => "claude-opus-4-thinking-latest",
Self::ClaudeOpus4_1Thinking => "claude-opus-4-1-thinking-latest", Self::ClaudeOpus4_1Thinking => "claude-opus-4-1-thinking-latest",
Self::ClaudeOpus4_5 => "claude-opus-4-5-latest",
Self::ClaudeOpus4_5Thinking => "claude-opus-4-5-thinking-latest",
Self::ClaudeSonnet4 => "claude-sonnet-4-latest", Self::ClaudeSonnet4 => "claude-sonnet-4-latest",
Self::ClaudeSonnet4Thinking => "claude-sonnet-4-thinking-latest", Self::ClaudeSonnet4Thinking => "claude-sonnet-4-thinking-latest",
Self::ClaudeSonnet4_5 => "claude-sonnet-4-5-latest", Self::ClaudeSonnet4_5 => "claude-sonnet-4-5-latest",
@@ -247,7 +230,6 @@ impl Model {
match self { match self {
Self::ClaudeOpus4 | Self::ClaudeOpus4Thinking => "claude-opus-4-20250514", Self::ClaudeOpus4 | Self::ClaudeOpus4Thinking => "claude-opus-4-20250514",
Self::ClaudeOpus4_1 | Self::ClaudeOpus4_1Thinking => "claude-opus-4-1-20250805", Self::ClaudeOpus4_1 | Self::ClaudeOpus4_1Thinking => "claude-opus-4-1-20250805",
Self::ClaudeOpus4_5 | Self::ClaudeOpus4_5Thinking => "claude-opus-4-5-20251101",
Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking => "claude-sonnet-4-20250514", Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking => "claude-sonnet-4-20250514",
Self::ClaudeSonnet4_5 | Self::ClaudeSonnet4_5Thinking => "claude-sonnet-4-5-20250929", Self::ClaudeSonnet4_5 | Self::ClaudeSonnet4_5Thinking => "claude-sonnet-4-5-20250929",
Self::Claude3_5Sonnet => "claude-3-5-sonnet-latest", Self::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
@@ -267,8 +249,6 @@ impl Model {
Self::ClaudeOpus4_1 => "Claude Opus 4.1", Self::ClaudeOpus4_1 => "Claude Opus 4.1",
Self::ClaudeOpus4Thinking => "Claude Opus 4 Thinking", Self::ClaudeOpus4Thinking => "Claude Opus 4 Thinking",
Self::ClaudeOpus4_1Thinking => "Claude Opus 4.1 Thinking", Self::ClaudeOpus4_1Thinking => "Claude Opus 4.1 Thinking",
Self::ClaudeOpus4_5 => "Claude Opus 4.5",
Self::ClaudeOpus4_5Thinking => "Claude Opus 4.5 Thinking",
Self::ClaudeSonnet4 => "Claude Sonnet 4", Self::ClaudeSonnet4 => "Claude Sonnet 4",
Self::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking", Self::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking",
Self::ClaudeSonnet4_5 => "Claude Sonnet 4.5", Self::ClaudeSonnet4_5 => "Claude Sonnet 4.5",
@@ -294,8 +274,6 @@ impl Model {
| Self::ClaudeOpus4_1 | Self::ClaudeOpus4_1
| Self::ClaudeOpus4Thinking | Self::ClaudeOpus4Thinking
| Self::ClaudeOpus4_1Thinking | Self::ClaudeOpus4_1Thinking
| Self::ClaudeOpus4_5
| Self::ClaudeOpus4_5Thinking
| Self::ClaudeSonnet4 | Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking | Self::ClaudeSonnet4Thinking
| Self::ClaudeSonnet4_5 | Self::ClaudeSonnet4_5
@@ -325,8 +303,6 @@ impl Model {
| Self::ClaudeOpus4_1 | Self::ClaudeOpus4_1
| Self::ClaudeOpus4Thinking | Self::ClaudeOpus4Thinking
| Self::ClaudeOpus4_1Thinking | Self::ClaudeOpus4_1Thinking
| Self::ClaudeOpus4_5
| Self::ClaudeOpus4_5Thinking
| Self::ClaudeSonnet4 | Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking | Self::ClaudeSonnet4Thinking
| Self::ClaudeSonnet4_5 | Self::ClaudeSonnet4_5
@@ -350,8 +326,6 @@ impl Model {
| Self::ClaudeOpus4_1 | Self::ClaudeOpus4_1
| Self::ClaudeOpus4Thinking | Self::ClaudeOpus4Thinking
| Self::ClaudeOpus4_1Thinking | Self::ClaudeOpus4_1Thinking
| Self::ClaudeOpus4_5
| Self::ClaudeOpus4_5Thinking
| Self::ClaudeSonnet4 | Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking | Self::ClaudeSonnet4Thinking
| Self::ClaudeSonnet4_5 | Self::ClaudeSonnet4_5
@@ -374,8 +348,6 @@ impl Model {
| Self::ClaudeOpus4_1 | Self::ClaudeOpus4_1
| Self::ClaudeOpus4Thinking | Self::ClaudeOpus4Thinking
| Self::ClaudeOpus4_1Thinking | Self::ClaudeOpus4_1Thinking
| Self::ClaudeOpus4_5
| Self::ClaudeOpus4_5Thinking
| Self::ClaudeSonnet4 | Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking | Self::ClaudeSonnet4Thinking
| Self::ClaudeSonnet4_5 | Self::ClaudeSonnet4_5
@@ -400,7 +372,6 @@ impl Model {
match self { match self {
Self::ClaudeOpus4 Self::ClaudeOpus4
| Self::ClaudeOpus4_1 | Self::ClaudeOpus4_1
| Self::ClaudeOpus4_5
| Self::ClaudeSonnet4 | Self::ClaudeSonnet4
| Self::ClaudeSonnet4_5 | Self::ClaudeSonnet4_5
| Self::Claude3_5Sonnet | Self::Claude3_5Sonnet
@@ -412,7 +383,6 @@ impl Model {
| Self::Claude3Haiku => AnthropicModelMode::Default, | Self::Claude3Haiku => AnthropicModelMode::Default,
Self::ClaudeOpus4Thinking Self::ClaudeOpus4Thinking
| Self::ClaudeOpus4_1Thinking | Self::ClaudeOpus4_1Thinking
| Self::ClaudeOpus4_5Thinking
| Self::ClaudeSonnet4Thinking | Self::ClaudeSonnet4Thinking
| Self::ClaudeSonnet4_5Thinking | Self::ClaudeSonnet4_5Thinking
| Self::ClaudeHaiku4_5Thinking | Self::ClaudeHaiku4_5Thinking
@@ -423,8 +393,13 @@ impl Model {
} }
} }
pub fn beta_headers(&self) -> Option<String> { pub const DEFAULT_BETA_HEADERS: &[&str] = &["prompt-caching-2024-07-31"];
let mut headers = vec![];
pub fn beta_headers(&self) -> String {
let mut headers = Self::DEFAULT_BETA_HEADERS
.iter()
.map(|header| header.to_string())
.collect::<Vec<_>>();
match self { match self {
Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => { Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => {
@@ -445,11 +420,7 @@ impl Model {
_ => {} _ => {}
} }
if headers.is_empty() { headers.join(",")
None
} else {
Some(headers.join(","))
}
} }
pub fn tool_model_id(&self) -> &str { pub fn tool_model_id(&self) -> &str {
@@ -465,12 +436,56 @@ impl Model {
} }
} }
pub async fn complete(
client: &dyn HttpClient,
api_url: &str,
api_key: &str,
request: Request,
beta_headers: String,
) -> Result<Response, AnthropicError> {
let uri = format!("{api_url}/v1/messages");
let request_builder = HttpRequest::builder()
.method(Method::POST)
.uri(uri)
.header("Anthropic-Version", "2023-06-01")
.header("Anthropic-Beta", beta_headers)
.header("X-Api-Key", api_key.trim())
.header("Content-Type", "application/json");
let serialized_request =
serde_json::to_string(&request).map_err(AnthropicError::SerializeRequest)?;
let request = request_builder
.body(AsyncBody::from(serialized_request))
.map_err(AnthropicError::BuildRequestBody)?;
let mut response = client
.send(request)
.await
.map_err(AnthropicError::HttpSend)?;
let status_code = response.status();
let mut body = String::new();
response
.body_mut()
.read_to_string(&mut body)
.await
.map_err(AnthropicError::ReadResponse)?;
if status_code.is_success() {
Ok(serde_json::from_str(&body).map_err(AnthropicError::DeserializeResponse)?)
} else {
Err(AnthropicError::HttpResponseError {
status_code,
message: body,
})
}
}
pub async fn stream_completion( pub async fn stream_completion(
client: &dyn HttpClient, client: &dyn HttpClient,
api_url: &str, api_url: &str,
api_key: &str, api_key: &str,
request: Request, request: Request,
beta_headers: Option<String>, beta_headers: String,
) -> Result<BoxStream<'static, Result<Event, AnthropicError>>, AnthropicError> { ) -> Result<BoxStream<'static, Result<Event, AnthropicError>>, AnthropicError> {
stream_completion_with_rate_limit_info(client, api_url, api_key, request, beta_headers) stream_completion_with_rate_limit_info(client, api_url, api_key, request, beta_headers)
.await .await
@@ -568,7 +583,7 @@ pub async fn stream_completion_with_rate_limit_info(
api_url: &str, api_url: &str,
api_key: &str, api_key: &str,
request: Request, request: Request,
beta_headers: Option<String>, beta_headers: String,
) -> Result< ) -> Result<
( (
BoxStream<'static, Result<Event, AnthropicError>>, BoxStream<'static, Result<Event, AnthropicError>>,
@@ -582,17 +597,13 @@ pub async fn stream_completion_with_rate_limit_info(
}; };
let uri = format!("{api_url}/v1/messages"); let uri = format!("{api_url}/v1/messages");
let mut request_builder = HttpRequest::builder() let request_builder = HttpRequest::builder()
.method(Method::POST) .method(Method::POST)
.uri(uri) .uri(uri)
.header("Anthropic-Version", "2023-06-01") .header("Anthropic-Version", "2023-06-01")
.header("Anthropic-Beta", beta_headers)
.header("X-Api-Key", api_key.trim()) .header("X-Api-Key", api_key.trim())
.header("Content-Type", "application/json"); .header("Content-Type", "application/json");
if let Some(beta_headers) = beta_headers {
request_builder = request_builder.header("Anthropic-Beta", beta_headers);
}
let serialized_request = let serialized_request =
serde_json::to_string(&request).map_err(AnthropicError::SerializeRequest)?; serde_json::to_string(&request).map_err(AnthropicError::SerializeRequest)?;
let request = request_builder let request = request_builder

View File

@@ -205,9 +205,13 @@ impl PasswordProxy {
} else { } else {
ShellKind::Posix ShellKind::Posix
}; };
let askpass_program = ASKPASS_PROGRAM.get_or_init(|| current_exec); let askpass_program = ASKPASS_PROGRAM
.get_or_init(|| current_exec)
.try_shell_safe(shell_kind)
.context("Failed to shell-escape Askpass program path.")?
.to_string();
// Create an askpass script that communicates back to this process. // Create an askpass script that communicates back to this process.
let askpass_script = generate_askpass_script(shell_kind, askpass_program, &askpass_socket)?; let askpass_script = generate_askpass_script(&askpass_program, &askpass_socket);
let _task = executor.spawn(async move { let _task = executor.spawn(async move {
maybe!(async move { maybe!(async move {
let listener = let listener =
@@ -330,51 +334,23 @@ pub fn set_askpass_program(path: std::path::PathBuf) {
#[inline] #[inline]
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
fn generate_askpass_script( fn generate_askpass_script(askpass_program: &str, askpass_socket: &std::path::Path) -> String {
shell_kind: ShellKind, format!(
askpass_program: &std::path::Path,
askpass_socket: &std::path::Path,
) -> Result<String> {
let askpass_program = shell_kind.prepend_command_prefix(
askpass_program
.to_str()
.context("Askpass program is on a non-utf8 path")?,
);
let askpass_program = shell_kind
.try_quote_prefix_aware(&askpass_program)
.context("Failed to shell-escape Askpass program path")?;
let askpass_socket = askpass_socket
.try_shell_safe(shell_kind)
.context("Failed to shell-escape Askpass socket path")?;
let print_args = "printf '%s\\0' \"$@\"";
let shebang = "#!/bin/sh";
Ok(format!(
"{shebang}\n{print_args} | {askpass_program} --askpass={askpass_socket} 2> /dev/null \n", "{shebang}\n{print_args} | {askpass_program} --askpass={askpass_socket} 2> /dev/null \n",
)) askpass_socket = askpass_socket.display(),
print_args = "printf '%s\\0' \"$@\"",
shebang = "#!/bin/sh",
)
} }
#[inline] #[inline]
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
fn generate_askpass_script( fn generate_askpass_script(askpass_program: &str, askpass_socket: &std::path::Path) -> String {
shell_kind: ShellKind, format!(
askpass_program: &std::path::Path,
askpass_socket: &std::path::Path,
) -> Result<String> {
let askpass_program = shell_kind.prepend_command_prefix(
askpass_program
.to_str()
.context("Askpass program is on a non-utf8 path")?,
);
let askpass_program = shell_kind
.try_quote_prefix_aware(&askpass_program)
.context("Failed to shell-escape Askpass program path")?;
let askpass_socket = askpass_socket
.try_shell_safe(shell_kind)
.context("Failed to shell-escape Askpass socket path")?;
Ok(format!(
r#" r#"
$ErrorActionPreference = 'Stop'; $ErrorActionPreference = 'Stop';
($args -join [char]0) | {askpass_program} --askpass={askpass_socket} 2> $null ($args -join [char]0) | & {askpass_program} --askpass={askpass_socket} 2> $null
"#, "#,
)) askpass_socket = askpass_socket.display(),
)
} }

View File

@@ -29,7 +29,6 @@ fs.workspace = true
futures.workspace = true futures.workspace = true
fuzzy.workspace = true fuzzy.workspace = true
gpui.workspace = true gpui.workspace = true
itertools.workspace = true
language.workspace = true language.workspace = true
language_model.workspace = true language_model.workspace = true
log.workspace = true log.workspace = true

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