Compare commits
5 Commits
arm_github
...
debugger-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
586fc30222 | ||
|
|
0a21521872 | ||
|
|
9029a34756 | ||
|
|
b7f648ccb9 | ||
|
|
483b675490 |
32
.github/actions/build_docs/action.yml
vendored
32
.github/actions/build_docs/action.yml
vendored
@@ -1,32 +0,0 @@
|
|||||||
name: "Build docs"
|
|
||||||
description: "Build the docs"
|
|
||||||
|
|
||||||
runs:
|
|
||||||
using: "composite"
|
|
||||||
steps:
|
|
||||||
- name: Setup mdBook
|
|
||||||
uses: peaceiris/actions-mdbook@ee69d230fe19748b7abf22df32acaa93833fad08 # v2
|
|
||||||
with:
|
|
||||||
mdbook-version: "0.4.37"
|
|
||||||
|
|
||||||
- name: Cache dependencies
|
|
||||||
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
|
||||||
with:
|
|
||||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
|
||||||
cache-provider: "buildjet"
|
|
||||||
|
|
||||||
- name: Install Linux dependencies
|
|
||||||
shell: bash -euxo pipefail {0}
|
|
||||||
run: ./script/linux
|
|
||||||
|
|
||||||
- name: Check for broken links
|
|
||||||
uses: lycheeverse/lychee-action@82202e5e9c2f4ef1a55a3d02563e1cb6041e5332 # v2.4.1
|
|
||||||
with:
|
|
||||||
args: --no-progress './docs/src/**/*'
|
|
||||||
fail: true
|
|
||||||
|
|
||||||
- name: Build book
|
|
||||||
shell: bash -euxo pipefail {0}
|
|
||||||
run: |
|
|
||||||
mkdir -p target/deploy
|
|
||||||
mdbook build ./docs --dest-dir=../target/deploy/docs/
|
|
||||||
85
.github/workflows/ci.yml
vendored
85
.github/workflows/ci.yml
vendored
@@ -191,27 +191,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
config: ./typos.toml
|
config: ./typos.toml
|
||||||
|
|
||||||
check_docs:
|
|
||||||
timeout-minutes: 60
|
|
||||||
name: Check docs
|
|
||||||
needs: [job_spec]
|
|
||||||
if: github.repository_owner == 'zed-industries'
|
|
||||||
runs-on:
|
|
||||||
- buildjet-8vcpu-ubuntu-2204
|
|
||||||
steps:
|
|
||||||
- name: Checkout repo
|
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
|
||||||
with:
|
|
||||||
clean: false
|
|
||||||
|
|
||||||
- name: Configure CI
|
|
||||||
run: |
|
|
||||||
mkdir -p ./../.cargo
|
|
||||||
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
|
|
||||||
|
|
||||||
- name: Build docs
|
|
||||||
uses: ./.github/actions/build_docs
|
|
||||||
|
|
||||||
macos_tests:
|
macos_tests:
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
name: (macOS) Run Clippy and tests
|
name: (macOS) Run Clippy and tests
|
||||||
@@ -683,7 +662,7 @@ jobs:
|
|||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
name: Linux arm64 release bundle
|
name: Linux arm64 release bundle
|
||||||
runs-on:
|
runs-on:
|
||||||
- hosted-linux-arm-1
|
- buildjet-16vcpu-ubuntu-2204-arm
|
||||||
if: |
|
if: |
|
||||||
startsWith(github.ref, 'refs/tags/v')
|
startsWith(github.ref, 'refs/tags/v')
|
||||||
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
|
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
|
||||||
@@ -736,64 +715,6 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
freebsd:
|
|
||||||
timeout-minutes: 60
|
|
||||||
runs-on: github-8vcpu-ubuntu-2404
|
|
||||||
if: |
|
|
||||||
startsWith(github.ref, 'refs/tags/v')
|
|
||||||
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
|
|
||||||
needs: [linux_tests]
|
|
||||||
name: Build Zed on FreeBSD
|
|
||||||
# env:
|
|
||||||
# MYTOKEN : ${{ secrets.MYTOKEN }}
|
|
||||||
# MYTOKEN2: "value2"
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Build FreeBSD remote-server
|
|
||||||
id: freebsd-build
|
|
||||||
uses: vmactions/freebsd-vm@c3ae29a132c8ef1924775414107a97cac042aad5 # v1.2.0
|
|
||||||
with:
|
|
||||||
# envs: "MYTOKEN MYTOKEN2"
|
|
||||||
usesh: true
|
|
||||||
release: 13.5
|
|
||||||
copyback: true
|
|
||||||
prepare: |
|
|
||||||
pkg install -y \
|
|
||||||
bash curl jq git \
|
|
||||||
rustup-init cmake-core llvm-devel-lite pkgconf protobuf # ibx11 alsa-lib rust-bindgen-cli
|
|
||||||
run: |
|
|
||||||
freebsd-version
|
|
||||||
sysctl hw.model
|
|
||||||
sysctl hw.ncpu
|
|
||||||
sysctl hw.physmem
|
|
||||||
sysctl hw.usermem
|
|
||||||
git config --global --add safe.directory /home/runner/work/zed/zed
|
|
||||||
rustup-init --profile minimal --default-toolchain none -y
|
|
||||||
. "$HOME/.cargo/env"
|
|
||||||
./script/bundle-freebsd
|
|
||||||
mkdir -p out/
|
|
||||||
mv "target/zed-remote-server-freebsd-x86_64.gz" out/
|
|
||||||
rm -rf target/
|
|
||||||
cargo clean
|
|
||||||
|
|
||||||
- name: Upload Artifact to Workflow - zed-remote-server (run-bundling)
|
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
|
||||||
if: contains(github.event.pull_request.labels.*.name, 'run-bundling')
|
|
||||||
with:
|
|
||||||
name: zed-remote-server-${{ github.event.pull_request.head.sha || github.sha }}-x86_64-unknown-freebsd.gz
|
|
||||||
path: out/zed-remote-server-freebsd-x86_64.gz
|
|
||||||
|
|
||||||
- name: Upload Artifacts to release
|
|
||||||
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
|
|
||||||
if: ${{ !(contains(github.event.pull_request.labels.*.name, 'run-bundling')) }}
|
|
||||||
with:
|
|
||||||
draft: true
|
|
||||||
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
|
||||||
files: |
|
|
||||||
out/zed-remote-server-freebsd-x86_64.gz
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
nix-build:
|
nix-build:
|
||||||
name: Build with Nix
|
name: Build with Nix
|
||||||
uses: ./.github/workflows/nix.yml
|
uses: ./.github/workflows/nix.yml
|
||||||
@@ -808,12 +729,12 @@ jobs:
|
|||||||
if: |
|
if: |
|
||||||
startsWith(github.ref, 'refs/tags/v')
|
startsWith(github.ref, 'refs/tags/v')
|
||||||
&& endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre')
|
&& endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre')
|
||||||
needs: [bundle-mac, bundle-linux-x86_x64, bundle-linux-aarch64, freebsd]
|
needs: [bundle-mac, bundle-linux-x86_x64, bundle-linux-aarch64]
|
||||||
runs-on:
|
runs-on:
|
||||||
- self-hosted
|
- self-hosted
|
||||||
- bundle
|
- bundle
|
||||||
steps:
|
steps:
|
||||||
- name: gh release
|
- name: gh release
|
||||||
run: gh release edit $GITHUB_REF_NAME --draft=false
|
run: gh release edit $GITHUB_REF_NAME --draft=true
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
19
.github/workflows/deploy_cloudflare.yml
vendored
19
.github/workflows/deploy_cloudflare.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
|||||||
deploy-docs:
|
deploy-docs:
|
||||||
name: Deploy Docs
|
name: Deploy Docs
|
||||||
if: github.repository_owner == 'zed-industries'
|
if: github.repository_owner == 'zed-industries'
|
||||||
runs-on: buildjet-16vcpu-ubuntu-2204
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
@@ -17,11 +17,24 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
clean: false
|
clean: false
|
||||||
|
|
||||||
|
- name: Setup mdBook
|
||||||
|
uses: peaceiris/actions-mdbook@ee69d230fe19748b7abf22df32acaa93833fad08 # v2
|
||||||
|
with:
|
||||||
|
mdbook-version: "0.4.37"
|
||||||
|
|
||||||
- name: Set up default .cargo/config.toml
|
- name: Set up default .cargo/config.toml
|
||||||
run: cp ./.cargo/collab-config.toml ./.cargo/config.toml
|
run: cp ./.cargo/collab-config.toml ./.cargo/config.toml
|
||||||
|
|
||||||
- name: Build docs
|
- name: Install system dependencies
|
||||||
uses: ./.github/actions/build_docs
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install libxkbcommon-dev libxkbcommon-x11-dev
|
||||||
|
|
||||||
|
- name: Build book
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
mkdir -p target/deploy
|
||||||
|
mdbook build ./docs --dest-dir=../target/deploy/docs/
|
||||||
|
|
||||||
- name: Deploy Docs
|
- name: Deploy Docs
|
||||||
uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3
|
uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3
|
||||||
|
|||||||
44
.github/workflows/release_nightly.yml
vendored
44
.github/workflows/release_nightly.yml
vendored
@@ -167,50 +167,6 @@ jobs:
|
|||||||
- name: Upload Zed Nightly
|
- name: Upload Zed Nightly
|
||||||
run: script/upload-nightly linux-targz
|
run: script/upload-nightly linux-targz
|
||||||
|
|
||||||
freebsd:
|
|
||||||
timeout-minutes: 60
|
|
||||||
if: github.repository_owner == 'zed-industries'
|
|
||||||
runs-on: github-8vcpu-ubuntu-2404
|
|
||||||
needs: tests
|
|
||||||
env:
|
|
||||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
|
||||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
|
||||||
name: Build Zed on FreeBSD
|
|
||||||
# env:
|
|
||||||
# MYTOKEN : ${{ secrets.MYTOKEN }}
|
|
||||||
# MYTOKEN2: "value2"
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Build FreeBSD remote-server
|
|
||||||
id: freebsd-build
|
|
||||||
uses: vmactions/freebsd-vm@c3ae29a132c8ef1924775414107a97cac042aad5 # v1.2.0
|
|
||||||
with:
|
|
||||||
# envs: "MYTOKEN MYTOKEN2"
|
|
||||||
usesh: true
|
|
||||||
release: 13.5
|
|
||||||
copyback: true
|
|
||||||
prepare: |
|
|
||||||
pkg install -y \
|
|
||||||
bash curl jq git \
|
|
||||||
rustup-init cmake-core llvm-devel-lite pkgconf protobuf # ibx11 alsa-lib rust-bindgen-cli
|
|
||||||
run: |
|
|
||||||
freebsd-version
|
|
||||||
sysctl hw.model
|
|
||||||
sysctl hw.ncpu
|
|
||||||
sysctl hw.physmem
|
|
||||||
sysctl hw.usermem
|
|
||||||
git config --global --add safe.directory /home/runner/work/zed/zed
|
|
||||||
rustup-init --profile minimal --default-toolchain none -y
|
|
||||||
. "$HOME/.cargo/env"
|
|
||||||
./script/bundle-freebsd
|
|
||||||
mkdir -p out/
|
|
||||||
mv "target/zed-remote-server-freebsd-x86_64.gz" out/
|
|
||||||
rm -rf target/
|
|
||||||
cargo clean
|
|
||||||
|
|
||||||
- name: Upload Zed Nightly
|
|
||||||
run: script/upload-nightly freebsd
|
|
||||||
|
|
||||||
bundle-nix:
|
bundle-nix:
|
||||||
name: Build and cache Nix package
|
name: Build and cache Nix package
|
||||||
needs: tests
|
needs: tests
|
||||||
|
|||||||
85
.github/workflows/unit_evals.yml
vendored
85
.github/workflows/unit_evals.yml
vendored
@@ -1,85 +0,0 @@
|
|||||||
name: Run Unit Evals
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
# GitHub might drop jobs at busy times, so we choose a random time in the middle of the night.
|
|
||||||
- cron: "47 1 * * *"
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
# Allow only one workflow per any non-`main` branch.
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
env:
|
|
||||||
CARGO_TERM_COLOR: always
|
|
||||||
CARGO_INCREMENTAL: 0
|
|
||||||
RUST_BACKTRACE: 1
|
|
||||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
unit_evals:
|
|
||||||
timeout-minutes: 60
|
|
||||||
name: Run unit evals
|
|
||||||
runs-on:
|
|
||||||
- buildjet-16vcpu-ubuntu-2204
|
|
||||||
steps:
|
|
||||||
- name: Add Rust to the PATH
|
|
||||||
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
|
||||||
|
|
||||||
- name: Checkout repo
|
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
|
||||||
with:
|
|
||||||
clean: false
|
|
||||||
|
|
||||||
- name: Cache dependencies
|
|
||||||
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
|
||||||
with:
|
|
||||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
|
||||||
cache-provider: "buildjet"
|
|
||||||
|
|
||||||
- name: Install Linux dependencies
|
|
||||||
run: ./script/linux
|
|
||||||
|
|
||||||
- name: Configure CI
|
|
||||||
run: |
|
|
||||||
mkdir -p ./../.cargo
|
|
||||||
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
|
|
||||||
|
|
||||||
- name: Install Rust
|
|
||||||
shell: bash -euxo pipefail {0}
|
|
||||||
run: |
|
|
||||||
cargo install cargo-nextest --locked
|
|
||||||
|
|
||||||
- name: Install Node
|
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
|
||||||
with:
|
|
||||||
node-version: "18"
|
|
||||||
|
|
||||||
- name: Limit target directory size
|
|
||||||
shell: bash -euxo pipefail {0}
|
|
||||||
run: script/clear-target-dir-if-larger-than 100
|
|
||||||
|
|
||||||
- name: Run unit evals
|
|
||||||
shell: bash -euxo pipefail {0}
|
|
||||||
run: cargo nextest run --workspace --no-fail-fast --features eval --no-capture -E 'test(::eval_)' --test-threads 1
|
|
||||||
env:
|
|
||||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
||||||
|
|
||||||
- name: Send failure message to Slack channel if needed
|
|
||||||
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 }}"
|
|
||||||
|
|
||||||
# Even the Linux runner is not stateful, in theory there is no need to do this cleanup.
|
|
||||||
# But, to avoid potential issues in the future if we choose to use a stateful Linux runner and forget to add code
|
|
||||||
# to clean up the config file, I’ve included the cleanup code here as a precaution.
|
|
||||||
# While it’s not strictly necessary at this moment, I believe it’s better to err on the side of caution.
|
|
||||||
- name: Clean CI config file
|
|
||||||
if: always()
|
|
||||||
run: rm -rf ./../.cargo
|
|
||||||
6
.rules
6
.rules
@@ -5,12 +5,6 @@
|
|||||||
* Prefer implementing functionality in existing files unless it is a new logical component. Avoid creating many small files.
|
* Prefer implementing functionality in existing files unless it is a new logical component. Avoid creating many small files.
|
||||||
* Avoid using functions that panic like `unwrap()`, instead use mechanisms like `?` to propagate errors.
|
* Avoid using functions that panic like `unwrap()`, instead use mechanisms like `?` to propagate errors.
|
||||||
* Be careful with operations like indexing which may panic if the indexes are out of bounds.
|
* Be careful with operations like indexing which may panic if the indexes are out of bounds.
|
||||||
* Never silently discard errors with `let _ =` on fallible operations. Always handle errors appropriately:
|
|
||||||
- Propagate errors with `?` when the calling function should handle them
|
|
||||||
- Use `.log_err()` or similar when you need to ignore errors but want visibility
|
|
||||||
- Use explicit error handling with `match` or `if let Err(...)` when you need custom logic
|
|
||||||
- Example: avoid `let _ = client.request(...).await?;` - use `client.request(...).await?;` instead
|
|
||||||
* When implementing async operations that may fail, ensure errors propagate to the UI layer so users get meaningful feedback.
|
|
||||||
* Never create files with `mod.rs` paths - prefer `src/some_module.rs` instead of `src/some_module/mod.rs`.
|
* Never create files with `mod.rs` paths - prefer `src/some_module.rs` instead of `src/some_module/mod.rs`.
|
||||||
|
|
||||||
# GPUI
|
# GPUI
|
||||||
|
|||||||
@@ -47,7 +47,6 @@
|
|||||||
"remove_trailing_whitespace_on_save": true,
|
"remove_trailing_whitespace_on_save": true,
|
||||||
"ensure_final_newline_on_save": true,
|
"ensure_final_newline_on_save": true,
|
||||||
"file_scan_exclusions": [
|
"file_scan_exclusions": [
|
||||||
"crates/assistant_tools/src/evals/fixtures",
|
|
||||||
"crates/eval/worktrees/",
|
"crates/eval/worktrees/",
|
||||||
"crates/eval/repos/",
|
"crates/eval/repos/",
|
||||||
"**/.git",
|
"**/.git",
|
||||||
|
|||||||
58
Cargo.lock
generated
58
Cargo.lock
generated
@@ -59,7 +59,7 @@ dependencies = [
|
|||||||
"assistant_slash_command",
|
"assistant_slash_command",
|
||||||
"assistant_slash_commands",
|
"assistant_slash_commands",
|
||||||
"assistant_tool",
|
"assistant_tool",
|
||||||
"assistant_tools",
|
"async-watch",
|
||||||
"audio",
|
"audio",
|
||||||
"buffer_diff",
|
"buffer_diff",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -130,7 +130,6 @@ dependencies = [
|
|||||||
"urlencoding",
|
"urlencoding",
|
||||||
"util",
|
"util",
|
||||||
"uuid",
|
"uuid",
|
||||||
"watch",
|
|
||||||
"workspace",
|
"workspace",
|
||||||
"workspace-hack",
|
"workspace-hack",
|
||||||
"zed_actions",
|
"zed_actions",
|
||||||
@@ -148,6 +147,7 @@ dependencies = [
|
|||||||
"deepseek",
|
"deepseek",
|
||||||
"fs",
|
"fs",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"indexmap",
|
||||||
"language_model",
|
"language_model",
|
||||||
"lmstudio",
|
"lmstudio",
|
||||||
"log",
|
"log",
|
||||||
@@ -652,7 +652,6 @@ dependencies = [
|
|||||||
"settings",
|
"settings",
|
||||||
"text",
|
"text",
|
||||||
"util",
|
"util",
|
||||||
"watch",
|
|
||||||
"workspace",
|
"workspace",
|
||||||
"workspace-hack",
|
"workspace-hack",
|
||||||
"zlog",
|
"zlog",
|
||||||
@@ -665,6 +664,7 @@ dependencies = [
|
|||||||
"agent_settings",
|
"agent_settings",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"assistant_tool",
|
"assistant_tool",
|
||||||
|
"async-watch",
|
||||||
"buffer_diff",
|
"buffer_diff",
|
||||||
"chrono",
|
"chrono",
|
||||||
"client",
|
"client",
|
||||||
@@ -715,7 +715,6 @@ dependencies = [
|
|||||||
"ui",
|
"ui",
|
||||||
"unindent",
|
"unindent",
|
||||||
"util",
|
"util",
|
||||||
"watch",
|
|
||||||
"web_search",
|
"web_search",
|
||||||
"which 6.0.3",
|
"which 6.0.3",
|
||||||
"workspace",
|
"workspace",
|
||||||
@@ -1074,6 +1073,15 @@ dependencies = [
|
|||||||
"tungstenite 0.26.2",
|
"tungstenite 0.26.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-watch"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a078faf4e27c0c6cc0efb20e5da59dcccc04968ebf2801d8e0b2195124cdcdb2"
|
||||||
|
dependencies = [
|
||||||
|
"event-listener 2.5.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async_zip"
|
name = "async_zip"
|
||||||
version = "0.0.17"
|
version = "0.0.17"
|
||||||
@@ -2978,6 +2986,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"assistant_context_editor",
|
"assistant_context_editor",
|
||||||
"assistant_slash_command",
|
"assistant_slash_command",
|
||||||
|
"assistant_tool",
|
||||||
"async-stripe",
|
"async-stripe",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"async-tungstenite",
|
"async-tungstenite",
|
||||||
@@ -4224,7 +4233,6 @@ dependencies = [
|
|||||||
"futures 0.3.31",
|
"futures 0.3.31",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
"gpui",
|
"gpui",
|
||||||
"itertools 0.14.0",
|
|
||||||
"language",
|
"language",
|
||||||
"log",
|
"log",
|
||||||
"menu",
|
"menu",
|
||||||
@@ -4534,8 +4542,6 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
"command_palette",
|
|
||||||
"gpui",
|
|
||||||
"mdbook",
|
"mdbook",
|
||||||
"regex",
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -4543,7 +4549,6 @@ dependencies = [
|
|||||||
"settings",
|
"settings",
|
||||||
"util",
|
"util",
|
||||||
"workspace-hack",
|
"workspace-hack",
|
||||||
"zed",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5004,6 +5009,7 @@ dependencies = [
|
|||||||
"assistant_tool",
|
"assistant_tool",
|
||||||
"assistant_tools",
|
"assistant_tools",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
"async-watch",
|
||||||
"buffer_diff",
|
"buffer_diff",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
@@ -5045,7 +5051,6 @@ dependencies = [
|
|||||||
"unindent",
|
"unindent",
|
||||||
"util",
|
"util",
|
||||||
"uuid",
|
"uuid",
|
||||||
"watch",
|
|
||||||
"workspace-hack",
|
"workspace-hack",
|
||||||
"zed_llm_client",
|
"zed_llm_client",
|
||||||
]
|
]
|
||||||
@@ -8730,6 +8735,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
"async-watch",
|
||||||
"clock",
|
"clock",
|
||||||
"collections",
|
"collections",
|
||||||
"ctor",
|
"ctor",
|
||||||
@@ -8779,7 +8785,6 @@ dependencies = [
|
|||||||
"unicase",
|
"unicase",
|
||||||
"unindent",
|
"unindent",
|
||||||
"util",
|
"util",
|
||||||
"watch",
|
|
||||||
"workspace-hack",
|
"workspace-hack",
|
||||||
"zlog",
|
"zlog",
|
||||||
]
|
]
|
||||||
@@ -10138,6 +10143,7 @@ dependencies = [
|
|||||||
"async-std",
|
"async-std",
|
||||||
"async-tar",
|
"async-tar",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
"async-watch",
|
||||||
"futures 0.3.31",
|
"futures 0.3.31",
|
||||||
"http_client",
|
"http_client",
|
||||||
"log",
|
"log",
|
||||||
@@ -10147,7 +10153,6 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"smol",
|
"smol",
|
||||||
"util",
|
"util",
|
||||||
"watch",
|
|
||||||
"which 6.0.3",
|
"which 6.0.3",
|
||||||
"workspace-hack",
|
"workspace-hack",
|
||||||
]
|
]
|
||||||
@@ -10196,7 +10201,6 @@ dependencies = [
|
|||||||
"util",
|
"util",
|
||||||
"workspace",
|
"workspace",
|
||||||
"workspace-hack",
|
"workspace-hack",
|
||||||
"zed_actions",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -12109,7 +12113,6 @@ dependencies = [
|
|||||||
"unindent",
|
"unindent",
|
||||||
"url",
|
"url",
|
||||||
"util",
|
"util",
|
||||||
"uuid",
|
|
||||||
"which 6.0.3",
|
"which 6.0.3",
|
||||||
"workspace-hack",
|
"workspace-hack",
|
||||||
"worktree",
|
"worktree",
|
||||||
@@ -12998,6 +13001,7 @@ dependencies = [
|
|||||||
"askpass",
|
"askpass",
|
||||||
"assistant_tool",
|
"assistant_tool",
|
||||||
"assistant_tools",
|
"assistant_tools",
|
||||||
|
"async-watch",
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"cargo_toml",
|
"cargo_toml",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -13044,7 +13048,6 @@ dependencies = [
|
|||||||
"toml 0.8.20",
|
"toml 0.8.20",
|
||||||
"unindent",
|
"unindent",
|
||||||
"util",
|
"util",
|
||||||
"watch",
|
|
||||||
"worktree",
|
"worktree",
|
||||||
"zlog",
|
"zlog",
|
||||||
]
|
]
|
||||||
@@ -15724,7 +15727,6 @@ dependencies = [
|
|||||||
"task",
|
"task",
|
||||||
"theme",
|
"theme",
|
||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
"url",
|
|
||||||
"util",
|
"util",
|
||||||
"windows 0.61.1",
|
"windows 0.61.1",
|
||||||
"workspace-hack",
|
"workspace-hack",
|
||||||
@@ -16506,9 +16508,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tree-sitter"
|
name = "tree-sitter"
|
||||||
version = "0.25.6"
|
version = "0.25.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a7cf18d43cbf0bfca51f657132cc616a5097edc4424d538bae6fa60142eaf9f0"
|
checksum = "ac5fff5c47490dfdf473b5228039bfacad9d765d9b6939d26bf7cc064c1c7822"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"regex",
|
"regex",
|
||||||
@@ -16521,9 +16523,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tree-sitter-bash"
|
name = "tree-sitter-bash"
|
||||||
version = "0.25.0"
|
version = "0.23.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "871b0606e667e98a1237ebdc1b0d7056e0aebfdc3141d12b399865d4cb6ed8a6"
|
checksum = "329a4d48623ac337d42b1df84e81a1c9dbb2946907c102ca72db158c1964a52e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"tree-sitter-language",
|
"tree-sitter-language",
|
||||||
@@ -17127,7 +17129,6 @@ dependencies = [
|
|||||||
"futures-lite 1.13.0",
|
"futures-lite 1.13.0",
|
||||||
"git2",
|
"git2",
|
||||||
"globset",
|
"globset",
|
||||||
"indoc",
|
|
||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
@@ -17906,19 +17907,6 @@ dependencies = [
|
|||||||
"leb128",
|
"leb128",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "watch"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"ctor",
|
|
||||||
"futures 0.3.31",
|
|
||||||
"gpui",
|
|
||||||
"parking_lot",
|
|
||||||
"rand 0.8.5",
|
|
||||||
"workspace-hack",
|
|
||||||
"zlog",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wayland-backend"
|
name = "wayland-backend"
|
||||||
version = "0.3.8"
|
version = "0.3.8"
|
||||||
@@ -19718,7 +19706,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zed"
|
name = "zed"
|
||||||
version = "0.191.0"
|
version = "0.190.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"activity_indicator",
|
"activity_indicator",
|
||||||
"agent",
|
"agent",
|
||||||
@@ -19730,6 +19718,7 @@ dependencies = [
|
|||||||
"assistant_context_editor",
|
"assistant_context_editor",
|
||||||
"assistant_tool",
|
"assistant_tool",
|
||||||
"assistant_tools",
|
"assistant_tools",
|
||||||
|
"async-watch",
|
||||||
"audio",
|
"audio",
|
||||||
"auto_update",
|
"auto_update",
|
||||||
"auto_update_ui",
|
"auto_update_ui",
|
||||||
@@ -19846,7 +19835,6 @@ dependencies = [
|
|||||||
"uuid",
|
"uuid",
|
||||||
"vim",
|
"vim",
|
||||||
"vim_mode_setting",
|
"vim_mode_setting",
|
||||||
"watch",
|
|
||||||
"web_search",
|
"web_search",
|
||||||
"web_search_providers",
|
"web_search_providers",
|
||||||
"welcome",
|
"welcome",
|
||||||
|
|||||||
@@ -165,7 +165,6 @@ members = [
|
|||||||
"crates/util_macros",
|
"crates/util_macros",
|
||||||
"crates/vim",
|
"crates/vim",
|
||||||
"crates/vim_mode_setting",
|
"crates/vim_mode_setting",
|
||||||
"crates/watch",
|
|
||||||
"crates/web_search",
|
"crates/web_search",
|
||||||
"crates/web_search_providers",
|
"crates/web_search_providers",
|
||||||
"crates/welcome",
|
"crates/welcome",
|
||||||
@@ -374,7 +373,6 @@ util = { path = "crates/util" }
|
|||||||
util_macros = { path = "crates/util_macros" }
|
util_macros = { path = "crates/util_macros" }
|
||||||
vim = { path = "crates/vim" }
|
vim = { path = "crates/vim" }
|
||||||
vim_mode_setting = { path = "crates/vim_mode_setting" }
|
vim_mode_setting = { path = "crates/vim_mode_setting" }
|
||||||
watch = { path = "crates/watch" }
|
|
||||||
web_search = { path = "crates/web_search" }
|
web_search = { path = "crates/web_search" }
|
||||||
web_search_providers = { path = "crates/web_search_providers" }
|
web_search_providers = { path = "crates/web_search_providers" }
|
||||||
welcome = { path = "crates/welcome" }
|
welcome = { path = "crates/welcome" }
|
||||||
@@ -405,6 +403,7 @@ async-recursion = "1.0.0"
|
|||||||
async-tar = "0.5.0"
|
async-tar = "0.5.0"
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
async-tungstenite = "0.29.1"
|
async-tungstenite = "0.29.1"
|
||||||
|
async-watch = "0.3.1"
|
||||||
async_zip = { version = "0.0.17", 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 = [
|
||||||
@@ -575,8 +574,8 @@ tokio = { version = "1" }
|
|||||||
tokio-tungstenite = { version = "0.26", features = ["__rustls-tls"] }
|
tokio-tungstenite = { version = "0.26", features = ["__rustls-tls"] }
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
tower-http = "0.4.4"
|
tower-http = "0.4.4"
|
||||||
tree-sitter = { version = "0.25.6", features = ["wasm"] }
|
tree-sitter = { version = "0.25.5", features = ["wasm"] }
|
||||||
tree-sitter-bash = "0.25.0"
|
tree-sitter-bash = "0.23"
|
||||||
tree-sitter-c = "0.23"
|
tree-sitter-c = "0.23"
|
||||||
tree-sitter-cpp = "0.23"
|
tree-sitter-cpp = "0.23"
|
||||||
tree-sitter-css = "0.23"
|
tree-sitter-css = "0.23"
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-list-todo-icon lucide-list-todo"><rect x="3" y="5" width="6" height="6" rx="1"/><path d="m3 17 2 2 4-4"/><path d="M13 6h8"/><path d="M13 12h8"/><path d="M13 18h8"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 373 B |
@@ -120,7 +120,7 @@
|
|||||||
"ctrl-'": "editor::ToggleSelectedDiffHunks",
|
"ctrl-'": "editor::ToggleSelectedDiffHunks",
|
||||||
"ctrl-\"": "editor::ExpandAllDiffHunks",
|
"ctrl-\"": "editor::ExpandAllDiffHunks",
|
||||||
"ctrl-i": "editor::ShowSignatureHelp",
|
"ctrl-i": "editor::ShowSignatureHelp",
|
||||||
"alt-g b": "git::Blame",
|
"alt-g b": "editor::ToggleGitBlame",
|
||||||
"menu": "editor::OpenContextMenu",
|
"menu": "editor::OpenContextMenu",
|
||||||
"shift-f10": "editor::OpenContextMenu",
|
"shift-f10": "editor::OpenContextMenu",
|
||||||
"ctrl-shift-e": "editor::ToggleEditPrediction",
|
"ctrl-shift-e": "editor::ToggleEditPrediction",
|
||||||
@@ -153,7 +153,8 @@
|
|||||||
"context": "Editor && mode == full && edit_prediction",
|
"context": "Editor && mode == full && edit_prediction",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"alt-]": "editor::NextEditPrediction",
|
"alt-]": "editor::NextEditPrediction",
|
||||||
"alt-[": "editor::PreviousEditPrediction"
|
"alt-[": "editor::PreviousEditPrediction",
|
||||||
|
"alt-right": "editor::AcceptPartialEditPrediction"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -218,6 +219,7 @@
|
|||||||
"ctrl-enter": "assistant::Assist",
|
"ctrl-enter": "assistant::Assist",
|
||||||
"ctrl-s": "workspace::Save",
|
"ctrl-s": "workspace::Save",
|
||||||
"save": "workspace::Save",
|
"save": "workspace::Save",
|
||||||
|
"ctrl->": "assistant::QuoteSelection",
|
||||||
"ctrl-<": "assistant::InsertIntoEditor",
|
"ctrl-<": "assistant::InsertIntoEditor",
|
||||||
"shift-enter": "assistant::Split",
|
"shift-enter": "assistant::Split",
|
||||||
"ctrl-r": "assistant::CycleMessageRole",
|
"ctrl-r": "assistant::CycleMessageRole",
|
||||||
@@ -243,7 +245,6 @@
|
|||||||
"ctrl-shift-j": "agent::ToggleNavigationMenu",
|
"ctrl-shift-j": "agent::ToggleNavigationMenu",
|
||||||
"ctrl-shift-i": "agent::ToggleOptionsMenu",
|
"ctrl-shift-i": "agent::ToggleOptionsMenu",
|
||||||
"shift-alt-escape": "agent::ExpandMessageEditor",
|
"shift-alt-escape": "agent::ExpandMessageEditor",
|
||||||
"ctrl->": "assistant::QuoteSelection",
|
|
||||||
"ctrl-alt-e": "agent::RemoveAllContext",
|
"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",
|
||||||
@@ -277,9 +278,7 @@
|
|||||||
"enter": "agent::Chat",
|
"enter": "agent::Chat",
|
||||||
"ctrl-enter": "agent::ChatWithFollow",
|
"ctrl-enter": "agent::ChatWithFollow",
|
||||||
"ctrl-i": "agent::ToggleProfileSelector",
|
"ctrl-i": "agent::ToggleProfileSelector",
|
||||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
"shift-ctrl-r": "agent::OpenAgentDiff"
|
||||||
"ctrl-shift-y": "agent::KeepAll",
|
|
||||||
"ctrl-shift-n": "agent::RejectAll"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -511,14 +510,14 @@
|
|||||||
{
|
{
|
||||||
"context": "Workspace",
|
"context": "Workspace",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"alt-open": ["projects::OpenRecent", { "create_new_window": false }],
|
|
||||||
// Change the default action on `menu::Confirm` by setting the parameter
|
// Change the default action on `menu::Confirm` by setting the parameter
|
||||||
// "alt-ctrl-o": ["projects::OpenRecent", { "create_new_window": true }],
|
// "alt-ctrl-o": ["projects::OpenRecent", { "create_new_window": true }],
|
||||||
"alt-ctrl-o": ["projects::OpenRecent", { "create_new_window": false }],
|
"alt-open": "projects::OpenRecent",
|
||||||
"alt-shift-open": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }],
|
"alt-ctrl-o": "projects::OpenRecent",
|
||||||
|
"alt-shift-open": "projects::OpenRemote",
|
||||||
|
"alt-ctrl-shift-o": "projects::OpenRemote",
|
||||||
// Change to open path modal for existing remote connection by setting the parameter
|
// Change to open path modal for existing remote connection by setting the parameter
|
||||||
// "alt-ctrl-shift-o": "["projects::OpenRemote", { "from_existing_connection": true }]",
|
// "alt-ctrl-shift-o": "["projects::OpenRemote", { "from_existing_connection": true }]",
|
||||||
"alt-ctrl-shift-o": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }],
|
|
||||||
"alt-ctrl-shift-b": "branches::OpenRecent",
|
"alt-ctrl-shift-b": "branches::OpenRecent",
|
||||||
"alt-shift-enter": "toast::RunAction",
|
"alt-shift-enter": "toast::RunAction",
|
||||||
"ctrl-~": "workspace::NewTerminal",
|
"ctrl-~": "workspace::NewTerminal",
|
||||||
@@ -661,16 +660,14 @@
|
|||||||
"bindings": {
|
"bindings": {
|
||||||
"alt-tab": "editor::AcceptEditPrediction",
|
"alt-tab": "editor::AcceptEditPrediction",
|
||||||
"alt-l": "editor::AcceptEditPrediction",
|
"alt-l": "editor::AcceptEditPrediction",
|
||||||
"tab": "editor::AcceptEditPrediction",
|
"tab": "editor::AcceptEditPrediction"
|
||||||
"alt-right": "editor::AcceptPartialEditPrediction"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && edit_prediction_conflict",
|
"context": "Editor && edit_prediction_conflict",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"alt-tab": "editor::AcceptEditPrediction",
|
"alt-tab": "editor::AcceptEditPrediction",
|
||||||
"alt-l": "editor::AcceptEditPrediction",
|
"alt-l": "editor::AcceptEditPrediction"
|
||||||
"alt-right": "editor::AcceptPartialEditPrediction"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -912,9 +909,7 @@
|
|||||||
"context": "CollabPanel && not_editing",
|
"context": "CollabPanel && not_editing",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"ctrl-backspace": "collab_panel::Remove",
|
"ctrl-backspace": "collab_panel::Remove",
|
||||||
"space": "menu::Confirm",
|
"space": "menu::Confirm"
|
||||||
"ctrl-up": "collab_panel::MoveChannelUp",
|
|
||||||
"ctrl-down": "collab_panel::MoveChannelDown"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -138,7 +138,7 @@
|
|||||||
"cmd-;": "editor::ToggleLineNumbers",
|
"cmd-;": "editor::ToggleLineNumbers",
|
||||||
"cmd-'": "editor::ToggleSelectedDiffHunks",
|
"cmd-'": "editor::ToggleSelectedDiffHunks",
|
||||||
"cmd-\"": "editor::ExpandAllDiffHunks",
|
"cmd-\"": "editor::ExpandAllDiffHunks",
|
||||||
"cmd-alt-g b": "git::Blame",
|
"cmd-alt-g b": "editor::ToggleGitBlame",
|
||||||
"cmd-i": "editor::ShowSignatureHelp",
|
"cmd-i": "editor::ShowSignatureHelp",
|
||||||
"f9": "editor::ToggleBreakpoint",
|
"f9": "editor::ToggleBreakpoint",
|
||||||
"shift-f9": "editor::EditLogBreakpoint",
|
"shift-f9": "editor::EditLogBreakpoint",
|
||||||
@@ -181,7 +181,8 @@
|
|||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"alt-tab": "editor::NextEditPrediction",
|
"alt-tab": "editor::NextEditPrediction",
|
||||||
"alt-shift-tab": "editor::PreviousEditPrediction"
|
"alt-shift-tab": "editor::PreviousEditPrediction",
|
||||||
|
"ctrl-cmd-right": "editor::AcceptPartialEditPrediction"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -252,6 +253,7 @@
|
|||||||
"bindings": {
|
"bindings": {
|
||||||
"cmd-enter": "assistant::Assist",
|
"cmd-enter": "assistant::Assist",
|
||||||
"cmd-s": "workspace::Save",
|
"cmd-s": "workspace::Save",
|
||||||
|
"cmd->": "assistant::QuoteSelection",
|
||||||
"cmd-<": "assistant::InsertIntoEditor",
|
"cmd-<": "assistant::InsertIntoEditor",
|
||||||
"shift-enter": "assistant::Split",
|
"shift-enter": "assistant::Split",
|
||||||
"ctrl-r": "assistant::CycleMessageRole",
|
"ctrl-r": "assistant::CycleMessageRole",
|
||||||
@@ -278,7 +280,6 @@
|
|||||||
"cmd-shift-j": "agent::ToggleNavigationMenu",
|
"cmd-shift-j": "agent::ToggleNavigationMenu",
|
||||||
"cmd-shift-i": "agent::ToggleOptionsMenu",
|
"cmd-shift-i": "agent::ToggleOptionsMenu",
|
||||||
"shift-alt-escape": "agent::ExpandMessageEditor",
|
"shift-alt-escape": "agent::ExpandMessageEditor",
|
||||||
"cmd->": "assistant::QuoteSelection",
|
|
||||||
"cmd-alt-e": "agent::RemoveAllContext",
|
"cmd-alt-e": "agent::RemoveAllContext",
|
||||||
"cmd-shift-e": "project_panel::ToggleFocus",
|
"cmd-shift-e": "project_panel::ToggleFocus",
|
||||||
"cmd-shift-enter": "agent::ContinueThread",
|
"cmd-shift-enter": "agent::ContinueThread",
|
||||||
@@ -314,9 +315,7 @@
|
|||||||
"enter": "agent::Chat",
|
"enter": "agent::Chat",
|
||||||
"cmd-enter": "agent::ChatWithFollow",
|
"cmd-enter": "agent::ChatWithFollow",
|
||||||
"cmd-i": "agent::ToggleProfileSelector",
|
"cmd-i": "agent::ToggleProfileSelector",
|
||||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
"shift-ctrl-r": "agent::OpenAgentDiff"
|
||||||
"cmd-shift-y": "agent::KeepAll",
|
|
||||||
"cmd-shift-n": "agent::RejectAll"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -583,9 +582,9 @@
|
|||||||
"bindings": {
|
"bindings": {
|
||||||
// Change the default action on `menu::Confirm` by setting the parameter
|
// Change the default action on `menu::Confirm` by setting the parameter
|
||||||
// "alt-cmd-o": ["projects::OpenRecent", {"create_new_window": true }],
|
// "alt-cmd-o": ["projects::OpenRecent", {"create_new_window": true }],
|
||||||
"alt-cmd-o": ["projects::OpenRecent", { "create_new_window": false }],
|
"alt-cmd-o": "projects::OpenRecent",
|
||||||
"ctrl-cmd-o": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }],
|
"ctrl-cmd-o": "projects::OpenRemote",
|
||||||
"ctrl-cmd-shift-o": ["projects::OpenRemote", { "from_existing_connection": true, "create_new_window": false }],
|
"ctrl-cmd-shift-o": ["projects::OpenRemote", { "from_existing_connection": true }],
|
||||||
"alt-cmd-b": "branches::OpenRecent",
|
"alt-cmd-b": "branches::OpenRecent",
|
||||||
"ctrl-~": "workspace::NewTerminal",
|
"ctrl-~": "workspace::NewTerminal",
|
||||||
"cmd-s": "workspace::Save",
|
"cmd-s": "workspace::Save",
|
||||||
@@ -718,16 +717,14 @@
|
|||||||
"context": "Editor && edit_prediction",
|
"context": "Editor && edit_prediction",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"alt-tab": "editor::AcceptEditPrediction",
|
"alt-tab": "editor::AcceptEditPrediction",
|
||||||
"tab": "editor::AcceptEditPrediction",
|
"tab": "editor::AcceptEditPrediction"
|
||||||
"ctrl-cmd-right": "editor::AcceptPartialEditPrediction"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && edit_prediction_conflict",
|
"context": "Editor && edit_prediction_conflict",
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"alt-tab": "editor::AcceptEditPrediction",
|
"alt-tab": "editor::AcceptEditPrediction"
|
||||||
"ctrl-cmd-right": "editor::AcceptPartialEditPrediction"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -968,9 +965,7 @@
|
|||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"ctrl-backspace": "collab_panel::Remove",
|
"ctrl-backspace": "collab_panel::Remove",
|
||||||
"space": "menu::Confirm",
|
"space": "menu::Confirm"
|
||||||
"cmd-up": "collab_panel::MoveChannelUp",
|
|
||||||
"cmd-down": "collab_panel::MoveChannelDown"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -13,9 +13,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && vim_mode == insert && !menu",
|
"context": "Editor",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
// "j k": "vim::SwitchToNormalMode"
|
// "j k": ["workspace::SendKeystrokes", "escape"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -198,8 +198,6 @@
|
|||||||
"9": ["vim::Number", 9],
|
"9": ["vim::Number", 9],
|
||||||
"ctrl-w d": "editor::GoToDefinitionSplit",
|
"ctrl-w d": "editor::GoToDefinitionSplit",
|
||||||
"ctrl-w g d": "editor::GoToDefinitionSplit",
|
"ctrl-w g d": "editor::GoToDefinitionSplit",
|
||||||
"ctrl-w ]": "editor::GoToDefinitionSplit",
|
|
||||||
"ctrl-w ctrl-]": "editor::GoToDefinitionSplit",
|
|
||||||
"ctrl-w shift-d": "editor::GoToTypeDefinitionSplit",
|
"ctrl-w shift-d": "editor::GoToTypeDefinitionSplit",
|
||||||
"ctrl-w g shift-d": "editor::GoToTypeDefinitionSplit",
|
"ctrl-w g shift-d": "editor::GoToTypeDefinitionSplit",
|
||||||
"ctrl-w space": "editor::OpenExcerptsSplit",
|
"ctrl-w space": "editor::OpenExcerptsSplit",
|
||||||
@@ -711,7 +709,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "AgentPanel || GitPanel || ProjectPanel || CollabPanel || OutlinePanel || ChatPanel || VimControl || EmptyPane || SharedScreen || MarkdownPreview || KeyContextView || DebugPanel",
|
"context": "GitPanel || ProjectPanel || CollabPanel || OutlinePanel || ChatPanel || VimControl || EmptyPane || SharedScreen || MarkdownPreview || KeyContextView || DebugPanel",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
// window related commands (ctrl-w X)
|
// window related commands (ctrl-w X)
|
||||||
"ctrl-w": null,
|
"ctrl-w": null,
|
||||||
|
|||||||
@@ -17,13 +17,13 @@ You are a highly skilled software engineer with extensive knowledge in many prog
|
|||||||
4. Use only the tools that are currently available.
|
4. Use only the tools that are currently available.
|
||||||
5. DO NOT use a tool that is not available just because it appears in the conversation. This means the user turned it off.
|
5. DO NOT use a tool that is not available just because it appears in the conversation. This means the user turned it off.
|
||||||
6. NEVER run commands that don't terminate on their own such as web servers (like `npm run start`, `npm run dev`, `python -m http.server`, etc) or file watchers.
|
6. NEVER run commands that don't terminate on their own such as web servers (like `npm run start`, `npm run dev`, `python -m http.server`, etc) or file watchers.
|
||||||
7. Avoid HTML entity escaping - use plain characters instead.
|
|
||||||
|
|
||||||
## Searching and Reading
|
## Searching and Reading
|
||||||
|
|
||||||
If you are unsure how to fulfill the user's request, gather more information with tool calls and/or clarifying questions.
|
If you are unsure how to fulfill the user's request, gather more information with tool calls and/or clarifying questions.
|
||||||
|
|
||||||
{{! TODO: If there are files, we should mention it but otherwise omit that fact }}
|
{{! TODO: If there are files, we should mention it but otherwise omit that fact }}
|
||||||
|
{{#if has_tools}}
|
||||||
If appropriate, use tool calls to explore the current project, which contains the following root directories:
|
If appropriate, use tool calls to explore the current project, which contains the following root directories:
|
||||||
|
|
||||||
{{#each worktrees}}
|
{{#each worktrees}}
|
||||||
@@ -38,6 +38,7 @@ If appropriate, use tool calls to explore the current project, which contains th
|
|||||||
- As you learn about the structure of the project, use that information to scope `grep` searches to targeted subtrees of the project.
|
- As you learn about the structure of the project, use that information to scope `grep` searches to targeted subtrees of the project.
|
||||||
- The user might specify a partial file path. If you don't know the full path, use `find_path` (not `grep`) before you read the file.
|
- The user might specify a partial file path. If you don't know the full path, use `find_path` (not `grep`) before you read the file.
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
{{else}}
|
{{else}}
|
||||||
You are being tasked with providing a response, but you have no ability to use tools or to read or write any aspect of the user's system (other than any context the user might have provided to you).
|
You are being tasked with providing a response, but you have no ability to use tools or to read or write any aspect of the user's system (other than any context the user might have provided to you).
|
||||||
|
|
||||||
|
|||||||
@@ -533,9 +533,6 @@
|
|||||||
"function": false
|
"function": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Whether to resize all the panels in a dock when resizing the dock.
|
|
||||||
// Can be a combination of "left", "right" and "bottom".
|
|
||||||
"resize_all_panels_in_dock": ["left"],
|
|
||||||
"project_panel": {
|
"project_panel": {
|
||||||
// Whether to show the project panel button in the status bar
|
// Whether to show the project panel button in the status bar
|
||||||
"button": true,
|
"button": true,
|
||||||
@@ -771,6 +768,7 @@
|
|||||||
"tools": {
|
"tools": {
|
||||||
"copy_path": true,
|
"copy_path": true,
|
||||||
"create_directory": true,
|
"create_directory": true,
|
||||||
|
"create_file": true,
|
||||||
"delete_path": true,
|
"delete_path": true,
|
||||||
"diagnostics": true,
|
"diagnostics": true,
|
||||||
"edit_file": true,
|
"edit_file": true,
|
||||||
@@ -1033,14 +1031,6 @@
|
|||||||
"button": true,
|
"button": true,
|
||||||
// Whether to show warnings or not by default.
|
// Whether to show warnings or not by default.
|
||||||
"include_warnings": true,
|
"include_warnings": true,
|
||||||
// Settings for using LSP pull diagnostics mechanism in Zed.
|
|
||||||
"lsp_pull_diagnostics": {
|
|
||||||
// Whether to pull for diagnostics or not.
|
|
||||||
"enabled": true,
|
|
||||||
// Minimum time to wait before pulling diagnostics from the language server(s).
|
|
||||||
// 0 turns the debounce off.
|
|
||||||
"debounce_ms": 50
|
|
||||||
},
|
|
||||||
// Settings for inline diagnostics
|
// Settings for inline diagnostics
|
||||||
"inline": {
|
"inline": {
|
||||||
// Whether to show diagnostics inline or not
|
// Whether to show diagnostics inline or not
|
||||||
@@ -1464,9 +1454,7 @@
|
|||||||
"language_servers": ["erlang-ls", "!elp", "..."]
|
"language_servers": ["erlang-ls", "!elp", "..."]
|
||||||
},
|
},
|
||||||
"Git Commit": {
|
"Git Commit": {
|
||||||
"allow_rewrap": "anywhere",
|
"allow_rewrap": "anywhere"
|
||||||
"soft_wrap": "editor_width",
|
|
||||||
"preferred_line_length": 72
|
|
||||||
},
|
},
|
||||||
"Go": {
|
"Go": {
|
||||||
"code_actions_on_format": {
|
"code_actions_on_format": {
|
||||||
@@ -1509,11 +1497,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"LaTeX": {
|
"LaTeX": {
|
||||||
|
"format_on_save": "on",
|
||||||
"formatter": "language_server",
|
"formatter": "language_server",
|
||||||
"language_servers": ["texlab", "..."],
|
"language_servers": ["texlab", "..."],
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"allowed": true,
|
"allowed": false
|
||||||
"plugins": ["prettier-plugin-latex"]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Markdown": {
|
"Markdown": {
|
||||||
@@ -1537,13 +1525,19 @@
|
|||||||
"allow_rewrap": "anywhere"
|
"allow_rewrap": "anywhere"
|
||||||
},
|
},
|
||||||
"Ruby": {
|
"Ruby": {
|
||||||
"language_servers": ["solargraph", "!ruby-lsp", "!rubocop", "!sorbet", "!steep", "..."]
|
"language_servers": ["solargraph", "!ruby-lsp", "!rubocop", "..."]
|
||||||
},
|
},
|
||||||
"SCSS": {
|
"SCSS": {
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"allowed": true
|
"allowed": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"SQL": {
|
||||||
|
"prettier": {
|
||||||
|
"allowed": true,
|
||||||
|
"plugins": ["prettier-plugin-sql"]
|
||||||
|
}
|
||||||
|
},
|
||||||
"Starlark": {
|
"Starlark": {
|
||||||
"language_servers": ["starpls", "!buck2-lsp", "..."]
|
"language_servers": ["starpls", "!buck2-lsp", "..."]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -261,11 +261,6 @@
|
|||||||
"font_style": null,
|
"font_style": null,
|
||||||
"font_weight": null
|
"font_weight": null
|
||||||
},
|
},
|
||||||
"namespace": {
|
|
||||||
"color": "#bfbdb6ff",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"number": {
|
"number": {
|
||||||
"color": "#d2a6ffff",
|
"color": "#d2a6ffff",
|
||||||
"font_style": null,
|
"font_style": null,
|
||||||
@@ -321,16 +316,6 @@
|
|||||||
"font_style": null,
|
"font_style": null,
|
||||||
"font_weight": null
|
"font_weight": null
|
||||||
},
|
},
|
||||||
"selector": {
|
|
||||||
"color": "#d2a6ffff",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"selector.pseudo": {
|
|
||||||
"color": "#5ac1feff",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"string": {
|
"string": {
|
||||||
"color": "#a9d94bff",
|
"color": "#a9d94bff",
|
||||||
"font_style": null,
|
"font_style": null,
|
||||||
@@ -457,9 +442,9 @@
|
|||||||
"terminal.foreground": "#5c6166ff",
|
"terminal.foreground": "#5c6166ff",
|
||||||
"terminal.bright_foreground": "#5c6166ff",
|
"terminal.bright_foreground": "#5c6166ff",
|
||||||
"terminal.dim_foreground": "#fcfcfcff",
|
"terminal.dim_foreground": "#fcfcfcff",
|
||||||
"terminal.ansi.black": "#5c6166ff",
|
"terminal.ansi.black": "#fcfcfcff",
|
||||||
"terminal.ansi.bright_black": "#3b9ee5ff",
|
"terminal.ansi.bright_black": "#bcbec0ff",
|
||||||
"terminal.ansi.dim_black": "#9c9fa2ff",
|
"terminal.ansi.dim_black": "#5c6166ff",
|
||||||
"terminal.ansi.red": "#ef7271ff",
|
"terminal.ansi.red": "#ef7271ff",
|
||||||
"terminal.ansi.bright_red": "#febab6ff",
|
"terminal.ansi.bright_red": "#febab6ff",
|
||||||
"terminal.ansi.dim_red": "#833538ff",
|
"terminal.ansi.dim_red": "#833538ff",
|
||||||
@@ -478,9 +463,9 @@
|
|||||||
"terminal.ansi.cyan": "#4dbf99ff",
|
"terminal.ansi.cyan": "#4dbf99ff",
|
||||||
"terminal.ansi.bright_cyan": "#ace0cbff",
|
"terminal.ansi.bright_cyan": "#ace0cbff",
|
||||||
"terminal.ansi.dim_cyan": "#2a5f4aff",
|
"terminal.ansi.dim_cyan": "#2a5f4aff",
|
||||||
"terminal.ansi.white": "#fcfcfcff",
|
"terminal.ansi.white": "#5c6166ff",
|
||||||
"terminal.ansi.bright_white": "#fcfcfcff",
|
"terminal.ansi.bright_white": "#5c6166ff",
|
||||||
"terminal.ansi.dim_white": "#bcbec0ff",
|
"terminal.ansi.dim_white": "#9c9fa2ff",
|
||||||
"link_text.hover": "#3b9ee5ff",
|
"link_text.hover": "#3b9ee5ff",
|
||||||
"conflict": "#f1ad49ff",
|
"conflict": "#f1ad49ff",
|
||||||
"conflict.background": "#ffeedaff",
|
"conflict.background": "#ffeedaff",
|
||||||
@@ -647,11 +632,6 @@
|
|||||||
"font_style": null,
|
"font_style": null,
|
||||||
"font_weight": null
|
"font_weight": null
|
||||||
},
|
},
|
||||||
"namespace": {
|
|
||||||
"color": "#5c6166ff",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"number": {
|
"number": {
|
||||||
"color": "#a37accff",
|
"color": "#a37accff",
|
||||||
"font_style": null,
|
"font_style": null,
|
||||||
@@ -707,16 +687,6 @@
|
|||||||
"font_style": null,
|
"font_style": null,
|
||||||
"font_weight": null
|
"font_weight": null
|
||||||
},
|
},
|
||||||
"selector": {
|
|
||||||
"color": "#a37accff",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"selector.pseudo": {
|
|
||||||
"color": "#3b9ee5ff",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"string": {
|
"string": {
|
||||||
"color": "#86b300ff",
|
"color": "#86b300ff",
|
||||||
"font_style": null,
|
"font_style": null,
|
||||||
@@ -1033,11 +1003,6 @@
|
|||||||
"font_style": null,
|
"font_style": null,
|
||||||
"font_weight": null
|
"font_weight": null
|
||||||
},
|
},
|
||||||
"namespace": {
|
|
||||||
"color": "#cccac2ff",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"number": {
|
"number": {
|
||||||
"color": "#dfbfffff",
|
"color": "#dfbfffff",
|
||||||
"font_style": null,
|
"font_style": null,
|
||||||
@@ -1093,16 +1058,6 @@
|
|||||||
"font_style": null,
|
"font_style": null,
|
||||||
"font_weight": null
|
"font_weight": null
|
||||||
},
|
},
|
||||||
"selector": {
|
|
||||||
"color": "#dfbfffff",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"selector.pseudo": {
|
|
||||||
"color": "#72cffeff",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"string": {
|
"string": {
|
||||||
"color": "#d4fe7fff",
|
"color": "#d4fe7fff",
|
||||||
"font_style": null,
|
"font_style": null,
|
||||||
|
|||||||
@@ -270,11 +270,6 @@
|
|||||||
"font_style": null,
|
"font_style": null,
|
||||||
"font_weight": null
|
"font_weight": null
|
||||||
},
|
},
|
||||||
"namespace": {
|
|
||||||
"color": "#83a598ff",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"number": {
|
"number": {
|
||||||
"color": "#d3869bff",
|
"color": "#d3869bff",
|
||||||
"font_style": null,
|
"font_style": null,
|
||||||
@@ -330,16 +325,6 @@
|
|||||||
"font_style": null,
|
"font_style": null,
|
||||||
"font_weight": null
|
"font_weight": null
|
||||||
},
|
},
|
||||||
"selector": {
|
|
||||||
"color": "#fabd2eff",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"selector.pseudo": {
|
|
||||||
"color": "#83a598ff",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"string": {
|
"string": {
|
||||||
"color": "#b8bb25ff",
|
"color": "#b8bb25ff",
|
||||||
"font_style": null,
|
"font_style": null,
|
||||||
@@ -670,11 +655,6 @@
|
|||||||
"font_style": null,
|
"font_style": null,
|
||||||
"font_weight": null
|
"font_weight": null
|
||||||
},
|
},
|
||||||
"namespace": {
|
|
||||||
"color": "#83a598ff",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"number": {
|
"number": {
|
||||||
"color": "#d3869bff",
|
"color": "#d3869bff",
|
||||||
"font_style": null,
|
"font_style": null,
|
||||||
@@ -730,16 +710,6 @@
|
|||||||
"font_style": null,
|
"font_style": null,
|
||||||
"font_weight": null
|
"font_weight": null
|
||||||
},
|
},
|
||||||
"selector": {
|
|
||||||
"color": "#fabd2eff",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"selector.pseudo": {
|
|
||||||
"color": "#83a598ff",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"string": {
|
"string": {
|
||||||
"color": "#b8bb25ff",
|
"color": "#b8bb25ff",
|
||||||
"font_style": null,
|
"font_style": null,
|
||||||
@@ -1070,11 +1040,6 @@
|
|||||||
"font_style": null,
|
"font_style": null,
|
||||||
"font_weight": null
|
"font_weight": null
|
||||||
},
|
},
|
||||||
"namespace": {
|
|
||||||
"color": "#83a598ff",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"number": {
|
"number": {
|
||||||
"color": "#d3869bff",
|
"color": "#d3869bff",
|
||||||
"font_style": null,
|
"font_style": null,
|
||||||
@@ -1130,16 +1095,6 @@
|
|||||||
"font_style": null,
|
"font_style": null,
|
||||||
"font_weight": null
|
"font_weight": null
|
||||||
},
|
},
|
||||||
"selector": {
|
|
||||||
"color": "#fabd2eff",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"selector.pseudo": {
|
|
||||||
"color": "#83a598ff",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"string": {
|
"string": {
|
||||||
"color": "#b8bb25ff",
|
"color": "#b8bb25ff",
|
||||||
"font_style": null,
|
"font_style": null,
|
||||||
@@ -1272,9 +1227,9 @@
|
|||||||
"terminal.foreground": "#282828ff",
|
"terminal.foreground": "#282828ff",
|
||||||
"terminal.bright_foreground": "#282828ff",
|
"terminal.bright_foreground": "#282828ff",
|
||||||
"terminal.dim_foreground": "#fbf1c7ff",
|
"terminal.dim_foreground": "#fbf1c7ff",
|
||||||
"terminal.ansi.black": "#282828ff",
|
"terminal.ansi.black": "#fbf1c7ff",
|
||||||
"terminal.ansi.bright_black": "#0b6678ff",
|
"terminal.ansi.bright_black": "#b0a189ff",
|
||||||
"terminal.ansi.dim_black": "#5f5650ff",
|
"terminal.ansi.dim_black": "#282828ff",
|
||||||
"terminal.ansi.red": "#9d0308ff",
|
"terminal.ansi.red": "#9d0308ff",
|
||||||
"terminal.ansi.bright_red": "#db8b7aff",
|
"terminal.ansi.bright_red": "#db8b7aff",
|
||||||
"terminal.ansi.dim_red": "#4e1207ff",
|
"terminal.ansi.dim_red": "#4e1207ff",
|
||||||
@@ -1293,9 +1248,9 @@
|
|||||||
"terminal.ansi.cyan": "#437b59ff",
|
"terminal.ansi.cyan": "#437b59ff",
|
||||||
"terminal.ansi.bright_cyan": "#9fbca8ff",
|
"terminal.ansi.bright_cyan": "#9fbca8ff",
|
||||||
"terminal.ansi.dim_cyan": "#253e2eff",
|
"terminal.ansi.dim_cyan": "#253e2eff",
|
||||||
"terminal.ansi.white": "#fbf1c7ff",
|
"terminal.ansi.white": "#282828ff",
|
||||||
"terminal.ansi.bright_white": "#fbf1c7ff",
|
"terminal.ansi.bright_white": "#282828ff",
|
||||||
"terminal.ansi.dim_white": "#b0a189ff",
|
"terminal.ansi.dim_white": "#73675eff",
|
||||||
"link_text.hover": "#0b6678ff",
|
"link_text.hover": "#0b6678ff",
|
||||||
"version_control.added": "#797410ff",
|
"version_control.added": "#797410ff",
|
||||||
"version_control.modified": "#b57615ff",
|
"version_control.modified": "#b57615ff",
|
||||||
@@ -1470,11 +1425,6 @@
|
|||||||
"font_style": null,
|
"font_style": null,
|
||||||
"font_weight": null
|
"font_weight": null
|
||||||
},
|
},
|
||||||
"namespace": {
|
|
||||||
"color": "#066578ff",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"number": {
|
"number": {
|
||||||
"color": "#8f3e71ff",
|
"color": "#8f3e71ff",
|
||||||
"font_style": null,
|
"font_style": null,
|
||||||
@@ -1530,16 +1480,6 @@
|
|||||||
"font_style": null,
|
"font_style": null,
|
||||||
"font_weight": null
|
"font_weight": null
|
||||||
},
|
},
|
||||||
"selector": {
|
|
||||||
"color": "#b57613ff",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"selector.pseudo": {
|
|
||||||
"color": "#0b6678ff",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"string": {
|
"string": {
|
||||||
"color": "#79740eff",
|
"color": "#79740eff",
|
||||||
"font_style": null,
|
"font_style": null,
|
||||||
@@ -1672,9 +1612,9 @@
|
|||||||
"terminal.foreground": "#282828ff",
|
"terminal.foreground": "#282828ff",
|
||||||
"terminal.bright_foreground": "#282828ff",
|
"terminal.bright_foreground": "#282828ff",
|
||||||
"terminal.dim_foreground": "#f9f5d7ff",
|
"terminal.dim_foreground": "#f9f5d7ff",
|
||||||
"terminal.ansi.black": "#282828ff",
|
"terminal.ansi.black": "#f9f5d7ff",
|
||||||
"terminal.ansi.bright_black": "#73675eff",
|
"terminal.ansi.bright_black": "#b0a189ff",
|
||||||
"terminal.ansi.dim_black": "#f9f5d7ff",
|
"terminal.ansi.dim_black": "#282828ff",
|
||||||
"terminal.ansi.red": "#9d0308ff",
|
"terminal.ansi.red": "#9d0308ff",
|
||||||
"terminal.ansi.bright_red": "#db8b7aff",
|
"terminal.ansi.bright_red": "#db8b7aff",
|
||||||
"terminal.ansi.dim_red": "#4e1207ff",
|
"terminal.ansi.dim_red": "#4e1207ff",
|
||||||
@@ -1693,9 +1633,9 @@
|
|||||||
"terminal.ansi.cyan": "#437b59ff",
|
"terminal.ansi.cyan": "#437b59ff",
|
||||||
"terminal.ansi.bright_cyan": "#9fbca8ff",
|
"terminal.ansi.bright_cyan": "#9fbca8ff",
|
||||||
"terminal.ansi.dim_cyan": "#253e2eff",
|
"terminal.ansi.dim_cyan": "#253e2eff",
|
||||||
"terminal.ansi.white": "#f9f5d7ff",
|
"terminal.ansi.white": "#282828ff",
|
||||||
"terminal.ansi.bright_white": "#f9f5d7ff",
|
"terminal.ansi.bright_white": "#282828ff",
|
||||||
"terminal.ansi.dim_white": "#b0a189ff",
|
"terminal.ansi.dim_white": "#73675eff",
|
||||||
"link_text.hover": "#0b6678ff",
|
"link_text.hover": "#0b6678ff",
|
||||||
"version_control.added": "#797410ff",
|
"version_control.added": "#797410ff",
|
||||||
"version_control.modified": "#b57615ff",
|
"version_control.modified": "#b57615ff",
|
||||||
@@ -1870,11 +1810,6 @@
|
|||||||
"font_style": null,
|
"font_style": null,
|
||||||
"font_weight": null
|
"font_weight": null
|
||||||
},
|
},
|
||||||
"namespace": {
|
|
||||||
"color": "#066578ff",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"number": {
|
"number": {
|
||||||
"color": "#8f3e71ff",
|
"color": "#8f3e71ff",
|
||||||
"font_style": null,
|
"font_style": null,
|
||||||
@@ -1930,16 +1865,6 @@
|
|||||||
"font_style": null,
|
"font_style": null,
|
||||||
"font_weight": null
|
"font_weight": null
|
||||||
},
|
},
|
||||||
"selector": {
|
|
||||||
"color": "#b57613ff",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"selector.pseudo": {
|
|
||||||
"color": "#0b6678ff",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"string": {
|
"string": {
|
||||||
"color": "#79740eff",
|
"color": "#79740eff",
|
||||||
"font_style": null,
|
"font_style": null,
|
||||||
@@ -2072,9 +1997,9 @@
|
|||||||
"terminal.foreground": "#282828ff",
|
"terminal.foreground": "#282828ff",
|
||||||
"terminal.bright_foreground": "#282828ff",
|
"terminal.bright_foreground": "#282828ff",
|
||||||
"terminal.dim_foreground": "#f2e5bcff",
|
"terminal.dim_foreground": "#f2e5bcff",
|
||||||
"terminal.ansi.black": "#282828ff",
|
"terminal.ansi.black": "#f2e5bcff",
|
||||||
"terminal.ansi.bright_black": "#73675eff",
|
"terminal.ansi.bright_black": "#b0a189ff",
|
||||||
"terminal.ansi.dim_black": "#f2e5bcff",
|
"terminal.ansi.dim_black": "#282828ff",
|
||||||
"terminal.ansi.red": "#9d0308ff",
|
"terminal.ansi.red": "#9d0308ff",
|
||||||
"terminal.ansi.bright_red": "#db8b7aff",
|
"terminal.ansi.bright_red": "#db8b7aff",
|
||||||
"terminal.ansi.dim_red": "#4e1207ff",
|
"terminal.ansi.dim_red": "#4e1207ff",
|
||||||
@@ -2093,9 +2018,9 @@
|
|||||||
"terminal.ansi.cyan": "#437b59ff",
|
"terminal.ansi.cyan": "#437b59ff",
|
||||||
"terminal.ansi.bright_cyan": "#9fbca8ff",
|
"terminal.ansi.bright_cyan": "#9fbca8ff",
|
||||||
"terminal.ansi.dim_cyan": "#253e2eff",
|
"terminal.ansi.dim_cyan": "#253e2eff",
|
||||||
"terminal.ansi.white": "#f2e5bcff",
|
"terminal.ansi.white": "#282828ff",
|
||||||
"terminal.ansi.bright_white": "#f2e5bcff",
|
"terminal.ansi.bright_white": "#282828ff",
|
||||||
"terminal.ansi.dim_white": "#b0a189ff",
|
"terminal.ansi.dim_white": "#73675eff",
|
||||||
"link_text.hover": "#0b6678ff",
|
"link_text.hover": "#0b6678ff",
|
||||||
"version_control.added": "#797410ff",
|
"version_control.added": "#797410ff",
|
||||||
"version_control.modified": "#b57615ff",
|
"version_control.modified": "#b57615ff",
|
||||||
@@ -2270,11 +2195,6 @@
|
|||||||
"font_style": null,
|
"font_style": null,
|
||||||
"font_weight": null
|
"font_weight": null
|
||||||
},
|
},
|
||||||
"namespace": {
|
|
||||||
"color": "#066578ff",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"number": {
|
"number": {
|
||||||
"color": "#8f3e71ff",
|
"color": "#8f3e71ff",
|
||||||
"font_style": null,
|
"font_style": null,
|
||||||
@@ -2330,16 +2250,6 @@
|
|||||||
"font_style": null,
|
"font_style": null,
|
||||||
"font_weight": null
|
"font_weight": null
|
||||||
},
|
},
|
||||||
"selector": {
|
|
||||||
"color": "#b57613ff",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"selector.pseudo": {
|
|
||||||
"color": "#0b6678ff",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"string": {
|
"string": {
|
||||||
"color": "#79740eff",
|
"color": "#79740eff",
|
||||||
"font_style": null,
|
"font_style": null,
|
||||||
|
|||||||
@@ -264,11 +264,6 @@
|
|||||||
"font_style": null,
|
"font_style": null,
|
||||||
"font_weight": null
|
"font_weight": null
|
||||||
},
|
},
|
||||||
"namespace": {
|
|
||||||
"color": "#dce0e5ff",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"number": {
|
"number": {
|
||||||
"color": "#bf956aff",
|
"color": "#bf956aff",
|
||||||
"font_style": null,
|
"font_style": null,
|
||||||
@@ -324,16 +319,6 @@
|
|||||||
"font_style": null,
|
"font_style": null,
|
||||||
"font_weight": null
|
"font_weight": null
|
||||||
},
|
},
|
||||||
"selector": {
|
|
||||||
"color": "#dfc184ff",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"selector.pseudo": {
|
|
||||||
"color": "#74ade8ff",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"string": {
|
"string": {
|
||||||
"color": "#a1c181ff",
|
"color": "#a1c181ff",
|
||||||
"font_style": null,
|
"font_style": null,
|
||||||
@@ -465,9 +450,9 @@
|
|||||||
"terminal.foreground": "#242529ff",
|
"terminal.foreground": "#242529ff",
|
||||||
"terminal.bright_foreground": "#242529ff",
|
"terminal.bright_foreground": "#242529ff",
|
||||||
"terminal.dim_foreground": "#fafafaff",
|
"terminal.dim_foreground": "#fafafaff",
|
||||||
"terminal.ansi.black": "#242529ff",
|
"terminal.ansi.black": "#fafafaff",
|
||||||
"terminal.ansi.bright_black": "#242529ff",
|
"terminal.ansi.bright_black": "#aaaaaaff",
|
||||||
"terminal.ansi.dim_black": "#97979aff",
|
"terminal.ansi.dim_black": "#242529ff",
|
||||||
"terminal.ansi.red": "#d36151ff",
|
"terminal.ansi.red": "#d36151ff",
|
||||||
"terminal.ansi.bright_red": "#f0b0a4ff",
|
"terminal.ansi.bright_red": "#f0b0a4ff",
|
||||||
"terminal.ansi.dim_red": "#6f312aff",
|
"terminal.ansi.dim_red": "#6f312aff",
|
||||||
@@ -486,9 +471,9 @@
|
|||||||
"terminal.ansi.cyan": "#3a82b7ff",
|
"terminal.ansi.cyan": "#3a82b7ff",
|
||||||
"terminal.ansi.bright_cyan": "#a3bedaff",
|
"terminal.ansi.bright_cyan": "#a3bedaff",
|
||||||
"terminal.ansi.dim_cyan": "#254058ff",
|
"terminal.ansi.dim_cyan": "#254058ff",
|
||||||
"terminal.ansi.white": "#fafafaff",
|
"terminal.ansi.white": "#242529ff",
|
||||||
"terminal.ansi.bright_white": "#fafafaff",
|
"terminal.ansi.bright_white": "#242529ff",
|
||||||
"terminal.ansi.dim_white": "#aaaaaaff",
|
"terminal.ansi.dim_white": "#97979aff",
|
||||||
"link_text.hover": "#5c78e2ff",
|
"link_text.hover": "#5c78e2ff",
|
||||||
"version_control.added": "#27a657ff",
|
"version_control.added": "#27a657ff",
|
||||||
"version_control.modified": "#d3b020ff",
|
"version_control.modified": "#d3b020ff",
|
||||||
@@ -658,11 +643,6 @@
|
|||||||
"font_style": null,
|
"font_style": null,
|
||||||
"font_weight": null
|
"font_weight": null
|
||||||
},
|
},
|
||||||
"namespace": {
|
|
||||||
"color": "#242529ff",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"number": {
|
"number": {
|
||||||
"color": "#ad6e25ff",
|
"color": "#ad6e25ff",
|
||||||
"font_style": null,
|
"font_style": null,
|
||||||
@@ -718,16 +698,6 @@
|
|||||||
"font_style": null,
|
"font_style": null,
|
||||||
"font_weight": null
|
"font_weight": null
|
||||||
},
|
},
|
||||||
"selector": {
|
|
||||||
"color": "#669f59ff",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"selector.pseudo": {
|
|
||||||
"color": "#5c78e2ff",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"string": {
|
"string": {
|
||||||
"color": "#649f57ff",
|
"color": "#649f57ff",
|
||||||
"font_style": null,
|
"font_style": null,
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ assistant_context_editor.workspace = true
|
|||||||
assistant_slash_command.workspace = true
|
assistant_slash_command.workspace = true
|
||||||
assistant_slash_commands.workspace = true
|
assistant_slash_commands.workspace = true
|
||||||
assistant_tool.workspace = true
|
assistant_tool.workspace = true
|
||||||
|
async-watch.workspace = true
|
||||||
audio.workspace = true
|
audio.workspace = true
|
||||||
buffer_diff.workspace = true
|
buffer_diff.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
@@ -94,7 +95,6 @@ ui_input.workspace = true
|
|||||||
urlencoding.workspace = true
|
urlencoding.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
watch.workspace = true
|
|
||||||
workspace-hack.workspace = true
|
workspace-hack.workspace = true
|
||||||
workspace.workspace = true
|
workspace.workspace = true
|
||||||
zed_actions.workspace = true
|
zed_actions.workspace = true
|
||||||
@@ -102,7 +102,6 @@ zed_llm_client.workspace = true
|
|||||||
zstd.workspace = true
|
zstd.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
assistant_tools.workspace = true
|
|
||||||
buffer_diff = { workspace = true, features = ["test-support"] }
|
buffer_diff = { workspace = true, features = ["test-support"] }
|
||||||
editor = { workspace = true, features = ["test-support"] }
|
editor = { workspace = true, features = ["test-support"] }
|
||||||
gpui = { workspace = true, "features" = ["test-support"] }
|
gpui = { workspace = true, "features" = ["test-support"] }
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
use crate::AgentPanel;
|
||||||
use crate::context::{AgentContextHandle, RULES_ICON};
|
use crate::context::{AgentContextHandle, RULES_ICON};
|
||||||
use crate::context_picker::{ContextPicker, MentionLink};
|
use crate::context_picker::{ContextPicker, MentionLink};
|
||||||
use crate::context_store::ContextStore;
|
use crate::context_store::ContextStore;
|
||||||
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
|
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
|
||||||
use crate::message_editor::{extract_message_creases, insert_message_creases};
|
use crate::message_editor::insert_message_creases;
|
||||||
use crate::thread::{
|
use crate::thread::{
|
||||||
LastRestoreCheckpoint, MessageCrease, MessageId, MessageSegment, Thread, ThreadError,
|
LastRestoreCheckpoint, MessageCrease, MessageId, MessageSegment, Thread, ThreadError,
|
||||||
ThreadEvent, ThreadFeedback, ThreadSummary,
|
ThreadEvent, ThreadFeedback, ThreadSummary,
|
||||||
@@ -12,7 +13,6 @@ use crate::tool_use::{PendingToolUseStatus, ToolUse};
|
|||||||
use crate::ui::{
|
use crate::ui::{
|
||||||
AddedContext, AgentNotification, AgentNotificationEvent, AnimatedLabel, ContextPill,
|
AddedContext, AgentNotification, AgentNotificationEvent, AnimatedLabel, ContextPill,
|
||||||
};
|
};
|
||||||
use crate::{AgentPanel, ModelUsageContext};
|
|
||||||
use agent_settings::{AgentSettings, NotifyWhenAgentWaiting};
|
use agent_settings::{AgentSettings, NotifyWhenAgentWaiting};
|
||||||
use anyhow::Context as _;
|
use anyhow::Context as _;
|
||||||
use assistant_tool::ToolUseStatus;
|
use assistant_tool::ToolUseStatus;
|
||||||
@@ -1144,10 +1144,6 @@ impl ActiveThread {
|
|||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
ThreadEvent::ProfileChanged => {
|
|
||||||
self.save_thread(cx);
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1352,7 +1348,6 @@ impl ActiveThread {
|
|||||||
Some(self.text_thread_store.downgrade()),
|
Some(self.text_thread_store.downgrade()),
|
||||||
context_picker_menu_handle.clone(),
|
context_picker_menu_handle.clone(),
|
||||||
SuggestContextKind::File,
|
SuggestContextKind::File,
|
||||||
ModelUsageContext::Thread(self.thread.clone()),
|
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
@@ -1522,7 +1517,31 @@ impl ActiveThread {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn paste(&mut self, _: &Paste, _window: &mut Window, cx: &mut Context<Self>) {
|
fn paste(&mut self, _: &Paste, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
attach_pasted_images_as_context(&self.context_store, cx);
|
let images = cx
|
||||||
|
.read_from_clipboard()
|
||||||
|
.map(|item| {
|
||||||
|
item.into_entries()
|
||||||
|
.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 cancel_editing_message(
|
fn cancel_editing_message(
|
||||||
@@ -1567,8 +1586,6 @@ impl ActiveThread {
|
|||||||
|
|
||||||
let edited_text = state.editor.read(cx).text(cx);
|
let edited_text = state.editor.read(cx).text(cx);
|
||||||
|
|
||||||
let creases = state.editor.update(cx, extract_message_creases);
|
|
||||||
|
|
||||||
let new_context = self
|
let new_context = self
|
||||||
.context_store
|
.context_store
|
||||||
.read(cx)
|
.read(cx)
|
||||||
@@ -1593,7 +1610,6 @@ impl ActiveThread {
|
|||||||
message_id,
|
message_id,
|
||||||
Role::User,
|
Role::User,
|
||||||
vec![MessageSegment::Text(edited_text)],
|
vec![MessageSegment::Text(edited_text)],
|
||||||
creases,
|
|
||||||
Some(context.loaded_context),
|
Some(context.loaded_context),
|
||||||
checkpoint.ok(),
|
checkpoint.ok(),
|
||||||
cx,
|
cx,
|
||||||
@@ -1807,10 +1823,9 @@ impl ActiveThread {
|
|||||||
|
|
||||||
// Get all the data we need from thread before we start using it in closures
|
// Get all the data we need from thread before we start using it in closures
|
||||||
let checkpoint = thread.checkpoint_for_message(message_id);
|
let checkpoint = thread.checkpoint_for_message(message_id);
|
||||||
let configured_model = thread.configured_model().map(|m| m.model);
|
|
||||||
let added_context = thread
|
let added_context = thread
|
||||||
.context_for_message(message_id)
|
.context_for_message(message_id)
|
||||||
.map(|context| AddedContext::new_attached(context, configured_model.as_ref(), cx))
|
.map(|context| AddedContext::new_attached(context, cx))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let tool_uses = thread.tool_uses_for_message(message_id, cx);
|
let tool_uses = thread.tool_uses_for_message(message_id, cx);
|
||||||
@@ -3633,38 +3648,6 @@ pub(crate) fn open_context(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn attach_pasted_images_as_context(
|
|
||||||
context_store: &Entity<ContextStore>,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> bool {
|
|
||||||
let images = cx
|
|
||||||
.read_from_clipboard()
|
|
||||||
.map(|item| {
|
|
||||||
item.into_entries()
|
|
||||||
.filter_map(|entry| {
|
|
||||||
if let ClipboardEntry::Image(image) = entry {
|
|
||||||
Some(image)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
})
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
if images.is_empty() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
cx.stop_propagation();
|
|
||||||
|
|
||||||
context_store.update(cx, |store, cx| {
|
|
||||||
for image in images {
|
|
||||||
store.add_image_instance(Arc::new(image), cx);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn open_editor_at_position(
|
fn open_editor_at_position(
|
||||||
project_path: project::ProjectPath,
|
project_path: project::ProjectPath,
|
||||||
target_position: Point,
|
target_position: Point,
|
||||||
@@ -3694,13 +3677,10 @@ fn open_editor_at_position(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use assistant_tool::{ToolRegistry, ToolWorkingSet};
|
use assistant_tool::{ToolRegistry, ToolWorkingSet};
|
||||||
use editor::{EditorSettings, display_map::CreaseMetadata};
|
use editor::EditorSettings;
|
||||||
use fs::FakeFs;
|
use fs::FakeFs;
|
||||||
use gpui::{AppContext, TestAppContext, VisualTestContext};
|
use gpui::{AppContext, TestAppContext, VisualTestContext};
|
||||||
use language_model::{
|
use language_model::{LanguageModel, fake_provider::FakeLanguageModel};
|
||||||
ConfiguredModel, LanguageModel, LanguageModelRegistry,
|
|
||||||
fake_provider::{FakeLanguageModel, FakeLanguageModelProvider},
|
|
||||||
};
|
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use prompt_store::PromptBuilder;
|
use prompt_store::PromptBuilder;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
@@ -3761,87 +3741,6 @@ mod tests {
|
|||||||
assert!(!cx.read(|cx| workspace.read(cx).is_being_followed(CollaboratorId::Agent)));
|
assert!(!cx.read(|cx| workspace.read(cx).is_being_followed(CollaboratorId::Agent)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_reinserting_creases_for_edited_message(cx: &mut TestAppContext) {
|
|
||||||
init_test_settings(cx);
|
|
||||||
|
|
||||||
let project = create_test_project(cx, json!({})).await;
|
|
||||||
|
|
||||||
let (cx, active_thread, _, thread, model) =
|
|
||||||
setup_test_environment(cx, project.clone()).await;
|
|
||||||
cx.update(|_, cx| {
|
|
||||||
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
|
|
||||||
registry.set_default_model(
|
|
||||||
Some(ConfiguredModel {
|
|
||||||
provider: Arc::new(FakeLanguageModelProvider),
|
|
||||||
model,
|
|
||||||
}),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
let creases = vec![MessageCrease {
|
|
||||||
range: 14..22,
|
|
||||||
metadata: CreaseMetadata {
|
|
||||||
icon_path: "icon".into(),
|
|
||||||
label: "foo.txt".into(),
|
|
||||||
},
|
|
||||||
context: None,
|
|
||||||
}];
|
|
||||||
|
|
||||||
let message = thread.update(cx, |thread, cx| {
|
|
||||||
let message_id = thread.insert_user_message(
|
|
||||||
"Tell me about @foo.txt",
|
|
||||||
ContextLoadResult::default(),
|
|
||||||
None,
|
|
||||||
creases,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
thread.message(message_id).cloned().unwrap()
|
|
||||||
});
|
|
||||||
|
|
||||||
active_thread.update_in(cx, |active_thread, window, cx| {
|
|
||||||
active_thread.start_editing_message(
|
|
||||||
message.id,
|
|
||||||
message.segments.as_slice(),
|
|
||||||
message.creases.as_slice(),
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
let editor = active_thread
|
|
||||||
.editing_message
|
|
||||||
.as_ref()
|
|
||||||
.unwrap()
|
|
||||||
.1
|
|
||||||
.editor
|
|
||||||
.clone();
|
|
||||||
editor.update(cx, |editor, cx| editor.edit([(0..13, "modified")], cx));
|
|
||||||
active_thread.confirm_editing_message(&Default::default(), window, cx);
|
|
||||||
});
|
|
||||||
cx.run_until_parked();
|
|
||||||
|
|
||||||
let message = thread.update(cx, |thread, _| thread.message(message.id).cloned().unwrap());
|
|
||||||
active_thread.update_in(cx, |active_thread, window, cx| {
|
|
||||||
active_thread.start_editing_message(
|
|
||||||
message.id,
|
|
||||||
message.segments.as_slice(),
|
|
||||||
message.creases.as_slice(),
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
let editor = active_thread
|
|
||||||
.editing_message
|
|
||||||
.as_ref()
|
|
||||||
.unwrap()
|
|
||||||
.1
|
|
||||||
.editor
|
|
||||||
.clone();
|
|
||||||
let text = editor.update(cx, |editor, cx| editor.text(cx));
|
|
||||||
assert_eq!(text, "modified @foo.txt");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn init_test_settings(cx: &mut TestAppContext) {
|
fn init_test_settings(cx: &mut TestAppContext) {
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
let settings_store = SettingsStore::test(cx);
|
let settings_store = SettingsStore::test(cx);
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ mod agent_configuration;
|
|||||||
mod agent_diff;
|
mod agent_diff;
|
||||||
mod agent_model_selector;
|
mod agent_model_selector;
|
||||||
mod agent_panel;
|
mod agent_panel;
|
||||||
mod agent_profile;
|
|
||||||
mod buffer_codegen;
|
mod buffer_codegen;
|
||||||
mod context;
|
mod context;
|
||||||
mod context_picker;
|
mod context_picker;
|
||||||
@@ -34,11 +33,9 @@ use assistant_slash_command::SlashCommandRegistry;
|
|||||||
use client::Client;
|
use client::Client;
|
||||||
use feature_flags::FeatureFlagAppExt as _;
|
use feature_flags::FeatureFlagAppExt as _;
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use gpui::{App, Entity, actions, impl_actions};
|
use gpui::{App, actions, impl_actions};
|
||||||
use language::LanguageRegistry;
|
use language::LanguageRegistry;
|
||||||
use language_model::{
|
use language_model::{LanguageModelId, LanguageModelProviderId, LanguageModelRegistry};
|
||||||
ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry,
|
|
||||||
};
|
|
||||||
use prompt_store::PromptBuilder;
|
use prompt_store::PromptBuilder;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@@ -118,28 +115,6 @@ impl ManageProfiles {
|
|||||||
|
|
||||||
impl_actions!(agent, [NewThread, ManageProfiles]);
|
impl_actions!(agent, [NewThread, ManageProfiles]);
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub(crate) enum ModelUsageContext {
|
|
||||||
Thread(Entity<Thread>),
|
|
||||||
InlineAssistant,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ModelUsageContext {
|
|
||||||
pub fn configured_model(&self, cx: &App) -> Option<ConfiguredModel> {
|
|
||||||
match self {
|
|
||||||
Self::Thread(thread) => thread.read(cx).configured_model(),
|
|
||||||
Self::InlineAssistant => {
|
|
||||||
LanguageModelRegistry::read_global(cx).inline_assistant_model()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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.
|
||||||
pub fn init(
|
pub fn init(
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
|
|||||||
@@ -2,21 +2,25 @@ mod profile_modal_header;
|
|||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use agent_settings::{AgentProfileId, AgentSettings, builtin_profiles};
|
use agent_settings::{AgentProfile, AgentProfileId, AgentSettings, builtin_profiles};
|
||||||
use assistant_tool::ToolWorkingSet;
|
use assistant_tool::ToolWorkingSet;
|
||||||
|
use convert_case::{Case, Casing as _};
|
||||||
use editor::Editor;
|
use editor::Editor;
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription, prelude::*};
|
use gpui::{
|
||||||
use settings::Settings as _;
|
DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription, WeakEntity,
|
||||||
|
prelude::*,
|
||||||
|
};
|
||||||
|
use settings::{Settings as _, update_settings_file};
|
||||||
use ui::{
|
use ui::{
|
||||||
KeyBinding, ListItem, ListItemSpacing, ListSeparator, Navigable, NavigableEntry, prelude::*,
|
KeyBinding, ListItem, ListItemSpacing, ListSeparator, Navigable, NavigableEntry, prelude::*,
|
||||||
};
|
};
|
||||||
|
use util::ResultExt as _;
|
||||||
use workspace::{ModalView, Workspace};
|
use workspace::{ModalView, Workspace};
|
||||||
|
|
||||||
use crate::agent_configuration::manage_profiles_modal::profile_modal_header::ProfileModalHeader;
|
use crate::agent_configuration::manage_profiles_modal::profile_modal_header::ProfileModalHeader;
|
||||||
use crate::agent_configuration::tool_picker::{ToolPicker, ToolPickerDelegate};
|
use crate::agent_configuration::tool_picker::{ToolPicker, ToolPickerDelegate};
|
||||||
use crate::agent_profile::AgentProfile;
|
use crate::{AgentPanel, ManageProfiles, ThreadStore};
|
||||||
use crate::{AgentPanel, ManageProfiles};
|
|
||||||
|
|
||||||
use super::tool_picker::ToolPickerMode;
|
use super::tool_picker::ToolPickerMode;
|
||||||
|
|
||||||
@@ -99,6 +103,7 @@ pub struct NewProfileMode {
|
|||||||
pub struct ManageProfilesModal {
|
pub struct ManageProfilesModal {
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
tools: Entity<ToolWorkingSet>,
|
tools: Entity<ToolWorkingSet>,
|
||||||
|
thread_store: WeakEntity<ThreadStore>,
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
}
|
}
|
||||||
@@ -114,8 +119,9 @@ impl ManageProfilesModal {
|
|||||||
let fs = workspace.app_state().fs.clone();
|
let fs = workspace.app_state().fs.clone();
|
||||||
let thread_store = panel.read(cx).thread_store();
|
let thread_store = panel.read(cx).thread_store();
|
||||||
let tools = thread_store.read(cx).tools();
|
let tools = thread_store.read(cx).tools();
|
||||||
|
let thread_store = thread_store.downgrade();
|
||||||
workspace.toggle_modal(window, cx, |window, cx| {
|
workspace.toggle_modal(window, cx, |window, cx| {
|
||||||
let mut this = Self::new(fs, tools, window, cx);
|
let mut this = Self::new(fs, tools, thread_store, window, cx);
|
||||||
|
|
||||||
if let Some(profile_id) = action.customize_tools.clone() {
|
if let Some(profile_id) = action.customize_tools.clone() {
|
||||||
this.configure_builtin_tools(profile_id, window, cx);
|
this.configure_builtin_tools(profile_id, window, cx);
|
||||||
@@ -130,6 +136,7 @@ impl ManageProfilesModal {
|
|||||||
pub fn new(
|
pub fn new(
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
tools: Entity<ToolWorkingSet>,
|
tools: Entity<ToolWorkingSet>,
|
||||||
|
thread_store: WeakEntity<ThreadStore>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
@@ -138,6 +145,7 @@ impl ManageProfilesModal {
|
|||||||
Self {
|
Self {
|
||||||
fs,
|
fs,
|
||||||
tools,
|
tools,
|
||||||
|
thread_store,
|
||||||
focus_handle,
|
focus_handle,
|
||||||
mode: Mode::choose_profile(window, cx),
|
mode: Mode::choose_profile(window, cx),
|
||||||
}
|
}
|
||||||
@@ -198,6 +206,7 @@ impl ManageProfilesModal {
|
|||||||
ToolPickerMode::McpTools,
|
ToolPickerMode::McpTools,
|
||||||
self.fs.clone(),
|
self.fs.clone(),
|
||||||
self.tools.clone(),
|
self.tools.clone(),
|
||||||
|
self.thread_store.clone(),
|
||||||
profile_id.clone(),
|
profile_id.clone(),
|
||||||
profile,
|
profile,
|
||||||
cx,
|
cx,
|
||||||
@@ -235,6 +244,7 @@ impl ManageProfilesModal {
|
|||||||
ToolPickerMode::BuiltinTools,
|
ToolPickerMode::BuiltinTools,
|
||||||
self.fs.clone(),
|
self.fs.clone(),
|
||||||
self.tools.clone(),
|
self.tools.clone(),
|
||||||
|
self.thread_store.clone(),
|
||||||
profile_id.clone(),
|
profile_id.clone(),
|
||||||
profile,
|
profile,
|
||||||
cx,
|
cx,
|
||||||
@@ -260,10 +270,32 @@ impl ManageProfilesModal {
|
|||||||
match &self.mode {
|
match &self.mode {
|
||||||
Mode::ChooseProfile { .. } => {}
|
Mode::ChooseProfile { .. } => {}
|
||||||
Mode::NewProfile(mode) => {
|
Mode::NewProfile(mode) => {
|
||||||
let name = mode.name_editor.read(cx).text(cx);
|
let settings = AgentSettings::get_global(cx);
|
||||||
|
|
||||||
let profile_id =
|
let base_profile = mode
|
||||||
AgentProfile::create(name, mode.base_profile_id.clone(), self.fs.clone(), cx);
|
.base_profile_id
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|profile_id| settings.profiles.get(profile_id).cloned());
|
||||||
|
|
||||||
|
let name = mode.name_editor.read(cx).text(cx);
|
||||||
|
let profile_id = AgentProfileId(name.to_case(Case::Kebab).into());
|
||||||
|
|
||||||
|
let profile = AgentProfile {
|
||||||
|
name: name.into(),
|
||||||
|
tools: base_profile
|
||||||
|
.as_ref()
|
||||||
|
.map(|profile| profile.tools.clone())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
enable_all_context_servers: base_profile
|
||||||
|
.as_ref()
|
||||||
|
.map(|profile| profile.enable_all_context_servers)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
context_servers: base_profile
|
||||||
|
.map(|profile| profile.context_servers)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.create_profile(profile_id.clone(), profile, cx);
|
||||||
self.view_profile(profile_id, window, cx);
|
self.view_profile(profile_id, window, cx);
|
||||||
}
|
}
|
||||||
Mode::ViewProfile(_) => {}
|
Mode::ViewProfile(_) => {}
|
||||||
@@ -293,6 +325,19 @@ impl ManageProfilesModal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_profile(
|
||||||
|
&self,
|
||||||
|
profile_id: AgentProfileId,
|
||||||
|
profile: AgentProfile,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
update_settings_file::<AgentSettings>(self.fs.clone(), cx, {
|
||||||
|
move |settings, _cx| {
|
||||||
|
settings.create_profile(profile_id, profile).log_err();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ModalView for ManageProfilesModal {}
|
impl ModalView for ManageProfilesModal {}
|
||||||
@@ -475,13 +520,14 @@ impl ManageProfilesModal {
|
|||||||
) -> impl IntoElement {
|
) -> impl IntoElement {
|
||||||
let settings = AgentSettings::get_global(cx);
|
let settings = AgentSettings::get_global(cx);
|
||||||
|
|
||||||
|
let profile_id = &settings.default_profile;
|
||||||
let profile_name = settings
|
let profile_name = settings
|
||||||
.profiles
|
.profiles
|
||||||
.get(&mode.profile_id)
|
.get(&mode.profile_id)
|
||||||
.map(|profile| profile.name.clone())
|
.map(|profile| profile.name.clone())
|
||||||
.unwrap_or_else(|| "Unknown".into());
|
.unwrap_or_else(|| "Unknown".into());
|
||||||
|
|
||||||
let icon = match mode.profile_id.as_str() {
|
let icon = match profile_id.as_str() {
|
||||||
"write" => IconName::Pencil,
|
"write" => IconName::Pencil,
|
||||||
"ask" => IconName::MessageBubbles,
|
"ask" => IconName::MessageBubbles,
|
||||||
_ => IconName::UserRoundPen,
|
_ => IconName::UserRoundPen,
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
use std::{collections::BTreeMap, sync::Arc};
|
use std::{collections::BTreeMap, sync::Arc};
|
||||||
|
|
||||||
use agent_settings::{
|
use agent_settings::{
|
||||||
AgentProfileContent, AgentProfileId, AgentProfileSettings, AgentSettings, AgentSettingsContent,
|
AgentProfile, AgentProfileContent, AgentProfileId, AgentSettings, AgentSettingsContent,
|
||||||
ContextServerPresetContent,
|
ContextServerPresetContent,
|
||||||
};
|
};
|
||||||
use assistant_tool::{ToolSource, ToolWorkingSet};
|
use assistant_tool::{ToolSource, ToolWorkingSet};
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use gpui::{App, Context, DismissEvent, Entity, EventEmitter, Focusable, Task, WeakEntity, Window};
|
use gpui::{App, Context, DismissEvent, Entity, EventEmitter, Focusable, Task, WeakEntity, Window};
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
use settings::update_settings_file;
|
use settings::{Settings as _, update_settings_file};
|
||||||
use ui::{ListItem, ListItemSpacing, prelude::*};
|
use ui::{ListItem, ListItemSpacing, prelude::*};
|
||||||
use util::ResultExt as _;
|
use util::ResultExt as _;
|
||||||
|
|
||||||
|
use crate::ThreadStore;
|
||||||
|
|
||||||
pub struct ToolPicker {
|
pub struct ToolPicker {
|
||||||
picker: Entity<Picker<ToolPickerDelegate>>,
|
picker: Entity<Picker<ToolPickerDelegate>>,
|
||||||
}
|
}
|
||||||
@@ -69,10 +71,11 @@ pub enum PickerItem {
|
|||||||
|
|
||||||
pub struct ToolPickerDelegate {
|
pub struct ToolPickerDelegate {
|
||||||
tool_picker: WeakEntity<ToolPicker>,
|
tool_picker: WeakEntity<ToolPicker>,
|
||||||
|
thread_store: WeakEntity<ThreadStore>,
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
items: Arc<Vec<PickerItem>>,
|
items: Arc<Vec<PickerItem>>,
|
||||||
profile_id: AgentProfileId,
|
profile_id: AgentProfileId,
|
||||||
profile_settings: AgentProfileSettings,
|
profile: AgentProfile,
|
||||||
filtered_items: Vec<PickerItem>,
|
filtered_items: Vec<PickerItem>,
|
||||||
selected_index: usize,
|
selected_index: usize,
|
||||||
mode: ToolPickerMode,
|
mode: ToolPickerMode,
|
||||||
@@ -83,18 +86,20 @@ impl ToolPickerDelegate {
|
|||||||
mode: ToolPickerMode,
|
mode: ToolPickerMode,
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
tool_set: Entity<ToolWorkingSet>,
|
tool_set: Entity<ToolWorkingSet>,
|
||||||
|
thread_store: WeakEntity<ThreadStore>,
|
||||||
profile_id: AgentProfileId,
|
profile_id: AgentProfileId,
|
||||||
profile_settings: AgentProfileSettings,
|
profile: AgentProfile,
|
||||||
cx: &mut Context<ToolPicker>,
|
cx: &mut Context<ToolPicker>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let items = Arc::new(Self::resolve_items(mode, &tool_set, cx));
|
let items = Arc::new(Self::resolve_items(mode, &tool_set, cx));
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
tool_picker: cx.entity().downgrade(),
|
tool_picker: cx.entity().downgrade(),
|
||||||
|
thread_store,
|
||||||
fs,
|
fs,
|
||||||
items,
|
items,
|
||||||
profile_id,
|
profile_id,
|
||||||
profile_settings,
|
profile,
|
||||||
filtered_items: Vec::new(),
|
filtered_items: Vec::new(),
|
||||||
selected_index: 0,
|
selected_index: 0,
|
||||||
mode,
|
mode,
|
||||||
@@ -244,31 +249,28 @@ impl PickerDelegate for ToolPickerDelegate {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let is_currently_enabled = if let Some(server_id) = server_id.clone() {
|
let is_currently_enabled = if let Some(server_id) = server_id.clone() {
|
||||||
let preset = self
|
let preset = self.profile.context_servers.entry(server_id).or_default();
|
||||||
.profile_settings
|
|
||||||
.context_servers
|
|
||||||
.entry(server_id)
|
|
||||||
.or_default();
|
|
||||||
let is_enabled = *preset.tools.entry(tool_name.clone()).or_default();
|
let is_enabled = *preset.tools.entry(tool_name.clone()).or_default();
|
||||||
*preset.tools.entry(tool_name.clone()).or_default() = !is_enabled;
|
*preset.tools.entry(tool_name.clone()).or_default() = !is_enabled;
|
||||||
is_enabled
|
is_enabled
|
||||||
} else {
|
} else {
|
||||||
let is_enabled = *self
|
let is_enabled = *self.profile.tools.entry(tool_name.clone()).or_default();
|
||||||
.profile_settings
|
*self.profile.tools.entry(tool_name.clone()).or_default() = !is_enabled;
|
||||||
.tools
|
|
||||||
.entry(tool_name.clone())
|
|
||||||
.or_default();
|
|
||||||
*self
|
|
||||||
.profile_settings
|
|
||||||
.tools
|
|
||||||
.entry(tool_name.clone())
|
|
||||||
.or_default() = !is_enabled;
|
|
||||||
is_enabled
|
is_enabled
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let active_profile_id = &AgentSettings::get_global(cx).default_profile;
|
||||||
|
if active_profile_id == &self.profile_id {
|
||||||
|
self.thread_store
|
||||||
|
.update(cx, |this, cx| {
|
||||||
|
this.load_profile(self.profile.clone(), cx);
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
|
||||||
update_settings_file::<AgentSettings>(self.fs.clone(), cx, {
|
update_settings_file::<AgentSettings>(self.fs.clone(), cx, {
|
||||||
let profile_id = self.profile_id.clone();
|
let profile_id = self.profile_id.clone();
|
||||||
let default_profile = self.profile_settings.clone();
|
let default_profile = self.profile.clone();
|
||||||
let server_id = server_id.clone();
|
let server_id = server_id.clone();
|
||||||
let tool_name = tool_name.clone();
|
let tool_name = tool_name.clone();
|
||||||
move |settings: &mut AgentSettingsContent, _cx| {
|
move |settings: &mut AgentSettingsContent, _cx| {
|
||||||
@@ -346,18 +348,14 @@ impl PickerDelegate for ToolPickerDelegate {
|
|||||||
),
|
),
|
||||||
PickerItem::Tool { name, server_id } => {
|
PickerItem::Tool { name, server_id } => {
|
||||||
let is_enabled = if let Some(server_id) = server_id {
|
let is_enabled = if let Some(server_id) = server_id {
|
||||||
self.profile_settings
|
self.profile
|
||||||
.context_servers
|
.context_servers
|
||||||
.get(server_id.as_ref())
|
.get(server_id.as_ref())
|
||||||
.and_then(|preset| preset.tools.get(name))
|
.and_then(|preset| preset.tools.get(name))
|
||||||
.copied()
|
.copied()
|
||||||
.unwrap_or(self.profile_settings.enable_all_context_servers)
|
.unwrap_or(self.profile.enable_all_context_servers)
|
||||||
} else {
|
} else {
|
||||||
self.profile_settings
|
self.profile.tools.get(name).copied().unwrap_or(false)
|
||||||
.tools
|
|
||||||
.get(name)
|
|
||||||
.copied()
|
|
||||||
.unwrap_or(false)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(
|
Some(
|
||||||
|
|||||||
@@ -1086,7 +1086,7 @@ impl Render for AgentDiffToolbar {
|
|||||||
.child(vertical_divider())
|
.child(vertical_divider())
|
||||||
.when_some(editor.read(cx).workspace(), |this, _workspace| {
|
.when_some(editor.read(cx).workspace(), |this, _workspace| {
|
||||||
this.child(
|
this.child(
|
||||||
IconButton::new("review", IconName::ListTodo)
|
IconButton::new("review", IconName::ListCollapse)
|
||||||
.icon_size(IconSize::Small)
|
.icon_size(IconSize::Small)
|
||||||
.tooltip(Tooltip::for_action_title_in(
|
.tooltip(Tooltip::for_action_title_in(
|
||||||
"Review All Files",
|
"Review All Files",
|
||||||
@@ -1116,13 +1116,8 @@ impl Render for AgentDiffToolbar {
|
|||||||
return Empty.into_any();
|
return Empty.into_any();
|
||||||
};
|
};
|
||||||
|
|
||||||
let has_pending_edit_tool_use = agent_diff
|
let is_generating = agent_diff.read(cx).thread.read(cx).is_generating();
|
||||||
.read(cx)
|
if is_generating {
|
||||||
.thread
|
|
||||||
.read(cx)
|
|
||||||
.has_pending_edit_tool_uses();
|
|
||||||
|
|
||||||
if has_pending_edit_tool_use {
|
|
||||||
return div().px_2().child(spinner_icon).into_any();
|
return div().px_2().child(spinner_icon).into_any();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1378,8 +1373,7 @@ impl AgentDiff {
|
|||||||
| ThreadEvent::CheckpointChanged
|
| ThreadEvent::CheckpointChanged
|
||||||
| ThreadEvent::ToolConfirmationNeeded
|
| ThreadEvent::ToolConfirmationNeeded
|
||||||
| ThreadEvent::ToolUseLimitReached
|
| ThreadEvent::ToolUseLimitReached
|
||||||
| ThreadEvent::CancelEditing
|
| ThreadEvent::CancelEditing => {}
|
||||||
| ThreadEvent::ProfileChanged => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1513,7 +1507,7 @@ impl AgentDiff {
|
|||||||
multibuffer.add_diff(diff_handle.clone(), cx);
|
multibuffer.add_diff(diff_handle.clone(), cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
let new_state = if thread.read(cx).has_pending_edit_tool_uses() {
|
let new_state = if thread.read(cx).is_generating() {
|
||||||
EditorState::Generating
|
EditorState::Generating
|
||||||
} else {
|
} else {
|
||||||
EditorState::Reviewing
|
EditorState::Reviewing
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use fs::Fs;
|
|||||||
use gpui::{Entity, FocusHandle, SharedString};
|
use gpui::{Entity, FocusHandle, SharedString};
|
||||||
use picker::popover_menu::PickerPopoverMenu;
|
use picker::popover_menu::PickerPopoverMenu;
|
||||||
|
|
||||||
use crate::ModelUsageContext;
|
use crate::Thread;
|
||||||
use assistant_context_editor::language_model_selector::{
|
use assistant_context_editor::language_model_selector::{
|
||||||
LanguageModelSelector, ToggleModelSelector, language_model_selector,
|
LanguageModelSelector, ToggleModelSelector, language_model_selector,
|
||||||
};
|
};
|
||||||
@@ -12,6 +12,12 @@ use settings::update_settings_file;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use ui::{PopoverMenuHandle, Tooltip, prelude::*};
|
use ui::{PopoverMenuHandle, Tooltip, prelude::*};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum ModelType {
|
||||||
|
Default(Entity<Thread>),
|
||||||
|
InlineAssistant,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct AgentModelSelector {
|
pub struct AgentModelSelector {
|
||||||
selector: Entity<LanguageModelSelector>,
|
selector: Entity<LanguageModelSelector>,
|
||||||
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
|
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
|
||||||
@@ -23,7 +29,7 @@ impl AgentModelSelector {
|
|||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
|
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
model_usage_context: ModelUsageContext,
|
model_type: ModelType,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
@@ -32,14 +38,19 @@ impl AgentModelSelector {
|
|||||||
let fs = fs.clone();
|
let fs = fs.clone();
|
||||||
language_model_selector(
|
language_model_selector(
|
||||||
{
|
{
|
||||||
let model_context = model_usage_context.clone();
|
let model_type = model_type.clone();
|
||||||
move |cx| model_context.configured_model(cx)
|
move |cx| match &model_type {
|
||||||
|
ModelType::Default(thread) => thread.read(cx).configured_model(),
|
||||||
|
ModelType::InlineAssistant => {
|
||||||
|
LanguageModelRegistry::read_global(cx).inline_assistant_model()
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
move |model, cx| {
|
move |model, cx| {
|
||||||
let provider = model.provider_id().0.to_string();
|
let provider = model.provider_id().0.to_string();
|
||||||
let model_id = model.id().0.to_string();
|
let model_id = model.id().0.to_string();
|
||||||
match &model_usage_context {
|
match &model_type {
|
||||||
ModelUsageContext::Thread(thread) => {
|
ModelType::Default(thread) => {
|
||||||
thread.update(cx, |thread, cx| {
|
thread.update(cx, |thread, cx| {
|
||||||
let registry = LanguageModelRegistry::read_global(cx);
|
let registry = LanguageModelRegistry::read_global(cx);
|
||||||
if let Some(provider) = registry.provider(&model.provider_id())
|
if let Some(provider) = registry.provider(&model.provider_id())
|
||||||
@@ -61,7 +72,7 @@ impl AgentModelSelector {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
ModelUsageContext::InlineAssistant => {
|
ModelType::InlineAssistant => {
|
||||||
update_settings_file::<AgentSettings>(
|
update_settings_file::<AgentSettings>(
|
||||||
fs.clone(),
|
fs.clone(),
|
||||||
cx,
|
cx,
|
||||||
|
|||||||
@@ -1,334 +0,0 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use agent_settings::{AgentProfileId, AgentProfileSettings, AgentSettings};
|
|
||||||
use assistant_tool::{Tool, ToolSource, ToolWorkingSet};
|
|
||||||
use collections::IndexMap;
|
|
||||||
use convert_case::{Case, Casing};
|
|
||||||
use fs::Fs;
|
|
||||||
use gpui::{App, Entity};
|
|
||||||
use settings::{Settings, update_settings_file};
|
|
||||||
use ui::SharedString;
|
|
||||||
use util::ResultExt;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
||||||
pub struct AgentProfile {
|
|
||||||
id: AgentProfileId,
|
|
||||||
tool_set: Entity<ToolWorkingSet>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type AvailableProfiles = IndexMap<AgentProfileId, SharedString>;
|
|
||||||
|
|
||||||
impl AgentProfile {
|
|
||||||
pub fn new(id: AgentProfileId, tool_set: Entity<ToolWorkingSet>) -> Self {
|
|
||||||
Self { id, tool_set }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Saves a new profile to the settings.
|
|
||||||
pub fn create(
|
|
||||||
name: String,
|
|
||||||
base_profile_id: Option<AgentProfileId>,
|
|
||||||
fs: Arc<dyn Fs>,
|
|
||||||
cx: &App,
|
|
||||||
) -> AgentProfileId {
|
|
||||||
let id = AgentProfileId(name.to_case(Case::Kebab).into());
|
|
||||||
|
|
||||||
let base_profile =
|
|
||||||
base_profile_id.and_then(|id| AgentSettings::get_global(cx).profiles.get(&id).cloned());
|
|
||||||
|
|
||||||
let profile_settings = AgentProfileSettings {
|
|
||||||
name: name.into(),
|
|
||||||
tools: base_profile
|
|
||||||
.as_ref()
|
|
||||||
.map(|profile| profile.tools.clone())
|
|
||||||
.unwrap_or_default(),
|
|
||||||
enable_all_context_servers: base_profile
|
|
||||||
.as_ref()
|
|
||||||
.map(|profile| profile.enable_all_context_servers)
|
|
||||||
.unwrap_or_default(),
|
|
||||||
context_servers: base_profile
|
|
||||||
.map(|profile| profile.context_servers)
|
|
||||||
.unwrap_or_default(),
|
|
||||||
};
|
|
||||||
|
|
||||||
update_settings_file::<AgentSettings>(fs, cx, {
|
|
||||||
let id = id.clone();
|
|
||||||
move |settings, _cx| {
|
|
||||||
settings.create_profile(id, profile_settings).log_err();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
id
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a map of AgentProfileIds to their names
|
|
||||||
pub fn available_profiles(cx: &App) -> AvailableProfiles {
|
|
||||||
let mut profiles = AvailableProfiles::default();
|
|
||||||
for (id, profile) in AgentSettings::get_global(cx).profiles.iter() {
|
|
||||||
profiles.insert(id.clone(), profile.name.clone());
|
|
||||||
}
|
|
||||||
profiles
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn id(&self) -> &AgentProfileId {
|
|
||||||
&self.id
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn enabled_tools(&self, cx: &App) -> Vec<Arc<dyn Tool>> {
|
|
||||||
let Some(settings) = AgentSettings::get_global(cx).profiles.get(&self.id) else {
|
|
||||||
return Vec::new();
|
|
||||||
};
|
|
||||||
|
|
||||||
self.tool_set
|
|
||||||
.read(cx)
|
|
||||||
.tools(cx)
|
|
||||||
.into_iter()
|
|
||||||
.filter(|tool| Self::is_enabled(settings, tool.source(), tool.name()))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_enabled(settings: &AgentProfileSettings, source: ToolSource, name: String) -> bool {
|
|
||||||
match source {
|
|
||||||
ToolSource::Native => *settings.tools.get(name.as_str()).unwrap_or(&false),
|
|
||||||
ToolSource::ContextServer { id } => {
|
|
||||||
if settings.enable_all_context_servers {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some(preset) = settings.context_servers.get(id.as_ref()) else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
*preset.tools.get(name.as_str()).unwrap_or(&false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use agent_settings::ContextServerPreset;
|
|
||||||
use assistant_tool::ToolRegistry;
|
|
||||||
use collections::IndexMap;
|
|
||||||
use gpui::{AppContext, TestAppContext};
|
|
||||||
use http_client::FakeHttpClient;
|
|
||||||
use project::Project;
|
|
||||||
use settings::{Settings, SettingsStore};
|
|
||||||
use ui::SharedString;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_enabled_built_in_tools_for_profile(cx: &mut TestAppContext) {
|
|
||||||
init_test_settings(cx);
|
|
||||||
|
|
||||||
let id = AgentProfileId::default();
|
|
||||||
let profile_settings = cx.read(|cx| {
|
|
||||||
AgentSettings::get_global(cx)
|
|
||||||
.profiles
|
|
||||||
.get(&id)
|
|
||||||
.unwrap()
|
|
||||||
.clone()
|
|
||||||
});
|
|
||||||
let tool_set = default_tool_set(cx);
|
|
||||||
|
|
||||||
let profile = AgentProfile::new(id.clone(), tool_set);
|
|
||||||
|
|
||||||
let mut enabled_tools = cx
|
|
||||||
.read(|cx| profile.enabled_tools(cx))
|
|
||||||
.into_iter()
|
|
||||||
.map(|tool| tool.name())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
enabled_tools.sort();
|
|
||||||
|
|
||||||
let mut expected_tools = profile_settings
|
|
||||||
.tools
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|(tool, enabled)| enabled.then_some(tool.to_string()))
|
|
||||||
// Provider dependent
|
|
||||||
.filter(|tool| tool != "web_search")
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
// Plus all registered MCP tools
|
|
||||||
expected_tools.extend(["enabled_mcp_tool".into(), "disabled_mcp_tool".into()]);
|
|
||||||
expected_tools.sort();
|
|
||||||
|
|
||||||
assert_eq!(enabled_tools, expected_tools);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_custom_mcp_settings(cx: &mut TestAppContext) {
|
|
||||||
init_test_settings(cx);
|
|
||||||
|
|
||||||
let id = AgentProfileId("custom_mcp".into());
|
|
||||||
let profile_settings = cx.read(|cx| {
|
|
||||||
AgentSettings::get_global(cx)
|
|
||||||
.profiles
|
|
||||||
.get(&id)
|
|
||||||
.unwrap()
|
|
||||||
.clone()
|
|
||||||
});
|
|
||||||
let tool_set = default_tool_set(cx);
|
|
||||||
|
|
||||||
let profile = AgentProfile::new(id.clone(), tool_set);
|
|
||||||
|
|
||||||
let mut enabled_tools = cx
|
|
||||||
.read(|cx| profile.enabled_tools(cx))
|
|
||||||
.into_iter()
|
|
||||||
.map(|tool| tool.name())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
enabled_tools.sort();
|
|
||||||
|
|
||||||
let mut expected_tools = profile_settings.context_servers["mcp"]
|
|
||||||
.tools
|
|
||||||
.iter()
|
|
||||||
.filter_map(|(key, enabled)| enabled.then(|| key.to_string()))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
expected_tools.sort();
|
|
||||||
|
|
||||||
assert_eq!(enabled_tools, expected_tools);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_only_built_in(cx: &mut TestAppContext) {
|
|
||||||
init_test_settings(cx);
|
|
||||||
|
|
||||||
let id = AgentProfileId("write_minus_mcp".into());
|
|
||||||
let profile_settings = cx.read(|cx| {
|
|
||||||
AgentSettings::get_global(cx)
|
|
||||||
.profiles
|
|
||||||
.get(&id)
|
|
||||||
.unwrap()
|
|
||||||
.clone()
|
|
||||||
});
|
|
||||||
let tool_set = default_tool_set(cx);
|
|
||||||
|
|
||||||
let profile = AgentProfile::new(id.clone(), tool_set);
|
|
||||||
|
|
||||||
let mut enabled_tools = cx
|
|
||||||
.read(|cx| profile.enabled_tools(cx))
|
|
||||||
.into_iter()
|
|
||||||
.map(|tool| tool.name())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
enabled_tools.sort();
|
|
||||||
|
|
||||||
let mut expected_tools = profile_settings
|
|
||||||
.tools
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|(tool, enabled)| enabled.then_some(tool.to_string()))
|
|
||||||
// Provider dependent
|
|
||||||
.filter(|tool| tool != "web_search")
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
expected_tools.sort();
|
|
||||||
|
|
||||||
assert_eq!(enabled_tools, expected_tools);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn init_test_settings(cx: &mut TestAppContext) {
|
|
||||||
cx.update(|cx| {
|
|
||||||
let settings_store = SettingsStore::test(cx);
|
|
||||||
cx.set_global(settings_store);
|
|
||||||
Project::init_settings(cx);
|
|
||||||
AgentSettings::register(cx);
|
|
||||||
language_model::init_settings(cx);
|
|
||||||
ToolRegistry::default_global(cx);
|
|
||||||
assistant_tools::init(FakeHttpClient::with_404_response(), cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.update(|cx| {
|
|
||||||
let mut agent_settings = AgentSettings::get_global(cx).clone();
|
|
||||||
agent_settings.profiles.insert(
|
|
||||||
AgentProfileId("write_minus_mcp".into()),
|
|
||||||
AgentProfileSettings {
|
|
||||||
name: "write_minus_mcp".into(),
|
|
||||||
enable_all_context_servers: false,
|
|
||||||
..agent_settings.profiles[&AgentProfileId::default()].clone()
|
|
||||||
},
|
|
||||||
);
|
|
||||||
agent_settings.profiles.insert(
|
|
||||||
AgentProfileId("custom_mcp".into()),
|
|
||||||
AgentProfileSettings {
|
|
||||||
name: "mcp".into(),
|
|
||||||
tools: IndexMap::default(),
|
|
||||||
enable_all_context_servers: false,
|
|
||||||
context_servers: IndexMap::from_iter([("mcp".into(), context_server_preset())]),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
AgentSettings::override_global(agent_settings, cx);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn context_server_preset() -> ContextServerPreset {
|
|
||||||
ContextServerPreset {
|
|
||||||
tools: IndexMap::from_iter([
|
|
||||||
("enabled_mcp_tool".into(), true),
|
|
||||||
("disabled_mcp_tool".into(), false),
|
|
||||||
]),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_tool_set(cx: &mut TestAppContext) -> Entity<ToolWorkingSet> {
|
|
||||||
cx.new(|_| {
|
|
||||||
let mut tool_set = ToolWorkingSet::default();
|
|
||||||
tool_set.insert(Arc::new(FakeTool::new("enabled_mcp_tool", "mcp")));
|
|
||||||
tool_set.insert(Arc::new(FakeTool::new("disabled_mcp_tool", "mcp")));
|
|
||||||
tool_set
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
struct FakeTool {
|
|
||||||
name: String,
|
|
||||||
source: SharedString,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FakeTool {
|
|
||||||
fn new(name: impl Into<String>, source: impl Into<SharedString>) -> Self {
|
|
||||||
Self {
|
|
||||||
name: name.into(),
|
|
||||||
source: source.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Tool for FakeTool {
|
|
||||||
fn name(&self) -> String {
|
|
||||||
self.name.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn source(&self) -> ToolSource {
|
|
||||||
ToolSource::ContextServer {
|
|
||||||
id: self.source.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn description(&self) -> String {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn icon(&self) -> ui::IconName {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn needs_confirmation(&self, _input: &serde_json::Value, _cx: &App) -> bool {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ui_text(&self, _input: &serde_json::Value) -> String {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run(
|
|
||||||
self: Arc<Self>,
|
|
||||||
_input: serde_json::Value,
|
|
||||||
_request: Arc<language_model::LanguageModelRequest>,
|
|
||||||
_project: Entity<Project>,
|
|
||||||
_action_log: Entity<assistant_tool::ActionLog>,
|
|
||||||
_model: Arc<dyn language_model::LanguageModel>,
|
|
||||||
_window: Option<gpui::AnyWindowHandle>,
|
|
||||||
_cx: &mut App,
|
|
||||||
) -> assistant_tool::ToolResult {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn may_perform_edits(&self) -> bool {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -745,7 +745,6 @@ pub struct ImageContext {
|
|||||||
pub enum ImageStatus {
|
pub enum ImageStatus {
|
||||||
Loading,
|
Loading,
|
||||||
Error,
|
Error,
|
||||||
Warning,
|
|
||||||
Ready,
|
Ready,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -762,17 +761,11 @@ impl ImageContext {
|
|||||||
self.image_task.clone().now_or_never().flatten()
|
self.image_task.clone().now_or_never().flatten()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn status(&self, model: Option<&Arc<dyn language_model::LanguageModel>>) -> ImageStatus {
|
pub fn status(&self) -> ImageStatus {
|
||||||
match self.image_task.clone().now_or_never() {
|
match self.image_task.clone().now_or_never() {
|
||||||
None => ImageStatus::Loading,
|
None => ImageStatus::Loading,
|
||||||
Some(None) => ImageStatus::Error,
|
Some(None) => ImageStatus::Error,
|
||||||
Some(Some(_)) => {
|
Some(Some(_)) => ImageStatus::Ready,
|
||||||
if model.is_some_and(|model| !model.supports_images()) {
|
|
||||||
ImageStatus::Warning
|
|
||||||
} else {
|
|
||||||
ImageStatus::Ready
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
use std::cell::RefCell;
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::rc::Rc;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::AtomicBool;
|
use std::sync::atomic::AtomicBool;
|
||||||
|
|
||||||
@@ -765,7 +767,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
|||||||
|
|
||||||
let snapshot = buffer.read(cx).snapshot();
|
let snapshot = buffer.read(cx).snapshot();
|
||||||
let source_range = snapshot.anchor_before(state.source_range.start)
|
let source_range = snapshot.anchor_before(state.source_range.start)
|
||||||
..snapshot.anchor_after(state.source_range.end);
|
..snapshot.anchor_before(state.source_range.end);
|
||||||
|
|
||||||
let thread_store = self.thread_store.clone();
|
let thread_store = self.thread_store.clone();
|
||||||
let text_thread_store = self.text_thread_store.clone();
|
let text_thread_store = self.text_thread_store.clone();
|
||||||
@@ -910,13 +912,22 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn resolve_completions(
|
||||||
|
&self,
|
||||||
|
_buffer: Entity<Buffer>,
|
||||||
|
_completion_indices: Vec<usize>,
|
||||||
|
_completions: Rc<RefCell<Box<[Completion]>>>,
|
||||||
|
_cx: &mut Context<Editor>,
|
||||||
|
) -> Task<Result<bool>> {
|
||||||
|
Task::ready(Ok(true))
|
||||||
|
}
|
||||||
|
|
||||||
fn is_completion_trigger(
|
fn is_completion_trigger(
|
||||||
&self,
|
&self,
|
||||||
buffer: &Entity<language::Buffer>,
|
buffer: &Entity<language::Buffer>,
|
||||||
position: language::Anchor,
|
position: language::Anchor,
|
||||||
_text: &str,
|
_: &str,
|
||||||
_trigger_in_words: bool,
|
_: bool,
|
||||||
_menu_is_open: bool,
|
|
||||||
cx: &mut Context<Editor>,
|
cx: &mut Context<Editor>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let buffer = buffer.read(cx);
|
let buffer = buffer.read(cx);
|
||||||
@@ -1065,7 +1076,7 @@ mod tests {
|
|||||||
use project::{Project, ProjectPath};
|
use project::{Project, ProjectPath};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
use std::{ops::Deref, rc::Rc};
|
use std::ops::Deref;
|
||||||
use util::{path, separator};
|
use util::{path, separator};
|
||||||
use workspace::{AppState, Item};
|
use workspace::{AppState, Item};
|
||||||
|
|
||||||
|
|||||||
@@ -51,10 +51,6 @@ impl Tool for ContextServerTool {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn may_perform_edits(&self) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
||||||
let mut schema = self.tool.input_schema.clone();
|
let mut schema = self.tool.input_schema.clone();
|
||||||
assistant_tool::adapt_schema_to_format(&mut schema, format)?;
|
assistant_tool::adapt_schema_to_format(&mut schema, format)?;
|
||||||
@@ -104,15 +100,7 @@ impl Tool for ContextServerTool {
|
|||||||
tool_name,
|
tool_name,
|
||||||
arguments
|
arguments
|
||||||
);
|
);
|
||||||
let response = protocol
|
let response = protocol.run_tool(tool_name, arguments).await?;
|
||||||
.request::<context_server::types::request::CallTool>(
|
|
||||||
context_server::types::CallToolParams {
|
|
||||||
name: tool_name,
|
|
||||||
arguments,
|
|
||||||
meta: None,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let mut result = String::new();
|
let mut result = String::new();
|
||||||
for content in response.content {
|
for content in response.content {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ use crate::thread_store::{TextThreadStore, ThreadStore};
|
|||||||
use crate::ui::{AddedContext, ContextPill};
|
use crate::ui::{AddedContext, ContextPill};
|
||||||
use crate::{
|
use crate::{
|
||||||
AcceptSuggestedContext, AgentPanel, FocusDown, FocusLeft, FocusRight, FocusUp,
|
AcceptSuggestedContext, AgentPanel, FocusDown, FocusLeft, FocusRight, FocusUp,
|
||||||
ModelUsageContext, RemoveAllContext, RemoveFocusedContext, ToggleContextPicker,
|
RemoveAllContext, RemoveFocusedContext, ToggleContextPicker,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct ContextStrip {
|
pub struct ContextStrip {
|
||||||
@@ -37,7 +37,6 @@ pub struct ContextStrip {
|
|||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
focused_index: Option<usize>,
|
focused_index: Option<usize>,
|
||||||
children_bounds: Option<Vec<Bounds<Pixels>>>,
|
children_bounds: Option<Vec<Bounds<Pixels>>>,
|
||||||
model_usage_context: ModelUsageContext,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ContextStrip {
|
impl ContextStrip {
|
||||||
@@ -48,7 +47,6 @@ impl ContextStrip {
|
|||||||
text_thread_store: Option<WeakEntity<TextThreadStore>>,
|
text_thread_store: Option<WeakEntity<TextThreadStore>>,
|
||||||
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||||
suggest_context_kind: SuggestContextKind,
|
suggest_context_kind: SuggestContextKind,
|
||||||
model_usage_context: ModelUsageContext,
|
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
@@ -83,7 +81,6 @@ impl ContextStrip {
|
|||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
focused_index: None,
|
focused_index: None,
|
||||||
children_bounds: None,
|
children_bounds: None,
|
||||||
model_usage_context,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,20 +98,11 @@ impl ContextStrip {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|thread_store| thread_store.upgrade())
|
.and_then(|thread_store| thread_store.upgrade())
|
||||||
.and_then(|thread_store| thread_store.read(cx).prompt_store().as_ref());
|
.and_then(|thread_store| thread_store.read(cx).prompt_store().as_ref());
|
||||||
|
|
||||||
let current_model = self.model_usage_context.language_model(cx);
|
|
||||||
|
|
||||||
self.context_store
|
self.context_store
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.context()
|
.context()
|
||||||
.flat_map(|context| {
|
.flat_map(|context| {
|
||||||
AddedContext::new_pending(
|
AddedContext::new_pending(context.clone(), prompt_store, project, cx)
|
||||||
context.clone(),
|
|
||||||
prompt_store,
|
|
||||||
project,
|
|
||||||
current_model.as_ref(),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1011,7 +1011,7 @@ impl InlineAssistant {
|
|||||||
self.update_editor_highlights(&editor, cx);
|
self.update_editor_highlights(&editor, cx);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
entry.get_mut().highlight_updates.send(()).ok();
|
entry.get().highlight_updates.send(()).ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1519,7 +1519,7 @@ impl InlineAssistant {
|
|||||||
struct EditorInlineAssists {
|
struct EditorInlineAssists {
|
||||||
assist_ids: Vec<InlineAssistId>,
|
assist_ids: Vec<InlineAssistId>,
|
||||||
scroll_lock: Option<InlineAssistScrollLock>,
|
scroll_lock: Option<InlineAssistScrollLock>,
|
||||||
highlight_updates: watch::Sender<()>,
|
highlight_updates: async_watch::Sender<()>,
|
||||||
_update_highlights: Task<Result<()>>,
|
_update_highlights: Task<Result<()>>,
|
||||||
_subscriptions: Vec<gpui::Subscription>,
|
_subscriptions: Vec<gpui::Subscription>,
|
||||||
}
|
}
|
||||||
@@ -1531,7 +1531,7 @@ struct InlineAssistScrollLock {
|
|||||||
|
|
||||||
impl EditorInlineAssists {
|
impl EditorInlineAssists {
|
||||||
fn new(editor: &Entity<Editor>, window: &mut Window, cx: &mut App) -> Self {
|
fn new(editor: &Entity<Editor>, window: &mut Window, cx: &mut App) -> Self {
|
||||||
let (highlight_updates_tx, mut highlight_updates_rx) = watch::channel(());
|
let (highlight_updates_tx, mut highlight_updates_rx) = async_watch::channel(());
|
||||||
Self {
|
Self {
|
||||||
assist_ids: Vec::new(),
|
assist_ids: Vec::new(),
|
||||||
scroll_lock: None,
|
scroll_lock: None,
|
||||||
@@ -1689,7 +1689,7 @@ impl InlineAssist {
|
|||||||
if let Some(editor) = editor.upgrade() {
|
if let Some(editor) = editor.upgrade() {
|
||||||
InlineAssistant::update_global(cx, |this, cx| {
|
InlineAssistant::update_global(cx, |this, cx| {
|
||||||
if let Some(editor_assists) =
|
if let Some(editor_assists) =
|
||||||
this.assists_by_editor.get_mut(&editor.downgrade())
|
this.assists_by_editor.get(&editor.downgrade())
|
||||||
{
|
{
|
||||||
editor_assists.highlight_updates.send(()).ok();
|
editor_assists.highlight_updates.send(()).ok();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::agent_model_selector::AgentModelSelector;
|
use crate::agent_model_selector::{AgentModelSelector, ModelType};
|
||||||
use crate::buffer_codegen::BufferCodegen;
|
use crate::buffer_codegen::BufferCodegen;
|
||||||
use crate::context::ContextCreasesAddon;
|
use crate::context::ContextCreasesAddon;
|
||||||
use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider};
|
use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider};
|
||||||
@@ -7,13 +7,12 @@ use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
|
|||||||
use crate::message_editor::{extract_message_creases, insert_message_creases};
|
use crate::message_editor::{extract_message_creases, insert_message_creases};
|
||||||
use crate::terminal_codegen::TerminalCodegen;
|
use crate::terminal_codegen::TerminalCodegen;
|
||||||
use crate::thread_store::{TextThreadStore, ThreadStore};
|
use crate::thread_store::{TextThreadStore, ThreadStore};
|
||||||
use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext};
|
use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist};
|
||||||
use crate::{RemoveAllContext, ToggleContextPicker};
|
use crate::{RemoveAllContext, ToggleContextPicker};
|
||||||
use assistant_context_editor::language_model_selector::ToggleModelSelector;
|
use assistant_context_editor::language_model_selector::ToggleModelSelector;
|
||||||
use client::ErrorExt;
|
use client::ErrorExt;
|
||||||
use collections::VecDeque;
|
use collections::VecDeque;
|
||||||
use db::kvp::Dismissable;
|
use db::kvp::Dismissable;
|
||||||
use editor::actions::Paste;
|
|
||||||
use editor::display_map::EditorMargins;
|
use editor::display_map::EditorMargins;
|
||||||
use editor::{
|
use editor::{
|
||||||
ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
|
ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
|
||||||
@@ -100,7 +99,6 @@ impl<T: 'static> Render for PromptEditor<T> {
|
|||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.key_context("PromptEditor")
|
.key_context("PromptEditor")
|
||||||
.capture_action(cx.listener(Self::paste))
|
|
||||||
.bg(cx.theme().colors().editor_background)
|
.bg(cx.theme().colors().editor_background)
|
||||||
.block_mouse_except_scroll()
|
.block_mouse_except_scroll()
|
||||||
.gap_0p5()
|
.gap_0p5()
|
||||||
@@ -305,10 +303,6 @@ 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>) {
|
|
||||||
crate::active_thread::attach_pasted_images_as_context(&self.context_store, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn toggle_rate_limit_notice(
|
fn toggle_rate_limit_notice(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: &ClickEvent,
|
_: &ClickEvent,
|
||||||
@@ -918,7 +912,6 @@ impl PromptEditor<BufferCodegen> {
|
|||||||
text_thread_store.clone(),
|
text_thread_store.clone(),
|
||||||
context_picker_menu_handle.clone(),
|
context_picker_menu_handle.clone(),
|
||||||
SuggestContextKind::Thread,
|
SuggestContextKind::Thread,
|
||||||
ModelUsageContext::InlineAssistant,
|
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
@@ -937,7 +930,7 @@ impl PromptEditor<BufferCodegen> {
|
|||||||
fs,
|
fs,
|
||||||
model_selector_menu_handle,
|
model_selector_menu_handle,
|
||||||
prompt_editor.focus_handle(cx),
|
prompt_editor.focus_handle(cx),
|
||||||
ModelUsageContext::InlineAssistant,
|
ModelType::InlineAssistant,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
@@ -1090,7 +1083,6 @@ impl PromptEditor<TerminalCodegen> {
|
|||||||
text_thread_store.clone(),
|
text_thread_store.clone(),
|
||||||
context_picker_menu_handle.clone(),
|
context_picker_menu_handle.clone(),
|
||||||
SuggestContextKind::Thread,
|
SuggestContextKind::Thread,
|
||||||
ModelUsageContext::InlineAssistant,
|
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
@@ -1109,7 +1101,7 @@ impl PromptEditor<TerminalCodegen> {
|
|||||||
fs,
|
fs,
|
||||||
model_selector_menu_handle.clone(),
|
model_selector_menu_handle.clone(),
|
||||||
prompt_editor.focus_handle(cx),
|
prompt_editor.focus_handle(cx),
|
||||||
ModelUsageContext::InlineAssistant,
|
ModelType::InlineAssistant,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ use std::collections::BTreeMap;
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::agent_model_selector::AgentModelSelector;
|
use crate::agent_model_selector::{AgentModelSelector, ModelType};
|
||||||
use crate::context::{AgentContextKey, ContextCreasesAddon, ContextLoadResult, load_context};
|
use crate::context::{AgentContextKey, ContextCreasesAddon, ContextLoadResult, load_context};
|
||||||
use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip};
|
use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip};
|
||||||
use crate::ui::{
|
use crate::ui::{
|
||||||
MaxModeTooltip,
|
AnimatedLabel, MaxModeTooltip,
|
||||||
preview::{AgentPreview, UsageCallout},
|
preview::{AgentPreview, UsageCallout},
|
||||||
};
|
};
|
||||||
use agent_settings::{AgentSettings, CompletionMode};
|
use agent_settings::{AgentSettings, CompletionMode};
|
||||||
@@ -24,10 +24,10 @@ use fs::Fs;
|
|||||||
use futures::future::Shared;
|
use futures::future::Shared;
|
||||||
use futures::{FutureExt as _, future};
|
use futures::{FutureExt as _, future};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Animation, AnimationExt, App, Entity, EventEmitter, Focusable, Subscription, Task, TextStyle,
|
Animation, AnimationExt, App, ClipboardEntry, Entity, EventEmitter, Focusable, Subscription,
|
||||||
WeakEntity, linear_color_stop, linear_gradient, point, pulsating_between,
|
Task, TextStyle, WeakEntity, linear_color_stop, linear_gradient, point, pulsating_between,
|
||||||
};
|
};
|
||||||
use language::{Buffer, Language, Point};
|
use language::{Buffer, Language};
|
||||||
use language_model::{
|
use language_model::{
|
||||||
ConfiguredModel, LanguageModelRequestMessage, MessageContent, RequestUsage,
|
ConfiguredModel, LanguageModelRequestMessage, MessageContent, RequestUsage,
|
||||||
ZED_CLOUD_PROVIDER_ID,
|
ZED_CLOUD_PROVIDER_ID,
|
||||||
@@ -51,9 +51,9 @@ use crate::profile_selector::ProfileSelector;
|
|||||||
use crate::thread::{MessageCrease, Thread, TokenUsageRatio};
|
use crate::thread::{MessageCrease, Thread, TokenUsageRatio};
|
||||||
use crate::thread_store::{TextThreadStore, ThreadStore};
|
use crate::thread_store::{TextThreadStore, ThreadStore};
|
||||||
use crate::{
|
use crate::{
|
||||||
ActiveThread, AgentDiffPane, Chat, ChatWithFollow, ExpandMessageEditor, Follow, KeepAll,
|
ActiveThread, AgentDiffPane, Chat, ChatWithFollow, ExpandMessageEditor, Follow, NewThread,
|
||||||
ModelUsageContext, NewThread, OpenAgentDiff, RejectAll, RemoveAllContext, ToggleBurnMode,
|
OpenAgentDiff, RemoveAllContext, ToggleBurnMode, ToggleContextPicker, ToggleProfileSelector,
|
||||||
ToggleContextPicker, ToggleProfileSelector, register_agent_preview,
|
register_agent_preview,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(RegisterComponent)]
|
#[derive(RegisterComponent)]
|
||||||
@@ -169,13 +169,13 @@ impl MessageEditor {
|
|||||||
Some(text_thread_store.clone()),
|
Some(text_thread_store.clone()),
|
||||||
context_picker_menu_handle.clone(),
|
context_picker_menu_handle.clone(),
|
||||||
SuggestContextKind::File,
|
SuggestContextKind::File,
|
||||||
ModelUsageContext::Thread(thread.clone()),
|
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
let incompatible_tools = cx.new(|cx| IncompatibleToolsState::new(thread.clone(), cx));
|
let incompatible_tools =
|
||||||
|
cx.new(|cx| IncompatibleToolsState::new(thread.read(cx).tools().clone(), cx));
|
||||||
|
|
||||||
let subscriptions = vec![
|
let subscriptions = vec![
|
||||||
cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event),
|
cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event),
|
||||||
@@ -197,14 +197,21 @@ impl MessageEditor {
|
|||||||
fs.clone(),
|
fs.clone(),
|
||||||
model_selector_menu_handle,
|
model_selector_menu_handle,
|
||||||
editor.focus_handle(cx),
|
editor.focus_handle(cx),
|
||||||
ModelUsageContext::Thread(thread.clone()),
|
ModelType::Default(thread.clone()),
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
let profile_selector =
|
let profile_selector = cx.new(|cx| {
|
||||||
cx.new(|cx| ProfileSelector::new(fs, thread.clone(), editor.focus_handle(cx), cx));
|
ProfileSelector::new(
|
||||||
|
fs,
|
||||||
|
thread.clone(),
|
||||||
|
thread_store,
|
||||||
|
editor.focus_handle(cx),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
editor: editor.clone(),
|
editor: editor.clone(),
|
||||||
@@ -424,24 +431,39 @@ impl MessageEditor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn paste(&mut self, _: &Paste, _: &mut Window, cx: &mut Context<Self>) {
|
fn paste(&mut self, _: &Paste, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
crate::active_thread::attach_pasted_images_as_context(&self.context_store, cx);
|
let images = cx
|
||||||
|
.read_from_clipboard()
|
||||||
|
.map(|item| {
|
||||||
|
item.into_entries()
|
||||||
|
.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_review_click(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn handle_review_click(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
if self.thread.read(cx).has_pending_edit_tool_uses() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.edits_expanded = true;
|
self.edits_expanded = true;
|
||||||
AgentDiffPane::deploy(self.thread.clone(), self.workspace.clone(), window, cx).log_err();
|
AgentDiffPane::deploy(self.thread.clone(), self.workspace.clone(), window, cx).log_err();
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_edit_bar_expand(&mut self, cx: &mut Context<Self>) {
|
|
||||||
self.edits_expanded = !self.edits_expanded;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_file_click(
|
fn handle_file_click(
|
||||||
&self,
|
&self,
|
||||||
buffer: Entity<Buffer>,
|
buffer: Entity<Buffer>,
|
||||||
@@ -472,40 +494,6 @@ impl MessageEditor {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_accept_all(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
if self.thread.read(cx).has_pending_edit_tool_uses() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.thread.update(cx, |thread, cx| {
|
|
||||||
thread.keep_all_edits(cx);
|
|
||||||
});
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_reject_all(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
if self.thread.read(cx).has_pending_edit_tool_uses() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Since there's no reject_all_edits method in the thread API,
|
|
||||||
// we need to iterate through all buffers and reject their edits
|
|
||||||
let action_log = self.thread.read(cx).action_log().clone();
|
|
||||||
let changed_buffers = action_log.read(cx).changed_buffers(cx);
|
|
||||||
|
|
||||||
for (buffer, _) in changed_buffers {
|
|
||||||
self.thread.update(cx, |thread, cx| {
|
|
||||||
let buffer_snapshot = buffer.read(cx);
|
|
||||||
let start = buffer_snapshot.anchor_before(Point::new(0, 0));
|
|
||||||
let end = buffer_snapshot.anchor_after(buffer_snapshot.max_point());
|
|
||||||
thread
|
|
||||||
.reject_edits_in_ranges(buffer, vec![start..end], cx)
|
|
||||||
.detach();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_max_mode_toggle(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
fn render_max_mode_toggle(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
||||||
let thread = self.thread.read(cx);
|
let thread = self.thread.read(cx);
|
||||||
let model = thread.configured_model();
|
let model = thread.configured_model();
|
||||||
@@ -627,12 +615,6 @@ impl MessageEditor {
|
|||||||
.on_action(cx.listener(Self::move_up))
|
.on_action(cx.listener(Self::move_up))
|
||||||
.on_action(cx.listener(Self::expand_message_editor))
|
.on_action(cx.listener(Self::expand_message_editor))
|
||||||
.on_action(cx.listener(Self::toggle_burn_mode))
|
.on_action(cx.listener(Self::toggle_burn_mode))
|
||||||
.on_action(
|
|
||||||
cx.listener(|this, _: &KeepAll, window, cx| this.handle_accept_all(window, cx)),
|
|
||||||
)
|
|
||||||
.on_action(
|
|
||||||
cx.listener(|this, _: &RejectAll, window, cx| this.handle_reject_all(window, cx)),
|
|
||||||
)
|
|
||||||
.capture_action(cx.listener(Self::paste))
|
.capture_action(cx.listener(Self::paste))
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.p_2()
|
.p_2()
|
||||||
@@ -888,10 +870,7 @@ impl MessageEditor {
|
|||||||
let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
|
let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
|
||||||
|
|
||||||
let is_edit_changes_expanded = self.edits_expanded;
|
let is_edit_changes_expanded = self.edits_expanded;
|
||||||
let thread = self.thread.read(cx);
|
let is_generating = self.thread.read(cx).is_generating();
|
||||||
let pending_edits = thread.has_pending_edit_tool_uses();
|
|
||||||
|
|
||||||
const EDIT_NOT_READY_TOOLTIP_LABEL: &str = "Wait until file edits are complete.";
|
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.mt_1()
|
.mt_1()
|
||||||
@@ -909,28 +888,31 @@ impl MessageEditor {
|
|||||||
}])
|
}])
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.p_1()
|
.id("edits-container")
|
||||||
|
.cursor_pointer()
|
||||||
|
.p_1p5()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.when(is_edit_changes_expanded, |this| {
|
.when(is_edit_changes_expanded, |this| {
|
||||||
this.border_b_1().border_color(border_color)
|
this.border_b_1().border_color(border_color)
|
||||||
})
|
})
|
||||||
|
.on_click(
|
||||||
|
cx.listener(|this, _, window, cx| this.handle_review_click(window, cx)),
|
||||||
|
)
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.id("edits-container")
|
|
||||||
.cursor_pointer()
|
|
||||||
.w_full()
|
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(
|
.child(
|
||||||
Disclosure::new("edits-disclosure", is_edit_changes_expanded)
|
Disclosure::new("edits-disclosure", is_edit_changes_expanded)
|
||||||
.on_click(cx.listener(|this, _, _, cx| {
|
.on_click(cx.listener(|this, _ev, _window, cx| {
|
||||||
this.handle_edit_bar_expand(cx)
|
this.edits_expanded = !this.edits_expanded;
|
||||||
|
cx.notify();
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.map(|this| {
|
.map(|this| {
|
||||||
if pending_edits {
|
if is_generating {
|
||||||
this.child(
|
this.child(
|
||||||
Label::new(format!(
|
AnimatedLabel::new(format!(
|
||||||
"Editing {} {}…",
|
"Editing {} {}",
|
||||||
changed_buffers.len(),
|
changed_buffers.len(),
|
||||||
if changed_buffers.len() == 1 {
|
if changed_buffers.len() == 1 {
|
||||||
"file"
|
"file"
|
||||||
@@ -938,15 +920,7 @@ impl MessageEditor {
|
|||||||
"files"
|
"files"
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
.color(Color::Muted)
|
.size(LabelSize::Small),
|
||||||
.size(LabelSize::Small)
|
|
||||||
.with_animation(
|
|
||||||
"edit-label",
|
|
||||||
Animation::new(Duration::from_secs(2))
|
|
||||||
.repeat()
|
|
||||||
.with_easing(pulsating_between(0.3, 0.7)),
|
|
||||||
|label, delta| label.alpha(delta),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
this.child(
|
this.child(
|
||||||
@@ -971,74 +945,23 @@ impl MessageEditor {
|
|||||||
.color(Color::Muted),
|
.color(Color::Muted),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
.on_click(
|
|
||||||
cx.listener(|this, _, _, cx| this.handle_edit_bar_expand(cx)),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
Button::new("review", "Review Changes")
|
||||||
.gap_1()
|
.label_size(LabelSize::Small)
|
||||||
.child(
|
.key_binding(
|
||||||
IconButton::new("review-changes", IconName::ListTodo)
|
KeyBinding::for_action_in(
|
||||||
.icon_size(IconSize::Small)
|
&OpenAgentDiff,
|
||||||
.tooltip({
|
&focus_handle,
|
||||||
let focus_handle = focus_handle.clone();
|
window,
|
||||||
move |window, cx| {
|
cx,
|
||||||
Tooltip::for_action_in(
|
)
|
||||||
"Review Changes",
|
.map(|kb| kb.size(rems_from_px(12.))),
|
||||||
&OpenAgentDiff,
|
|
||||||
&focus_handle,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.on_click(cx.listener(|this, _, window, cx| {
|
|
||||||
this.handle_review_click(window, cx)
|
|
||||||
})),
|
|
||||||
)
|
)
|
||||||
.child(ui::Divider::vertical().color(ui::DividerColor::Border))
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
.child(
|
this.handle_review_click(window, cx)
|
||||||
Button::new("reject-all-changes", "Reject All")
|
})),
|
||||||
.label_size(LabelSize::Small)
|
|
||||||
.disabled(pending_edits)
|
|
||||||
.when(pending_edits, |this| {
|
|
||||||
this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
|
|
||||||
})
|
|
||||||
.key_binding(
|
|
||||||
KeyBinding::for_action_in(
|
|
||||||
&RejectAll,
|
|
||||||
&focus_handle.clone(),
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.map(|kb| kb.size(rems_from_px(10.))),
|
|
||||||
)
|
|
||||||
.on_click(cx.listener(|this, _, window, cx| {
|
|
||||||
this.handle_reject_all(window, cx)
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
Button::new("accept-all-changes", "Accept All")
|
|
||||||
.label_size(LabelSize::Small)
|
|
||||||
.disabled(pending_edits)
|
|
||||||
.when(pending_edits, |this| {
|
|
||||||
this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
|
|
||||||
})
|
|
||||||
.key_binding(
|
|
||||||
KeyBinding::for_action_in(
|
|
||||||
&KeepAll,
|
|
||||||
&focus_handle,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.map(|kb| kb.size(rems_from_px(10.))),
|
|
||||||
)
|
|
||||||
.on_click(cx.listener(|this, _, window, cx| {
|
|
||||||
this.handle_accept_all(window, cx)
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.when(is_edit_changes_expanded, |parent| {
|
.when(is_edit_changes_expanded, |parent| {
|
||||||
|
|||||||
@@ -1,24 +1,26 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use agent_settings::{AgentDockPosition, AgentProfileId, AgentSettings, builtin_profiles};
|
use agent_settings::{
|
||||||
|
AgentDockPosition, AgentProfile, AgentProfileId, AgentSettings, GroupedAgentProfiles,
|
||||||
|
builtin_profiles,
|
||||||
|
};
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use gpui::{Action, Empty, Entity, FocusHandle, Subscription, prelude::*};
|
use gpui::{Action, Empty, Entity, FocusHandle, Subscription, WeakEntity, prelude::*};
|
||||||
use language_model::LanguageModelRegistry;
|
use language_model::LanguageModelRegistry;
|
||||||
use settings::{Settings as _, SettingsStore, update_settings_file};
|
use settings::{Settings as _, SettingsStore, update_settings_file};
|
||||||
use ui::{
|
use ui::{
|
||||||
ContextMenu, ContextMenuEntry, DocumentationSide, PopoverMenu, PopoverMenuHandle, Tooltip,
|
ContextMenu, ContextMenuEntry, DocumentationSide, PopoverMenu, PopoverMenuHandle, Tooltip,
|
||||||
prelude::*,
|
prelude::*,
|
||||||
};
|
};
|
||||||
|
use util::ResultExt as _;
|
||||||
|
|
||||||
use crate::{
|
use crate::{ManageProfiles, Thread, ThreadStore, ToggleProfileSelector};
|
||||||
ManageProfiles, Thread, ToggleProfileSelector,
|
|
||||||
agent_profile::{AgentProfile, AvailableProfiles},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct ProfileSelector {
|
pub struct ProfileSelector {
|
||||||
profiles: AvailableProfiles,
|
profiles: GroupedAgentProfiles,
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
thread: Entity<Thread>,
|
thread: Entity<Thread>,
|
||||||
|
thread_store: WeakEntity<ThreadStore>,
|
||||||
menu_handle: PopoverMenuHandle<ContextMenu>,
|
menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
@@ -28,6 +30,7 @@ impl ProfileSelector {
|
|||||||
pub fn new(
|
pub fn new(
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
thread: Entity<Thread>,
|
thread: Entity<Thread>,
|
||||||
|
thread_store: WeakEntity<ThreadStore>,
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
@@ -36,9 +39,10 @@ impl ProfileSelector {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
profiles: AgentProfile::available_profiles(cx),
|
profiles: GroupedAgentProfiles::from_settings(AgentSettings::get_global(cx)),
|
||||||
fs,
|
fs,
|
||||||
thread,
|
thread,
|
||||||
|
thread_store,
|
||||||
menu_handle: PopoverMenuHandle::default(),
|
menu_handle: PopoverMenuHandle::default(),
|
||||||
focus_handle,
|
focus_handle,
|
||||||
_subscriptions: vec![settings_subscription],
|
_subscriptions: vec![settings_subscription],
|
||||||
@@ -50,7 +54,7 @@ impl ProfileSelector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn refresh_profiles(&mut self, cx: &mut Context<Self>) {
|
fn refresh_profiles(&mut self, cx: &mut Context<Self>) {
|
||||||
self.profiles = AgentProfile::available_profiles(cx);
|
self.profiles = GroupedAgentProfiles::from_settings(AgentSettings::get_global(cx));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_context_menu(
|
fn build_context_menu(
|
||||||
@@ -60,30 +64,21 @@ impl ProfileSelector {
|
|||||||
) -> Entity<ContextMenu> {
|
) -> Entity<ContextMenu> {
|
||||||
ContextMenu::build(window, cx, |mut menu, _window, cx| {
|
ContextMenu::build(window, cx, |mut menu, _window, cx| {
|
||||||
let settings = AgentSettings::get_global(cx);
|
let settings = AgentSettings::get_global(cx);
|
||||||
|
for (profile_id, profile) in self.profiles.builtin.iter() {
|
||||||
let mut found_non_builtin = false;
|
|
||||||
for (profile_id, profile_name) in self.profiles.iter() {
|
|
||||||
if !builtin_profiles::is_builtin(profile_id) {
|
|
||||||
found_non_builtin = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
menu = menu.item(self.menu_entry_for_profile(
|
menu = menu.item(self.menu_entry_for_profile(
|
||||||
profile_id.clone(),
|
profile_id.clone(),
|
||||||
profile_name,
|
profile,
|
||||||
settings,
|
settings,
|
||||||
cx,
|
cx,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if found_non_builtin {
|
if !self.profiles.custom.is_empty() {
|
||||||
menu = menu.separator().header("Custom Profiles");
|
menu = menu.separator().header("Custom Profiles");
|
||||||
for (profile_id, profile_name) in self.profiles.iter() {
|
for (profile_id, profile) in self.profiles.custom.iter() {
|
||||||
if builtin_profiles::is_builtin(profile_id) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
menu = menu.item(self.menu_entry_for_profile(
|
menu = menu.item(self.menu_entry_for_profile(
|
||||||
profile_id.clone(),
|
profile_id.clone(),
|
||||||
profile_name,
|
profile,
|
||||||
settings,
|
settings,
|
||||||
cx,
|
cx,
|
||||||
));
|
));
|
||||||
@@ -104,20 +99,19 @@ impl ProfileSelector {
|
|||||||
fn menu_entry_for_profile(
|
fn menu_entry_for_profile(
|
||||||
&self,
|
&self,
|
||||||
profile_id: AgentProfileId,
|
profile_id: AgentProfileId,
|
||||||
profile_name: &SharedString,
|
profile: &AgentProfile,
|
||||||
settings: &AgentSettings,
|
settings: &AgentSettings,
|
||||||
cx: &App,
|
_cx: &App,
|
||||||
) -> ContextMenuEntry {
|
) -> ContextMenuEntry {
|
||||||
let documentation = match profile_name.to_lowercase().as_str() {
|
let documentation = match profile.name.to_lowercase().as_str() {
|
||||||
builtin_profiles::WRITE => Some("Get help to write anything."),
|
builtin_profiles::WRITE => Some("Get help to write anything."),
|
||||||
builtin_profiles::ASK => Some("Chat about your codebase."),
|
builtin_profiles::ASK => Some("Chat about your codebase."),
|
||||||
builtin_profiles::MINIMAL => Some("Chat about anything with no tools."),
|
builtin_profiles::MINIMAL => Some("Chat about anything with no tools."),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
let thread_profile_id = self.thread.read(cx).profile().id();
|
|
||||||
|
|
||||||
let entry = ContextMenuEntry::new(profile_name.clone())
|
let entry = ContextMenuEntry::new(profile.name.clone())
|
||||||
.toggleable(IconPosition::End, &profile_id == thread_profile_id);
|
.toggleable(IconPosition::End, profile_id == settings.default_profile);
|
||||||
|
|
||||||
let entry = if let Some(doc_text) = documentation {
|
let entry = if let Some(doc_text) = documentation {
|
||||||
entry.documentation_aside(documentation_side(settings.dock), move |_| {
|
entry.documentation_aside(documentation_side(settings.dock), move |_| {
|
||||||
@@ -129,7 +123,7 @@ impl ProfileSelector {
|
|||||||
|
|
||||||
entry.handler({
|
entry.handler({
|
||||||
let fs = self.fs.clone();
|
let fs = self.fs.clone();
|
||||||
let thread = self.thread.clone();
|
let thread_store = self.thread_store.clone();
|
||||||
let profile_id = profile_id.clone();
|
let profile_id = profile_id.clone();
|
||||||
move |_window, cx| {
|
move |_window, cx| {
|
||||||
update_settings_file::<AgentSettings>(fs.clone(), cx, {
|
update_settings_file::<AgentSettings>(fs.clone(), cx, {
|
||||||
@@ -139,9 +133,11 @@ impl ProfileSelector {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
thread.update(cx, |this, cx| {
|
thread_store
|
||||||
this.set_profile(profile_id.clone(), cx);
|
.update(cx, |this, cx| {
|
||||||
});
|
this.load_profile_by_id(profile_id.clone(), cx);
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -150,7 +146,7 @@ impl ProfileSelector {
|
|||||||
impl Render for ProfileSelector {
|
impl Render for ProfileSelector {
|
||||||
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 settings = AgentSettings::get_global(cx);
|
let settings = AgentSettings::get_global(cx);
|
||||||
let profile_id = self.thread.read(cx).profile().id();
|
let profile_id = &settings.default_profile;
|
||||||
let profile = settings.profiles.get(profile_id);
|
let profile = settings.profiles.get(profile_id);
|
||||||
|
|
||||||
let selected_profile = profile
|
let selected_profile = profile
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use std::ops::Range;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
use agent_settings::{AgentProfileId, AgentSettings, CompletionMode};
|
use agent_settings::{AgentSettings, CompletionMode};
|
||||||
use anyhow::{Result, anyhow};
|
use anyhow::{Result, anyhow};
|
||||||
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet};
|
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
@@ -41,7 +41,6 @@ use uuid::Uuid;
|
|||||||
use zed_llm_client::{CompletionIntent, CompletionRequestStatus};
|
use zed_llm_client::{CompletionIntent, CompletionRequestStatus};
|
||||||
|
|
||||||
use crate::ThreadStore;
|
use crate::ThreadStore;
|
||||||
use crate::agent_profile::AgentProfile;
|
|
||||||
use crate::context::{AgentContext, AgentContextHandle, ContextLoadResult, LoadedContext};
|
use crate::context::{AgentContext, AgentContextHandle, ContextLoadResult, LoadedContext};
|
||||||
use crate::thread_store::{
|
use crate::thread_store::{
|
||||||
SerializedCrease, SerializedLanguageModel, SerializedMessage, SerializedMessageSegment,
|
SerializedCrease, SerializedLanguageModel, SerializedMessage, SerializedMessageSegment,
|
||||||
@@ -361,7 +360,6 @@ pub struct Thread {
|
|||||||
>,
|
>,
|
||||||
remaining_turns: u32,
|
remaining_turns: u32,
|
||||||
configured_model: Option<ConfiguredModel>,
|
configured_model: Option<ConfiguredModel>,
|
||||||
profile: AgentProfile,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
@@ -409,7 +407,6 @@ impl Thread {
|
|||||||
) -> Self {
|
) -> Self {
|
||||||
let (detailed_summary_tx, detailed_summary_rx) = postage::watch::channel();
|
let (detailed_summary_tx, detailed_summary_rx) = postage::watch::channel();
|
||||||
let configured_model = LanguageModelRegistry::read_global(cx).default_model();
|
let configured_model = LanguageModelRegistry::read_global(cx).default_model();
|
||||||
let profile_id = AgentSettings::get_global(cx).default_profile.clone();
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
id: ThreadId::new(),
|
id: ThreadId::new(),
|
||||||
@@ -452,7 +449,6 @@ impl Thread {
|
|||||||
request_callback: None,
|
request_callback: None,
|
||||||
remaining_turns: u32::MAX,
|
remaining_turns: u32::MAX,
|
||||||
configured_model,
|
configured_model,
|
||||||
profile: AgentProfile::new(profile_id, tools),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -499,9 +495,6 @@ impl Thread {
|
|||||||
let completion_mode = serialized
|
let completion_mode = serialized
|
||||||
.completion_mode
|
.completion_mode
|
||||||
.unwrap_or_else(|| AgentSettings::get_global(cx).preferred_completion_mode);
|
.unwrap_or_else(|| AgentSettings::get_global(cx).preferred_completion_mode);
|
||||||
let profile_id = serialized
|
|
||||||
.profile
|
|
||||||
.unwrap_or_else(|| AgentSettings::get_global(cx).default_profile.clone());
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
@@ -561,7 +554,7 @@ impl Thread {
|
|||||||
pending_checkpoint: None,
|
pending_checkpoint: None,
|
||||||
project: project.clone(),
|
project: project.clone(),
|
||||||
prompt_builder,
|
prompt_builder,
|
||||||
tools: tools.clone(),
|
tools,
|
||||||
tool_use,
|
tool_use,
|
||||||
action_log: cx.new(|_| ActionLog::new(project)),
|
action_log: cx.new(|_| ActionLog::new(project)),
|
||||||
initial_project_snapshot: Task::ready(serialized.initial_project_snapshot).shared(),
|
initial_project_snapshot: Task::ready(serialized.initial_project_snapshot).shared(),
|
||||||
@@ -577,7 +570,6 @@ impl Thread {
|
|||||||
request_callback: None,
|
request_callback: None,
|
||||||
remaining_turns: u32::MAX,
|
remaining_turns: u32::MAX,
|
||||||
configured_model,
|
configured_model,
|
||||||
profile: AgentProfile::new(profile_id, tools),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -593,17 +585,6 @@ impl Thread {
|
|||||||
&self.id
|
&self.id
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn profile(&self) -> &AgentProfile {
|
|
||||||
&self.profile
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_profile(&mut self, id: AgentProfileId, cx: &mut Context<Self>) {
|
|
||||||
if &id != self.profile.id() {
|
|
||||||
self.profile = AgentProfile::new(id, self.tools.clone());
|
|
||||||
cx.emit(ThreadEvent::ProfileChanged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.messages.is_empty()
|
self.messages.is_empty()
|
||||||
}
|
}
|
||||||
@@ -890,16 +871,7 @@ impl Thread {
|
|||||||
self.tool_use
|
self.tool_use
|
||||||
.pending_tool_uses()
|
.pending_tool_uses()
|
||||||
.iter()
|
.iter()
|
||||||
.all(|pending_tool_use| pending_tool_use.status.is_error())
|
.all(|tool_use| tool_use.status.is_error())
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns whether any pending tool uses may perform edits
|
|
||||||
pub fn has_pending_edit_tool_uses(&self) -> bool {
|
|
||||||
self.tool_use
|
|
||||||
.pending_tool_uses()
|
|
||||||
.iter()
|
|
||||||
.filter(|pending_tool_use| !pending_tool_use.status.is_error())
|
|
||||||
.any(|pending_tool_use| pending_tool_use.may_perform_edits)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tool_uses_for_message(&self, id: MessageId, cx: &App) -> Vec<ToolUse> {
|
pub fn tool_uses_for_message(&self, id: MessageId, cx: &App) -> Vec<ToolUse> {
|
||||||
@@ -938,7 +910,8 @@ impl Thread {
|
|||||||
model: Arc<dyn LanguageModel>,
|
model: Arc<dyn LanguageModel>,
|
||||||
) -> Vec<LanguageModelRequestTool> {
|
) -> Vec<LanguageModelRequestTool> {
|
||||||
if model.supports_tools() {
|
if model.supports_tools() {
|
||||||
self.profile
|
self.tools()
|
||||||
|
.read(cx)
|
||||||
.enabled_tools(cx)
|
.enabled_tools(cx)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|tool| {
|
.filter_map(|tool| {
|
||||||
@@ -1050,7 +1023,6 @@ impl Thread {
|
|||||||
id: MessageId,
|
id: MessageId,
|
||||||
new_role: Role,
|
new_role: Role,
|
||||||
new_segments: Vec<MessageSegment>,
|
new_segments: Vec<MessageSegment>,
|
||||||
creases: Vec<MessageCrease>,
|
|
||||||
loaded_context: Option<LoadedContext>,
|
loaded_context: Option<LoadedContext>,
|
||||||
checkpoint: Option<GitStoreCheckpoint>,
|
checkpoint: Option<GitStoreCheckpoint>,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
@@ -1060,7 +1032,6 @@ impl Thread {
|
|||||||
};
|
};
|
||||||
message.role = new_role;
|
message.role = new_role;
|
||||||
message.segments = new_segments;
|
message.segments = new_segments;
|
||||||
message.creases = creases;
|
|
||||||
if let Some(context) = loaded_context {
|
if let Some(context) = loaded_context {
|
||||||
message.loaded_context = context;
|
message.loaded_context = context;
|
||||||
}
|
}
|
||||||
@@ -1198,7 +1169,6 @@ impl Thread {
|
|||||||
}),
|
}),
|
||||||
completion_mode: Some(this.completion_mode),
|
completion_mode: Some(this.completion_mode),
|
||||||
tool_use_limit_reached: this.tool_use_limit_reached,
|
tool_use_limit_reached: this.tool_use_limit_reached,
|
||||||
profile: Some(this.profile.id().clone()),
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -2140,7 +2110,7 @@ impl Thread {
|
|||||||
window: Option<AnyWindowHandle>,
|
window: Option<AnyWindowHandle>,
|
||||||
cx: &mut Context<Thread>,
|
cx: &mut Context<Thread>,
|
||||||
) {
|
) {
|
||||||
let available_tools = self.profile.enabled_tools(cx);
|
let available_tools = self.tools.read(cx).enabled_tools(cx);
|
||||||
|
|
||||||
let tool_list = available_tools
|
let tool_list = available_tools
|
||||||
.iter()
|
.iter()
|
||||||
@@ -2232,15 +2202,19 @@ impl Thread {
|
|||||||
) -> Task<()> {
|
) -> Task<()> {
|
||||||
let tool_name: Arc<str> = tool.name().into();
|
let tool_name: Arc<str> = tool.name().into();
|
||||||
|
|
||||||
let tool_result = tool.run(
|
let tool_result = if self.tools.read(cx).is_disabled(&tool.source(), &tool_name) {
|
||||||
input,
|
Task::ready(Err(anyhow!("tool is disabled: {tool_name}"))).into()
|
||||||
request,
|
} else {
|
||||||
self.project.clone(),
|
tool.run(
|
||||||
self.action_log.clone(),
|
input,
|
||||||
model,
|
request,
|
||||||
window,
|
self.project.clone(),
|
||||||
cx,
|
self.action_log.clone(),
|
||||||
);
|
model,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
// Store the card separately if it exists
|
// Store the card separately if it exists
|
||||||
if let Some(card) = tool_result.card.clone() {
|
if let Some(card) = tool_result.card.clone() {
|
||||||
@@ -2359,7 +2333,8 @@ impl Thread {
|
|||||||
let client = self.project.read(cx).client();
|
let client = self.project.read(cx).client();
|
||||||
|
|
||||||
let enabled_tool_names: Vec<String> = self
|
let enabled_tool_names: Vec<String> = self
|
||||||
.profile
|
.tools()
|
||||||
|
.read(cx)
|
||||||
.enabled_tools(cx)
|
.enabled_tools(cx)
|
||||||
.iter()
|
.iter()
|
||||||
.map(|tool| tool.name())
|
.map(|tool| tool.name())
|
||||||
@@ -2872,7 +2847,6 @@ pub enum ThreadEvent {
|
|||||||
ToolUseLimitReached,
|
ToolUseLimitReached,
|
||||||
CancelEditing,
|
CancelEditing,
|
||||||
CompletionCanceled,
|
CompletionCanceled,
|
||||||
ProfileChanged,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<ThreadEvent> for Thread {}
|
impl EventEmitter<ThreadEvent> for Thread {}
|
||||||
@@ -2887,7 +2861,7 @@ struct PendingCompletion {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{ThreadStore, context::load_context, context_store::ContextStore, thread_store};
|
use crate::{ThreadStore, context::load_context, context_store::ContextStore, thread_store};
|
||||||
use agent_settings::{AgentProfileId, AgentSettings, LanguageModelParameters};
|
use agent_settings::{AgentSettings, LanguageModelParameters};
|
||||||
use assistant_tool::ToolRegistry;
|
use assistant_tool::ToolRegistry;
|
||||||
use editor::EditorSettings;
|
use editor::EditorSettings;
|
||||||
use gpui::TestAppContext;
|
use gpui::TestAppContext;
|
||||||
@@ -3300,71 +3274,6 @@ fn main() {{
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_storing_profile_setting_per_thread(cx: &mut TestAppContext) {
|
|
||||||
init_test_settings(cx);
|
|
||||||
|
|
||||||
let project = create_test_project(
|
|
||||||
cx,
|
|
||||||
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let (_workspace, thread_store, thread, _context_store, _model) =
|
|
||||||
setup_test_environment(cx, project.clone()).await;
|
|
||||||
|
|
||||||
// Check that we are starting with the default profile
|
|
||||||
let profile = cx.read(|cx| thread.read(cx).profile.clone());
|
|
||||||
let tool_set = cx.read(|cx| thread_store.read(cx).tools());
|
|
||||||
assert_eq!(
|
|
||||||
profile,
|
|
||||||
AgentProfile::new(AgentProfileId::default(), tool_set)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_serializing_thread_profile(cx: &mut TestAppContext) {
|
|
||||||
init_test_settings(cx);
|
|
||||||
|
|
||||||
let project = create_test_project(
|
|
||||||
cx,
|
|
||||||
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let (_workspace, thread_store, thread, _context_store, _model) =
|
|
||||||
setup_test_environment(cx, project.clone()).await;
|
|
||||||
|
|
||||||
// Profile gets serialized with default values
|
|
||||||
let serialized = thread
|
|
||||||
.update(cx, |thread, cx| thread.serialize(cx))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(serialized.profile, Some(AgentProfileId::default()));
|
|
||||||
|
|
||||||
let deserialized = cx.update(|cx| {
|
|
||||||
thread.update(cx, |thread, cx| {
|
|
||||||
Thread::deserialize(
|
|
||||||
thread.id.clone(),
|
|
||||||
serialized,
|
|
||||||
thread.project.clone(),
|
|
||||||
thread.tools.clone(),
|
|
||||||
thread.prompt_builder.clone(),
|
|
||||||
thread.project_context.clone(),
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
});
|
|
||||||
let tool_set = cx.read(|cx| thread_store.read(cx).tools());
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
deserialized.profile,
|
|
||||||
AgentProfile::new(AgentProfileId::default(), tool_set)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_temperature_setting(cx: &mut TestAppContext) {
|
async fn test_temperature_setting(cx: &mut TestAppContext) {
|
||||||
init_test_settings(cx);
|
init_test_settings(cx);
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ use std::path::{Path, PathBuf};
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use agent_settings::{AgentProfileId, CompletionMode};
|
use agent_settings::{AgentProfile, AgentProfileId, AgentSettings, CompletionMode};
|
||||||
use anyhow::{Context as _, Result, anyhow};
|
use anyhow::{Context as _, Result, anyhow};
|
||||||
use assistant_tool::{ToolId, ToolWorkingSet};
|
use assistant_tool::{ToolId, ToolSource, ToolWorkingSet};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use context_server::ContextServerId;
|
use context_server::ContextServerId;
|
||||||
@@ -25,6 +25,7 @@ use prompt_store::{
|
|||||||
UserRulesContext, WorktreeContext,
|
UserRulesContext, WorktreeContext,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use settings::{Settings as _, SettingsStore};
|
||||||
use ui::Window;
|
use ui::Window;
|
||||||
use util::ResultExt as _;
|
use util::ResultExt as _;
|
||||||
|
|
||||||
@@ -69,15 +70,13 @@ impl Column for DataType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const RULES_FILE_NAMES: [&'static str; 8] = [
|
const RULES_FILE_NAMES: [&'static str; 6] = [
|
||||||
".rules",
|
".rules",
|
||||||
".cursorrules",
|
".cursorrules",
|
||||||
".windsurfrules",
|
".windsurfrules",
|
||||||
".clinerules",
|
".clinerules",
|
||||||
".github/copilot-instructions.md",
|
".github/copilot-instructions.md",
|
||||||
"CLAUDE.md",
|
"CLAUDE.md",
|
||||||
"AGENT.md",
|
|
||||||
"AGENTS.md",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
pub fn init(cx: &mut App) {
|
pub fn init(cx: &mut App) {
|
||||||
@@ -146,7 +145,12 @@ impl ThreadStore {
|
|||||||
prompt_store: Option<Entity<PromptStore>>,
|
prompt_store: Option<Entity<PromptStore>>,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> (Self, oneshot::Receiver<()>) {
|
) -> (Self, oneshot::Receiver<()>) {
|
||||||
let mut subscriptions = vec![cx.subscribe(&project, Self::handle_project_event)];
|
let mut subscriptions = vec![
|
||||||
|
cx.observe_global::<SettingsStore>(move |this: &mut Self, cx| {
|
||||||
|
this.load_default_profile(cx);
|
||||||
|
}),
|
||||||
|
cx.subscribe(&project, Self::handle_project_event),
|
||||||
|
];
|
||||||
|
|
||||||
if let Some(prompt_store) = prompt_store.as_ref() {
|
if let Some(prompt_store) = prompt_store.as_ref() {
|
||||||
subscriptions.push(cx.subscribe(
|
subscriptions.push(cx.subscribe(
|
||||||
@@ -194,6 +198,7 @@ impl ThreadStore {
|
|||||||
_reload_system_prompt_task: reload_system_prompt_task,
|
_reload_system_prompt_task: reload_system_prompt_task,
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
};
|
};
|
||||||
|
this.load_default_profile(cx);
|
||||||
this.register_context_server_handlers(cx);
|
this.register_context_server_handlers(cx);
|
||||||
this.reload(cx).detach_and_log_err(cx);
|
this.reload(cx).detach_and_log_err(cx);
|
||||||
(this, ready_rx)
|
(this, ready_rx)
|
||||||
@@ -513,17 +518,94 @@ impl ThreadStore {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn register_context_server_handlers(&self, cx: &mut Context<Self>) {
|
fn load_default_profile(&self, cx: &mut Context<Self>) {
|
||||||
let context_server_store = self.project.read(cx).context_server_store();
|
let assistant_settings = AgentSettings::get_global(cx);
|
||||||
cx.subscribe(&context_server_store, Self::handle_context_server_event)
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
// Check for any servers that were already running before the handler was registered
|
self.load_profile_by_id(assistant_settings.default_profile.clone(), cx);
|
||||||
for server in context_server_store.read(cx).running_servers() {
|
}
|
||||||
self.load_context_server_tools(server.id(), context_server_store.clone(), cx);
|
|
||||||
|
pub fn load_profile_by_id(&self, profile_id: AgentProfileId, cx: &mut Context<Self>) {
|
||||||
|
let assistant_settings = AgentSettings::get_global(cx);
|
||||||
|
|
||||||
|
if let Some(profile) = assistant_settings.profiles.get(&profile_id) {
|
||||||
|
self.load_profile(profile.clone(), cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn load_profile(&self, profile: AgentProfile, cx: &mut Context<Self>) {
|
||||||
|
self.tools.update(cx, |tools, cx| {
|
||||||
|
tools.disable_all_tools(cx);
|
||||||
|
tools.enable(
|
||||||
|
ToolSource::Native,
|
||||||
|
&profile
|
||||||
|
.tools
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(tool, enabled)| enabled.then(|| tool))
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if profile.enable_all_context_servers {
|
||||||
|
for context_server_id in self
|
||||||
|
.project
|
||||||
|
.read(cx)
|
||||||
|
.context_server_store()
|
||||||
|
.read(cx)
|
||||||
|
.all_server_ids()
|
||||||
|
{
|
||||||
|
self.tools.update(cx, |tools, cx| {
|
||||||
|
tools.enable_source(
|
||||||
|
ToolSource::ContextServer {
|
||||||
|
id: context_server_id.0.into(),
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Enable all the tools from all context servers, but disable the ones that are explicitly disabled
|
||||||
|
for (context_server_id, preset) in profile.context_servers {
|
||||||
|
self.tools.update(cx, |tools, cx| {
|
||||||
|
tools.disable(
|
||||||
|
ToolSource::ContextServer {
|
||||||
|
id: context_server_id.into(),
|
||||||
|
},
|
||||||
|
&preset
|
||||||
|
.tools
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(tool, enabled)| (!enabled).then(|| tool))
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (context_server_id, preset) in profile.context_servers {
|
||||||
|
self.tools.update(cx, |tools, cx| {
|
||||||
|
tools.enable(
|
||||||
|
ToolSource::ContextServer {
|
||||||
|
id: context_server_id.into(),
|
||||||
|
},
|
||||||
|
&preset
|
||||||
|
.tools
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(tool, enabled)| enabled.then(|| tool))
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_context_server_handlers(&self, cx: &mut Context<Self>) {
|
||||||
|
cx.subscribe(
|
||||||
|
&self.project.read(cx).context_server_store(),
|
||||||
|
Self::handle_context_server_event,
|
||||||
|
)
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_context_server_event(
|
fn handle_context_server_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
context_server_store: Entity<ContextServerStore>,
|
context_server_store: Entity<ContextServerStore>,
|
||||||
@@ -534,71 +616,71 @@ impl ThreadStore {
|
|||||||
match event {
|
match event {
|
||||||
project::context_server_store::Event::ServerStatusChanged { server_id, status } => {
|
project::context_server_store::Event::ServerStatusChanged { server_id, status } => {
|
||||||
match status {
|
match status {
|
||||||
ContextServerStatus::Starting => {}
|
|
||||||
ContextServerStatus::Running => {
|
ContextServerStatus::Running => {
|
||||||
self.load_context_server_tools(server_id.clone(), context_server_store, cx);
|
if let Some(server) =
|
||||||
|
context_server_store.read(cx).get_running_server(server_id)
|
||||||
|
{
|
||||||
|
let context_server_manager = context_server_store.clone();
|
||||||
|
cx.spawn({
|
||||||
|
let server = server.clone();
|
||||||
|
let server_id = server_id.clone();
|
||||||
|
async move |this, cx| {
|
||||||
|
let Some(protocol) = server.client() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if protocol.capable(context_server::protocol::ServerCapability::Tools) {
|
||||||
|
if let Some(tools) = protocol.list_tools().await.log_err() {
|
||||||
|
let tool_ids = tool_working_set
|
||||||
|
.update(cx, |tool_working_set, _| {
|
||||||
|
tools
|
||||||
|
.tools
|
||||||
|
.into_iter()
|
||||||
|
.map(|tool| {
|
||||||
|
log::info!(
|
||||||
|
"registering context server tool: {:?}",
|
||||||
|
tool.name
|
||||||
|
);
|
||||||
|
tool_working_set.insert(Arc::new(
|
||||||
|
ContextServerTool::new(
|
||||||
|
context_server_manager.clone(),
|
||||||
|
server.id(),
|
||||||
|
tool,
|
||||||
|
),
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
|
||||||
|
if let Some(tool_ids) = tool_ids {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.context_server_tool_ids
|
||||||
|
.insert(server_id, tool_ids);
|
||||||
|
this.load_default_profile(cx);
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ContextServerStatus::Stopped | ContextServerStatus::Error(_) => {
|
ContextServerStatus::Stopped | ContextServerStatus::Error(_) => {
|
||||||
if let Some(tool_ids) = self.context_server_tool_ids.remove(server_id) {
|
if let Some(tool_ids) = self.context_server_tool_ids.remove(server_id) {
|
||||||
tool_working_set.update(cx, |tool_working_set, _| {
|
tool_working_set.update(cx, |tool_working_set, _| {
|
||||||
tool_working_set.remove(&tool_ids);
|
tool_working_set.remove(&tool_ids);
|
||||||
});
|
});
|
||||||
|
self.load_default_profile(cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_context_server_tools(
|
|
||||||
&self,
|
|
||||||
server_id: ContextServerId,
|
|
||||||
context_server_store: Entity<ContextServerStore>,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
let Some(server) = context_server_store.read(cx).get_running_server(&server_id) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let tool_working_set = self.tools.clone();
|
|
||||||
cx.spawn(async move |this, cx| {
|
|
||||||
let Some(protocol) = server.client() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
if protocol.capable(context_server::protocol::ServerCapability::Tools) {
|
|
||||||
if let Some(response) = protocol
|
|
||||||
.request::<context_server::types::request::ListTools>(())
|
|
||||||
.await
|
|
||||||
.log_err()
|
|
||||||
{
|
|
||||||
let tool_ids = tool_working_set
|
|
||||||
.update(cx, |tool_working_set, _| {
|
|
||||||
response
|
|
||||||
.tools
|
|
||||||
.into_iter()
|
|
||||||
.map(|tool| {
|
|
||||||
log::info!("registering context server tool: {:?}", tool.name);
|
|
||||||
tool_working_set.insert(Arc::new(ContextServerTool::new(
|
|
||||||
context_server_store.clone(),
|
|
||||||
server.id(),
|
|
||||||
tool,
|
|
||||||
)))
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
})
|
|
||||||
.log_err();
|
|
||||||
|
|
||||||
if let Some(tool_ids) = tool_ids {
|
|
||||||
this.update(cx, |this, _| {
|
|
||||||
this.context_server_tool_ids.insert(server_id, tool_ids);
|
|
||||||
})
|
|
||||||
.log_err();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -630,8 +712,6 @@ pub struct SerializedThread {
|
|||||||
pub completion_mode: Option<CompletionMode>,
|
pub completion_mode: Option<CompletionMode>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub tool_use_limit_reached: bool,
|
pub tool_use_limit_reached: bool,
|
||||||
#[serde(default)]
|
|
||||||
pub profile: Option<AgentProfileId>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
@@ -774,7 +854,6 @@ impl LegacySerializedThread {
|
|||||||
model: None,
|
model: None,
|
||||||
completion_mode: None,
|
completion_mode: None,
|
||||||
tool_use_limit_reached: false,
|
tool_use_limit_reached: false,
|
||||||
profile: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,30 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use assistant_tool::{Tool, ToolSource};
|
use assistant_tool::{Tool, ToolSource, ToolWorkingSet, ToolWorkingSetEvent};
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use gpui::{App, Context, Entity, IntoElement, Render, Subscription, Window};
|
use gpui::{App, Context, Entity, IntoElement, Render, Subscription, Window};
|
||||||
use language_model::{LanguageModel, LanguageModelToolSchemaFormat};
|
use language_model::{LanguageModel, LanguageModelToolSchemaFormat};
|
||||||
use ui::prelude::*;
|
use ui::prelude::*;
|
||||||
|
|
||||||
use crate::{Thread, ThreadEvent};
|
|
||||||
|
|
||||||
pub struct IncompatibleToolsState {
|
pub struct IncompatibleToolsState {
|
||||||
cache: HashMap<LanguageModelToolSchemaFormat, Vec<Arc<dyn Tool>>>,
|
cache: HashMap<LanguageModelToolSchemaFormat, Vec<Arc<dyn Tool>>>,
|
||||||
thread: Entity<Thread>,
|
tool_working_set: Entity<ToolWorkingSet>,
|
||||||
_thread_subscription: Subscription,
|
_tool_working_set_subscription: Subscription,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IncompatibleToolsState {
|
impl IncompatibleToolsState {
|
||||||
pub fn new(thread: Entity<Thread>, cx: &mut Context<Self>) -> Self {
|
pub fn new(tool_working_set: Entity<ToolWorkingSet>, cx: &mut Context<Self>) -> Self {
|
||||||
let _tool_working_set_subscription =
|
let _tool_working_set_subscription =
|
||||||
cx.subscribe(&thread, |this, _, event, _| match event {
|
cx.subscribe(&tool_working_set, |this, _, event, _| match event {
|
||||||
ThreadEvent::ProfileChanged => {
|
ToolWorkingSetEvent::EnabledToolsChanged => {
|
||||||
this.cache.clear();
|
this.cache.clear();
|
||||||
}
|
}
|
||||||
_ => {}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
cache: HashMap::default(),
|
cache: HashMap::default(),
|
||||||
thread,
|
tool_working_set,
|
||||||
_thread_subscription: _tool_working_set_subscription,
|
_tool_working_set_subscription,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,9 +36,8 @@ impl IncompatibleToolsState {
|
|||||||
self.cache
|
self.cache
|
||||||
.entry(model.tool_input_format())
|
.entry(model.tool_input_format())
|
||||||
.or_insert_with(|| {
|
.or_insert_with(|| {
|
||||||
self.thread
|
self.tool_working_set
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.profile()
|
|
||||||
.enabled_tools(cx)
|
.enabled_tools(cx)
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|tool| tool.input_schema(model.tool_input_format()).is_err())
|
.filter(|tool| tool.input_schema(model.tool_input_format()).is_err())
|
||||||
|
|||||||
@@ -337,12 +337,6 @@ impl ToolUseState {
|
|||||||
)
|
)
|
||||||
.into();
|
.into();
|
||||||
|
|
||||||
let may_perform_edits = self
|
|
||||||
.tools
|
|
||||||
.read(cx)
|
|
||||||
.tool(&tool_use.name, cx)
|
|
||||||
.is_some_and(|tool| tool.may_perform_edits());
|
|
||||||
|
|
||||||
self.pending_tool_uses_by_id.insert(
|
self.pending_tool_uses_by_id.insert(
|
||||||
tool_use.id.clone(),
|
tool_use.id.clone(),
|
||||||
PendingToolUse {
|
PendingToolUse {
|
||||||
@@ -351,7 +345,6 @@ impl ToolUseState {
|
|||||||
name: tool_use.name.clone(),
|
name: tool_use.name.clone(),
|
||||||
ui_text: ui_text.clone(),
|
ui_text: ui_text.clone(),
|
||||||
input: tool_use.input,
|
input: tool_use.input,
|
||||||
may_perform_edits,
|
|
||||||
status,
|
status,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -525,7 +518,6 @@ pub struct PendingToolUse {
|
|||||||
pub ui_text: Arc<str>,
|
pub ui_text: Arc<str>,
|
||||||
pub input: serde_json::Value,
|
pub input: serde_json::Value,
|
||||||
pub status: PendingToolUseStatus,
|
pub status: PendingToolUseStatus,
|
||||||
pub may_perform_edits: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|||||||
@@ -93,9 +93,20 @@ impl ContextPill {
|
|||||||
Self::Suggested {
|
Self::Suggested {
|
||||||
icon_path: Some(icon_path),
|
icon_path: Some(icon_path),
|
||||||
..
|
..
|
||||||
|
}
|
||||||
|
| Self::Added {
|
||||||
|
context:
|
||||||
|
AddedContext {
|
||||||
|
icon_path: Some(icon_path),
|
||||||
|
..
|
||||||
|
},
|
||||||
|
..
|
||||||
} => Icon::from_path(icon_path),
|
} => Icon::from_path(icon_path),
|
||||||
Self::Suggested { kind, .. } => Icon::new(kind.icon()),
|
Self::Suggested { kind, .. }
|
||||||
Self::Added { context, .. } => context.icon(),
|
| Self::Added {
|
||||||
|
context: AddedContext { kind, .. },
|
||||||
|
..
|
||||||
|
} => Icon::new(kind.icon()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,7 +133,6 @@ impl RenderOnce for ContextPill {
|
|||||||
on_click,
|
on_click,
|
||||||
} => {
|
} => {
|
||||||
let status_is_error = matches!(context.status, ContextStatus::Error { .. });
|
let status_is_error = matches!(context.status, ContextStatus::Error { .. });
|
||||||
let status_is_warning = matches!(context.status, ContextStatus::Warning { .. });
|
|
||||||
|
|
||||||
base_pill
|
base_pill
|
||||||
.pr(if on_remove.is_some() { px(2.) } else { px(4.) })
|
.pr(if on_remove.is_some() { px(2.) } else { px(4.) })
|
||||||
@@ -130,9 +140,6 @@ impl RenderOnce for ContextPill {
|
|||||||
if status_is_error {
|
if status_is_error {
|
||||||
pill.bg(cx.theme().status().error_background)
|
pill.bg(cx.theme().status().error_background)
|
||||||
.border_color(cx.theme().status().error_border)
|
.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 {
|
} else if *focused {
|
||||||
pill.bg(color.element_background)
|
pill.bg(color.element_background)
|
||||||
.border_color(color.border_focused)
|
.border_color(color.border_focused)
|
||||||
@@ -188,8 +195,7 @@ impl RenderOnce for ContextPill {
|
|||||||
|label, delta| label.opacity(delta),
|
|label, delta| label.opacity(delta),
|
||||||
)
|
)
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
ContextStatus::Warning { message }
|
ContextStatus::Error { message } => element
|
||||||
| ContextStatus::Error { message } => element
|
|
||||||
.tooltip(ui::Tooltip::text(message.clone()))
|
.tooltip(ui::Tooltip::text(message.clone()))
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
}),
|
}),
|
||||||
@@ -264,7 +270,6 @@ pub enum ContextStatus {
|
|||||||
Ready,
|
Ready,
|
||||||
Loading { message: SharedString },
|
Loading { message: SharedString },
|
||||||
Error { message: SharedString },
|
Error { message: SharedString },
|
||||||
Warning { message: SharedString },
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(RegisterComponent)]
|
#[derive(RegisterComponent)]
|
||||||
@@ -280,19 +285,6 @@ pub struct AddedContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AddedContext {
|
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
|
/// Creates an `AddedContext` by retrieving relevant details of `AgentContext`. This returns a
|
||||||
/// `None` if `DirectoryContext` or `RulesContext` no longer exist.
|
/// `None` if `DirectoryContext` or `RulesContext` no longer exist.
|
||||||
///
|
///
|
||||||
@@ -301,7 +293,6 @@ impl AddedContext {
|
|||||||
handle: AgentContextHandle,
|
handle: AgentContextHandle,
|
||||||
prompt_store: Option<&Entity<PromptStore>>,
|
prompt_store: Option<&Entity<PromptStore>>,
|
||||||
project: &Project,
|
project: &Project,
|
||||||
model: Option<&Arc<dyn language_model::LanguageModel>>,
|
|
||||||
cx: &App,
|
cx: &App,
|
||||||
) -> Option<AddedContext> {
|
) -> Option<AddedContext> {
|
||||||
match handle {
|
match handle {
|
||||||
@@ -313,15 +304,11 @@ impl AddedContext {
|
|||||||
AgentContextHandle::Thread(handle) => Some(Self::pending_thread(handle, cx)),
|
AgentContextHandle::Thread(handle) => Some(Self::pending_thread(handle, cx)),
|
||||||
AgentContextHandle::TextThread(handle) => Some(Self::pending_text_thread(handle, cx)),
|
AgentContextHandle::TextThread(handle) => Some(Self::pending_text_thread(handle, cx)),
|
||||||
AgentContextHandle::Rules(handle) => Self::pending_rules(handle, prompt_store, cx),
|
AgentContextHandle::Rules(handle) => Self::pending_rules(handle, prompt_store, cx),
|
||||||
AgentContextHandle::Image(handle) => Some(Self::image(handle, model, cx)),
|
AgentContextHandle::Image(handle) => Some(Self::image(handle, cx)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_attached(
|
pub fn new_attached(context: &AgentContext, cx: &App) -> AddedContext {
|
||||||
context: &AgentContext,
|
|
||||||
model: Option<&Arc<dyn language_model::LanguageModel>>,
|
|
||||||
cx: &App,
|
|
||||||
) -> AddedContext {
|
|
||||||
match context {
|
match context {
|
||||||
AgentContext::File(context) => Self::attached_file(context, cx),
|
AgentContext::File(context) => Self::attached_file(context, cx),
|
||||||
AgentContext::Directory(context) => Self::attached_directory(context),
|
AgentContext::Directory(context) => Self::attached_directory(context),
|
||||||
@@ -331,7 +318,7 @@ impl AddedContext {
|
|||||||
AgentContext::Thread(context) => Self::attached_thread(context),
|
AgentContext::Thread(context) => Self::attached_thread(context),
|
||||||
AgentContext::TextThread(context) => Self::attached_text_thread(context),
|
AgentContext::TextThread(context) => Self::attached_text_thread(context),
|
||||||
AgentContext::Rules(context) => Self::attached_rules(context),
|
AgentContext::Rules(context) => Self::attached_rules(context),
|
||||||
AgentContext::Image(context) => Self::image(context.clone(), model, cx),
|
AgentContext::Image(context) => Self::image(context.clone(), cx),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -606,11 +593,7 @@ impl AddedContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn image(
|
fn image(context: ImageContext, cx: &App) -> AddedContext {
|
||||||
context: ImageContext,
|
|
||||||
model: Option<&Arc<dyn language_model::LanguageModel>>,
|
|
||||||
cx: &App,
|
|
||||||
) -> AddedContext {
|
|
||||||
let (name, parent, icon_path) = if let Some(full_path) = context.full_path.as_ref() {
|
let (name, parent, icon_path) = if let Some(full_path) = context.full_path.as_ref() {
|
||||||
let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into();
|
let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into();
|
||||||
let (name, parent) =
|
let (name, parent) =
|
||||||
@@ -621,30 +604,21 @@ impl AddedContext {
|
|||||||
("Image".into(), None, None)
|
("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 {
|
AddedContext {
|
||||||
kind: ContextKind::Image,
|
kind: ContextKind::Image,
|
||||||
name,
|
name,
|
||||||
parent,
|
parent,
|
||||||
tooltip: None,
|
tooltip: None,
|
||||||
icon_path,
|
icon_path,
|
||||||
status,
|
status: match context.status() {
|
||||||
|
ImageStatus::Loading => ContextStatus::Loading {
|
||||||
|
message: "Loading…".into(),
|
||||||
|
},
|
||||||
|
ImageStatus::Error => ContextStatus::Error {
|
||||||
|
message: "Failed to load image".into(),
|
||||||
|
},
|
||||||
|
ImageStatus::Ready => ContextStatus::Ready,
|
||||||
|
},
|
||||||
render_hover: Some(Rc::new({
|
render_hover: Some(Rc::new({
|
||||||
let image = context.original_image.clone();
|
let image = context.original_image.clone();
|
||||||
move |_, cx| {
|
move |_, cx| {
|
||||||
@@ -813,7 +787,6 @@ impl Component for AddedContext {
|
|||||||
original_image: Arc::new(Image::empty()),
|
original_image: Arc::new(Image::empty()),
|
||||||
image_task: Task::ready(Some(LanguageModelImage::empty())).shared(),
|
image_task: Task::ready(Some(LanguageModelImage::empty())).shared(),
|
||||||
},
|
},
|
||||||
None,
|
|
||||||
cx,
|
cx,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -833,7 +806,6 @@ impl Component for AddedContext {
|
|||||||
})
|
})
|
||||||
.shared(),
|
.shared(),
|
||||||
},
|
},
|
||||||
None,
|
|
||||||
cx,
|
cx,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -848,7 +820,6 @@ impl Component for AddedContext {
|
|||||||
original_image: Arc::new(Image::empty()),
|
original_image: Arc::new(Image::empty()),
|
||||||
image_task: Task::ready(None).shared(),
|
image_task: Task::ready(None).shared(),
|
||||||
},
|
},
|
||||||
None,
|
|
||||||
cx,
|
cx,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -870,60 +841,3 @@ impl Component for AddedContext {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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), 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, 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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ anthropic = { workspace = true, features = ["schemars"] }
|
|||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
indexmap.workspace = true
|
||||||
language_model.workspace = true
|
language_model.workspace = true
|
||||||
lmstudio = { workspace = true, features = ["schemars"] }
|
lmstudio = { workspace = true, features = ["schemars"] }
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
|||||||
@@ -17,6 +17,29 @@ pub mod builtin_profiles {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct GroupedAgentProfiles {
|
||||||
|
pub builtin: IndexMap<AgentProfileId, AgentProfile>,
|
||||||
|
pub custom: IndexMap<AgentProfileId, AgentProfile>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GroupedAgentProfiles {
|
||||||
|
pub fn from_settings(settings: &crate::AgentSettings) -> Self {
|
||||||
|
let mut builtin = IndexMap::default();
|
||||||
|
let mut custom = IndexMap::default();
|
||||||
|
|
||||||
|
for (profile_id, profile) in settings.profiles.clone() {
|
||||||
|
if builtin_profiles::is_builtin(&profile_id) {
|
||||||
|
builtin.insert(profile_id, profile);
|
||||||
|
} else {
|
||||||
|
custom.insert(profile_id, profile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Self { builtin, custom }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, JsonSchema)]
|
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, JsonSchema)]
|
||||||
pub struct AgentProfileId(pub Arc<str>);
|
pub struct AgentProfileId(pub Arc<str>);
|
||||||
|
|
||||||
@@ -40,7 +63,7 @@ impl Default for AgentProfileId {
|
|||||||
|
|
||||||
/// A profile for the Zed Agent that controls its behavior.
|
/// A profile for the Zed Agent that controls its behavior.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct AgentProfileSettings {
|
pub struct AgentProfile {
|
||||||
/// The name of the profile.
|
/// The name of the profile.
|
||||||
pub name: SharedString,
|
pub name: SharedString,
|
||||||
pub tools: IndexMap<Arc<str>, bool>,
|
pub tools: IndexMap<Arc<str>, bool>,
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ pub struct AgentSettings {
|
|||||||
pub using_outdated_settings_version: bool,
|
pub using_outdated_settings_version: bool,
|
||||||
pub default_profile: AgentProfileId,
|
pub default_profile: AgentProfileId,
|
||||||
pub default_view: DefaultView,
|
pub default_view: DefaultView,
|
||||||
pub profiles: IndexMap<AgentProfileId, AgentProfileSettings>,
|
pub profiles: IndexMap<AgentProfileId, AgentProfile>,
|
||||||
pub always_allow_tool_actions: bool,
|
pub always_allow_tool_actions: bool,
|
||||||
pub notify_when_agent_waiting: NotifyWhenAgentWaiting,
|
pub notify_when_agent_waiting: NotifyWhenAgentWaiting,
|
||||||
pub play_sound_when_agent_done: bool,
|
pub play_sound_when_agent_done: bool,
|
||||||
@@ -531,7 +531,7 @@ impl AgentSettingsContent {
|
|||||||
pub fn create_profile(
|
pub fn create_profile(
|
||||||
&mut self,
|
&mut self,
|
||||||
profile_id: AgentProfileId,
|
profile_id: AgentProfileId,
|
||||||
profile_settings: AgentProfileSettings,
|
profile: AgentProfile,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
self.v2_setting(|settings| {
|
self.v2_setting(|settings| {
|
||||||
let profiles = settings.profiles.get_or_insert_default();
|
let profiles = settings.profiles.get_or_insert_default();
|
||||||
@@ -542,10 +542,10 @@ impl AgentSettingsContent {
|
|||||||
profiles.insert(
|
profiles.insert(
|
||||||
profile_id,
|
profile_id,
|
||||||
AgentProfileContent {
|
AgentProfileContent {
|
||||||
name: profile_settings.name.into(),
|
name: profile.name.into(),
|
||||||
tools: profile_settings.tools,
|
tools: profile.tools,
|
||||||
enable_all_context_servers: Some(profile_settings.enable_all_context_servers),
|
enable_all_context_servers: Some(profile.enable_all_context_servers),
|
||||||
context_servers: profile_settings
|
context_servers: profile
|
||||||
.context_servers
|
.context_servers
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(server_id, preset)| {
|
.map(|(server_id, preset)| {
|
||||||
@@ -910,7 +910,7 @@ impl Settings for AgentSettings {
|
|||||||
.extend(profiles.into_iter().map(|(id, profile)| {
|
.extend(profiles.into_iter().map(|(id, profile)| {
|
||||||
(
|
(
|
||||||
id,
|
id,
|
||||||
AgentProfileSettings {
|
AgentProfile {
|
||||||
name: profile.name.into(),
|
name: profile.name.into(),
|
||||||
tools: profile.tools,
|
tools: profile.tools,
|
||||||
enable_all_context_servers: profile
|
enable_all_context_servers: profile
|
||||||
|
|||||||
@@ -809,37 +809,74 @@ impl ContextStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn register_context_server_handlers(&self, cx: &mut Context<Self>) {
|
fn register_context_server_handlers(&self, cx: &mut Context<Self>) {
|
||||||
let context_server_store = self.project.read(cx).context_server_store();
|
cx.subscribe(
|
||||||
cx.subscribe(&context_server_store, Self::handle_context_server_event)
|
&self.project.read(cx).context_server_store(),
|
||||||
.detach();
|
Self::handle_context_server_event,
|
||||||
|
)
|
||||||
// Check for any servers that were already running before the handler was registered
|
.detach();
|
||||||
for server in context_server_store.read(cx).running_servers() {
|
|
||||||
self.load_context_server_slash_commands(server.id(), context_server_store.clone(), cx);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_context_server_event(
|
fn handle_context_server_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
context_server_store: Entity<ContextServerStore>,
|
context_server_manager: Entity<ContextServerStore>,
|
||||||
event: &project::context_server_store::Event,
|
event: &project::context_server_store::Event,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
|
let slash_command_working_set = self.slash_commands.clone();
|
||||||
match event {
|
match event {
|
||||||
project::context_server_store::Event::ServerStatusChanged { server_id, status } => {
|
project::context_server_store::Event::ServerStatusChanged { server_id, status } => {
|
||||||
match status {
|
match status {
|
||||||
ContextServerStatus::Running => {
|
ContextServerStatus::Running => {
|
||||||
self.load_context_server_slash_commands(
|
if let Some(server) = context_server_manager
|
||||||
server_id.clone(),
|
.read(cx)
|
||||||
context_server_store.clone(),
|
.get_running_server(server_id)
|
||||||
cx,
|
{
|
||||||
);
|
let context_server_manager = context_server_manager.clone();
|
||||||
|
cx.spawn({
|
||||||
|
let server = server.clone();
|
||||||
|
let server_id = server_id.clone();
|
||||||
|
async move |this, cx| {
|
||||||
|
let Some(protocol) = server.client() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if protocol.capable(context_server::protocol::ServerCapability::Prompts) {
|
||||||
|
if let Some(prompts) = protocol.list_prompts().await.log_err() {
|
||||||
|
let slash_command_ids = prompts
|
||||||
|
.into_iter()
|
||||||
|
.filter(assistant_slash_commands::acceptable_prompt)
|
||||||
|
.map(|prompt| {
|
||||||
|
log::info!(
|
||||||
|
"registering context server command: {:?}",
|
||||||
|
prompt.name
|
||||||
|
);
|
||||||
|
slash_command_working_set.insert(Arc::new(
|
||||||
|
assistant_slash_commands::ContextServerSlashCommand::new(
|
||||||
|
context_server_manager.clone(),
|
||||||
|
server.id(),
|
||||||
|
prompt,
|
||||||
|
),
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
this.update( cx, |this, _cx| {
|
||||||
|
this.context_server_slash_command_ids
|
||||||
|
.insert(server_id.clone(), slash_command_ids);
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ContextServerStatus::Stopped | ContextServerStatus::Error(_) => {
|
ContextServerStatus::Stopped | ContextServerStatus::Error(_) => {
|
||||||
if let Some(slash_command_ids) =
|
if let Some(slash_command_ids) =
|
||||||
self.context_server_slash_command_ids.remove(server_id)
|
self.context_server_slash_command_ids.remove(server_id)
|
||||||
{
|
{
|
||||||
self.slash_commands.remove(&slash_command_ids);
|
slash_command_working_set.remove(&slash_command_ids);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -847,52 +884,4 @@ impl ContextStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_context_server_slash_commands(
|
|
||||||
&self,
|
|
||||||
server_id: ContextServerId,
|
|
||||||
context_server_store: Entity<ContextServerStore>,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
let Some(server) = context_server_store.read(cx).get_running_server(&server_id) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let slash_command_working_set = self.slash_commands.clone();
|
|
||||||
cx.spawn(async move |this, cx| {
|
|
||||||
let Some(protocol) = server.client() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
if protocol.capable(context_server::protocol::ServerCapability::Prompts) {
|
|
||||||
if let Some(response) = protocol
|
|
||||||
.request::<context_server::types::request::PromptsList>(())
|
|
||||||
.await
|
|
||||||
.log_err()
|
|
||||||
{
|
|
||||||
let slash_command_ids = response
|
|
||||||
.prompts
|
|
||||||
.into_iter()
|
|
||||||
.filter(assistant_slash_commands::acceptable_prompt)
|
|
||||||
.map(|prompt| {
|
|
||||||
log::info!("registering context server command: {:?}", prompt.name);
|
|
||||||
slash_command_working_set.insert(Arc::new(
|
|
||||||
assistant_slash_commands::ContextServerSlashCommand::new(
|
|
||||||
context_server_store.clone(),
|
|
||||||
server.id(),
|
|
||||||
prompt,
|
|
||||||
),
|
|
||||||
))
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
this.update(cx, |this, _cx| {
|
|
||||||
this.context_server_slash_command_ids
|
|
||||||
.insert(server_id.clone(), slash_command_ids);
|
|
||||||
})
|
|
||||||
.log_err();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ use parking_lot::Mutex;
|
|||||||
use project::{CompletionIntent, CompletionSource, lsp_store::CompletionDocumentation};
|
use project::{CompletionIntent, CompletionSource, lsp_store::CompletionDocumentation};
|
||||||
use rope::Point;
|
use rope::Point;
|
||||||
use std::{
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
ops::Range,
|
ops::Range,
|
||||||
|
rc::Rc,
|
||||||
sync::{
|
sync::{
|
||||||
Arc,
|
Arc,
|
||||||
atomic::{AtomicBool, Ordering::SeqCst},
|
atomic::{AtomicBool, Ordering::SeqCst},
|
||||||
@@ -238,14 +240,13 @@ impl SlashCommandCompletionProvider {
|
|||||||
|
|
||||||
Ok(vec![project::CompletionResponse {
|
Ok(vec![project::CompletionResponse {
|
||||||
completions,
|
completions,
|
||||||
// TODO: Could have slash commands indicate whether their completions are incomplete.
|
is_incomplete: false,
|
||||||
is_incomplete: true,
|
|
||||||
}])
|
}])
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
Task::ready(Ok(vec![project::CompletionResponse {
|
Task::ready(Ok(vec![project::CompletionResponse {
|
||||||
completions: Vec::new(),
|
completions: Vec::new(),
|
||||||
is_incomplete: true,
|
is_incomplete: false,
|
||||||
}]))
|
}]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -274,17 +275,17 @@ impl CompletionProvider for SlashCommandCompletionProvider {
|
|||||||
position.row,
|
position.row,
|
||||||
call.arguments.last().map_or(call.name.end, |arg| arg.end) as u32,
|
call.arguments.last().map_or(call.name.end, |arg| arg.end) as u32,
|
||||||
);
|
);
|
||||||
let command_range = buffer.anchor_before(command_range_start)
|
let command_range = buffer.anchor_after(command_range_start)
|
||||||
..buffer.anchor_after(command_range_end);
|
..buffer.anchor_after(command_range_end);
|
||||||
|
|
||||||
let name = line[call.name.clone()].to_string();
|
let name = line[call.name.clone()].to_string();
|
||||||
let (arguments, last_argument_range) = if let Some(argument) = call.arguments.last()
|
let (arguments, last_argument_range) = if let Some(argument) = call.arguments.last()
|
||||||
{
|
{
|
||||||
let last_arg_start =
|
let last_arg_start =
|
||||||
buffer.anchor_before(Point::new(position.row, argument.start as u32));
|
buffer.anchor_after(Point::new(position.row, argument.start as u32));
|
||||||
let first_arg_start = call.arguments.first().expect("we have the last element");
|
let first_arg_start = call.arguments.first().expect("we have the last element");
|
||||||
let first_arg_start = buffer
|
let first_arg_start =
|
||||||
.anchor_before(Point::new(position.row, first_arg_start.start as u32));
|
buffer.anchor_after(Point::new(position.row, first_arg_start.start as u32));
|
||||||
let arguments = call
|
let arguments = call
|
||||||
.arguments
|
.arguments
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -297,7 +298,7 @@ impl CompletionProvider for SlashCommandCompletionProvider {
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
let start =
|
let start =
|
||||||
buffer.anchor_before(Point::new(position.row, call.name.start as u32));
|
buffer.anchor_after(Point::new(position.row, call.name.start as u32));
|
||||||
(None, start..buffer_position)
|
(None, start..buffer_position)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -325,13 +326,22 @@ impl CompletionProvider for SlashCommandCompletionProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn resolve_completions(
|
||||||
|
&self,
|
||||||
|
_: Entity<Buffer>,
|
||||||
|
_: Vec<usize>,
|
||||||
|
_: Rc<RefCell<Box<[project::Completion]>>>,
|
||||||
|
_: &mut Context<Editor>,
|
||||||
|
) -> Task<Result<bool>> {
|
||||||
|
Task::ready(Ok(true))
|
||||||
|
}
|
||||||
|
|
||||||
fn is_completion_trigger(
|
fn is_completion_trigger(
|
||||||
&self,
|
&self,
|
||||||
buffer: &Entity<Buffer>,
|
buffer: &Entity<Buffer>,
|
||||||
position: language::Anchor,
|
position: language::Anchor,
|
||||||
_text: &str,
|
_text: &str,
|
||||||
_trigger_in_words: bool,
|
_trigger_in_words: bool,
|
||||||
_menu_is_open: bool,
|
|
||||||
cx: &mut Context<Editor>,
|
cx: &mut Context<Editor>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let buffer = buffer.read(cx);
|
let buffer = buffer.read(cx);
|
||||||
|
|||||||
@@ -86,26 +86,20 @@ impl SlashCommand for ContextServerSlashCommand {
|
|||||||
cx.foreground_executor().spawn(async move {
|
cx.foreground_executor().spawn(async move {
|
||||||
let protocol = server.client().context("Context server not initialized")?;
|
let protocol = server.client().context("Context server not initialized")?;
|
||||||
|
|
||||||
let response = protocol
|
let completion_result = protocol
|
||||||
.request::<context_server::types::request::CompletionComplete>(
|
.completion(
|
||||||
context_server::types::CompletionCompleteParams {
|
context_server::types::CompletionReference::Prompt(
|
||||||
reference: context_server::types::CompletionReference::Prompt(
|
context_server::types::PromptReference {
|
||||||
context_server::types::PromptReference {
|
r#type: context_server::types::PromptReferenceType::Prompt,
|
||||||
ty: context_server::types::PromptReferenceType::Prompt,
|
name: prompt_name,
|
||||||
name: prompt_name,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
argument: context_server::types::CompletionArgument {
|
|
||||||
name: arg_name,
|
|
||||||
value: arg_value,
|
|
||||||
},
|
},
|
||||||
meta: None,
|
),
|
||||||
},
|
arg_name,
|
||||||
|
arg_value,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let completions = response
|
let completions = completion_result
|
||||||
.completion
|
|
||||||
.values
|
.values
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|value| ArgumentCompletion {
|
.map(|value| ArgumentCompletion {
|
||||||
@@ -144,18 +138,10 @@ impl SlashCommand for ContextServerSlashCommand {
|
|||||||
if let Some(server) = store.get_running_server(&server_id) {
|
if let Some(server) = store.get_running_server(&server_id) {
|
||||||
cx.foreground_executor().spawn(async move {
|
cx.foreground_executor().spawn(async move {
|
||||||
let protocol = server.client().context("Context server not initialized")?;
|
let protocol = server.client().context("Context server not initialized")?;
|
||||||
let response = protocol
|
let result = protocol.run_prompt(&prompt_name, prompt_args).await?;
|
||||||
.request::<context_server::types::request::PromptsGet>(
|
|
||||||
context_server::types::PromptsGetParams {
|
|
||||||
name: prompt_name.clone(),
|
|
||||||
arguments: Some(prompt_args),
|
|
||||||
meta: None,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
anyhow::ensure!(
|
anyhow::ensure!(
|
||||||
response
|
result
|
||||||
.messages
|
.messages
|
||||||
.iter()
|
.iter()
|
||||||
.all(|msg| matches!(msg.role, context_server::types::Role::User)),
|
.all(|msg| matches!(msg.role, context_server::types::Role::User)),
|
||||||
@@ -163,7 +149,7 @@ impl SlashCommand for ContextServerSlashCommand {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Extract text from user messages into a single prompt string
|
// Extract text from user messages into a single prompt string
|
||||||
let mut prompt = response
|
let mut prompt = result
|
||||||
.messages
|
.messages
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|msg| match msg.content {
|
.filter_map(|msg| match msg.content {
|
||||||
@@ -181,7 +167,7 @@ impl SlashCommand for ContextServerSlashCommand {
|
|||||||
range: 0..(prompt.len()),
|
range: 0..(prompt.len()),
|
||||||
icon: IconName::ZedAssistant,
|
icon: IconName::ZedAssistant,
|
||||||
label: SharedString::from(
|
label: SharedString::from(
|
||||||
response
|
result
|
||||||
.description
|
.description
|
||||||
.unwrap_or(format!("Result from {}", prompt_name)),
|
.unwrap_or(format!("Result from {}", prompt_name)),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ serde.workspace = true
|
|||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
text.workspace = true
|
text.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
watch.workspace = true
|
|
||||||
workspace.workspace = true
|
workspace.workspace = true
|
||||||
workspace-hack.workspace = true
|
workspace-hack.workspace = true
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result};
|
||||||
use buffer_diff::BufferDiff;
|
use buffer_diff::BufferDiff;
|
||||||
use collections::BTreeMap;
|
use collections::BTreeMap;
|
||||||
use futures::{FutureExt, StreamExt, channel::mpsc};
|
use futures::{StreamExt, channel::mpsc};
|
||||||
use gpui::{App, AppContext, AsyncApp, Context, Entity, Subscription, Task, WeakEntity};
|
use gpui::{App, AppContext, AsyncApp, Context, Entity, Subscription, Task, WeakEntity};
|
||||||
use language::{Anchor, Buffer, BufferEvent, DiskState, Point, ToPoint};
|
use language::{Anchor, Buffer, BufferEvent, DiskState, Point, ToPoint};
|
||||||
use project::{Project, ProjectItem, lsp_store::OpenLspBufferHandle};
|
use project::{Project, ProjectItem, lsp_store::OpenLspBufferHandle};
|
||||||
@@ -92,21 +92,21 @@ impl ActionLog {
|
|||||||
let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
|
let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
|
||||||
let (diff_update_tx, diff_update_rx) = mpsc::unbounded();
|
let (diff_update_tx, diff_update_rx) = mpsc::unbounded();
|
||||||
let diff_base;
|
let diff_base;
|
||||||
let unreviewed_edits;
|
let unreviewed_changes;
|
||||||
if is_created {
|
if is_created {
|
||||||
diff_base = Rope::default();
|
diff_base = Rope::default();
|
||||||
unreviewed_edits = Patch::new(vec![Edit {
|
unreviewed_changes = Patch::new(vec![Edit {
|
||||||
old: 0..1,
|
old: 0..1,
|
||||||
new: 0..text_snapshot.max_point().row + 1,
|
new: 0..text_snapshot.max_point().row + 1,
|
||||||
}])
|
}])
|
||||||
} else {
|
} else {
|
||||||
diff_base = buffer.read(cx).as_rope().clone();
|
diff_base = buffer.read(cx).as_rope().clone();
|
||||||
unreviewed_edits = Patch::default();
|
unreviewed_changes = Patch::default();
|
||||||
}
|
}
|
||||||
TrackedBuffer {
|
TrackedBuffer {
|
||||||
buffer: buffer.clone(),
|
buffer: buffer.clone(),
|
||||||
diff_base,
|
diff_base,
|
||||||
unreviewed_edits: unreviewed_edits,
|
unreviewed_changes,
|
||||||
snapshot: text_snapshot.clone(),
|
snapshot: text_snapshot.clone(),
|
||||||
status,
|
status,
|
||||||
version: buffer.read(cx).version(),
|
version: buffer.read(cx).version(),
|
||||||
@@ -175,7 +175,7 @@ impl ActionLog {
|
|||||||
.map_or(false, |file| file.disk_state() != DiskState::Deleted)
|
.map_or(false, |file| file.disk_state() != DiskState::Deleted)
|
||||||
{
|
{
|
||||||
// If the buffer had been deleted by a tool, but it got
|
// If the buffer had been deleted by a tool, but it got
|
||||||
// resurrected externally, we want to clear the edits we
|
// resurrected externally, we want to clear the changes we
|
||||||
// were tracking and reset the buffer's state.
|
// were tracking and reset the buffer's state.
|
||||||
self.tracked_buffers.remove(&buffer);
|
self.tracked_buffers.remove(&buffer);
|
||||||
self.track_buffer_internal(buffer, false, cx);
|
self.track_buffer_internal(buffer, false, cx);
|
||||||
@@ -188,274 +188,108 @@ impl ActionLog {
|
|||||||
async fn maintain_diff(
|
async fn maintain_diff(
|
||||||
this: WeakEntity<Self>,
|
this: WeakEntity<Self>,
|
||||||
buffer: Entity<Buffer>,
|
buffer: Entity<Buffer>,
|
||||||
mut buffer_updates: mpsc::UnboundedReceiver<(ChangeAuthor, text::BufferSnapshot)>,
|
mut diff_update: mpsc::UnboundedReceiver<(ChangeAuthor, text::BufferSnapshot)>,
|
||||||
cx: &mut AsyncApp,
|
cx: &mut AsyncApp,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let git_store = this.read_with(cx, |this, cx| this.project.read(cx).git_store().clone())?;
|
while let Some((author, buffer_snapshot)) = diff_update.next().await {
|
||||||
let git_diff = this
|
let (rebase, diff, language, language_registry) =
|
||||||
.update(cx, |this, cx| {
|
this.read_with(cx, |this, cx| {
|
||||||
this.project.update(cx, |project, cx| {
|
let tracked_buffer = this
|
||||||
project.open_uncommitted_diff(buffer.clone(), cx)
|
.tracked_buffers
|
||||||
})
|
.get(&buffer)
|
||||||
})?
|
.context("buffer not tracked")?;
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
let buffer_repo = git_store.read_with(cx, |git_store, cx| {
|
|
||||||
git_store.repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let (mut git_diff_updates_tx, mut git_diff_updates_rx) = watch::channel(());
|
let rebase = cx.background_spawn({
|
||||||
let _repo_subscription =
|
let mut base_text = tracked_buffer.diff_base.clone();
|
||||||
if let Some((git_diff, (buffer_repo, _))) = git_diff.as_ref().zip(buffer_repo) {
|
let old_snapshot = tracked_buffer.snapshot.clone();
|
||||||
cx.update(|cx| {
|
let new_snapshot = buffer_snapshot.clone();
|
||||||
let mut old_head = buffer_repo.read(cx).head_commit.clone();
|
let unreviewed_changes = tracked_buffer.unreviewed_changes.clone();
|
||||||
Some(cx.subscribe(git_diff, move |_, event, cx| match event {
|
async move {
|
||||||
buffer_diff::BufferDiffEvent::DiffChanged { .. } => {
|
let edits = diff_snapshots(&old_snapshot, &new_snapshot);
|
||||||
let new_head = buffer_repo.read(cx).head_commit.clone();
|
if let ChangeAuthor::User = author {
|
||||||
if new_head != old_head {
|
apply_non_conflicting_edits(
|
||||||
old_head = new_head;
|
&unreviewed_changes,
|
||||||
git_diff_updates_tx.send(()).ok();
|
edits,
|
||||||
|
&mut base_text,
|
||||||
|
new_snapshot.as_rope(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
(Arc::new(base_text.to_string()), base_text)
|
||||||
}
|
}
|
||||||
_ => {}
|
});
|
||||||
}))
|
|
||||||
})?
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
loop {
|
anyhow::Ok((
|
||||||
futures::select_biased! {
|
rebase,
|
||||||
buffer_update = buffer_updates.next() => {
|
tracked_buffer.diff.clone(),
|
||||||
if let Some((author, buffer_snapshot)) = buffer_update {
|
tracked_buffer.buffer.read(cx).language().cloned(),
|
||||||
Self::track_edits(&this, &buffer, author, buffer_snapshot, cx).await?;
|
tracked_buffer.buffer.read(cx).language_registry(),
|
||||||
} else {
|
))
|
||||||
break;
|
})??;
|
||||||
}
|
|
||||||
}
|
let (new_base_text, new_diff_base) = rebase.await;
|
||||||
_ = git_diff_updates_rx.changed().fuse() => {
|
let diff_snapshot = BufferDiff::update_diff(
|
||||||
if let Some(git_diff) = git_diff.as_ref() {
|
diff.clone(),
|
||||||
Self::keep_committed_edits(&this, &buffer, &git_diff, cx).await?;
|
buffer_snapshot.clone(),
|
||||||
}
|
Some(new_base_text),
|
||||||
}
|
true,
|
||||||
|
false,
|
||||||
|
language,
|
||||||
|
language_registry,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let mut unreviewed_changes = Patch::default();
|
||||||
|
if let Ok(diff_snapshot) = diff_snapshot {
|
||||||
|
unreviewed_changes = cx
|
||||||
|
.background_spawn({
|
||||||
|
let diff_snapshot = diff_snapshot.clone();
|
||||||
|
let buffer_snapshot = buffer_snapshot.clone();
|
||||||
|
let new_diff_base = new_diff_base.clone();
|
||||||
|
async move {
|
||||||
|
let mut unreviewed_changes = Patch::default();
|
||||||
|
for hunk in diff_snapshot.hunks_intersecting_range(
|
||||||
|
Anchor::MIN..Anchor::MAX,
|
||||||
|
&buffer_snapshot,
|
||||||
|
) {
|
||||||
|
let old_range = new_diff_base
|
||||||
|
.offset_to_point(hunk.diff_base_byte_range.start)
|
||||||
|
..new_diff_base.offset_to_point(hunk.diff_base_byte_range.end);
|
||||||
|
let new_range = hunk.range.start..hunk.range.end;
|
||||||
|
unreviewed_changes.push(point_to_row_edit(
|
||||||
|
Edit {
|
||||||
|
old: old_range,
|
||||||
|
new: new_range,
|
||||||
|
},
|
||||||
|
&new_diff_base,
|
||||||
|
&buffer_snapshot.as_rope(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
unreviewed_changes
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
diff.update(cx, |diff, cx| {
|
||||||
|
diff.set_snapshot(diff_snapshot, &buffer_snapshot, cx)
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
let tracked_buffer = this
|
||||||
|
.tracked_buffers
|
||||||
|
.get_mut(&buffer)
|
||||||
|
.context("buffer not tracked")?;
|
||||||
|
tracked_buffer.diff_base = new_diff_base;
|
||||||
|
tracked_buffer.snapshot = buffer_snapshot;
|
||||||
|
tracked_buffer.unreviewed_changes = unreviewed_changes;
|
||||||
|
cx.notify();
|
||||||
|
anyhow::Ok(())
|
||||||
|
})??;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn track_edits(
|
|
||||||
this: &WeakEntity<ActionLog>,
|
|
||||||
buffer: &Entity<Buffer>,
|
|
||||||
author: ChangeAuthor,
|
|
||||||
buffer_snapshot: text::BufferSnapshot,
|
|
||||||
cx: &mut AsyncApp,
|
|
||||||
) -> Result<()> {
|
|
||||||
let rebase = this.read_with(cx, |this, cx| {
|
|
||||||
let tracked_buffer = this
|
|
||||||
.tracked_buffers
|
|
||||||
.get(buffer)
|
|
||||||
.context("buffer not tracked")?;
|
|
||||||
|
|
||||||
let rebase = cx.background_spawn({
|
|
||||||
let mut base_text = tracked_buffer.diff_base.clone();
|
|
||||||
let old_snapshot = tracked_buffer.snapshot.clone();
|
|
||||||
let new_snapshot = buffer_snapshot.clone();
|
|
||||||
let unreviewed_edits = tracked_buffer.unreviewed_edits.clone();
|
|
||||||
async move {
|
|
||||||
let edits = diff_snapshots(&old_snapshot, &new_snapshot);
|
|
||||||
if let ChangeAuthor::User = author {
|
|
||||||
apply_non_conflicting_edits(
|
|
||||||
&unreviewed_edits,
|
|
||||||
edits,
|
|
||||||
&mut base_text,
|
|
||||||
new_snapshot.as_rope(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
(Arc::new(base_text.to_string()), base_text)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
anyhow::Ok(rebase)
|
|
||||||
})??;
|
|
||||||
let (new_base_text, new_diff_base) = rebase.await;
|
|
||||||
Self::update_diff(
|
|
||||||
this,
|
|
||||||
buffer,
|
|
||||||
buffer_snapshot,
|
|
||||||
new_base_text,
|
|
||||||
new_diff_base,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn keep_committed_edits(
|
|
||||||
this: &WeakEntity<ActionLog>,
|
|
||||||
buffer: &Entity<Buffer>,
|
|
||||||
git_diff: &Entity<BufferDiff>,
|
|
||||||
cx: &mut AsyncApp,
|
|
||||||
) -> Result<()> {
|
|
||||||
let buffer_snapshot = this.read_with(cx, |this, _cx| {
|
|
||||||
let tracked_buffer = this
|
|
||||||
.tracked_buffers
|
|
||||||
.get(buffer)
|
|
||||||
.context("buffer not tracked")?;
|
|
||||||
anyhow::Ok(tracked_buffer.snapshot.clone())
|
|
||||||
})??;
|
|
||||||
let (new_base_text, new_diff_base) = this
|
|
||||||
.read_with(cx, |this, cx| {
|
|
||||||
let tracked_buffer = this
|
|
||||||
.tracked_buffers
|
|
||||||
.get(buffer)
|
|
||||||
.context("buffer not tracked")?;
|
|
||||||
let old_unreviewed_edits = tracked_buffer.unreviewed_edits.clone();
|
|
||||||
let agent_diff_base = tracked_buffer.diff_base.clone();
|
|
||||||
let git_diff_base = git_diff.read(cx).base_text().as_rope().clone();
|
|
||||||
let buffer_text = tracked_buffer.snapshot.as_rope().clone();
|
|
||||||
anyhow::Ok(cx.background_spawn(async move {
|
|
||||||
let mut old_unreviewed_edits = old_unreviewed_edits.into_iter().peekable();
|
|
||||||
let committed_edits = language::line_diff(
|
|
||||||
&agent_diff_base.to_string(),
|
|
||||||
&git_diff_base.to_string(),
|
|
||||||
)
|
|
||||||
.into_iter()
|
|
||||||
.map(|(old, new)| Edit { old, new });
|
|
||||||
|
|
||||||
let mut new_agent_diff_base = agent_diff_base.clone();
|
|
||||||
let mut row_delta = 0i32;
|
|
||||||
for committed in committed_edits {
|
|
||||||
while let Some(unreviewed) = old_unreviewed_edits.peek() {
|
|
||||||
// If the committed edit matches the unreviewed
|
|
||||||
// edit, assume the user wants to keep it.
|
|
||||||
if committed.old == unreviewed.old {
|
|
||||||
let unreviewed_new =
|
|
||||||
buffer_text.slice_rows(unreviewed.new.clone()).to_string();
|
|
||||||
let committed_new =
|
|
||||||
git_diff_base.slice_rows(committed.new.clone()).to_string();
|
|
||||||
if unreviewed_new == committed_new {
|
|
||||||
let old_byte_start =
|
|
||||||
new_agent_diff_base.point_to_offset(Point::new(
|
|
||||||
(unreviewed.old.start as i32 + row_delta) as u32,
|
|
||||||
0,
|
|
||||||
));
|
|
||||||
let old_byte_end =
|
|
||||||
new_agent_diff_base.point_to_offset(cmp::min(
|
|
||||||
Point::new(
|
|
||||||
(unreviewed.old.end as i32 + row_delta) as u32,
|
|
||||||
0,
|
|
||||||
),
|
|
||||||
new_agent_diff_base.max_point(),
|
|
||||||
));
|
|
||||||
new_agent_diff_base
|
|
||||||
.replace(old_byte_start..old_byte_end, &unreviewed_new);
|
|
||||||
row_delta +=
|
|
||||||
unreviewed.new_len() as i32 - unreviewed.old_len() as i32;
|
|
||||||
}
|
|
||||||
} else if unreviewed.old.start >= committed.old.end {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
old_unreviewed_edits.next().unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(
|
|
||||||
Arc::new(new_agent_diff_base.to_string()),
|
|
||||||
new_agent_diff_base,
|
|
||||||
)
|
|
||||||
}))
|
|
||||||
})??
|
|
||||||
.await;
|
|
||||||
|
|
||||||
Self::update_diff(
|
|
||||||
this,
|
|
||||||
buffer,
|
|
||||||
buffer_snapshot,
|
|
||||||
new_base_text,
|
|
||||||
new_diff_base,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn update_diff(
|
|
||||||
this: &WeakEntity<ActionLog>,
|
|
||||||
buffer: &Entity<Buffer>,
|
|
||||||
buffer_snapshot: text::BufferSnapshot,
|
|
||||||
new_base_text: Arc<String>,
|
|
||||||
new_diff_base: Rope,
|
|
||||||
cx: &mut AsyncApp,
|
|
||||||
) -> Result<()> {
|
|
||||||
let (diff, language, language_registry) = this.read_with(cx, |this, cx| {
|
|
||||||
let tracked_buffer = this
|
|
||||||
.tracked_buffers
|
|
||||||
.get(buffer)
|
|
||||||
.context("buffer not tracked")?;
|
|
||||||
anyhow::Ok((
|
|
||||||
tracked_buffer.diff.clone(),
|
|
||||||
buffer.read(cx).language().cloned(),
|
|
||||||
buffer.read(cx).language_registry().clone(),
|
|
||||||
))
|
|
||||||
})??;
|
|
||||||
let diff_snapshot = BufferDiff::update_diff(
|
|
||||||
diff.clone(),
|
|
||||||
buffer_snapshot.clone(),
|
|
||||||
Some(new_base_text),
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
language,
|
|
||||||
language_registry,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
let mut unreviewed_edits = Patch::default();
|
|
||||||
if let Ok(diff_snapshot) = diff_snapshot {
|
|
||||||
unreviewed_edits = cx
|
|
||||||
.background_spawn({
|
|
||||||
let diff_snapshot = diff_snapshot.clone();
|
|
||||||
let buffer_snapshot = buffer_snapshot.clone();
|
|
||||||
let new_diff_base = new_diff_base.clone();
|
|
||||||
async move {
|
|
||||||
let mut unreviewed_edits = Patch::default();
|
|
||||||
for hunk in diff_snapshot
|
|
||||||
.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer_snapshot)
|
|
||||||
{
|
|
||||||
let old_range = new_diff_base
|
|
||||||
.offset_to_point(hunk.diff_base_byte_range.start)
|
|
||||||
..new_diff_base.offset_to_point(hunk.diff_base_byte_range.end);
|
|
||||||
let new_range = hunk.range.start..hunk.range.end;
|
|
||||||
unreviewed_edits.push(point_to_row_edit(
|
|
||||||
Edit {
|
|
||||||
old: old_range,
|
|
||||||
new: new_range,
|
|
||||||
},
|
|
||||||
&new_diff_base,
|
|
||||||
&buffer_snapshot.as_rope(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
unreviewed_edits
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
|
|
||||||
diff.update(cx, |diff, cx| {
|
|
||||||
diff.set_snapshot(diff_snapshot, &buffer_snapshot, cx);
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
let tracked_buffer = this
|
|
||||||
.tracked_buffers
|
|
||||||
.get_mut(buffer)
|
|
||||||
.context("buffer not tracked")?;
|
|
||||||
tracked_buffer.diff_base = new_diff_base;
|
|
||||||
tracked_buffer.snapshot = buffer_snapshot;
|
|
||||||
tracked_buffer.unreviewed_edits = unreviewed_edits;
|
|
||||||
cx.notify();
|
|
||||||
anyhow::Ok(())
|
|
||||||
})?
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Track a buffer as read, so we can notify the model about user edits.
|
/// Track a buffer as read, so we can notify the model about user edits.
|
||||||
pub fn buffer_read(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
|
pub fn buffer_read(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
|
||||||
self.track_buffer_internal(buffer, false, cx);
|
self.track_buffer_internal(buffer, false, cx);
|
||||||
@@ -516,7 +350,7 @@ impl ActionLog {
|
|||||||
buffer_range.start.to_point(buffer)..buffer_range.end.to_point(buffer);
|
buffer_range.start.to_point(buffer)..buffer_range.end.to_point(buffer);
|
||||||
let mut delta = 0i32;
|
let mut delta = 0i32;
|
||||||
|
|
||||||
tracked_buffer.unreviewed_edits.retain_mut(|edit| {
|
tracked_buffer.unreviewed_changes.retain_mut(|edit| {
|
||||||
edit.old.start = (edit.old.start as i32 + delta) as u32;
|
edit.old.start = (edit.old.start as i32 + delta) as u32;
|
||||||
edit.old.end = (edit.old.end as i32 + delta) as u32;
|
edit.old.end = (edit.old.end as i32 + delta) as u32;
|
||||||
|
|
||||||
@@ -627,7 +461,7 @@ impl ActionLog {
|
|||||||
.project
|
.project
|
||||||
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx));
|
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx));
|
||||||
|
|
||||||
// Clear all tracked edits for this buffer and start over as if we just read it.
|
// Clear all tracked changes for this buffer and start over as if we just read it.
|
||||||
self.tracked_buffers.remove(&buffer);
|
self.tracked_buffers.remove(&buffer);
|
||||||
self.buffer_read(buffer.clone(), cx);
|
self.buffer_read(buffer.clone(), cx);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@@ -643,7 +477,7 @@ impl ActionLog {
|
|||||||
.peekable();
|
.peekable();
|
||||||
|
|
||||||
let mut edits_to_revert = Vec::new();
|
let mut edits_to_revert = Vec::new();
|
||||||
for edit in tracked_buffer.unreviewed_edits.edits() {
|
for edit in tracked_buffer.unreviewed_changes.edits() {
|
||||||
let new_range = tracked_buffer
|
let new_range = tracked_buffer
|
||||||
.snapshot
|
.snapshot
|
||||||
.anchor_before(Point::new(edit.new.start, 0))
|
.anchor_before(Point::new(edit.new.start, 0))
|
||||||
@@ -695,7 +529,7 @@ impl ActionLog {
|
|||||||
.retain(|_buffer, tracked_buffer| match tracked_buffer.status {
|
.retain(|_buffer, tracked_buffer| match tracked_buffer.status {
|
||||||
TrackedBufferStatus::Deleted => false,
|
TrackedBufferStatus::Deleted => false,
|
||||||
_ => {
|
_ => {
|
||||||
tracked_buffer.unreviewed_edits.clear();
|
tracked_buffer.unreviewed_changes.clear();
|
||||||
tracked_buffer.diff_base = tracked_buffer.snapshot.as_rope().clone();
|
tracked_buffer.diff_base = tracked_buffer.snapshot.as_rope().clone();
|
||||||
tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
|
tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
|
||||||
true
|
true
|
||||||
@@ -704,11 +538,11 @@ impl ActionLog {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the set of buffers that contain edits that haven't been reviewed by the user.
|
/// Returns the set of buffers that contain changes that haven't been reviewed by the user.
|
||||||
pub fn changed_buffers(&self, cx: &App) -> BTreeMap<Entity<Buffer>, Entity<BufferDiff>> {
|
pub fn changed_buffers(&self, cx: &App) -> BTreeMap<Entity<Buffer>, Entity<BufferDiff>> {
|
||||||
self.tracked_buffers
|
self.tracked_buffers
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(_, tracked)| tracked.has_edits(cx))
|
.filter(|(_, tracked)| tracked.has_changes(cx))
|
||||||
.map(|(buffer, tracked)| (buffer.clone(), tracked.diff.clone()))
|
.map(|(buffer, tracked)| (buffer.clone(), tracked.diff.clone()))
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
@@ -828,7 +662,11 @@ fn point_to_row_edit(edit: Edit<Point>, old_text: &Rope, new_text: &Rope) -> Edi
|
|||||||
old: edit.old.start.row + 1..edit.old.end.row + 1,
|
old: edit.old.start.row + 1..edit.old.end.row + 1,
|
||||||
new: edit.new.start.row + 1..edit.new.end.row + 1,
|
new: edit.new.start.row + 1..edit.new.end.row + 1,
|
||||||
}
|
}
|
||||||
} else if edit.old.start.column == 0 && edit.old.end.column == 0 && edit.new.end.column == 0 {
|
} else if edit.old.start.column == 0
|
||||||
|
&& edit.old.end.column == 0
|
||||||
|
&& edit.new.end.column == 0
|
||||||
|
&& edit.old.end != old_text.max_point()
|
||||||
|
{
|
||||||
Edit {
|
Edit {
|
||||||
old: edit.old.start.row..edit.old.end.row,
|
old: edit.old.start.row..edit.old.end.row,
|
||||||
new: edit.new.start.row..edit.new.end.row,
|
new: edit.new.start.row..edit.new.end.row,
|
||||||
@@ -856,7 +694,7 @@ enum TrackedBufferStatus {
|
|||||||
struct TrackedBuffer {
|
struct TrackedBuffer {
|
||||||
buffer: Entity<Buffer>,
|
buffer: Entity<Buffer>,
|
||||||
diff_base: Rope,
|
diff_base: Rope,
|
||||||
unreviewed_edits: Patch<u32>,
|
unreviewed_changes: Patch<u32>,
|
||||||
status: TrackedBufferStatus,
|
status: TrackedBufferStatus,
|
||||||
version: clock::Global,
|
version: clock::Global,
|
||||||
diff: Entity<BufferDiff>,
|
diff: Entity<BufferDiff>,
|
||||||
@@ -868,7 +706,7 @@ struct TrackedBuffer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TrackedBuffer {
|
impl TrackedBuffer {
|
||||||
fn has_edits(&self, cx: &App) -> bool {
|
fn has_changes(&self, cx: &App) -> bool {
|
||||||
self.diff
|
self.diff
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.hunks(&self.buffer.read(cx), cx)
|
.hunks(&self.buffer.read(cx), cx)
|
||||||
@@ -889,6 +727,8 @@ pub struct ChangedBuffer {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use std::env;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use buffer_diff::DiffHunkStatusKind;
|
use buffer_diff::DiffHunkStatusKind;
|
||||||
use gpui::TestAppContext;
|
use gpui::TestAppContext;
|
||||||
@@ -897,7 +737,6 @@ mod tests {
|
|||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
use std::env;
|
|
||||||
use util::{RandomCharIter, path};
|
use util::{RandomCharIter, path};
|
||||||
|
|
||||||
#[ctor::ctor]
|
#[ctor::ctor]
|
||||||
@@ -1912,15 +1751,15 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let is_agent_edit = rng.gen_bool(0.5);
|
let is_agent_change = rng.gen_bool(0.5);
|
||||||
if is_agent_edit {
|
if is_agent_change {
|
||||||
log::info!("agent edit");
|
log::info!("agent edit");
|
||||||
} else {
|
} else {
|
||||||
log::info!("user edit");
|
log::info!("user edit");
|
||||||
}
|
}
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
buffer.update(cx, |buffer, cx| buffer.randomly_edit(&mut rng, 1, cx));
|
buffer.update(cx, |buffer, cx| buffer.randomly_edit(&mut rng, 1, cx));
|
||||||
if is_agent_edit {
|
if is_agent_change {
|
||||||
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
|
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1945,7 +1784,7 @@ mod tests {
|
|||||||
let tracked_buffer = log.tracked_buffers.get(&buffer).unwrap();
|
let tracked_buffer = log.tracked_buffers.get(&buffer).unwrap();
|
||||||
let mut old_text = tracked_buffer.diff_base.clone();
|
let mut old_text = tracked_buffer.diff_base.clone();
|
||||||
let new_text = buffer.read(cx).as_rope();
|
let new_text = buffer.read(cx).as_rope();
|
||||||
for edit in tracked_buffer.unreviewed_edits.edits() {
|
for edit in tracked_buffer.unreviewed_changes.edits() {
|
||||||
let old_start = old_text.point_to_offset(Point::new(edit.new.start, 0));
|
let old_start = old_text.point_to_offset(Point::new(edit.new.start, 0));
|
||||||
let old_end = old_text.point_to_offset(cmp::min(
|
let old_end = old_text.point_to_offset(cmp::min(
|
||||||
Point::new(edit.new.start + edit.old_len(), 0),
|
Point::new(edit.new.start + edit.old_len(), 0),
|
||||||
@@ -1961,171 +1800,6 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_keep_edits_on_commit(cx: &mut gpui::TestAppContext) {
|
|
||||||
init_test(cx);
|
|
||||||
|
|
||||||
let fs = FakeFs::new(cx.background_executor.clone());
|
|
||||||
fs.insert_tree(
|
|
||||||
path!("/project"),
|
|
||||||
json!({
|
|
||||||
".git": {},
|
|
||||||
"file.txt": "a\nb\nc\nd\ne\nf\ng\nh\ni\nj",
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
fs.set_head_for_repo(
|
|
||||||
path!("/project/.git").as_ref(),
|
|
||||||
&[("file.txt".into(), "a\nb\nc\nd\ne\nf\ng\nh\ni\nj".into())],
|
|
||||||
"0000000",
|
|
||||||
);
|
|
||||||
cx.run_until_parked();
|
|
||||||
|
|
||||||
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
|
|
||||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
|
||||||
|
|
||||||
let file_path = project
|
|
||||||
.read_with(cx, |project, cx| {
|
|
||||||
project.find_project_path(path!("/project/file.txt"), cx)
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
let buffer = project
|
|
||||||
.update(cx, |project, cx| project.open_buffer(file_path, cx))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
cx.update(|cx| {
|
|
||||||
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
|
|
||||||
buffer.update(cx, |buffer, cx| {
|
|
||||||
buffer.edit(
|
|
||||||
[
|
|
||||||
// Edit at the very start: a -> A
|
|
||||||
(Point::new(0, 0)..Point::new(0, 1), "A"),
|
|
||||||
// Deletion in the middle: remove lines d and e
|
|
||||||
(Point::new(3, 0)..Point::new(5, 0), ""),
|
|
||||||
// Modification: g -> GGG
|
|
||||||
(Point::new(6, 0)..Point::new(6, 1), "GGG"),
|
|
||||||
// Addition: insert new line after h
|
|
||||||
(Point::new(7, 1)..Point::new(7, 1), "\nNEW"),
|
|
||||||
// Edit the very last character: j -> J
|
|
||||||
(Point::new(9, 0)..Point::new(9, 1), "J"),
|
|
||||||
],
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
|
|
||||||
});
|
|
||||||
cx.run_until_parked();
|
|
||||||
assert_eq!(
|
|
||||||
unreviewed_hunks(&action_log, cx),
|
|
||||||
vec![(
|
|
||||||
buffer.clone(),
|
|
||||||
vec![
|
|
||||||
HunkStatus {
|
|
||||||
range: Point::new(0, 0)..Point::new(1, 0),
|
|
||||||
diff_status: DiffHunkStatusKind::Modified,
|
|
||||||
old_text: "a\n".into()
|
|
||||||
},
|
|
||||||
HunkStatus {
|
|
||||||
range: Point::new(3, 0)..Point::new(3, 0),
|
|
||||||
diff_status: DiffHunkStatusKind::Deleted,
|
|
||||||
old_text: "d\ne\n".into()
|
|
||||||
},
|
|
||||||
HunkStatus {
|
|
||||||
range: Point::new(4, 0)..Point::new(5, 0),
|
|
||||||
diff_status: DiffHunkStatusKind::Modified,
|
|
||||||
old_text: "g\n".into()
|
|
||||||
},
|
|
||||||
HunkStatus {
|
|
||||||
range: Point::new(6, 0)..Point::new(7, 0),
|
|
||||||
diff_status: DiffHunkStatusKind::Added,
|
|
||||||
old_text: "".into()
|
|
||||||
},
|
|
||||||
HunkStatus {
|
|
||||||
range: Point::new(8, 0)..Point::new(8, 1),
|
|
||||||
diff_status: DiffHunkStatusKind::Modified,
|
|
||||||
old_text: "j".into()
|
|
||||||
}
|
|
||||||
]
|
|
||||||
)]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Simulate a git commit that matches some edits but not others:
|
|
||||||
// - Accepts the first edit (a -> A)
|
|
||||||
// - Accepts the deletion (remove d and e)
|
|
||||||
// - Makes a different change to g (g -> G instead of GGG)
|
|
||||||
// - Ignores the NEW line addition
|
|
||||||
// - Ignores the last line edit (j stays as j)
|
|
||||||
fs.set_head_for_repo(
|
|
||||||
path!("/project/.git").as_ref(),
|
|
||||||
&[("file.txt".into(), "A\nb\nc\nf\nG\nh\ni\nj".into())],
|
|
||||||
"0000001",
|
|
||||||
);
|
|
||||||
cx.run_until_parked();
|
|
||||||
assert_eq!(
|
|
||||||
unreviewed_hunks(&action_log, cx),
|
|
||||||
vec![(
|
|
||||||
buffer.clone(),
|
|
||||||
vec![
|
|
||||||
HunkStatus {
|
|
||||||
range: Point::new(4, 0)..Point::new(5, 0),
|
|
||||||
diff_status: DiffHunkStatusKind::Modified,
|
|
||||||
old_text: "g\n".into()
|
|
||||||
},
|
|
||||||
HunkStatus {
|
|
||||||
range: Point::new(6, 0)..Point::new(7, 0),
|
|
||||||
diff_status: DiffHunkStatusKind::Added,
|
|
||||||
old_text: "".into()
|
|
||||||
},
|
|
||||||
HunkStatus {
|
|
||||||
range: Point::new(8, 0)..Point::new(8, 1),
|
|
||||||
diff_status: DiffHunkStatusKind::Modified,
|
|
||||||
old_text: "j".into()
|
|
||||||
}
|
|
||||||
]
|
|
||||||
)]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Make another commit that accepts the NEW line but with different content
|
|
||||||
fs.set_head_for_repo(
|
|
||||||
path!("/project/.git").as_ref(),
|
|
||||||
&[(
|
|
||||||
"file.txt".into(),
|
|
||||||
"A\nb\nc\nf\nGGG\nh\nDIFFERENT\ni\nj".into(),
|
|
||||||
)],
|
|
||||||
"0000002",
|
|
||||||
);
|
|
||||||
cx.run_until_parked();
|
|
||||||
assert_eq!(
|
|
||||||
unreviewed_hunks(&action_log, cx),
|
|
||||||
vec![(
|
|
||||||
buffer.clone(),
|
|
||||||
vec![
|
|
||||||
HunkStatus {
|
|
||||||
range: Point::new(6, 0)..Point::new(7, 0),
|
|
||||||
diff_status: DiffHunkStatusKind::Added,
|
|
||||||
old_text: "".into()
|
|
||||||
},
|
|
||||||
HunkStatus {
|
|
||||||
range: Point::new(8, 0)..Point::new(8, 1),
|
|
||||||
diff_status: DiffHunkStatusKind::Modified,
|
|
||||||
old_text: "j".into()
|
|
||||||
}
|
|
||||||
]
|
|
||||||
)]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Final commit that accepts all remaining edits
|
|
||||||
fs.set_head_for_repo(
|
|
||||||
path!("/project/.git").as_ref(),
|
|
||||||
&[("file.txt".into(), "A\nb\nc\nf\nGGG\nh\nNEW\ni\nJ".into())],
|
|
||||||
"0000003",
|
|
||||||
);
|
|
||||||
cx.run_until_parked();
|
|
||||||
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
struct HunkStatus {
|
struct HunkStatus {
|
||||||
range: Range<Point>,
|
range: Range<Point>,
|
||||||
|
|||||||
@@ -214,13 +214,10 @@ pub trait Tool: 'static + Send + Sync {
|
|||||||
ToolSource::Native
|
ToolSource::Native
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if the tool needs the users's confirmation
|
/// Returns true iff the tool needs the users's confirmation
|
||||||
/// before having permission to run.
|
/// before having permission to run.
|
||||||
fn needs_confirmation(&self, input: &serde_json::Value, cx: &App) -> bool;
|
fn needs_confirmation(&self, input: &serde_json::Value, cx: &App) -> bool;
|
||||||
|
|
||||||
/// Returns true if the tool may perform edits.
|
|
||||||
fn may_perform_edits(&self) -> bool;
|
|
||||||
|
|
||||||
/// Returns the JSON schema that describes the tool's input.
|
/// Returns the JSON schema that describes the tool's input.
|
||||||
fn input_schema(&self, _: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
fn input_schema(&self, _: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
||||||
Ok(serde_json::Value::Object(serde_json::Map::default()))
|
Ok(serde_json::Value::Object(serde_json::Map::default()))
|
||||||
|
|||||||
@@ -16,24 +16,11 @@ pub fn adapt_schema_to_format(
|
|||||||
}
|
}
|
||||||
|
|
||||||
match format {
|
match format {
|
||||||
LanguageModelToolSchemaFormat::JsonSchema => preprocess_json_schema(json),
|
LanguageModelToolSchemaFormat::JsonSchema => Ok(()),
|
||||||
LanguageModelToolSchemaFormat::JsonSchemaSubset => adapt_to_json_schema_subset(json),
|
LanguageModelToolSchemaFormat::JsonSchemaSubset => adapt_to_json_schema_subset(json),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn preprocess_json_schema(json: &mut Value) -> Result<()> {
|
|
||||||
// `additionalProperties` defaults to `false` unless explicitly specified.
|
|
||||||
// This prevents models from hallucinating tool parameters.
|
|
||||||
if let Value::Object(obj) = json {
|
|
||||||
if let Some(Value::String(type_str)) = obj.get("type") {
|
|
||||||
if type_str == "object" && !obj.contains_key("additionalProperties") {
|
|
||||||
obj.insert("additionalProperties".to_string(), Value::Bool(false));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Tries to adapt the json schema so that it is compatible with https://ai.google.dev/api/caching#Schema
|
/// Tries to adapt the json schema so that it is compatible with https://ai.google.dev/api/caching#Schema
|
||||||
fn adapt_to_json_schema_subset(json: &mut Value) -> Result<()> {
|
fn adapt_to_json_schema_subset(json: &mut Value) -> Result<()> {
|
||||||
if let Value::Object(obj) = json {
|
if let Value::Object(obj) = json {
|
||||||
@@ -250,59 +237,4 @@ mod tests {
|
|||||||
|
|
||||||
assert!(adapt_to_json_schema_subset(&mut json).is_err());
|
assert!(adapt_to_json_schema_subset(&mut json).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_preprocess_json_schema_adds_additional_properties() {
|
|
||||||
let mut json = json!({
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"name": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
preprocess_json_schema(&mut json).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
json,
|
|
||||||
json!({
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"name": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalProperties": false
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_preprocess_json_schema_preserves_additional_properties() {
|
|
||||||
let mut json = json!({
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"name": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalProperties": true
|
|
||||||
});
|
|
||||||
|
|
||||||
preprocess_json_schema(&mut json).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
json,
|
|
||||||
json!({
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"name": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalProperties": true
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use collections::{HashMap, IndexMap};
|
use collections::{HashMap, HashSet, IndexMap};
|
||||||
use gpui::App;
|
use gpui::{App, Context, EventEmitter};
|
||||||
|
|
||||||
use crate::{Tool, ToolRegistry, ToolSource};
|
use crate::{Tool, ToolRegistry, ToolSource};
|
||||||
|
|
||||||
@@ -13,9 +13,17 @@ pub struct ToolId(usize);
|
|||||||
pub struct ToolWorkingSet {
|
pub struct ToolWorkingSet {
|
||||||
context_server_tools_by_id: HashMap<ToolId, Arc<dyn Tool>>,
|
context_server_tools_by_id: HashMap<ToolId, Arc<dyn Tool>>,
|
||||||
context_server_tools_by_name: HashMap<String, Arc<dyn Tool>>,
|
context_server_tools_by_name: HashMap<String, Arc<dyn Tool>>,
|
||||||
|
enabled_sources: HashSet<ToolSource>,
|
||||||
|
enabled_tools_by_source: HashMap<ToolSource, HashSet<Arc<str>>>,
|
||||||
next_tool_id: ToolId,
|
next_tool_id: ToolId,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum ToolWorkingSetEvent {
|
||||||
|
EnabledToolsChanged,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<ToolWorkingSetEvent> for ToolWorkingSet {}
|
||||||
|
|
||||||
impl ToolWorkingSet {
|
impl ToolWorkingSet {
|
||||||
pub fn tool(&self, name: &str, cx: &App) -> Option<Arc<dyn Tool>> {
|
pub fn tool(&self, name: &str, cx: &App) -> Option<Arc<dyn Tool>> {
|
||||||
self.context_server_tools_by_name
|
self.context_server_tools_by_name
|
||||||
@@ -49,6 +57,42 @@ impl ToolWorkingSet {
|
|||||||
tools_by_source
|
tools_by_source
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn enabled_tools(&self, cx: &App) -> Vec<Arc<dyn Tool>> {
|
||||||
|
let all_tools = self.tools(cx);
|
||||||
|
|
||||||
|
all_tools
|
||||||
|
.into_iter()
|
||||||
|
.filter(|tool| self.is_enabled(&tool.source(), &tool.name().into()))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn disable_all_tools(&mut self, cx: &mut Context<Self>) {
|
||||||
|
self.enabled_tools_by_source.clear();
|
||||||
|
cx.emit(ToolWorkingSetEvent::EnabledToolsChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enable_source(&mut self, source: ToolSource, cx: &mut Context<Self>) {
|
||||||
|
self.enabled_sources.insert(source.clone());
|
||||||
|
|
||||||
|
let tools_by_source = self.tools_by_source(cx);
|
||||||
|
if let Some(tools) = tools_by_source.get(&source) {
|
||||||
|
self.enabled_tools_by_source.insert(
|
||||||
|
source,
|
||||||
|
tools
|
||||||
|
.into_iter()
|
||||||
|
.map(|tool| tool.name().into())
|
||||||
|
.collect::<HashSet<_>>(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
cx.emit(ToolWorkingSetEvent::EnabledToolsChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn disable_source(&mut self, source: &ToolSource, cx: &mut Context<Self>) {
|
||||||
|
self.enabled_sources.remove(source);
|
||||||
|
self.enabled_tools_by_source.remove(source);
|
||||||
|
cx.emit(ToolWorkingSetEvent::EnabledToolsChanged);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn insert(&mut self, tool: Arc<dyn Tool>) -> ToolId {
|
pub fn insert(&mut self, tool: Arc<dyn Tool>) -> ToolId {
|
||||||
let tool_id = self.next_tool_id;
|
let tool_id = self.next_tool_id;
|
||||||
self.next_tool_id.0 += 1;
|
self.next_tool_id.0 += 1;
|
||||||
@@ -58,6 +102,42 @@ impl ToolWorkingSet {
|
|||||||
tool_id
|
tool_id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_enabled(&self, source: &ToolSource, name: &Arc<str>) -> bool {
|
||||||
|
self.enabled_tools_by_source
|
||||||
|
.get(source)
|
||||||
|
.map_or(false, |enabled_tools| enabled_tools.contains(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_disabled(&self, source: &ToolSource, name: &Arc<str>) -> bool {
|
||||||
|
!self.is_enabled(source, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enable(
|
||||||
|
&mut self,
|
||||||
|
source: ToolSource,
|
||||||
|
tools_to_enable: &[Arc<str>],
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.enabled_tools_by_source
|
||||||
|
.entry(source)
|
||||||
|
.or_default()
|
||||||
|
.extend(tools_to_enable.into_iter().cloned());
|
||||||
|
cx.emit(ToolWorkingSetEvent::EnabledToolsChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn disable(
|
||||||
|
&mut self,
|
||||||
|
source: ToolSource,
|
||||||
|
tools_to_disable: &[Arc<str>],
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.enabled_tools_by_source
|
||||||
|
.entry(source)
|
||||||
|
.or_default()
|
||||||
|
.retain(|name| !tools_to_disable.contains(name));
|
||||||
|
cx.emit(ToolWorkingSetEvent::EnabledToolsChanged);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn remove(&mut self, tool_ids_to_remove: &[ToolId]) {
|
pub fn remove(&mut self, tool_ids_to_remove: &[ToolId]) {
|
||||||
self.context_server_tools_by_id
|
self.context_server_tools_by_id
|
||||||
.retain(|id, _| !tool_ids_to_remove.contains(id));
|
.retain(|id, _| !tool_ids_to_remove.contains(id));
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ eval = []
|
|||||||
agent_settings.workspace = true
|
agent_settings.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
assistant_tool.workspace = true
|
assistant_tool.workspace = true
|
||||||
|
async-watch.workspace = true
|
||||||
buffer_diff.workspace = true
|
buffer_diff.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
@@ -57,7 +58,6 @@ terminal_view.workspace = true
|
|||||||
theme.workspace = true
|
theme.workspace = true
|
||||||
ui.workspace = true
|
ui.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
watch.workspace = true
|
|
||||||
web_search.workspace = true
|
web_search.workspace = true
|
||||||
which.workspace = true
|
which.workspace = true
|
||||||
workspace-hack.workspace = true
|
workspace-hack.workspace = true
|
||||||
|
|||||||
@@ -37,13 +37,13 @@ use crate::diagnostics_tool::DiagnosticsTool;
|
|||||||
use crate::edit_file_tool::EditFileTool;
|
use crate::edit_file_tool::EditFileTool;
|
||||||
use crate::fetch_tool::FetchTool;
|
use crate::fetch_tool::FetchTool;
|
||||||
use crate::find_path_tool::FindPathTool;
|
use crate::find_path_tool::FindPathTool;
|
||||||
|
use crate::grep_tool::GrepTool;
|
||||||
use crate::list_directory_tool::ListDirectoryTool;
|
use crate::list_directory_tool::ListDirectoryTool;
|
||||||
use crate::now_tool::NowTool;
|
use crate::now_tool::NowTool;
|
||||||
use crate::thinking_tool::ThinkingTool;
|
use crate::thinking_tool::ThinkingTool;
|
||||||
|
|
||||||
pub use edit_file_tool::{EditFileMode, EditFileToolInput};
|
pub use edit_file_tool::{EditFileMode, EditFileToolInput};
|
||||||
pub use find_path_tool::FindPathToolInput;
|
pub use find_path_tool::FindPathToolInput;
|
||||||
pub use grep_tool::{GrepTool, GrepToolInput};
|
|
||||||
pub use open_tool::OpenTool;
|
pub use open_tool::OpenTool;
|
||||||
pub use read_file_tool::{ReadFileTool, ReadFileToolInput};
|
pub use read_file_tool::{ReadFileTool, ReadFileToolInput};
|
||||||
pub use terminal_tool::TerminalTool;
|
pub use terminal_tool::TerminalTool;
|
||||||
@@ -126,7 +126,6 @@ mod tests {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["location"],
|
"required": ["location"],
|
||||||
"additionalProperties": false
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,10 +48,6 @@ impl Tool for CopyPathTool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn may_perform_edits(&self) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn description(&self) -> String {
|
fn description(&self) -> String {
|
||||||
include_str!("./copy_path_tool/description.md").into()
|
include_str!("./copy_path_tool/description.md").into()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,16 +33,12 @@ impl Tool for CreateDirectoryTool {
|
|||||||
"create_directory".into()
|
"create_directory".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn description(&self) -> String {
|
|
||||||
include_str!("./create_directory_tool/description.md").into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn may_perform_edits(&self) -> bool {
|
fn description(&self) -> String {
|
||||||
false
|
include_str!("./create_directory_tool/description.md").into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn icon(&self) -> IconName {
|
fn icon(&self) -> IconName {
|
||||||
|
|||||||
@@ -37,10 +37,6 @@ impl Tool for DeletePathTool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn may_perform_edits(&self) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn description(&self) -> String {
|
fn description(&self) -> String {
|
||||||
include_str!("./delete_path_tool/description.md").into()
|
include_str!("./delete_path_tool/description.md").into()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,10 +50,6 @@ impl Tool for DiagnosticsTool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn may_perform_edits(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn description(&self) -> String {
|
fn description(&self) -> String {
|
||||||
include_str!("./diagnostics_tool/description.md").into()
|
include_str!("./diagnostics_tool/description.md").into()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ impl Template for EditFilePromptTemplate {
|
|||||||
pub enum EditAgentOutputEvent {
|
pub enum EditAgentOutputEvent {
|
||||||
ResolvingEditRange(Range<Anchor>),
|
ResolvingEditRange(Range<Anchor>),
|
||||||
UnresolvedEditRange,
|
UnresolvedEditRange,
|
||||||
AmbiguousEditRange(Vec<Range<usize>>),
|
|
||||||
Edited,
|
Edited,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,29 +269,16 @@ impl EditAgent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let (edit_events_, mut resolved_old_text) = resolve_old_text.await?;
|
let (edit_events_, resolved_old_text) = resolve_old_text.await?;
|
||||||
edit_events = edit_events_;
|
edit_events = edit_events_;
|
||||||
|
|
||||||
// If we can't resolve the old text, restart the loop waiting for a
|
// If we can't resolve the old text, restart the loop waiting for a
|
||||||
// new edit (or for the stream to end).
|
// new edit (or for the stream to end).
|
||||||
let resolved_old_text = match resolved_old_text.len() {
|
let Some(resolved_old_text) = resolved_old_text else {
|
||||||
1 => resolved_old_text.pop().unwrap(),
|
output_events
|
||||||
0 => {
|
.unbounded_send(EditAgentOutputEvent::UnresolvedEditRange)
|
||||||
output_events
|
.ok();
|
||||||
.unbounded_send(EditAgentOutputEvent::UnresolvedEditRange)
|
continue;
|
||||||
.ok();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
let ranges = resolved_old_text
|
|
||||||
.into_iter()
|
|
||||||
.map(|text| text.range)
|
|
||||||
.collect();
|
|
||||||
output_events
|
|
||||||
.unbounded_send(EditAgentOutputEvent::AmbiguousEditRange(ranges))
|
|
||||||
.ok();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Compute edits in the background and apply them as they become
|
// Compute edits in the background and apply them as they become
|
||||||
@@ -419,13 +405,13 @@ impl EditAgent {
|
|||||||
mut edit_events: T,
|
mut edit_events: T,
|
||||||
cx: &mut AsyncApp,
|
cx: &mut AsyncApp,
|
||||||
) -> (
|
) -> (
|
||||||
Task<Result<(T, Vec<ResolvedOldText>)>>,
|
Task<Result<(T, Option<ResolvedOldText>)>>,
|
||||||
watch::Receiver<Option<Range<usize>>>,
|
async_watch::Receiver<Option<Range<usize>>>,
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
T: 'static + Send + Unpin + Stream<Item = Result<EditParserEvent>>,
|
T: 'static + Send + Unpin + Stream<Item = Result<EditParserEvent>>,
|
||||||
{
|
{
|
||||||
let (mut old_range_tx, old_range_rx) = watch::channel(None);
|
let (old_range_tx, old_range_rx) = async_watch::channel(None);
|
||||||
let task = cx.background_spawn(async move {
|
let task = cx.background_spawn(async move {
|
||||||
let mut matcher = StreamingFuzzyMatcher::new(snapshot);
|
let mut matcher = StreamingFuzzyMatcher::new(snapshot);
|
||||||
while let Some(edit_event) = edit_events.next().await {
|
while let Some(edit_event) = edit_events.next().await {
|
||||||
@@ -439,29 +425,21 @@ impl EditAgent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let matches = matcher.finish();
|
let old_range = matcher.finish();
|
||||||
|
old_range_tx.send(old_range.clone())?;
|
||||||
let old_range = if matches.len() == 1 {
|
if let Some(old_range) = old_range {
|
||||||
matches.first()
|
let line_indent =
|
||||||
|
LineIndent::from_iter(matcher.query_lines().first().unwrap().chars());
|
||||||
|
Ok((
|
||||||
|
edit_events,
|
||||||
|
Some(ResolvedOldText {
|
||||||
|
range: old_range,
|
||||||
|
indent: line_indent,
|
||||||
|
}),
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
// No matches or multiple ambiguous matches
|
Ok((edit_events, None))
|
||||||
None
|
}
|
||||||
};
|
|
||||||
old_range_tx.send(old_range.cloned())?;
|
|
||||||
|
|
||||||
let indent = LineIndent::from_iter(
|
|
||||||
matcher
|
|
||||||
.query_lines()
|
|
||||||
.first()
|
|
||||||
.unwrap_or(&String::new())
|
|
||||||
.chars(),
|
|
||||||
);
|
|
||||||
let resolved_old_texts = matches
|
|
||||||
.into_iter()
|
|
||||||
.map(|range| ResolvedOldText { range, indent })
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
Ok((edit_events, resolved_old_texts))
|
|
||||||
});
|
});
|
||||||
|
|
||||||
(task, old_range_rx)
|
(task, old_range_rx)
|
||||||
@@ -1344,76 +1322,6 @@ mod tests {
|
|||||||
EditAgent::new(model, project, action_log, Templates::new())
|
EditAgent::new(model, project, action_log, Templates::new())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test(iterations = 10)]
|
|
||||||
async fn test_non_unique_text_error(cx: &mut TestAppContext, mut rng: StdRng) {
|
|
||||||
let agent = init_test(cx).await;
|
|
||||||
let original_text = indoc! {"
|
|
||||||
function foo() {
|
|
||||||
return 42;
|
|
||||||
}
|
|
||||||
|
|
||||||
function bar() {
|
|
||||||
return 42;
|
|
||||||
}
|
|
||||||
|
|
||||||
function baz() {
|
|
||||||
return 42;
|
|
||||||
}
|
|
||||||
"};
|
|
||||||
let buffer = cx.new(|cx| Buffer::local(original_text, cx));
|
|
||||||
let (apply, mut events) = agent.edit(
|
|
||||||
buffer.clone(),
|
|
||||||
String::new(),
|
|
||||||
&LanguageModelRequest::default(),
|
|
||||||
&mut cx.to_async(),
|
|
||||||
);
|
|
||||||
cx.run_until_parked();
|
|
||||||
|
|
||||||
// When <old_text> matches text in more than one place
|
|
||||||
simulate_llm_output(
|
|
||||||
&agent,
|
|
||||||
indoc! {"
|
|
||||||
<old_text>
|
|
||||||
return 42;
|
|
||||||
</old_text>
|
|
||||||
<new_text>
|
|
||||||
return 100;
|
|
||||||
</new_text>
|
|
||||||
"},
|
|
||||||
&mut rng,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
apply.await.unwrap();
|
|
||||||
|
|
||||||
// Then the text should remain unchanged
|
|
||||||
let result_text = buffer.read_with(cx, |buffer, _| buffer.snapshot().text());
|
|
||||||
assert_eq!(
|
|
||||||
result_text,
|
|
||||||
indoc! {"
|
|
||||||
function foo() {
|
|
||||||
return 42;
|
|
||||||
}
|
|
||||||
|
|
||||||
function bar() {
|
|
||||||
return 42;
|
|
||||||
}
|
|
||||||
|
|
||||||
function baz() {
|
|
||||||
return 42;
|
|
||||||
}
|
|
||||||
"},
|
|
||||||
"Text should remain unchanged when there are multiple matches"
|
|
||||||
);
|
|
||||||
|
|
||||||
// And AmbiguousEditRange even should be emitted
|
|
||||||
let events = drain_events(&mut events);
|
|
||||||
let ambiguous_ranges = vec![17..31, 52..66, 87..101];
|
|
||||||
assert!(
|
|
||||||
events.contains(&EditAgentOutputEvent::AmbiguousEditRange(ambiguous_ranges)),
|
|
||||||
"Should emit AmbiguousEditRange for non-unique text"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn drain_events(
|
fn drain_events(
|
||||||
stream: &mut UnboundedReceiver<EditAgentOutputEvent>,
|
stream: &mut UnboundedReceiver<EditAgentOutputEvent>,
|
||||||
) -> Vec<EditAgentOutputEvent> {
|
) -> Vec<EditAgentOutputEvent> {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ fn eval_extract_handle_command_output() {
|
|||||||
// Model | Pass rate
|
// Model | Pass rate
|
||||||
// ----------------------------|----------
|
// ----------------------------|----------
|
||||||
// claude-3.7-sonnet | 0.98
|
// claude-3.7-sonnet | 0.98
|
||||||
// gemini-2.5-pro-06-05 | 0.77
|
// gemini-2.5-pro | 0.86
|
||||||
// gemini-2.5-flash | 0.11
|
// gemini-2.5-flash | 0.11
|
||||||
// gpt-4.1 | 1.00
|
// gpt-4.1 | 1.00
|
||||||
|
|
||||||
@@ -58,7 +58,6 @@ fn eval_extract_handle_command_output() {
|
|||||||
eval(
|
eval(
|
||||||
100,
|
100,
|
||||||
0.7, // Taking the lower bar for Gemini
|
0.7, // Taking the lower bar for Gemini
|
||||||
0.05,
|
|
||||||
EvalInput::from_conversation(
|
EvalInput::from_conversation(
|
||||||
vec![
|
vec![
|
||||||
message(
|
message(
|
||||||
@@ -117,7 +116,6 @@ fn eval_delete_run_git_blame() {
|
|||||||
eval(
|
eval(
|
||||||
100,
|
100,
|
||||||
0.95,
|
0.95,
|
||||||
0.05,
|
|
||||||
EvalInput::from_conversation(
|
EvalInput::from_conversation(
|
||||||
vec![
|
vec![
|
||||||
message(
|
message(
|
||||||
@@ -180,7 +178,6 @@ fn eval_translate_doc_comments() {
|
|||||||
eval(
|
eval(
|
||||||
200,
|
200,
|
||||||
1.,
|
1.,
|
||||||
0.05,
|
|
||||||
EvalInput::from_conversation(
|
EvalInput::from_conversation(
|
||||||
vec![
|
vec![
|
||||||
message(
|
message(
|
||||||
@@ -244,7 +241,6 @@ fn eval_use_wasi_sdk_in_compile_parser_to_wasm() {
|
|||||||
eval(
|
eval(
|
||||||
100,
|
100,
|
||||||
0.95,
|
0.95,
|
||||||
0.05,
|
|
||||||
EvalInput::from_conversation(
|
EvalInput::from_conversation(
|
||||||
vec![
|
vec![
|
||||||
message(
|
message(
|
||||||
@@ -369,7 +365,6 @@ fn eval_disable_cursor_blinking() {
|
|||||||
eval(
|
eval(
|
||||||
100,
|
100,
|
||||||
0.95,
|
0.95,
|
||||||
0.05,
|
|
||||||
EvalInput::from_conversation(
|
EvalInput::from_conversation(
|
||||||
vec![
|
vec![
|
||||||
message(User, [text("Let's research how to cursor blinking works.")]),
|
message(User, [text("Let's research how to cursor blinking works.")]),
|
||||||
@@ -453,9 +448,6 @@ fn eval_from_pixels_constructor() {
|
|||||||
eval(
|
eval(
|
||||||
100,
|
100,
|
||||||
0.95,
|
0.95,
|
||||||
// For whatever reason, this eval produces more mismatched tags.
|
|
||||||
// Increasing for now, let's see if we can bring this down.
|
|
||||||
0.2,
|
|
||||||
EvalInput::from_conversation(
|
EvalInput::from_conversation(
|
||||||
vec![
|
vec![
|
||||||
message(
|
message(
|
||||||
@@ -656,7 +648,6 @@ fn eval_zode() {
|
|||||||
eval(
|
eval(
|
||||||
50,
|
50,
|
||||||
1.,
|
1.,
|
||||||
0.05,
|
|
||||||
EvalInput::from_conversation(
|
EvalInput::from_conversation(
|
||||||
vec![
|
vec![
|
||||||
message(User, [text(include_str!("evals/fixtures/zode/prompt.md"))]),
|
message(User, [text(include_str!("evals/fixtures/zode/prompt.md"))]),
|
||||||
@@ -763,7 +754,6 @@ fn eval_add_overwrite_test() {
|
|||||||
eval(
|
eval(
|
||||||
200,
|
200,
|
||||||
0.5, // TODO: make this eval better
|
0.5, // TODO: make this eval better
|
||||||
0.05,
|
|
||||||
EvalInput::from_conversation(
|
EvalInput::from_conversation(
|
||||||
vec![
|
vec![
|
||||||
message(
|
message(
|
||||||
@@ -1003,7 +993,6 @@ fn eval_create_empty_file() {
|
|||||||
eval(
|
eval(
|
||||||
100,
|
100,
|
||||||
0.99,
|
0.99,
|
||||||
0.05,
|
|
||||||
EvalInput::from_conversation(
|
EvalInput::from_conversation(
|
||||||
vec![
|
vec![
|
||||||
message(User, [text("Create a second empty todo file ")]),
|
message(User, [text("Create a second empty todo file ")]),
|
||||||
@@ -1290,12 +1279,7 @@ impl EvalAssertion {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn eval(
|
fn eval(iterations: usize, expected_pass_ratio: f32, mut eval: EvalInput) {
|
||||||
iterations: usize,
|
|
||||||
expected_pass_ratio: f32,
|
|
||||||
mismatched_tag_threshold: f32,
|
|
||||||
mut eval: EvalInput,
|
|
||||||
) {
|
|
||||||
let mut evaluated_count = 0;
|
let mut evaluated_count = 0;
|
||||||
let mut failed_count = 0;
|
let mut failed_count = 0;
|
||||||
report_progress(evaluated_count, failed_count, iterations);
|
report_progress(evaluated_count, failed_count, iterations);
|
||||||
@@ -1367,7 +1351,7 @@ fn eval(
|
|||||||
|
|
||||||
let mismatched_tag_ratio =
|
let mismatched_tag_ratio =
|
||||||
cumulative_parser_metrics.mismatched_tags as f32 / cumulative_parser_metrics.tags as f32;
|
cumulative_parser_metrics.mismatched_tags as f32 / cumulative_parser_metrics.tags as f32;
|
||||||
if mismatched_tag_ratio > mismatched_tag_threshold {
|
if mismatched_tag_ratio > 0.05 {
|
||||||
for eval_output in eval_outputs {
|
for eval_output in eval_outputs {
|
||||||
println!("{}", eval_output);
|
println!("{}", eval_output);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -498,7 +498,7 @@ client.with_options(max_retries=5).messages.create(
|
|||||||
### Timeouts
|
### Timeouts
|
||||||
|
|
||||||
By default requests time out after 10 minutes. You can configure this with a `timeout` option,
|
By default requests time out after 10 minutes. You can configure this with a `timeout` option,
|
||||||
which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/timeouts/#fine-tuning-the-configuration) object:
|
which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/#fine-tuning-the-configuration) object:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from anthropic import Anthropic
|
from anthropic import Anthropic
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ pub struct StreamingFuzzyMatcher {
|
|||||||
snapshot: TextBufferSnapshot,
|
snapshot: TextBufferSnapshot,
|
||||||
query_lines: Vec<String>,
|
query_lines: Vec<String>,
|
||||||
incomplete_line: String,
|
incomplete_line: String,
|
||||||
best_matches: Vec<Range<usize>>,
|
best_match: Option<Range<usize>>,
|
||||||
matrix: SearchMatrix,
|
matrix: SearchMatrix,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ impl StreamingFuzzyMatcher {
|
|||||||
snapshot,
|
snapshot,
|
||||||
query_lines: Vec::new(),
|
query_lines: Vec::new(),
|
||||||
incomplete_line: String::new(),
|
incomplete_line: String::new(),
|
||||||
best_matches: Vec::new(),
|
best_match: None,
|
||||||
matrix: SearchMatrix::new(buffer_line_count + 1),
|
matrix: SearchMatrix::new(buffer_line_count + 1),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -55,41 +55,31 @@ impl StreamingFuzzyMatcher {
|
|||||||
|
|
||||||
self.incomplete_line.replace_range(..last_pos + 1, "");
|
self.incomplete_line.replace_range(..last_pos + 1, "");
|
||||||
|
|
||||||
self.best_matches = self.resolve_location_fuzzy();
|
self.best_match = self.resolve_location_fuzzy();
|
||||||
|
|
||||||
if let Some(first_match) = self.best_matches.first() {
|
|
||||||
Some(first_match.clone())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if let Some(first_match) = self.best_matches.first() {
|
|
||||||
Some(first_match.clone())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.best_match.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finish processing and return the final best match(es).
|
/// Finish processing and return the final best match.
|
||||||
///
|
///
|
||||||
/// This processes any remaining incomplete line before returning the final
|
/// This processes any remaining incomplete line before returning the final
|
||||||
/// match result.
|
/// match result.
|
||||||
pub fn finish(&mut self) -> Vec<Range<usize>> {
|
pub fn finish(&mut self) -> Option<Range<usize>> {
|
||||||
// Process any remaining incomplete line
|
// Process any remaining incomplete line
|
||||||
if !self.incomplete_line.is_empty() {
|
if !self.incomplete_line.is_empty() {
|
||||||
self.query_lines.push(self.incomplete_line.clone());
|
self.query_lines.push(self.incomplete_line.clone());
|
||||||
self.incomplete_line.clear();
|
self.best_match = self.resolve_location_fuzzy();
|
||||||
self.best_matches = self.resolve_location_fuzzy();
|
|
||||||
}
|
}
|
||||||
self.best_matches.clone()
|
|
||||||
|
self.best_match.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_location_fuzzy(&mut self) -> Vec<Range<usize>> {
|
fn resolve_location_fuzzy(&mut self) -> Option<Range<usize>> {
|
||||||
let new_query_line_count = self.query_lines.len();
|
let new_query_line_count = self.query_lines.len();
|
||||||
let old_query_line_count = self.matrix.rows.saturating_sub(1);
|
let old_query_line_count = self.matrix.rows.saturating_sub(1);
|
||||||
if new_query_line_count == old_query_line_count {
|
if new_query_line_count == old_query_line_count {
|
||||||
return Vec::new();
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.matrix.resize_rows(new_query_line_count + 1);
|
self.matrix.resize_rows(new_query_line_count + 1);
|
||||||
@@ -142,61 +132,53 @@ impl StreamingFuzzyMatcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find all matches with the best cost
|
// Traceback to find the best match
|
||||||
let buffer_line_count = self.snapshot.max_point().row as usize + 1;
|
let buffer_line_count = self.snapshot.max_point().row as usize + 1;
|
||||||
|
let mut buffer_row_end = buffer_line_count as u32;
|
||||||
let mut best_cost = u32::MAX;
|
let mut best_cost = u32::MAX;
|
||||||
let mut matches_with_best_cost = Vec::new();
|
|
||||||
|
|
||||||
for col in 1..=buffer_line_count {
|
for col in 1..=buffer_line_count {
|
||||||
let cost = self.matrix.get(new_query_line_count, col).cost;
|
let cost = self.matrix.get(new_query_line_count, col).cost;
|
||||||
if cost < best_cost {
|
if cost < best_cost {
|
||||||
best_cost = cost;
|
best_cost = cost;
|
||||||
matches_with_best_cost.clear();
|
buffer_row_end = col as u32;
|
||||||
matches_with_best_cost.push(col as u32);
|
|
||||||
} else if cost == best_cost {
|
|
||||||
matches_with_best_cost.push(col as u32);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find ranges for the matches
|
let mut matched_lines = 0;
|
||||||
let mut valid_matches = Vec::new();
|
let mut query_row = new_query_line_count;
|
||||||
for &buffer_row_end in &matches_with_best_cost {
|
let mut buffer_row_start = buffer_row_end;
|
||||||
let mut matched_lines = 0;
|
while query_row > 0 && buffer_row_start > 0 {
|
||||||
let mut query_row = new_query_line_count;
|
let current = self.matrix.get(query_row, buffer_row_start as usize);
|
||||||
let mut buffer_row_start = buffer_row_end;
|
match current.direction {
|
||||||
while query_row > 0 && buffer_row_start > 0 {
|
SearchDirection::Diagonal => {
|
||||||
let current = self.matrix.get(query_row, buffer_row_start as usize);
|
query_row -= 1;
|
||||||
match current.direction {
|
buffer_row_start -= 1;
|
||||||
SearchDirection::Diagonal => {
|
matched_lines += 1;
|
||||||
query_row -= 1;
|
}
|
||||||
buffer_row_start -= 1;
|
SearchDirection::Up => {
|
||||||
matched_lines += 1;
|
query_row -= 1;
|
||||||
}
|
}
|
||||||
SearchDirection::Up => {
|
SearchDirection::Left => {
|
||||||
query_row -= 1;
|
buffer_row_start -= 1;
|
||||||
}
|
|
||||||
SearchDirection::Left => {
|
|
||||||
buffer_row_start -= 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let matched_buffer_row_count = buffer_row_end - buffer_row_start;
|
|
||||||
let matched_ratio = matched_lines as f32
|
|
||||||
/ (matched_buffer_row_count as f32).max(new_query_line_count as f32);
|
|
||||||
if matched_ratio >= 0.8 {
|
|
||||||
let buffer_start_ix = self
|
|
||||||
.snapshot
|
|
||||||
.point_to_offset(Point::new(buffer_row_start, 0));
|
|
||||||
let buffer_end_ix = self.snapshot.point_to_offset(Point::new(
|
|
||||||
buffer_row_end - 1,
|
|
||||||
self.snapshot.line_len(buffer_row_end - 1),
|
|
||||||
));
|
|
||||||
valid_matches.push((buffer_row_start, buffer_start_ix..buffer_end_ix));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
valid_matches.into_iter().map(|(_, range)| range).collect()
|
let matched_buffer_row_count = buffer_row_end - buffer_row_start;
|
||||||
|
let matched_ratio = matched_lines as f32
|
||||||
|
/ (matched_buffer_row_count as f32).max(new_query_line_count as f32);
|
||||||
|
if matched_ratio >= 0.8 {
|
||||||
|
let buffer_start_ix = self
|
||||||
|
.snapshot
|
||||||
|
.point_to_offset(Point::new(buffer_row_start, 0));
|
||||||
|
let buffer_end_ix = self.snapshot.point_to_offset(Point::new(
|
||||||
|
buffer_row_end - 1,
|
||||||
|
self.snapshot.line_len(buffer_row_end - 1),
|
||||||
|
));
|
||||||
|
Some(buffer_start_ix..buffer_end_ix)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -656,35 +638,28 @@ mod tests {
|
|||||||
matcher.push(chunk);
|
matcher.push(chunk);
|
||||||
}
|
}
|
||||||
|
|
||||||
let actual_ranges = matcher.finish();
|
let result = matcher.finish();
|
||||||
|
|
||||||
// If no expected ranges, we expect no match
|
// If no expected ranges, we expect no match
|
||||||
if expected_ranges.is_empty() {
|
if expected_ranges.is_empty() {
|
||||||
assert!(
|
assert_eq!(
|
||||||
actual_ranges.is_empty(),
|
result, None,
|
||||||
"Expected no match for query: {:?}, but found: {:?}",
|
"Expected no match for query: {:?}, but found: {:?}",
|
||||||
query,
|
query, result
|
||||||
actual_ranges
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
let mut actual_ranges = Vec::new();
|
||||||
|
if let Some(range) = result {
|
||||||
|
actual_ranges.push(range);
|
||||||
|
}
|
||||||
|
|
||||||
let text_with_actual_range = generate_marked_text(&text, &actual_ranges, false);
|
let text_with_actual_range = generate_marked_text(&text, &actual_ranges, false);
|
||||||
pretty_assertions::assert_eq!(
|
pretty_assertions::assert_eq!(
|
||||||
text_with_actual_range,
|
text_with_actual_range,
|
||||||
text_with_expected_range,
|
text_with_expected_range,
|
||||||
indoc! {"
|
"Query: {:?}, Chunks: {:?}",
|
||||||
Query: {:?}
|
|
||||||
Chunks: {:?}
|
|
||||||
Expected marked text: {}
|
|
||||||
Actual marked text: {}
|
|
||||||
Expected ranges: {:?}
|
|
||||||
Actual ranges: {:?}"
|
|
||||||
},
|
|
||||||
query,
|
query,
|
||||||
chunks,
|
chunks
|
||||||
text_with_expected_range,
|
|
||||||
text_with_actual_range,
|
|
||||||
expected_ranges,
|
|
||||||
actual_ranges
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -712,11 +687,8 @@ mod tests {
|
|||||||
|
|
||||||
fn finish(mut finder: StreamingFuzzyMatcher) -> Option<String> {
|
fn finish(mut finder: StreamingFuzzyMatcher) -> Option<String> {
|
||||||
let snapshot = finder.snapshot.clone();
|
let snapshot = finder.snapshot.clone();
|
||||||
let matches = finder.finish();
|
finder
|
||||||
if let Some(range) = matches.first() {
|
.finish()
|
||||||
Some(snapshot.text_for_range(range.clone()).collect::<String>())
|
.map(|range| snapshot.text_for_range(range).collect::<String>())
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,10 +129,6 @@ impl Tool for EditFileTool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn may_perform_edits(&self) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn description(&self) -> String {
|
fn description(&self) -> String {
|
||||||
include_str!("edit_file_tool/description.md").to_string()
|
include_str!("edit_file_tool/description.md").to_string()
|
||||||
}
|
}
|
||||||
@@ -239,7 +235,6 @@ impl Tool for EditFileTool {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut hallucinated_old_text = false;
|
let mut hallucinated_old_text = false;
|
||||||
let mut ambiguous_ranges = Vec::new();
|
|
||||||
while let Some(event) = events.next().await {
|
while let Some(event) = events.next().await {
|
||||||
match event {
|
match event {
|
||||||
EditAgentOutputEvent::Edited => {
|
EditAgentOutputEvent::Edited => {
|
||||||
@@ -248,7 +243,6 @@ impl Tool for EditFileTool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
EditAgentOutputEvent::UnresolvedEditRange => hallucinated_old_text = true,
|
EditAgentOutputEvent::UnresolvedEditRange => hallucinated_old_text = true,
|
||||||
EditAgentOutputEvent::AmbiguousEditRange(ranges) => ambiguous_ranges = ranges,
|
|
||||||
EditAgentOutputEvent::ResolvingEditRange(range) => {
|
EditAgentOutputEvent::ResolvingEditRange(range) => {
|
||||||
if let Some(card) = card_clone.as_ref() {
|
if let Some(card) = card_clone.as_ref() {
|
||||||
card.update(cx, |card, cx| card.reveal_range(range, cx))?;
|
card.update(cx, |card, cx| card.reveal_range(range, cx))?;
|
||||||
@@ -331,17 +325,6 @@ impl Tool for EditFileTool {
|
|||||||
I can perform the requested edits.
|
I can perform the requested edits.
|
||||||
"}
|
"}
|
||||||
);
|
);
|
||||||
anyhow::ensure!(
|
|
||||||
ambiguous_ranges.is_empty(),
|
|
||||||
// TODO: Include ambiguous_ranges, converted to line numbers.
|
|
||||||
// This would work best if we add `line_hint` parameter
|
|
||||||
// to edit_file_tool
|
|
||||||
formatdoc! {"
|
|
||||||
<old_text> matches more than one position in the file. Read the
|
|
||||||
relevant sections of {input_path} again and extend <old_text> so
|
|
||||||
that I can perform the requested edits.
|
|
||||||
"}
|
|
||||||
);
|
|
||||||
Ok(ToolResultOutput {
|
Ok(ToolResultOutput {
|
||||||
content: ToolResultContent::Text("No edits were made.".into()),
|
content: ToolResultContent::Text("No edits were made.".into()),
|
||||||
output: serde_json::to_value(output).ok(),
|
output: serde_json::to_value(output).ok(),
|
||||||
|
|||||||
@@ -118,11 +118,7 @@ impl Tool for FetchTool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
||||||
false
|
true
|
||||||
}
|
|
||||||
|
|
||||||
fn may_perform_edits(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn description(&self) -> String {
|
fn description(&self) -> String {
|
||||||
|
|||||||
@@ -59,10 +59,6 @@ impl Tool for FindPathTool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn may_perform_edits(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn description(&self) -> String {
|
fn description(&self) -> String {
|
||||||
include_str!("./find_path_tool/description.md").into()
|
include_str!("./find_path_tool/description.md").into()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,11 @@ use gpui::{AnyWindowHandle, App, Entity, Task};
|
|||||||
use language::{OffsetRangeExt, ParseStatus, Point};
|
use language::{OffsetRangeExt, ParseStatus, Point};
|
||||||
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
|
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
|
||||||
use project::{
|
use project::{
|
||||||
Project, WorktreeSettings,
|
Project,
|
||||||
search::{SearchQuery, SearchResult},
|
search::{SearchQuery, SearchResult},
|
||||||
};
|
};
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::Settings;
|
|
||||||
use std::{cmp, fmt::Write, sync::Arc};
|
use std::{cmp, fmt::Write, sync::Arc};
|
||||||
use ui::IconName;
|
use ui::IconName;
|
||||||
use util::RangeExt;
|
use util::RangeExt;
|
||||||
@@ -61,10 +60,6 @@ impl Tool for GrepTool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn may_perform_edits(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn description(&self) -> String {
|
fn description(&self) -> String {
|
||||||
include_str!("./grep_tool/description.md").into()
|
include_str!("./grep_tool/description.md").into()
|
||||||
}
|
}
|
||||||
@@ -131,23 +126,6 @@ impl Tool for GrepTool {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Exclude global file_scan_exclusions and private_files settings
|
|
||||||
let exclude_matcher = {
|
|
||||||
let global_settings = WorktreeSettings::get_global(cx);
|
|
||||||
let exclude_patterns = global_settings
|
|
||||||
.file_scan_exclusions
|
|
||||||
.sources()
|
|
||||||
.iter()
|
|
||||||
.chain(global_settings.private_files.sources().iter());
|
|
||||||
|
|
||||||
match PathMatcher::new(exclude_patterns) {
|
|
||||||
Ok(matcher) => matcher,
|
|
||||||
Err(error) => {
|
|
||||||
return Task::ready(Err(anyhow!("invalid exclude pattern: {error}"))).into();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let query = match SearchQuery::regex(
|
let query = match SearchQuery::regex(
|
||||||
&input.regex,
|
&input.regex,
|
||||||
false,
|
false,
|
||||||
@@ -155,7 +133,7 @@ impl Tool for GrepTool {
|
|||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
include_matcher,
|
include_matcher,
|
||||||
exclude_matcher,
|
PathMatcher::default(), // For now, keep it simple and don't enable an exclude pattern.
|
||||||
true, // Always match file include pattern against *full project paths* that start with a project root.
|
true, // Always match file include pattern against *full project paths* that start with a project root.
|
||||||
None,
|
None,
|
||||||
) {
|
) {
|
||||||
@@ -178,24 +156,12 @@ impl Tool for GrepTool {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let Ok((Some(path), mut parse_status)) = buffer.read_with(cx, |buffer, cx| {
|
let (Some(path), mut parse_status) = buffer.read_with(cx, |buffer, cx| {
|
||||||
(buffer.file().map(|file| file.full_path(cx)), buffer.parse_status())
|
(buffer.file().map(|file| file.full_path(cx)), buffer.parse_status())
|
||||||
}) else {
|
})? else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check if this file should be excluded based on its worktree settings
|
|
||||||
if let Ok(Some(project_path)) = project.read_with(cx, |project, cx| {
|
|
||||||
project.find_project_path(&path, cx)
|
|
||||||
}) {
|
|
||||||
if cx.update(|cx| {
|
|
||||||
let worktree_settings = WorktreeSettings::get(Some((&project_path).into()), cx);
|
|
||||||
worktree_settings.is_path_excluded(&project_path.path)
|
|
||||||
|| worktree_settings.is_path_private(&project_path.path)
|
|
||||||
}).unwrap_or(false) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
while *parse_status.borrow() != ParseStatus::Idle {
|
while *parse_status.borrow() != ParseStatus::Idle {
|
||||||
parse_status.changed().await?;
|
parse_status.changed().await?;
|
||||||
@@ -314,11 +280,10 @@ impl Tool for GrepTool {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use assistant_tool::Tool;
|
use assistant_tool::Tool;
|
||||||
use gpui::{AppContext, TestAppContext, UpdateGlobal};
|
use gpui::{AppContext, TestAppContext};
|
||||||
use language::{Language, LanguageConfig, LanguageMatcher};
|
use language::{Language, LanguageConfig, LanguageMatcher};
|
||||||
use language_model::fake_provider::FakeLanguageModel;
|
use language_model::fake_provider::FakeLanguageModel;
|
||||||
use project::{FakeFs, Project, WorktreeSettings};
|
use project::{FakeFs, Project};
|
||||||
use serde_json::json;
|
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
use unindent::Unindent;
|
use unindent::Unindent;
|
||||||
use util::path;
|
use util::path;
|
||||||
@@ -330,7 +295,7 @@ mod tests {
|
|||||||
|
|
||||||
let fs = FakeFs::new(cx.executor().clone());
|
let fs = FakeFs::new(cx.executor().clone());
|
||||||
fs.insert_tree(
|
fs.insert_tree(
|
||||||
path!("/root"),
|
"/root",
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"src": {
|
"src": {
|
||||||
"main.rs": "fn main() {\n println!(\"Hello, world!\");\n}",
|
"main.rs": "fn main() {\n println!(\"Hello, world!\");\n}",
|
||||||
@@ -418,7 +383,7 @@ mod tests {
|
|||||||
|
|
||||||
let fs = FakeFs::new(cx.executor().clone());
|
let fs = FakeFs::new(cx.executor().clone());
|
||||||
fs.insert_tree(
|
fs.insert_tree(
|
||||||
path!("/root"),
|
"/root",
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"case_test.txt": "This file has UPPERCASE and lowercase text.\nUPPERCASE patterns should match only with case_sensitive: true",
|
"case_test.txt": "This file has UPPERCASE and lowercase text.\nUPPERCASE patterns should match only with case_sensitive: true",
|
||||||
}),
|
}),
|
||||||
@@ -499,7 +464,7 @@ mod tests {
|
|||||||
|
|
||||||
// Create test file with syntax structures
|
// Create test file with syntax structures
|
||||||
fs.insert_tree(
|
fs.insert_tree(
|
||||||
path!("/root"),
|
"/root",
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"test_syntax.rs": r#"
|
"test_syntax.rs": r#"
|
||||||
fn top_level_function() {
|
fn top_level_function() {
|
||||||
@@ -820,488 +785,4 @@ mod tests {
|
|||||||
.with_outline_query(include_str!("../../languages/src/rust/outline.scm"))
|
.with_outline_query(include_str!("../../languages/src/rust/outline.scm"))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_grep_security_boundaries(cx: &mut TestAppContext) {
|
|
||||||
init_test(cx);
|
|
||||||
|
|
||||||
let fs = FakeFs::new(cx.executor());
|
|
||||||
|
|
||||||
fs.insert_tree(
|
|
||||||
path!("/"),
|
|
||||||
json!({
|
|
||||||
"project_root": {
|
|
||||||
"allowed_file.rs": "fn main() { println!(\"This file is in the project\"); }",
|
|
||||||
".mysecrets": "SECRET_KEY=abc123\nfn secret() { /* private */ }",
|
|
||||||
".secretdir": {
|
|
||||||
"config": "fn special_configuration() { /* excluded */ }"
|
|
||||||
},
|
|
||||||
".mymetadata": "fn custom_metadata() { /* excluded */ }",
|
|
||||||
"subdir": {
|
|
||||||
"normal_file.rs": "fn normal_file_content() { /* Normal */ }",
|
|
||||||
"special.privatekey": "fn private_key_content() { /* private */ }",
|
|
||||||
"data.mysensitive": "fn sensitive_data() { /* private */ }"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"outside_project": {
|
|
||||||
"sensitive_file.rs": "fn outside_function() { /* This file is outside the project */ }"
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
cx.update(|cx| {
|
|
||||||
use gpui::UpdateGlobal;
|
|
||||||
use project::WorktreeSettings;
|
|
||||||
use settings::SettingsStore;
|
|
||||||
SettingsStore::update_global(cx, |store, cx| {
|
|
||||||
store.update_user_settings::<WorktreeSettings>(cx, |settings| {
|
|
||||||
settings.file_scan_exclusions = Some(vec![
|
|
||||||
"**/.secretdir".to_string(),
|
|
||||||
"**/.mymetadata".to_string(),
|
|
||||||
]);
|
|
||||||
settings.private_files = Some(vec![
|
|
||||||
"**/.mysecrets".to_string(),
|
|
||||||
"**/*.privatekey".to_string(),
|
|
||||||
"**/*.mysensitive".to_string(),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
let project = Project::test(fs.clone(), [path!("/project_root").as_ref()], cx).await;
|
|
||||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
|
||||||
let model = Arc::new(FakeLanguageModel::default());
|
|
||||||
|
|
||||||
// Searching for files outside the project worktree should return no results
|
|
||||||
let result = cx
|
|
||||||
.update(|cx| {
|
|
||||||
let input = json!({
|
|
||||||
"regex": "outside_function"
|
|
||||||
});
|
|
||||||
Arc::new(GrepTool)
|
|
||||||
.run(
|
|
||||||
input,
|
|
||||||
Arc::default(),
|
|
||||||
project.clone(),
|
|
||||||
action_log.clone(),
|
|
||||||
model.clone(),
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.output
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
let results = result.unwrap();
|
|
||||||
let paths = extract_paths_from_results(&results.content.as_str().unwrap());
|
|
||||||
assert!(
|
|
||||||
paths.is_empty(),
|
|
||||||
"grep_tool should not find files outside the project worktree"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Searching within the project should succeed
|
|
||||||
let result = cx
|
|
||||||
.update(|cx| {
|
|
||||||
let input = json!({
|
|
||||||
"regex": "main"
|
|
||||||
});
|
|
||||||
Arc::new(GrepTool)
|
|
||||||
.run(
|
|
||||||
input,
|
|
||||||
Arc::default(),
|
|
||||||
project.clone(),
|
|
||||||
action_log.clone(),
|
|
||||||
model.clone(),
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.output
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
let results = result.unwrap();
|
|
||||||
let paths = extract_paths_from_results(&results.content.as_str().unwrap());
|
|
||||||
assert!(
|
|
||||||
paths.iter().any(|p| p.contains("allowed_file.rs")),
|
|
||||||
"grep_tool should be able to search files inside worktrees"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Searching files that match file_scan_exclusions should return no results
|
|
||||||
let result = cx
|
|
||||||
.update(|cx| {
|
|
||||||
let input = json!({
|
|
||||||
"regex": "special_configuration"
|
|
||||||
});
|
|
||||||
Arc::new(GrepTool)
|
|
||||||
.run(
|
|
||||||
input,
|
|
||||||
Arc::default(),
|
|
||||||
project.clone(),
|
|
||||||
action_log.clone(),
|
|
||||||
model.clone(),
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.output
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
let results = result.unwrap();
|
|
||||||
let paths = extract_paths_from_results(&results.content.as_str().unwrap());
|
|
||||||
assert!(
|
|
||||||
paths.is_empty(),
|
|
||||||
"grep_tool should not search files in .secretdir (file_scan_exclusions)"
|
|
||||||
);
|
|
||||||
|
|
||||||
let result = cx
|
|
||||||
.update(|cx| {
|
|
||||||
let input = json!({
|
|
||||||
"regex": "custom_metadata"
|
|
||||||
});
|
|
||||||
Arc::new(GrepTool)
|
|
||||||
.run(
|
|
||||||
input,
|
|
||||||
Arc::default(),
|
|
||||||
project.clone(),
|
|
||||||
action_log.clone(),
|
|
||||||
model.clone(),
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.output
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
let results = result.unwrap();
|
|
||||||
let paths = extract_paths_from_results(&results.content.as_str().unwrap());
|
|
||||||
assert!(
|
|
||||||
paths.is_empty(),
|
|
||||||
"grep_tool should not search .mymetadata files (file_scan_exclusions)"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Searching private files should return no results
|
|
||||||
let result = cx
|
|
||||||
.update(|cx| {
|
|
||||||
let input = json!({
|
|
||||||
"regex": "SECRET_KEY"
|
|
||||||
});
|
|
||||||
Arc::new(GrepTool)
|
|
||||||
.run(
|
|
||||||
input,
|
|
||||||
Arc::default(),
|
|
||||||
project.clone(),
|
|
||||||
action_log.clone(),
|
|
||||||
model.clone(),
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.output
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
let results = result.unwrap();
|
|
||||||
let paths = extract_paths_from_results(&results.content.as_str().unwrap());
|
|
||||||
assert!(
|
|
||||||
paths.is_empty(),
|
|
||||||
"grep_tool should not search .mysecrets (private_files)"
|
|
||||||
);
|
|
||||||
|
|
||||||
let result = cx
|
|
||||||
.update(|cx| {
|
|
||||||
let input = json!({
|
|
||||||
"regex": "private_key_content"
|
|
||||||
});
|
|
||||||
Arc::new(GrepTool)
|
|
||||||
.run(
|
|
||||||
input,
|
|
||||||
Arc::default(),
|
|
||||||
project.clone(),
|
|
||||||
action_log.clone(),
|
|
||||||
model.clone(),
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.output
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
let results = result.unwrap();
|
|
||||||
let paths = extract_paths_from_results(&results.content.as_str().unwrap());
|
|
||||||
assert!(
|
|
||||||
paths.is_empty(),
|
|
||||||
"grep_tool should not search .privatekey files (private_files)"
|
|
||||||
);
|
|
||||||
|
|
||||||
let result = cx
|
|
||||||
.update(|cx| {
|
|
||||||
let input = json!({
|
|
||||||
"regex": "sensitive_data"
|
|
||||||
});
|
|
||||||
Arc::new(GrepTool)
|
|
||||||
.run(
|
|
||||||
input,
|
|
||||||
Arc::default(),
|
|
||||||
project.clone(),
|
|
||||||
action_log.clone(),
|
|
||||||
model.clone(),
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.output
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
let results = result.unwrap();
|
|
||||||
let paths = extract_paths_from_results(&results.content.as_str().unwrap());
|
|
||||||
assert!(
|
|
||||||
paths.is_empty(),
|
|
||||||
"grep_tool should not search .mysensitive files (private_files)"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Searching a normal file should still work, even with private_files configured
|
|
||||||
let result = cx
|
|
||||||
.update(|cx| {
|
|
||||||
let input = json!({
|
|
||||||
"regex": "normal_file_content"
|
|
||||||
});
|
|
||||||
Arc::new(GrepTool)
|
|
||||||
.run(
|
|
||||||
input,
|
|
||||||
Arc::default(),
|
|
||||||
project.clone(),
|
|
||||||
action_log.clone(),
|
|
||||||
model.clone(),
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.output
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
let results = result.unwrap();
|
|
||||||
let paths = extract_paths_from_results(&results.content.as_str().unwrap());
|
|
||||||
assert!(
|
|
||||||
paths.iter().any(|p| p.contains("normal_file.rs")),
|
|
||||||
"Should be able to search normal files"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Path traversal attempts with .. in include_pattern should not escape project
|
|
||||||
let result = cx
|
|
||||||
.update(|cx| {
|
|
||||||
let input = json!({
|
|
||||||
"regex": "outside_function",
|
|
||||||
"include_pattern": "../outside_project/**/*.rs"
|
|
||||||
});
|
|
||||||
Arc::new(GrepTool)
|
|
||||||
.run(
|
|
||||||
input,
|
|
||||||
Arc::default(),
|
|
||||||
project.clone(),
|
|
||||||
action_log.clone(),
|
|
||||||
model.clone(),
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.output
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
let results = result.unwrap();
|
|
||||||
let paths = extract_paths_from_results(&results.content.as_str().unwrap());
|
|
||||||
assert!(
|
|
||||||
paths.is_empty(),
|
|
||||||
"grep_tool should not allow escaping project boundaries with relative paths"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_grep_with_multiple_worktree_settings(cx: &mut TestAppContext) {
|
|
||||||
init_test(cx);
|
|
||||||
|
|
||||||
let fs = FakeFs::new(cx.executor());
|
|
||||||
|
|
||||||
// Create first worktree with its own private files
|
|
||||||
fs.insert_tree(
|
|
||||||
path!("/worktree1"),
|
|
||||||
json!({
|
|
||||||
".zed": {
|
|
||||||
"settings.json": r#"{
|
|
||||||
"file_scan_exclusions": ["**/fixture.*"],
|
|
||||||
"private_files": ["**/secret.rs"]
|
|
||||||
}"#
|
|
||||||
},
|
|
||||||
"src": {
|
|
||||||
"main.rs": "fn main() { let secret_key = \"hidden\"; }",
|
|
||||||
"secret.rs": "const API_KEY: &str = \"secret_value\";",
|
|
||||||
"utils.rs": "pub fn get_config() -> String { \"config\".to_string() }"
|
|
||||||
},
|
|
||||||
"tests": {
|
|
||||||
"test.rs": "fn test_secret() { assert!(true); }",
|
|
||||||
"fixture.sql": "SELECT * FROM secret_table;"
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
// Create second worktree with different private files
|
|
||||||
fs.insert_tree(
|
|
||||||
path!("/worktree2"),
|
|
||||||
json!({
|
|
||||||
".zed": {
|
|
||||||
"settings.json": r#"{
|
|
||||||
"file_scan_exclusions": ["**/internal.*"],
|
|
||||||
"private_files": ["**/private.js", "**/data.json"]
|
|
||||||
}"#
|
|
||||||
},
|
|
||||||
"lib": {
|
|
||||||
"public.js": "export function getSecret() { return 'public'; }",
|
|
||||||
"private.js": "const SECRET_KEY = \"private_value\";",
|
|
||||||
"data.json": "{\"secret_data\": \"hidden\"}"
|
|
||||||
},
|
|
||||||
"docs": {
|
|
||||||
"README.md": "# Documentation with secret info",
|
|
||||||
"internal.md": "Internal secret documentation"
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
// Set global settings
|
|
||||||
cx.update(|cx| {
|
|
||||||
SettingsStore::update_global(cx, |store, cx| {
|
|
||||||
store.update_user_settings::<WorktreeSettings>(cx, |settings| {
|
|
||||||
settings.file_scan_exclusions =
|
|
||||||
Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]);
|
|
||||||
settings.private_files = Some(vec!["**/.env".to_string()]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
let project = Project::test(
|
|
||||||
fs.clone(),
|
|
||||||
[path!("/worktree1").as_ref(), path!("/worktree2").as_ref()],
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
// Wait for worktrees to be fully scanned
|
|
||||||
cx.executor().run_until_parked();
|
|
||||||
|
|
||||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
|
||||||
let model = Arc::new(FakeLanguageModel::default());
|
|
||||||
|
|
||||||
// Search for "secret" - should exclude files based on worktree-specific settings
|
|
||||||
let result = cx
|
|
||||||
.update(|cx| {
|
|
||||||
let input = json!({
|
|
||||||
"regex": "secret",
|
|
||||||
"case_sensitive": false
|
|
||||||
});
|
|
||||||
Arc::new(GrepTool)
|
|
||||||
.run(
|
|
||||||
input,
|
|
||||||
Arc::default(),
|
|
||||||
project.clone(),
|
|
||||||
action_log.clone(),
|
|
||||||
model.clone(),
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.output
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let content = result.content.as_str().unwrap();
|
|
||||||
let paths = extract_paths_from_results(&content);
|
|
||||||
|
|
||||||
// Should find matches in non-private files
|
|
||||||
assert!(
|
|
||||||
paths.iter().any(|p| p.contains("main.rs")),
|
|
||||||
"Should find 'secret' in worktree1/src/main.rs"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
paths.iter().any(|p| p.contains("test.rs")),
|
|
||||||
"Should find 'secret' in worktree1/tests/test.rs"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
paths.iter().any(|p| p.contains("public.js")),
|
|
||||||
"Should find 'secret' in worktree2/lib/public.js"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
paths.iter().any(|p| p.contains("README.md")),
|
|
||||||
"Should find 'secret' in worktree2/docs/README.md"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Should NOT find matches in private/excluded files based on worktree settings
|
|
||||||
assert!(
|
|
||||||
!paths.iter().any(|p| p.contains("secret.rs")),
|
|
||||||
"Should not search in worktree1/src/secret.rs (local private_files)"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
!paths.iter().any(|p| p.contains("fixture.sql")),
|
|
||||||
"Should not search in worktree1/tests/fixture.sql (local file_scan_exclusions)"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
!paths.iter().any(|p| p.contains("private.js")),
|
|
||||||
"Should not search in worktree2/lib/private.js (local private_files)"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
!paths.iter().any(|p| p.contains("data.json")),
|
|
||||||
"Should not search in worktree2/lib/data.json (local private_files)"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
!paths.iter().any(|p| p.contains("internal.md")),
|
|
||||||
"Should not search in worktree2/docs/internal.md (local file_scan_exclusions)"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test with `include_pattern` specific to one worktree
|
|
||||||
let result = cx
|
|
||||||
.update(|cx| {
|
|
||||||
let input = json!({
|
|
||||||
"regex": "secret",
|
|
||||||
"include_pattern": "worktree1/**/*.rs"
|
|
||||||
});
|
|
||||||
Arc::new(GrepTool)
|
|
||||||
.run(
|
|
||||||
input,
|
|
||||||
Arc::default(),
|
|
||||||
project.clone(),
|
|
||||||
action_log.clone(),
|
|
||||||
model.clone(),
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.output
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let content = result.content.as_str().unwrap();
|
|
||||||
let paths = extract_paths_from_results(&content);
|
|
||||||
|
|
||||||
// Should only find matches in worktree1 *.rs files (excluding private ones)
|
|
||||||
assert!(
|
|
||||||
paths.iter().any(|p| p.contains("main.rs")),
|
|
||||||
"Should find match in worktree1/src/main.rs"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
paths.iter().any(|p| p.contains("test.rs")),
|
|
||||||
"Should find match in worktree1/tests/test.rs"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
!paths.iter().any(|p| p.contains("secret.rs")),
|
|
||||||
"Should not find match in excluded worktree1/src/secret.rs"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
paths.iter().all(|p| !p.contains("worktree2")),
|
|
||||||
"Should not find any matches in worktree2"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to extract file paths from grep results
|
|
||||||
fn extract_paths_from_results(results: &str) -> Vec<String> {
|
|
||||||
results
|
|
||||||
.lines()
|
|
||||||
.filter(|line| line.starts_with("## Matches in "))
|
|
||||||
.map(|line| {
|
|
||||||
line.strip_prefix("## Matches in ")
|
|
||||||
.unwrap()
|
|
||||||
.trim()
|
|
||||||
.to_string()
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,4 +6,3 @@ Searches the contents of files in the project with a regular expression
|
|||||||
- Never use this tool to search for paths. Only search file contents with this tool.
|
- Never use this tool to search for paths. Only search file contents with this tool.
|
||||||
- Use this tool when you need to find files containing specific patterns
|
- Use this tool when you need to find files containing specific patterns
|
||||||
- Results are paginated with 20 matches per page. Use the optional 'offset' parameter to request subsequent pages.
|
- Results are paginated with 20 matches per page. Use the optional 'offset' parameter to request subsequent pages.
|
||||||
- DO NOT use HTML entities solely to escape characters in the tool parameters.
|
|
||||||
|
|||||||
@@ -3,10 +3,9 @@ use anyhow::{Result, anyhow};
|
|||||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||||
use gpui::{AnyWindowHandle, App, Entity, Task};
|
use gpui::{AnyWindowHandle, App, Entity, Task};
|
||||||
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
|
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
|
||||||
use project::{Project, WorktreeSettings};
|
use project::Project;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::Settings;
|
|
||||||
use std::{fmt::Write, path::Path, sync::Arc};
|
use std::{fmt::Write, path::Path, sync::Arc};
|
||||||
use ui::IconName;
|
use ui::IconName;
|
||||||
use util::markdown::MarkdownInlineCode;
|
use util::markdown::MarkdownInlineCode;
|
||||||
@@ -49,10 +48,6 @@ impl Tool for ListDirectoryTool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn may_perform_edits(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn description(&self) -> String {
|
fn description(&self) -> String {
|
||||||
include_str!("./list_directory_tool/description.md").into()
|
include_str!("./list_directory_tool/description.md").into()
|
||||||
}
|
}
|
||||||
@@ -120,80 +115,21 @@ impl Tool for ListDirectoryTool {
|
|||||||
else {
|
else {
|
||||||
return Task::ready(Err(anyhow!("Worktree not found"))).into();
|
return Task::ready(Err(anyhow!("Worktree not found"))).into();
|
||||||
};
|
};
|
||||||
|
let worktree = worktree.read(cx);
|
||||||
|
|
||||||
// Check if the directory whose contents we're listing is itself excluded or private
|
let Some(entry) = worktree.entry_for_path(&project_path.path) else {
|
||||||
let global_settings = WorktreeSettings::get_global(cx);
|
|
||||||
if global_settings.is_path_excluded(&project_path.path) {
|
|
||||||
return Task::ready(Err(anyhow!(
|
|
||||||
"Cannot list directory because its path matches the user's global `file_scan_exclusions` setting: {}",
|
|
||||||
&input.path
|
|
||||||
)))
|
|
||||||
.into();
|
|
||||||
}
|
|
||||||
|
|
||||||
if global_settings.is_path_private(&project_path.path) {
|
|
||||||
return Task::ready(Err(anyhow!(
|
|
||||||
"Cannot list directory because its path matches the user's global `private_files` setting: {}",
|
|
||||||
&input.path
|
|
||||||
)))
|
|
||||||
.into();
|
|
||||||
}
|
|
||||||
|
|
||||||
let worktree_settings = WorktreeSettings::get(Some((&project_path).into()), cx);
|
|
||||||
if worktree_settings.is_path_excluded(&project_path.path) {
|
|
||||||
return Task::ready(Err(anyhow!(
|
|
||||||
"Cannot list directory because its path matches the user's worktree`file_scan_exclusions` setting: {}",
|
|
||||||
&input.path
|
|
||||||
)))
|
|
||||||
.into();
|
|
||||||
}
|
|
||||||
|
|
||||||
if worktree_settings.is_path_private(&project_path.path) {
|
|
||||||
return Task::ready(Err(anyhow!(
|
|
||||||
"Cannot list directory because its path matches the user's worktree `private_paths` setting: {}",
|
|
||||||
&input.path
|
|
||||||
)))
|
|
||||||
.into();
|
|
||||||
}
|
|
||||||
|
|
||||||
let worktree_snapshot = worktree.read(cx).snapshot();
|
|
||||||
let worktree_root_name = worktree.read(cx).root_name().to_string();
|
|
||||||
|
|
||||||
let Some(entry) = worktree_snapshot.entry_for_path(&project_path.path) else {
|
|
||||||
return Task::ready(Err(anyhow!("Path not found: {}", input.path))).into();
|
return Task::ready(Err(anyhow!("Path not found: {}", input.path))).into();
|
||||||
};
|
};
|
||||||
|
|
||||||
if !entry.is_dir() {
|
if !entry.is_dir() {
|
||||||
return Task::ready(Err(anyhow!("{} is not a directory.", input.path))).into();
|
return Task::ready(Err(anyhow!("{} is not a directory.", input.path))).into();
|
||||||
}
|
}
|
||||||
let worktree_snapshot = worktree.read(cx).snapshot();
|
|
||||||
|
|
||||||
let mut folders = Vec::new();
|
let mut folders = Vec::new();
|
||||||
let mut files = Vec::new();
|
let mut files = Vec::new();
|
||||||
|
|
||||||
for entry in worktree_snapshot.child_entries(&project_path.path) {
|
for entry in worktree.child_entries(&project_path.path) {
|
||||||
// Skip private and excluded files and directories
|
let full_path = Path::new(worktree.root_name())
|
||||||
if global_settings.is_path_private(&entry.path)
|
|
||||||
|| global_settings.is_path_excluded(&entry.path)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if project
|
|
||||||
.read(cx)
|
|
||||||
.find_project_path(&entry.path, cx)
|
|
||||||
.map(|project_path| {
|
|
||||||
let worktree_settings = WorktreeSettings::get(Some((&project_path).into()), cx);
|
|
||||||
|
|
||||||
worktree_settings.is_path_excluded(&project_path.path)
|
|
||||||
|| worktree_settings.is_path_private(&project_path.path)
|
|
||||||
})
|
|
||||||
.unwrap_or(false)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let full_path = Path::new(&worktree_root_name)
|
|
||||||
.join(&entry.path)
|
.join(&entry.path)
|
||||||
.display()
|
.display()
|
||||||
.to_string();
|
.to_string();
|
||||||
@@ -226,10 +162,10 @@ impl Tool for ListDirectoryTool {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use assistant_tool::Tool;
|
use assistant_tool::Tool;
|
||||||
use gpui::{AppContext, TestAppContext, UpdateGlobal};
|
use gpui::{AppContext, TestAppContext};
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
use language_model::fake_provider::FakeLanguageModel;
|
use language_model::fake_provider::FakeLanguageModel;
|
||||||
use project::{FakeFs, Project, WorktreeSettings};
|
use project::{FakeFs, Project};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
use util::path;
|
use util::path;
|
||||||
@@ -257,7 +193,7 @@ mod tests {
|
|||||||
|
|
||||||
let fs = FakeFs::new(cx.executor());
|
let fs = FakeFs::new(cx.executor());
|
||||||
fs.insert_tree(
|
fs.insert_tree(
|
||||||
path!("/project"),
|
"/project",
|
||||||
json!({
|
json!({
|
||||||
"src": {
|
"src": {
|
||||||
"main.rs": "fn main() {}",
|
"main.rs": "fn main() {}",
|
||||||
@@ -387,7 +323,7 @@ mod tests {
|
|||||||
|
|
||||||
let fs = FakeFs::new(cx.executor());
|
let fs = FakeFs::new(cx.executor());
|
||||||
fs.insert_tree(
|
fs.insert_tree(
|
||||||
path!("/project"),
|
"/project",
|
||||||
json!({
|
json!({
|
||||||
"empty_dir": {}
|
"empty_dir": {}
|
||||||
}),
|
}),
|
||||||
@@ -419,7 +355,7 @@ mod tests {
|
|||||||
|
|
||||||
let fs = FakeFs::new(cx.executor());
|
let fs = FakeFs::new(cx.executor());
|
||||||
fs.insert_tree(
|
fs.insert_tree(
|
||||||
path!("/project"),
|
"/project",
|
||||||
json!({
|
json!({
|
||||||
"file.txt": "content"
|
"file.txt": "content"
|
||||||
}),
|
}),
|
||||||
@@ -472,394 +408,4 @@ mod tests {
|
|||||||
.contains("is not a directory")
|
.contains("is not a directory")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_list_directory_security(cx: &mut TestAppContext) {
|
|
||||||
init_test(cx);
|
|
||||||
|
|
||||||
let fs = FakeFs::new(cx.executor());
|
|
||||||
fs.insert_tree(
|
|
||||||
path!("/project"),
|
|
||||||
json!({
|
|
||||||
"normal_dir": {
|
|
||||||
"file1.txt": "content",
|
|
||||||
"file2.txt": "content"
|
|
||||||
},
|
|
||||||
".mysecrets": "SECRET_KEY=abc123",
|
|
||||||
".secretdir": {
|
|
||||||
"config": "special configuration",
|
|
||||||
"secret.txt": "secret content"
|
|
||||||
},
|
|
||||||
".mymetadata": "custom metadata",
|
|
||||||
"visible_dir": {
|
|
||||||
"normal.txt": "normal content",
|
|
||||||
"special.privatekey": "private key content",
|
|
||||||
"data.mysensitive": "sensitive data",
|
|
||||||
".hidden_subdir": {
|
|
||||||
"hidden_file.txt": "hidden content"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
// Configure settings explicitly
|
|
||||||
cx.update(|cx| {
|
|
||||||
SettingsStore::update_global(cx, |store, cx| {
|
|
||||||
store.update_user_settings::<WorktreeSettings>(cx, |settings| {
|
|
||||||
settings.file_scan_exclusions = Some(vec![
|
|
||||||
"**/.secretdir".to_string(),
|
|
||||||
"**/.mymetadata".to_string(),
|
|
||||||
"**/.hidden_subdir".to_string(),
|
|
||||||
]);
|
|
||||||
settings.private_files = Some(vec![
|
|
||||||
"**/.mysecrets".to_string(),
|
|
||||||
"**/*.privatekey".to_string(),
|
|
||||||
"**/*.mysensitive".to_string(),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
|
|
||||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
|
||||||
let model = Arc::new(FakeLanguageModel::default());
|
|
||||||
let tool = Arc::new(ListDirectoryTool);
|
|
||||||
|
|
||||||
// Listing root directory should exclude private and excluded files
|
|
||||||
let input = json!({
|
|
||||||
"path": "project"
|
|
||||||
});
|
|
||||||
|
|
||||||
let result = cx
|
|
||||||
.update(|cx| {
|
|
||||||
tool.clone().run(
|
|
||||||
input,
|
|
||||||
Arc::default(),
|
|
||||||
project.clone(),
|
|
||||||
action_log.clone(),
|
|
||||||
model.clone(),
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.output
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let content = result.content.as_str().unwrap();
|
|
||||||
|
|
||||||
// Should include normal directories
|
|
||||||
assert!(content.contains("normal_dir"), "Should list normal_dir");
|
|
||||||
assert!(content.contains("visible_dir"), "Should list visible_dir");
|
|
||||||
|
|
||||||
// Should NOT include excluded or private files
|
|
||||||
assert!(
|
|
||||||
!content.contains(".secretdir"),
|
|
||||||
"Should not list .secretdir (file_scan_exclusions)"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
!content.contains(".mymetadata"),
|
|
||||||
"Should not list .mymetadata (file_scan_exclusions)"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
!content.contains(".mysecrets"),
|
|
||||||
"Should not list .mysecrets (private_files)"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Trying to list an excluded directory should fail
|
|
||||||
let input = json!({
|
|
||||||
"path": "project/.secretdir"
|
|
||||||
});
|
|
||||||
|
|
||||||
let result = cx
|
|
||||||
.update(|cx| {
|
|
||||||
tool.clone().run(
|
|
||||||
input,
|
|
||||||
Arc::default(),
|
|
||||||
project.clone(),
|
|
||||||
action_log.clone(),
|
|
||||||
model.clone(),
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.output
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
result.is_err(),
|
|
||||||
"Should not be able to list excluded directory"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
result
|
|
||||||
.unwrap_err()
|
|
||||||
.to_string()
|
|
||||||
.contains("file_scan_exclusions"),
|
|
||||||
"Error should mention file_scan_exclusions"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Listing a directory should exclude private files within it
|
|
||||||
let input = json!({
|
|
||||||
"path": "project/visible_dir"
|
|
||||||
});
|
|
||||||
|
|
||||||
let result = cx
|
|
||||||
.update(|cx| {
|
|
||||||
tool.clone().run(
|
|
||||||
input,
|
|
||||||
Arc::default(),
|
|
||||||
project.clone(),
|
|
||||||
action_log.clone(),
|
|
||||||
model.clone(),
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.output
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let content = result.content.as_str().unwrap();
|
|
||||||
|
|
||||||
// Should include normal files
|
|
||||||
assert!(content.contains("normal.txt"), "Should list normal.txt");
|
|
||||||
|
|
||||||
// Should NOT include private files
|
|
||||||
assert!(
|
|
||||||
!content.contains("privatekey"),
|
|
||||||
"Should not list .privatekey files (private_files)"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
!content.contains("mysensitive"),
|
|
||||||
"Should not list .mysensitive files (private_files)"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Should NOT include subdirectories that match exclusions
|
|
||||||
assert!(
|
|
||||||
!content.contains(".hidden_subdir"),
|
|
||||||
"Should not list .hidden_subdir (file_scan_exclusions)"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_list_directory_with_multiple_worktree_settings(cx: &mut TestAppContext) {
|
|
||||||
init_test(cx);
|
|
||||||
|
|
||||||
let fs = FakeFs::new(cx.executor());
|
|
||||||
|
|
||||||
// Create first worktree with its own private files
|
|
||||||
fs.insert_tree(
|
|
||||||
path!("/worktree1"),
|
|
||||||
json!({
|
|
||||||
".zed": {
|
|
||||||
"settings.json": r#"{
|
|
||||||
"file_scan_exclusions": ["**/fixture.*"],
|
|
||||||
"private_files": ["**/secret.rs", "**/config.toml"]
|
|
||||||
}"#
|
|
||||||
},
|
|
||||||
"src": {
|
|
||||||
"main.rs": "fn main() { println!(\"Hello from worktree1\"); }",
|
|
||||||
"secret.rs": "const API_KEY: &str = \"secret_key_1\";",
|
|
||||||
"config.toml": "[database]\nurl = \"postgres://localhost/db1\""
|
|
||||||
},
|
|
||||||
"tests": {
|
|
||||||
"test.rs": "mod tests { fn test_it() {} }",
|
|
||||||
"fixture.sql": "CREATE TABLE users (id INT, name VARCHAR(255));"
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
// Create second worktree with different private files
|
|
||||||
fs.insert_tree(
|
|
||||||
path!("/worktree2"),
|
|
||||||
json!({
|
|
||||||
".zed": {
|
|
||||||
"settings.json": r#"{
|
|
||||||
"file_scan_exclusions": ["**/internal.*"],
|
|
||||||
"private_files": ["**/private.js", "**/data.json"]
|
|
||||||
}"#
|
|
||||||
},
|
|
||||||
"lib": {
|
|
||||||
"public.js": "export function greet() { return 'Hello from worktree2'; }",
|
|
||||||
"private.js": "const SECRET_TOKEN = \"private_token_2\";",
|
|
||||||
"data.json": "{\"api_key\": \"json_secret_key\"}"
|
|
||||||
},
|
|
||||||
"docs": {
|
|
||||||
"README.md": "# Public Documentation",
|
|
||||||
"internal.md": "# Internal Secrets and Configuration"
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
// Set global settings
|
|
||||||
cx.update(|cx| {
|
|
||||||
SettingsStore::update_global(cx, |store, cx| {
|
|
||||||
store.update_user_settings::<WorktreeSettings>(cx, |settings| {
|
|
||||||
settings.file_scan_exclusions =
|
|
||||||
Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]);
|
|
||||||
settings.private_files = Some(vec!["**/.env".to_string()]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
let project = Project::test(
|
|
||||||
fs.clone(),
|
|
||||||
[path!("/worktree1").as_ref(), path!("/worktree2").as_ref()],
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
// Wait for worktrees to be fully scanned
|
|
||||||
cx.executor().run_until_parked();
|
|
||||||
|
|
||||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
|
||||||
let model = Arc::new(FakeLanguageModel::default());
|
|
||||||
let tool = Arc::new(ListDirectoryTool);
|
|
||||||
|
|
||||||
// Test listing worktree1/src - should exclude secret.rs and config.toml based on local settings
|
|
||||||
let input = json!({
|
|
||||||
"path": "worktree1/src"
|
|
||||||
});
|
|
||||||
|
|
||||||
let result = cx
|
|
||||||
.update(|cx| {
|
|
||||||
tool.clone().run(
|
|
||||||
input,
|
|
||||||
Arc::default(),
|
|
||||||
project.clone(),
|
|
||||||
action_log.clone(),
|
|
||||||
model.clone(),
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.output
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let content = result.content.as_str().unwrap();
|
|
||||||
assert!(content.contains("main.rs"), "Should list main.rs");
|
|
||||||
assert!(
|
|
||||||
!content.contains("secret.rs"),
|
|
||||||
"Should not list secret.rs (local private_files)"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
!content.contains("config.toml"),
|
|
||||||
"Should not list config.toml (local private_files)"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test listing worktree1/tests - should exclude fixture.sql based on local settings
|
|
||||||
let input = json!({
|
|
||||||
"path": "worktree1/tests"
|
|
||||||
});
|
|
||||||
|
|
||||||
let result = cx
|
|
||||||
.update(|cx| {
|
|
||||||
tool.clone().run(
|
|
||||||
input,
|
|
||||||
Arc::default(),
|
|
||||||
project.clone(),
|
|
||||||
action_log.clone(),
|
|
||||||
model.clone(),
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.output
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let content = result.content.as_str().unwrap();
|
|
||||||
assert!(content.contains("test.rs"), "Should list test.rs");
|
|
||||||
assert!(
|
|
||||||
!content.contains("fixture.sql"),
|
|
||||||
"Should not list fixture.sql (local file_scan_exclusions)"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test listing worktree2/lib - should exclude private.js and data.json based on local settings
|
|
||||||
let input = json!({
|
|
||||||
"path": "worktree2/lib"
|
|
||||||
});
|
|
||||||
|
|
||||||
let result = cx
|
|
||||||
.update(|cx| {
|
|
||||||
tool.clone().run(
|
|
||||||
input,
|
|
||||||
Arc::default(),
|
|
||||||
project.clone(),
|
|
||||||
action_log.clone(),
|
|
||||||
model.clone(),
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.output
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let content = result.content.as_str().unwrap();
|
|
||||||
assert!(content.contains("public.js"), "Should list public.js");
|
|
||||||
assert!(
|
|
||||||
!content.contains("private.js"),
|
|
||||||
"Should not list private.js (local private_files)"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
!content.contains("data.json"),
|
|
||||||
"Should not list data.json (local private_files)"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test listing worktree2/docs - should exclude internal.md based on local settings
|
|
||||||
let input = json!({
|
|
||||||
"path": "worktree2/docs"
|
|
||||||
});
|
|
||||||
|
|
||||||
let result = cx
|
|
||||||
.update(|cx| {
|
|
||||||
tool.clone().run(
|
|
||||||
input,
|
|
||||||
Arc::default(),
|
|
||||||
project.clone(),
|
|
||||||
action_log.clone(),
|
|
||||||
model.clone(),
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.output
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let content = result.content.as_str().unwrap();
|
|
||||||
assert!(content.contains("README.md"), "Should list README.md");
|
|
||||||
assert!(
|
|
||||||
!content.contains("internal.md"),
|
|
||||||
"Should not list internal.md (local file_scan_exclusions)"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test trying to list an excluded directory directly
|
|
||||||
let input = json!({
|
|
||||||
"path": "worktree1/src/secret.rs"
|
|
||||||
});
|
|
||||||
|
|
||||||
let result = cx
|
|
||||||
.update(|cx| {
|
|
||||||
tool.clone().run(
|
|
||||||
input,
|
|
||||||
Arc::default(),
|
|
||||||
project.clone(),
|
|
||||||
action_log.clone(),
|
|
||||||
model.clone(),
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.output
|
|
||||||
.await;
|
|
||||||
|
|
||||||
// This should fail because we're trying to list a file, not a directory
|
|
||||||
assert!(result.is_err(), "Should fail when trying to list a file");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,10 +46,6 @@ impl Tool for MovePathTool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn may_perform_edits(&self) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn description(&self) -> String {
|
fn description(&self) -> String {
|
||||||
include_str!("./move_path_tool/description.md").into()
|
include_str!("./move_path_tool/description.md").into()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,10 +37,6 @@ impl Tool for NowTool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn may_perform_edits(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn description(&self) -> String {
|
fn description(&self) -> String {
|
||||||
"Returns the current datetime in RFC 3339 format. Only use this tool when the user specifically asks for it or the current task would benefit from knowing the current datetime.".into()
|
"Returns the current datetime in RFC 3339 format. Only use this tool when the user specifically asks for it or the current task would benefit from knowing the current datetime.".into()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,9 +26,7 @@ impl Tool for OpenTool {
|
|||||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
fn may_perform_edits(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
fn description(&self) -> String {
|
fn description(&self) -> String {
|
||||||
include_str!("./open_tool/description.md").to_string()
|
include_str!("./open_tool/description.md").to_string()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,10 +12,9 @@ use language::{Anchor, Point};
|
|||||||
use language_model::{
|
use language_model::{
|
||||||
LanguageModel, LanguageModelImage, LanguageModelRequest, LanguageModelToolSchemaFormat,
|
LanguageModel, LanguageModelImage, LanguageModelRequest, LanguageModelToolSchemaFormat,
|
||||||
};
|
};
|
||||||
use project::{AgentLocation, Project, WorktreeSettings};
|
use project::{AgentLocation, Project};
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::Settings;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use ui::IconName;
|
use ui::IconName;
|
||||||
use util::markdown::MarkdownInlineCode;
|
use util::markdown::MarkdownInlineCode;
|
||||||
@@ -59,10 +58,6 @@ impl Tool for ReadFileTool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn may_perform_edits(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn description(&self) -> String {
|
fn description(&self) -> String {
|
||||||
include_str!("./read_file_tool/description.md").into()
|
include_str!("./read_file_tool/description.md").into()
|
||||||
}
|
}
|
||||||
@@ -108,48 +103,12 @@ impl Tool for ReadFileTool {
|
|||||||
return Task::ready(Err(anyhow!("Path {} not found in project", &input.path))).into();
|
return Task::ready(Err(anyhow!("Path {} not found in project", &input.path))).into();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Error out if this path is either excluded or private in global settings
|
|
||||||
let global_settings = WorktreeSettings::get_global(cx);
|
|
||||||
if global_settings.is_path_excluded(&project_path.path) {
|
|
||||||
return Task::ready(Err(anyhow!(
|
|
||||||
"Cannot read file because its path matches the global `file_scan_exclusions` setting: {}",
|
|
||||||
&input.path
|
|
||||||
)))
|
|
||||||
.into();
|
|
||||||
}
|
|
||||||
|
|
||||||
if global_settings.is_path_private(&project_path.path) {
|
|
||||||
return Task::ready(Err(anyhow!(
|
|
||||||
"Cannot read file because its path matches the global `private_files` setting: {}",
|
|
||||||
&input.path
|
|
||||||
)))
|
|
||||||
.into();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error out if this path is either excluded or private in worktree settings
|
|
||||||
let worktree_settings = WorktreeSettings::get(Some((&project_path).into()), cx);
|
|
||||||
if worktree_settings.is_path_excluded(&project_path.path) {
|
|
||||||
return Task::ready(Err(anyhow!(
|
|
||||||
"Cannot read file because its path matches the worktree `file_scan_exclusions` setting: {}",
|
|
||||||
&input.path
|
|
||||||
)))
|
|
||||||
.into();
|
|
||||||
}
|
|
||||||
|
|
||||||
if worktree_settings.is_path_private(&project_path.path) {
|
|
||||||
return Task::ready(Err(anyhow!(
|
|
||||||
"Cannot read file because its path matches the worktree `private_files` setting: {}",
|
|
||||||
&input.path
|
|
||||||
)))
|
|
||||||
.into();
|
|
||||||
}
|
|
||||||
|
|
||||||
let file_path = input.path.clone();
|
let file_path = input.path.clone();
|
||||||
|
|
||||||
if image_store::is_image_file(&project, &project_path, cx) {
|
if image_store::is_image_file(&project, &project_path, cx) {
|
||||||
if !model.supports_images() {
|
if !model.supports_images() {
|
||||||
return Task::ready(Err(anyhow!(
|
return Task::ready(Err(anyhow!(
|
||||||
"Attempted to read an image, but Zed doesn't currently support sending images to {}.",
|
"Attempted to read an image, but Zed doesn't currently sending images to {}.",
|
||||||
model.name().0
|
model.name().0
|
||||||
)))
|
)))
|
||||||
.into();
|
.into();
|
||||||
@@ -289,10 +248,10 @@ impl Tool for ReadFileTool {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use gpui::{AppContext, TestAppContext, UpdateGlobal};
|
use gpui::{AppContext, TestAppContext};
|
||||||
use language::{Language, LanguageConfig, LanguageMatcher};
|
use language::{Language, LanguageConfig, LanguageMatcher};
|
||||||
use language_model::fake_provider::FakeLanguageModel;
|
use language_model::fake_provider::FakeLanguageModel;
|
||||||
use project::{FakeFs, Project, WorktreeSettings};
|
use project::{FakeFs, Project};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
use util::path;
|
use util::path;
|
||||||
@@ -302,7 +261,7 @@ mod test {
|
|||||||
init_test(cx);
|
init_test(cx);
|
||||||
|
|
||||||
let fs = FakeFs::new(cx.executor());
|
let fs = FakeFs::new(cx.executor());
|
||||||
fs.insert_tree(path!("/root"), json!({})).await;
|
fs.insert_tree("/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 model = Arc::new(FakeLanguageModel::default());
|
let model = Arc::new(FakeLanguageModel::default());
|
||||||
@@ -336,7 +295,7 @@ mod test {
|
|||||||
|
|
||||||
let fs = FakeFs::new(cx.executor());
|
let fs = FakeFs::new(cx.executor());
|
||||||
fs.insert_tree(
|
fs.insert_tree(
|
||||||
path!("/root"),
|
"/root",
|
||||||
json!({
|
json!({
|
||||||
"small_file.txt": "This is a small file content"
|
"small_file.txt": "This is a small file content"
|
||||||
}),
|
}),
|
||||||
@@ -375,7 +334,7 @@ mod test {
|
|||||||
|
|
||||||
let fs = FakeFs::new(cx.executor());
|
let fs = FakeFs::new(cx.executor());
|
||||||
fs.insert_tree(
|
fs.insert_tree(
|
||||||
path!("/root"),
|
"/root",
|
||||||
json!({
|
json!({
|
||||||
"large_file.rs": (0..1000).map(|i| format!("struct Test{} {{\n a: u32,\n b: usize,\n}}", i)).collect::<Vec<_>>().join("\n")
|
"large_file.rs": (0..1000).map(|i| format!("struct Test{} {{\n a: u32,\n b: usize,\n}}", i)).collect::<Vec<_>>().join("\n")
|
||||||
}),
|
}),
|
||||||
@@ -466,7 +425,7 @@ mod test {
|
|||||||
|
|
||||||
let fs = FakeFs::new(cx.executor());
|
let fs = FakeFs::new(cx.executor());
|
||||||
fs.insert_tree(
|
fs.insert_tree(
|
||||||
path!("/root"),
|
"/root",
|
||||||
json!({
|
json!({
|
||||||
"multiline.txt": "Line 1\nLine 2\nLine 3\nLine 4\nLine 5"
|
"multiline.txt": "Line 1\nLine 2\nLine 3\nLine 4\nLine 5"
|
||||||
}),
|
}),
|
||||||
@@ -507,7 +466,7 @@ mod test {
|
|||||||
|
|
||||||
let fs = FakeFs::new(cx.executor());
|
let fs = FakeFs::new(cx.executor());
|
||||||
fs.insert_tree(
|
fs.insert_tree(
|
||||||
path!("/root"),
|
"/root",
|
||||||
json!({
|
json!({
|
||||||
"multiline.txt": "Line 1\nLine 2\nLine 3\nLine 4\nLine 5"
|
"multiline.txt": "Line 1\nLine 2\nLine 3\nLine 4\nLine 5"
|
||||||
}),
|
}),
|
||||||
@@ -638,544 +597,4 @@ mod test {
|
|||||||
)
|
)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_read_file_security(cx: &mut TestAppContext) {
|
|
||||||
init_test(cx);
|
|
||||||
|
|
||||||
let fs = FakeFs::new(cx.executor());
|
|
||||||
|
|
||||||
fs.insert_tree(
|
|
||||||
path!("/"),
|
|
||||||
json!({
|
|
||||||
"project_root": {
|
|
||||||
"allowed_file.txt": "This file is in the project",
|
|
||||||
".mysecrets": "SECRET_KEY=abc123",
|
|
||||||
".secretdir": {
|
|
||||||
"config": "special configuration"
|
|
||||||
},
|
|
||||||
".mymetadata": "custom metadata",
|
|
||||||
"subdir": {
|
|
||||||
"normal_file.txt": "Normal file content",
|
|
||||||
"special.privatekey": "private key content",
|
|
||||||
"data.mysensitive": "sensitive data"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"outside_project": {
|
|
||||||
"sensitive_file.txt": "This file is outside the project"
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
cx.update(|cx| {
|
|
||||||
use gpui::UpdateGlobal;
|
|
||||||
use project::WorktreeSettings;
|
|
||||||
use settings::SettingsStore;
|
|
||||||
SettingsStore::update_global(cx, |store, cx| {
|
|
||||||
store.update_user_settings::<WorktreeSettings>(cx, |settings| {
|
|
||||||
settings.file_scan_exclusions = Some(vec![
|
|
||||||
"**/.secretdir".to_string(),
|
|
||||||
"**/.mymetadata".to_string(),
|
|
||||||
]);
|
|
||||||
settings.private_files = Some(vec![
|
|
||||||
"**/.mysecrets".to_string(),
|
|
||||||
"**/*.privatekey".to_string(),
|
|
||||||
"**/*.mysensitive".to_string(),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
let project = Project::test(fs.clone(), [path!("/project_root").as_ref()], cx).await;
|
|
||||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
|
||||||
let model = Arc::new(FakeLanguageModel::default());
|
|
||||||
|
|
||||||
// Reading a file outside the project worktree should fail
|
|
||||||
let result = cx
|
|
||||||
.update(|cx| {
|
|
||||||
let input = json!({
|
|
||||||
"path": "/outside_project/sensitive_file.txt"
|
|
||||||
});
|
|
||||||
Arc::new(ReadFileTool)
|
|
||||||
.run(
|
|
||||||
input,
|
|
||||||
Arc::default(),
|
|
||||||
project.clone(),
|
|
||||||
action_log.clone(),
|
|
||||||
model.clone(),
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.output
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
assert!(
|
|
||||||
result.is_err(),
|
|
||||||
"read_file_tool should error when attempting to read an absolute path outside a worktree"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Reading a file within the project should succeed
|
|
||||||
let result = cx
|
|
||||||
.update(|cx| {
|
|
||||||
let input = json!({
|
|
||||||
"path": "project_root/allowed_file.txt"
|
|
||||||
});
|
|
||||||
Arc::new(ReadFileTool)
|
|
||||||
.run(
|
|
||||||
input,
|
|
||||||
Arc::default(),
|
|
||||||
project.clone(),
|
|
||||||
action_log.clone(),
|
|
||||||
model.clone(),
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.output
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
assert!(
|
|
||||||
result.is_ok(),
|
|
||||||
"read_file_tool should be able to read files inside worktrees"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Reading files that match file_scan_exclusions should fail
|
|
||||||
let result = cx
|
|
||||||
.update(|cx| {
|
|
||||||
let input = json!({
|
|
||||||
"path": "project_root/.secretdir/config"
|
|
||||||
});
|
|
||||||
Arc::new(ReadFileTool)
|
|
||||||
.run(
|
|
||||||
input,
|
|
||||||
Arc::default(),
|
|
||||||
project.clone(),
|
|
||||||
action_log.clone(),
|
|
||||||
model.clone(),
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.output
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
assert!(
|
|
||||||
result.is_err(),
|
|
||||||
"read_file_tool should error when attempting to read files in .secretdir (file_scan_exclusions)"
|
|
||||||
);
|
|
||||||
|
|
||||||
let result = cx
|
|
||||||
.update(|cx| {
|
|
||||||
let input = json!({
|
|
||||||
"path": "project_root/.mymetadata"
|
|
||||||
});
|
|
||||||
Arc::new(ReadFileTool)
|
|
||||||
.run(
|
|
||||||
input,
|
|
||||||
Arc::default(),
|
|
||||||
project.clone(),
|
|
||||||
action_log.clone(),
|
|
||||||
model.clone(),
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.output
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
assert!(
|
|
||||||
result.is_err(),
|
|
||||||
"read_file_tool should error when attempting to read .mymetadata files (file_scan_exclusions)"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Reading private files should fail
|
|
||||||
let result = cx
|
|
||||||
.update(|cx| {
|
|
||||||
let input = json!({
|
|
||||||
"path": "project_root/.mysecrets"
|
|
||||||
});
|
|
||||||
Arc::new(ReadFileTool)
|
|
||||||
.run(
|
|
||||||
input,
|
|
||||||
Arc::default(),
|
|
||||||
project.clone(),
|
|
||||||
action_log.clone(),
|
|
||||||
model.clone(),
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.output
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
assert!(
|
|
||||||
result.is_err(),
|
|
||||||
"read_file_tool should error when attempting to read .mysecrets (private_files)"
|
|
||||||
);
|
|
||||||
|
|
||||||
let result = cx
|
|
||||||
.update(|cx| {
|
|
||||||
let input = json!({
|
|
||||||
"path": "project_root/subdir/special.privatekey"
|
|
||||||
});
|
|
||||||
Arc::new(ReadFileTool)
|
|
||||||
.run(
|
|
||||||
input,
|
|
||||||
Arc::default(),
|
|
||||||
project.clone(),
|
|
||||||
action_log.clone(),
|
|
||||||
model.clone(),
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.output
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
assert!(
|
|
||||||
result.is_err(),
|
|
||||||
"read_file_tool should error when attempting to read .privatekey files (private_files)"
|
|
||||||
);
|
|
||||||
|
|
||||||
let result = cx
|
|
||||||
.update(|cx| {
|
|
||||||
let input = json!({
|
|
||||||
"path": "project_root/subdir/data.mysensitive"
|
|
||||||
});
|
|
||||||
Arc::new(ReadFileTool)
|
|
||||||
.run(
|
|
||||||
input,
|
|
||||||
Arc::default(),
|
|
||||||
project.clone(),
|
|
||||||
action_log.clone(),
|
|
||||||
model.clone(),
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.output
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
assert!(
|
|
||||||
result.is_err(),
|
|
||||||
"read_file_tool should error when attempting to read .mysensitive files (private_files)"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Reading a normal file should still work, even with private_files configured
|
|
||||||
let result = cx
|
|
||||||
.update(|cx| {
|
|
||||||
let input = json!({
|
|
||||||
"path": "project_root/subdir/normal_file.txt"
|
|
||||||
});
|
|
||||||
Arc::new(ReadFileTool)
|
|
||||||
.run(
|
|
||||||
input,
|
|
||||||
Arc::default(),
|
|
||||||
project.clone(),
|
|
||||||
action_log.clone(),
|
|
||||||
model.clone(),
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.output
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
assert!(result.is_ok(), "Should be able to read normal files");
|
|
||||||
assert_eq!(
|
|
||||||
result.unwrap().content.as_str().unwrap(),
|
|
||||||
"Normal file content"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Path traversal attempts with .. should fail
|
|
||||||
let result = cx
|
|
||||||
.update(|cx| {
|
|
||||||
let input = json!({
|
|
||||||
"path": "project_root/../outside_project/sensitive_file.txt"
|
|
||||||
});
|
|
||||||
Arc::new(ReadFileTool)
|
|
||||||
.run(
|
|
||||||
input,
|
|
||||||
Arc::default(),
|
|
||||||
project.clone(),
|
|
||||||
action_log.clone(),
|
|
||||||
model.clone(),
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.output
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
assert!(
|
|
||||||
result.is_err(),
|
|
||||||
"read_file_tool should error when attempting to read a relative path that resolves to outside a worktree"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_read_file_with_multiple_worktree_settings(cx: &mut TestAppContext) {
|
|
||||||
init_test(cx);
|
|
||||||
|
|
||||||
let fs = FakeFs::new(cx.executor());
|
|
||||||
|
|
||||||
// Create first worktree with its own private_files setting
|
|
||||||
fs.insert_tree(
|
|
||||||
path!("/worktree1"),
|
|
||||||
json!({
|
|
||||||
"src": {
|
|
||||||
"main.rs": "fn main() { println!(\"Hello from worktree1\"); }",
|
|
||||||
"secret.rs": "const API_KEY: &str = \"secret_key_1\";",
|
|
||||||
"config.toml": "[database]\nurl = \"postgres://localhost/db1\""
|
|
||||||
},
|
|
||||||
"tests": {
|
|
||||||
"test.rs": "mod tests { fn test_it() {} }",
|
|
||||||
"fixture.sql": "CREATE TABLE users (id INT, name VARCHAR(255));"
|
|
||||||
},
|
|
||||||
".zed": {
|
|
||||||
"settings.json": r#"{
|
|
||||||
"file_scan_exclusions": ["**/fixture.*"],
|
|
||||||
"private_files": ["**/secret.rs", "**/config.toml"]
|
|
||||||
}"#
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
// Create second worktree with different private_files setting
|
|
||||||
fs.insert_tree(
|
|
||||||
path!("/worktree2"),
|
|
||||||
json!({
|
|
||||||
"lib": {
|
|
||||||
"public.js": "export function greet() { return 'Hello from worktree2'; }",
|
|
||||||
"private.js": "const SECRET_TOKEN = \"private_token_2\";",
|
|
||||||
"data.json": "{\"api_key\": \"json_secret_key\"}"
|
|
||||||
},
|
|
||||||
"docs": {
|
|
||||||
"README.md": "# Public Documentation",
|
|
||||||
"internal.md": "# Internal Secrets and Configuration"
|
|
||||||
},
|
|
||||||
".zed": {
|
|
||||||
"settings.json": r#"{
|
|
||||||
"file_scan_exclusions": ["**/internal.*"],
|
|
||||||
"private_files": ["**/private.js", "**/data.json"]
|
|
||||||
}"#
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
// Set global settings
|
|
||||||
cx.update(|cx| {
|
|
||||||
SettingsStore::update_global(cx, |store, cx| {
|
|
||||||
store.update_user_settings::<WorktreeSettings>(cx, |settings| {
|
|
||||||
settings.file_scan_exclusions =
|
|
||||||
Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]);
|
|
||||||
settings.private_files = Some(vec!["**/.env".to_string()]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
let project = Project::test(
|
|
||||||
fs.clone(),
|
|
||||||
[path!("/worktree1").as_ref(), path!("/worktree2").as_ref()],
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
|
||||||
let model = Arc::new(FakeLanguageModel::default());
|
|
||||||
let tool = Arc::new(ReadFileTool);
|
|
||||||
|
|
||||||
// Test reading allowed files in worktree1
|
|
||||||
let input = json!({
|
|
||||||
"path": "worktree1/src/main.rs"
|
|
||||||
});
|
|
||||||
|
|
||||||
let result = cx
|
|
||||||
.update(|cx| {
|
|
||||||
tool.clone().run(
|
|
||||||
input,
|
|
||||||
Arc::default(),
|
|
||||||
project.clone(),
|
|
||||||
action_log.clone(),
|
|
||||||
model.clone(),
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.output
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
result.content.as_str().unwrap(),
|
|
||||||
"fn main() { println!(\"Hello from worktree1\"); }"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test reading private file in worktree1 should fail
|
|
||||||
let input = json!({
|
|
||||||
"path": "worktree1/src/secret.rs"
|
|
||||||
});
|
|
||||||
|
|
||||||
let result = cx
|
|
||||||
.update(|cx| {
|
|
||||||
tool.clone().run(
|
|
||||||
input,
|
|
||||||
Arc::default(),
|
|
||||||
project.clone(),
|
|
||||||
action_log.clone(),
|
|
||||||
model.clone(),
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.output
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(result.is_err());
|
|
||||||
assert!(
|
|
||||||
result
|
|
||||||
.unwrap_err()
|
|
||||||
.to_string()
|
|
||||||
.contains("worktree `private_files` setting"),
|
|
||||||
"Error should mention worktree private_files setting"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test reading excluded file in worktree1 should fail
|
|
||||||
let input = json!({
|
|
||||||
"path": "worktree1/tests/fixture.sql"
|
|
||||||
});
|
|
||||||
|
|
||||||
let result = cx
|
|
||||||
.update(|cx| {
|
|
||||||
tool.clone().run(
|
|
||||||
input,
|
|
||||||
Arc::default(),
|
|
||||||
project.clone(),
|
|
||||||
action_log.clone(),
|
|
||||||
model.clone(),
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.output
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(result.is_err());
|
|
||||||
assert!(
|
|
||||||
result
|
|
||||||
.unwrap_err()
|
|
||||||
.to_string()
|
|
||||||
.contains("worktree `file_scan_exclusions` setting"),
|
|
||||||
"Error should mention worktree file_scan_exclusions setting"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test reading allowed files in worktree2
|
|
||||||
let input = json!({
|
|
||||||
"path": "worktree2/lib/public.js"
|
|
||||||
});
|
|
||||||
|
|
||||||
let result = cx
|
|
||||||
.update(|cx| {
|
|
||||||
tool.clone().run(
|
|
||||||
input,
|
|
||||||
Arc::default(),
|
|
||||||
project.clone(),
|
|
||||||
action_log.clone(),
|
|
||||||
model.clone(),
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.output
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
result.content.as_str().unwrap(),
|
|
||||||
"export function greet() { return 'Hello from worktree2'; }"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test reading private file in worktree2 should fail
|
|
||||||
let input = json!({
|
|
||||||
"path": "worktree2/lib/private.js"
|
|
||||||
});
|
|
||||||
|
|
||||||
let result = cx
|
|
||||||
.update(|cx| {
|
|
||||||
tool.clone().run(
|
|
||||||
input,
|
|
||||||
Arc::default(),
|
|
||||||
project.clone(),
|
|
||||||
action_log.clone(),
|
|
||||||
model.clone(),
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.output
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(result.is_err());
|
|
||||||
assert!(
|
|
||||||
result
|
|
||||||
.unwrap_err()
|
|
||||||
.to_string()
|
|
||||||
.contains("worktree `private_files` setting"),
|
|
||||||
"Error should mention worktree private_files setting"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test reading excluded file in worktree2 should fail
|
|
||||||
let input = json!({
|
|
||||||
"path": "worktree2/docs/internal.md"
|
|
||||||
});
|
|
||||||
|
|
||||||
let result = cx
|
|
||||||
.update(|cx| {
|
|
||||||
tool.clone().run(
|
|
||||||
input,
|
|
||||||
Arc::default(),
|
|
||||||
project.clone(),
|
|
||||||
action_log.clone(),
|
|
||||||
model.clone(),
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.output
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(result.is_err());
|
|
||||||
assert!(
|
|
||||||
result
|
|
||||||
.unwrap_err()
|
|
||||||
.to_string()
|
|
||||||
.contains("worktree `file_scan_exclusions` setting"),
|
|
||||||
"Error should mention worktree file_scan_exclusions setting"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test that files allowed in one worktree but not in another are handled correctly
|
|
||||||
// (e.g., config.toml is private in worktree1 but doesn't exist in worktree2)
|
|
||||||
let input = json!({
|
|
||||||
"path": "worktree1/src/config.toml"
|
|
||||||
});
|
|
||||||
|
|
||||||
let result = cx
|
|
||||||
.update(|cx| {
|
|
||||||
tool.clone().run(
|
|
||||||
input,
|
|
||||||
Arc::default(),
|
|
||||||
project.clone(),
|
|
||||||
action_log.clone(),
|
|
||||||
model.clone(),
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.output
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(result.is_err());
|
|
||||||
assert!(
|
|
||||||
result
|
|
||||||
.unwrap_err()
|
|
||||||
.to_string()
|
|
||||||
.contains("worktree `private_files` setting"),
|
|
||||||
"Config.toml should be blocked by worktree1's private_files setting"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,10 +80,6 @@ impl Tool for TerminalTool {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn may_perform_edits(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn description(&self) -> String {
|
fn description(&self) -> String {
|
||||||
include_str!("./terminal_tool/description.md").to_string()
|
include_str!("./terminal_tool/description.md").to_string()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,10 +28,6 @@ impl Tool for ThinkingTool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn may_perform_edits(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn description(&self) -> String {
|
fn description(&self) -> String {
|
||||||
include_str!("./thinking_tool/description.md").to_string()
|
include_str!("./thinking_tool/description.md").to_string()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,10 +36,6 @@ impl Tool for WebSearchTool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn may_perform_edits(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn description(&self) -> String {
|
fn description(&self) -> String {
|
||||||
"Search the web for information using your query. Use this when you need real-time information, facts, or data that might not be in your training. Results will include snippets and links from relevant web pages.".into()
|
"Search the web for information using your query. Use this when you need real-time information, facts, or data that might not be in your training. Results will include snippets and links from relevant web pages.".into()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,22 +71,20 @@ pub enum Model {
|
|||||||
// DeepSeek
|
// DeepSeek
|
||||||
DeepSeekR1,
|
DeepSeekR1,
|
||||||
// Meta models
|
// Meta models
|
||||||
MetaLlama38BInstructV1,
|
MetaLlama3_8BInstruct,
|
||||||
MetaLlama370BInstructV1,
|
MetaLlama3_70BInstruct,
|
||||||
MetaLlama318BInstructV1_128k,
|
MetaLlama31_8BInstruct,
|
||||||
MetaLlama318BInstructV1,
|
MetaLlama31_70BInstruct,
|
||||||
MetaLlama3170BInstructV1_128k,
|
MetaLlama31_405BInstruct,
|
||||||
MetaLlama3170BInstructV1,
|
MetaLlama32_1BInstruct,
|
||||||
MetaLlama31405BInstructV1,
|
MetaLlama32_3BInstruct,
|
||||||
MetaLlama321BInstructV1,
|
MetaLlama32_11BMultiModal,
|
||||||
MetaLlama323BInstructV1,
|
MetaLlama32_90BMultiModal,
|
||||||
MetaLlama3211BInstructV1,
|
MetaLlama33_70BInstruct,
|
||||||
MetaLlama3290BInstructV1,
|
|
||||||
MetaLlama3370BInstructV1,
|
|
||||||
#[allow(non_camel_case_types)]
|
#[allow(non_camel_case_types)]
|
||||||
MetaLlama4Scout17BInstructV1,
|
MetaLlama4Scout_17BInstruct,
|
||||||
#[allow(non_camel_case_types)]
|
#[allow(non_camel_case_types)]
|
||||||
MetaLlama4Maverick17BInstructV1,
|
MetaLlama4Maverick_17BInstruct,
|
||||||
// Mistral models
|
// Mistral models
|
||||||
MistralMistral7BInstructV0,
|
MistralMistral7BInstructV0,
|
||||||
MistralMixtral8x7BInstructV0,
|
MistralMixtral8x7BInstructV0,
|
||||||
@@ -131,64 +129,6 @@ impl Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn id(&self) -> &str {
|
pub fn id(&self) -> &str {
|
||||||
match self {
|
|
||||||
Model::ClaudeSonnet4 => "claude-4-sonnet",
|
|
||||||
Model::ClaudeSonnet4Thinking => "claude-4-sonnet-thinking",
|
|
||||||
Model::ClaudeOpus4 => "claude-4-opus",
|
|
||||||
Model::ClaudeOpus4Thinking => "claude-4-opus-thinking",
|
|
||||||
Model::Claude3_5SonnetV2 => "claude-3-5-sonnet-v2",
|
|
||||||
Model::Claude3_5Sonnet => "claude-3-5-sonnet",
|
|
||||||
Model::Claude3Opus => "claude-3-opus",
|
|
||||||
Model::Claude3Sonnet => "claude-3-sonnet",
|
|
||||||
Model::Claude3Haiku => "claude-3-haiku",
|
|
||||||
Model::Claude3_5Haiku => "claude-3-5-haiku",
|
|
||||||
Model::Claude3_7Sonnet => "claude-3-7-sonnet",
|
|
||||||
Model::Claude3_7SonnetThinking => "claude-3-7-sonnet-thinking",
|
|
||||||
Model::AmazonNovaLite => "amazon-nova-lite",
|
|
||||||
Model::AmazonNovaMicro => "amazon-nova-micro",
|
|
||||||
Model::AmazonNovaPro => "amazon-nova-pro",
|
|
||||||
Model::AmazonNovaPremier => "amazon-nova-premier",
|
|
||||||
Model::DeepSeekR1 => "deepseek-r1",
|
|
||||||
Model::AI21J2GrandeInstruct => "ai21-j2-grande-instruct",
|
|
||||||
Model::AI21J2JumboInstruct => "ai21-j2-jumbo-instruct",
|
|
||||||
Model::AI21J2Mid => "ai21-j2-mid",
|
|
||||||
Model::AI21J2MidV1 => "ai21-j2-mid-v1",
|
|
||||||
Model::AI21J2Ultra => "ai21-j2-ultra",
|
|
||||||
Model::AI21J2UltraV1_8k => "ai21-j2-ultra-v1-8k",
|
|
||||||
Model::AI21J2UltraV1 => "ai21-j2-ultra-v1",
|
|
||||||
Model::AI21JambaInstructV1 => "ai21-jamba-instruct-v1",
|
|
||||||
Model::AI21Jamba15LargeV1 => "ai21-jamba-1-5-large-v1",
|
|
||||||
Model::AI21Jamba15MiniV1 => "ai21-jamba-1-5-mini-v1",
|
|
||||||
Model::CohereCommandTextV14_4k => "cohere-command-text-v14-4k",
|
|
||||||
Model::CohereCommandRV1 => "cohere-command-r-v1",
|
|
||||||
Model::CohereCommandRPlusV1 => "cohere-command-r-plus-v1",
|
|
||||||
Model::CohereCommandLightTextV14_4k => "cohere-command-light-text-v14-4k",
|
|
||||||
Model::MetaLlama38BInstructV1 => "meta-llama3-8b-instruct-v1",
|
|
||||||
Model::MetaLlama370BInstructV1 => "meta-llama3-70b-instruct-v1",
|
|
||||||
Model::MetaLlama318BInstructV1_128k => "meta-llama3-1-8b-instruct-v1-128k",
|
|
||||||
Model::MetaLlama318BInstructV1 => "meta-llama3-1-8b-instruct-v1",
|
|
||||||
Model::MetaLlama3170BInstructV1_128k => "meta-llama3-1-70b-instruct-v1-128k",
|
|
||||||
Model::MetaLlama3170BInstructV1 => "meta-llama3-1-70b-instruct-v1",
|
|
||||||
Model::MetaLlama31405BInstructV1 => "meta-llama3-1-405b-instruct-v1",
|
|
||||||
Model::MetaLlama321BInstructV1 => "meta-llama3-2-1b-instruct-v1",
|
|
||||||
Model::MetaLlama323BInstructV1 => "meta-llama3-2-3b-instruct-v1",
|
|
||||||
Model::MetaLlama3211BInstructV1 => "meta-llama3-2-11b-instruct-v1",
|
|
||||||
Model::MetaLlama3290BInstructV1 => "meta-llama3-2-90b-instruct-v1",
|
|
||||||
Model::MetaLlama3370BInstructV1 => "meta-llama3-3-70b-instruct-v1",
|
|
||||||
Model::MetaLlama4Scout17BInstructV1 => "meta-llama4-scout-17b-instruct-v1",
|
|
||||||
Model::MetaLlama4Maverick17BInstructV1 => "meta-llama4-maverick-17b-instruct-v1",
|
|
||||||
Model::MistralMistral7BInstructV0 => "mistral-7b-instruct-v0",
|
|
||||||
Model::MistralMixtral8x7BInstructV0 => "mistral-mixtral-8x7b-instruct-v0",
|
|
||||||
Model::MistralMistralLarge2402V1 => "mistral-large-2402-v1",
|
|
||||||
Model::MistralMistralSmall2402V1 => "mistral-small-2402-v1",
|
|
||||||
Model::MistralPixtralLarge2502V1 => "mistral-pixtral-large-2502-v1",
|
|
||||||
Model::PalmyraWriterX4 => "palmyra-writer-x4",
|
|
||||||
Model::PalmyraWriterX5 => "palmyra-writer-x5",
|
|
||||||
Self::Custom { name, .. } => name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn request_id(&self) -> &str {
|
|
||||||
match self {
|
match self {
|
||||||
Model::ClaudeSonnet4 | Model::ClaudeSonnet4Thinking => {
|
Model::ClaudeSonnet4 | Model::ClaudeSonnet4Thinking => {
|
||||||
"anthropic.claude-sonnet-4-20250514-v1:0"
|
"anthropic.claude-sonnet-4-20250514-v1:0"
|
||||||
@@ -224,20 +164,18 @@ impl Model {
|
|||||||
Model::CohereCommandRV1 => "cohere.command-r-v1:0",
|
Model::CohereCommandRV1 => "cohere.command-r-v1:0",
|
||||||
Model::CohereCommandRPlusV1 => "cohere.command-r-plus-v1:0",
|
Model::CohereCommandRPlusV1 => "cohere.command-r-plus-v1:0",
|
||||||
Model::CohereCommandLightTextV14_4k => "cohere.command-light-text-v14:7:4k",
|
Model::CohereCommandLightTextV14_4k => "cohere.command-light-text-v14:7:4k",
|
||||||
Model::MetaLlama38BInstructV1 => "meta.llama3-8b-instruct-v1:0",
|
Model::MetaLlama3_8BInstruct => "meta.llama3-8b-instruct-v1:0",
|
||||||
Model::MetaLlama370BInstructV1 => "meta.llama3-70b-instruct-v1:0",
|
Model::MetaLlama3_70BInstruct => "meta.llama3-70b-instruct-v1:0",
|
||||||
Model::MetaLlama318BInstructV1_128k => "meta.llama3-1-8b-instruct-v1:0",
|
Model::MetaLlama31_8BInstruct => "meta.llama3-1-8b-instruct-v1:0",
|
||||||
Model::MetaLlama318BInstructV1 => "meta.llama3-1-8b-instruct-v1:0",
|
Model::MetaLlama31_70BInstruct => "meta.llama3-1-70b-instruct-v1:0",
|
||||||
Model::MetaLlama3170BInstructV1_128k => "meta.llama3-1-70b-instruct-v1:0",
|
Model::MetaLlama31_405BInstruct => "meta.llama3-1-405b-instruct-v1:0",
|
||||||
Model::MetaLlama3170BInstructV1 => "meta.llama3-1-70b-instruct-v1:0",
|
Model::MetaLlama32_11BMultiModal => "meta.llama3-2-11b-instruct-v1:0",
|
||||||
Model::MetaLlama31405BInstructV1 => "meta.llama3-1-405b-instruct-v1:0",
|
Model::MetaLlama32_90BMultiModal => "meta.llama3-2-90b-instruct-v1:0",
|
||||||
Model::MetaLlama3211BInstructV1 => "meta.llama3-2-11b-instruct-v1:0",
|
Model::MetaLlama32_1BInstruct => "meta.llama3-2-1b-instruct-v1:0",
|
||||||
Model::MetaLlama3290BInstructV1 => "meta.llama3-2-90b-instruct-v1:0",
|
Model::MetaLlama32_3BInstruct => "meta.llama3-2-3b-instruct-v1:0",
|
||||||
Model::MetaLlama321BInstructV1 => "meta.llama3-2-1b-instruct-v1:0",
|
Model::MetaLlama33_70BInstruct => "meta.llama3-3-70b-instruct-v1:0",
|
||||||
Model::MetaLlama323BInstructV1 => "meta.llama3-2-3b-instruct-v1:0",
|
Model::MetaLlama4Scout_17BInstruct => "meta.llama4-scout-17b-instruct-v1:0",
|
||||||
Model::MetaLlama3370BInstructV1 => "meta.llama3-3-70b-instruct-v1:0",
|
Model::MetaLlama4Maverick_17BInstruct => "meta.llama4-maverick-17b-instruct-v1:0",
|
||||||
Model::MetaLlama4Scout17BInstructV1 => "meta.llama4-scout-17b-instruct-v1:0",
|
|
||||||
Model::MetaLlama4Maverick17BInstructV1 => "meta.llama4-maverick-17b-instruct-v1:0",
|
|
||||||
Model::MistralMistral7BInstructV0 => "mistral.mistral-7b-instruct-v0:2",
|
Model::MistralMistral7BInstructV0 => "mistral.mistral-7b-instruct-v0:2",
|
||||||
Model::MistralMixtral8x7BInstructV0 => "mistral.mixtral-8x7b-instruct-v0:1",
|
Model::MistralMixtral8x7BInstructV0 => "mistral.mixtral-8x7b-instruct-v0:1",
|
||||||
Model::MistralMistralLarge2402V1 => "mistral.mistral-large-2402-v1:0",
|
Model::MistralMistralLarge2402V1 => "mistral.mistral-large-2402-v1:0",
|
||||||
@@ -282,20 +220,18 @@ impl Model {
|
|||||||
Self::CohereCommandRV1 => "Cohere Command R V1",
|
Self::CohereCommandRV1 => "Cohere Command R V1",
|
||||||
Self::CohereCommandRPlusV1 => "Cohere Command R Plus V1",
|
Self::CohereCommandRPlusV1 => "Cohere Command R Plus V1",
|
||||||
Self::CohereCommandLightTextV14_4k => "Cohere Command Light Text V14 4K",
|
Self::CohereCommandLightTextV14_4k => "Cohere Command Light Text V14 4K",
|
||||||
Self::MetaLlama38BInstructV1 => "Meta Llama 3 8B Instruct",
|
Self::MetaLlama3_8BInstruct => "Meta Llama 3 8B Instruct",
|
||||||
Self::MetaLlama370BInstructV1 => "Meta Llama 3 70B Instruct",
|
Self::MetaLlama3_70BInstruct => "Meta Llama 3 70B Instruct",
|
||||||
Self::MetaLlama318BInstructV1_128k => "Meta Llama 3.1 8B Instruct 128K",
|
Self::MetaLlama31_8BInstruct => "Meta Llama 3.1 8B Instruct",
|
||||||
Self::MetaLlama318BInstructV1 => "Meta Llama 3.1 8B Instruct",
|
Self::MetaLlama31_70BInstruct => "Meta Llama 3.1 70B Instruct",
|
||||||
Self::MetaLlama3170BInstructV1_128k => "Meta Llama 3.1 70B Instruct 128K",
|
Self::MetaLlama31_405BInstruct => "Meta Llama 3.1 405B Instruct",
|
||||||
Self::MetaLlama3170BInstructV1 => "Meta Llama 3.1 70B Instruct",
|
Self::MetaLlama32_11BMultiModal => "Meta Llama 3.2 11B Vision Instruct",
|
||||||
Self::MetaLlama31405BInstructV1 => "Meta Llama 3.1 405B Instruct",
|
Self::MetaLlama32_90BMultiModal => "Meta Llama 3.2 90B Vision Instruct",
|
||||||
Self::MetaLlama3211BInstructV1 => "Meta Llama 3.2 11B Instruct",
|
Self::MetaLlama32_1BInstruct => "Meta Llama 3.2 1B Instruct",
|
||||||
Self::MetaLlama3290BInstructV1 => "Meta Llama 3.2 90B Instruct",
|
Self::MetaLlama32_3BInstruct => "Meta Llama 3.2 3B Instruct",
|
||||||
Self::MetaLlama321BInstructV1 => "Meta Llama 3.2 1B Instruct",
|
Self::MetaLlama33_70BInstruct => "Meta Llama 3.3 70B Instruct",
|
||||||
Self::MetaLlama323BInstructV1 => "Meta Llama 3.2 3B Instruct",
|
Self::MetaLlama4Scout_17BInstruct => "Meta Llama 4 Scout 17B Instruct",
|
||||||
Self::MetaLlama3370BInstructV1 => "Meta Llama 3.3 70B Instruct",
|
Self::MetaLlama4Maverick_17BInstruct => "Meta Llama 4 Maverick 17B Instruct",
|
||||||
Self::MetaLlama4Scout17BInstructV1 => "Meta Llama 4 Scout 17B Instruct",
|
|
||||||
Self::MetaLlama4Maverick17BInstructV1 => "Meta Llama 4 Maverick 17B Instruct",
|
|
||||||
Self::MistralMistral7BInstructV0 => "Mistral 7B Instruct V0",
|
Self::MistralMistral7BInstructV0 => "Mistral 7B Instruct V0",
|
||||||
Self::MistralMixtral8x7BInstructV0 => "Mistral Mixtral 8x7B Instruct V0",
|
Self::MistralMixtral8x7BInstructV0 => "Mistral Mixtral 8x7B Instruct V0",
|
||||||
Self::MistralMistralLarge2402V1 => "Mistral Large 2402 V1",
|
Self::MistralMistralLarge2402V1 => "Mistral Large 2402 V1",
|
||||||
@@ -317,9 +253,7 @@ impl Model {
|
|||||||
| Self::Claude3_5Haiku
|
| Self::Claude3_5Haiku
|
||||||
| Self::Claude3_7Sonnet
|
| Self::Claude3_7Sonnet
|
||||||
| Self::ClaudeSonnet4
|
| Self::ClaudeSonnet4
|
||||||
| Self::ClaudeOpus4
|
| Self::ClaudeOpus4 => 200_000,
|
||||||
| Self::ClaudeSonnet4Thinking
|
|
||||||
| Self::ClaudeOpus4Thinking => 200_000,
|
|
||||||
Self::AmazonNovaPremier => 1_000_000,
|
Self::AmazonNovaPremier => 1_000_000,
|
||||||
Self::PalmyraWriterX5 => 1_000_000,
|
Self::PalmyraWriterX5 => 1_000_000,
|
||||||
Self::PalmyraWriterX4 => 128_000,
|
Self::PalmyraWriterX4 => 128_000,
|
||||||
@@ -428,11 +362,11 @@ impl Model {
|
|||||||
anyhow::bail!("Unsupported Region {region}");
|
anyhow::bail!("Unsupported Region {region}");
|
||||||
};
|
};
|
||||||
|
|
||||||
let model_id = self.request_id();
|
let model_id = self.id();
|
||||||
|
|
||||||
match (self, region_group) {
|
match (self, region_group) {
|
||||||
// Custom models can't have CRI IDs
|
// Custom models can't have CRI IDs
|
||||||
(Model::Custom { .. }, _) => Ok(self.request_id().into()),
|
(Model::Custom { .. }, _) => Ok(self.id().into()),
|
||||||
|
|
||||||
// Models with US Gov only
|
// Models with US Gov only
|
||||||
(Model::Claude3_5Sonnet, "us-gov") | (Model::Claude3Haiku, "us-gov") => {
|
(Model::Claude3_5Sonnet, "us-gov") | (Model::Claude3Haiku, "us-gov") => {
|
||||||
@@ -456,18 +390,16 @@ impl Model {
|
|||||||
| Model::Claude3Opus
|
| Model::Claude3Opus
|
||||||
| Model::Claude3Sonnet
|
| Model::Claude3Sonnet
|
||||||
| Model::DeepSeekR1
|
| Model::DeepSeekR1
|
||||||
| Model::MetaLlama31405BInstructV1
|
| Model::MetaLlama31_405BInstruct
|
||||||
| Model::MetaLlama3170BInstructV1_128k
|
| Model::MetaLlama31_70BInstruct
|
||||||
| Model::MetaLlama3170BInstructV1
|
| Model::MetaLlama31_8BInstruct
|
||||||
| Model::MetaLlama318BInstructV1_128k
|
| Model::MetaLlama32_11BMultiModal
|
||||||
| Model::MetaLlama318BInstructV1
|
| Model::MetaLlama32_1BInstruct
|
||||||
| Model::MetaLlama3211BInstructV1
|
| Model::MetaLlama32_3BInstruct
|
||||||
| Model::MetaLlama321BInstructV1
|
| Model::MetaLlama32_90BMultiModal
|
||||||
| Model::MetaLlama323BInstructV1
|
| Model::MetaLlama33_70BInstruct
|
||||||
| Model::MetaLlama3290BInstructV1
|
| Model::MetaLlama4Maverick_17BInstruct
|
||||||
| Model::MetaLlama3370BInstructV1
|
| Model::MetaLlama4Scout_17BInstruct
|
||||||
| Model::MetaLlama4Maverick17BInstructV1
|
|
||||||
| Model::MetaLlama4Scout17BInstructV1
|
|
||||||
| Model::MistralPixtralLarge2502V1
|
| Model::MistralPixtralLarge2502V1
|
||||||
| Model::PalmyraWriterX4
|
| Model::PalmyraWriterX4
|
||||||
| Model::PalmyraWriterX5,
|
| Model::PalmyraWriterX5,
|
||||||
@@ -481,8 +413,8 @@ impl Model {
|
|||||||
| Model::Claude3_7SonnetThinking
|
| Model::Claude3_7SonnetThinking
|
||||||
| Model::Claude3Haiku
|
| Model::Claude3Haiku
|
||||||
| Model::Claude3Sonnet
|
| Model::Claude3Sonnet
|
||||||
| Model::MetaLlama321BInstructV1
|
| Model::MetaLlama32_1BInstruct
|
||||||
| Model::MetaLlama323BInstructV1
|
| Model::MetaLlama32_3BInstruct
|
||||||
| Model::MistralPixtralLarge2502V1,
|
| Model::MistralPixtralLarge2502V1,
|
||||||
"eu",
|
"eu",
|
||||||
) => Ok(format!("{}.{}", region_group, model_id)),
|
) => Ok(format!("{}.{}", region_group, model_id)),
|
||||||
@@ -497,7 +429,7 @@ impl Model {
|
|||||||
) => Ok(format!("{}.{}", region_group, model_id)),
|
) => Ok(format!("{}.{}", region_group, model_id)),
|
||||||
|
|
||||||
// Any other combination is not supported
|
// Any other combination is not supported
|
||||||
_ => Ok(self.request_id().into()),
|
_ => Ok(self.id().into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -574,15 +506,15 @@ mod tests {
|
|||||||
fn test_meta_models_inference_ids() -> anyhow::Result<()> {
|
fn test_meta_models_inference_ids() -> anyhow::Result<()> {
|
||||||
// Test Meta models
|
// Test Meta models
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Model::MetaLlama370BInstructV1.cross_region_inference_id("us-east-1")?,
|
Model::MetaLlama3_70BInstruct.cross_region_inference_id("us-east-1")?,
|
||||||
"meta.llama3-70b-instruct-v1:0"
|
"meta.llama3-70b-instruct-v1:0"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Model::MetaLlama3170BInstructV1.cross_region_inference_id("us-east-1")?,
|
Model::MetaLlama31_70BInstruct.cross_region_inference_id("us-east-1")?,
|
||||||
"us.meta.llama3-1-70b-instruct-v1:0"
|
"us.meta.llama3-1-70b-instruct-v1:0"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Model::MetaLlama321BInstructV1.cross_region_inference_id("eu-west-1")?,
|
Model::MetaLlama32_1BInstruct.cross_region_inference_id("eu-west-1")?,
|
||||||
"eu.meta.llama3-2-1b-instruct-v1:0"
|
"eu.meta.llama3-2-1b-instruct-v1:0"
|
||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -652,39 +584,4 @@ mod tests {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_friendly_id_vs_request_id() {
|
|
||||||
// Test that id() returns friendly identifiers
|
|
||||||
assert_eq!(Model::Claude3_5SonnetV2.id(), "claude-3-5-sonnet-v2");
|
|
||||||
assert_eq!(Model::AmazonNovaLite.id(), "amazon-nova-lite");
|
|
||||||
assert_eq!(Model::DeepSeekR1.id(), "deepseek-r1");
|
|
||||||
assert_eq!(
|
|
||||||
Model::MetaLlama38BInstructV1.id(),
|
|
||||||
"meta-llama3-8b-instruct-v1"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test that request_id() returns actual backend model IDs
|
|
||||||
assert_eq!(
|
|
||||||
Model::Claude3_5SonnetV2.request_id(),
|
|
||||||
"anthropic.claude-3-5-sonnet-20241022-v2:0"
|
|
||||||
);
|
|
||||||
assert_eq!(Model::AmazonNovaLite.request_id(), "amazon.nova-lite-v1:0");
|
|
||||||
assert_eq!(Model::DeepSeekR1.request_id(), "deepseek.r1-v1:0");
|
|
||||||
assert_eq!(
|
|
||||||
Model::MetaLlama38BInstructV1.request_id(),
|
|
||||||
"meta.llama3-8b-instruct-v1:0"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test thinking models have different friendly IDs but same request IDs
|
|
||||||
assert_eq!(Model::ClaudeSonnet4.id(), "claude-4-sonnet");
|
|
||||||
assert_eq!(
|
|
||||||
Model::ClaudeSonnet4Thinking.id(),
|
|
||||||
"claude-4-sonnet-thinking"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Model::ClaudeSonnet4.request_id(),
|
|
||||||
Model::ClaudeSonnet4Thinking.request_id()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ pub struct Channel {
|
|||||||
pub name: SharedString,
|
pub name: SharedString,
|
||||||
pub visibility: proto::ChannelVisibility,
|
pub visibility: proto::ChannelVisibility,
|
||||||
pub parent_path: Vec<ChannelId>,
|
pub parent_path: Vec<ChannelId>,
|
||||||
pub channel_order: i32,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default, Debug)]
|
||||||
@@ -615,24 +614,7 @@ impl ChannelStore {
|
|||||||
to: to.0,
|
to: to.0,
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reorder_channel(
|
|
||||||
&mut self,
|
|
||||||
channel_id: ChannelId,
|
|
||||||
direction: proto::reorder_channel::Direction,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> Task<Result<()>> {
|
|
||||||
let client = self.client.clone();
|
|
||||||
cx.spawn(async move |_, _| {
|
|
||||||
client
|
|
||||||
.request(proto::ReorderChannel {
|
|
||||||
channel_id: channel_id.0,
|
|
||||||
direction: direction.into(),
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1045,18 +1027,6 @@ impl ChannelStore {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
pub fn reset(&mut self) {
|
|
||||||
self.channel_invitations.clear();
|
|
||||||
self.channel_index.clear();
|
|
||||||
self.channel_participants.clear();
|
|
||||||
self.outgoing_invites.clear();
|
|
||||||
self.opened_buffers.clear();
|
|
||||||
self.opened_chats.clear();
|
|
||||||
self.disconnect_channel_buffers_task = None;
|
|
||||||
self.channel_states.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn update_channels(
|
pub(crate) fn update_channels(
|
||||||
&mut self,
|
&mut self,
|
||||||
payload: proto::UpdateChannels,
|
payload: proto::UpdateChannels,
|
||||||
@@ -1081,7 +1051,6 @@ impl ChannelStore {
|
|||||||
visibility: channel.visibility(),
|
visibility: channel.visibility(),
|
||||||
name: channel.name.into(),
|
name: channel.name.into(),
|
||||||
parent_path: channel.parent_path.into_iter().map(ChannelId).collect(),
|
parent_path: channel.parent_path.into_iter().map(ChannelId).collect(),
|
||||||
channel_order: channel.channel_order,
|
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,13 +61,11 @@ impl ChannelPathsInsertGuard<'_> {
|
|||||||
|
|
||||||
ret = existing_channel.visibility != channel_proto.visibility()
|
ret = existing_channel.visibility != channel_proto.visibility()
|
||||||
|| existing_channel.name != channel_proto.name
|
|| existing_channel.name != channel_proto.name
|
||||||
|| existing_channel.parent_path != parent_path
|
|| existing_channel.parent_path != parent_path;
|
||||||
|| existing_channel.channel_order != channel_proto.channel_order;
|
|
||||||
|
|
||||||
existing_channel.visibility = channel_proto.visibility();
|
existing_channel.visibility = channel_proto.visibility();
|
||||||
existing_channel.name = channel_proto.name.into();
|
existing_channel.name = channel_proto.name.into();
|
||||||
existing_channel.parent_path = parent_path;
|
existing_channel.parent_path = parent_path;
|
||||||
existing_channel.channel_order = channel_proto.channel_order;
|
|
||||||
} else {
|
} else {
|
||||||
self.channels_by_id.insert(
|
self.channels_by_id.insert(
|
||||||
ChannelId(channel_proto.id),
|
ChannelId(channel_proto.id),
|
||||||
@@ -76,7 +74,6 @@ impl ChannelPathsInsertGuard<'_> {
|
|||||||
visibility: channel_proto.visibility(),
|
visibility: channel_proto.visibility(),
|
||||||
name: channel_proto.name.into(),
|
name: channel_proto.name.into(),
|
||||||
parent_path,
|
parent_path,
|
||||||
channel_order: channel_proto.channel_order,
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
self.insert_root(ChannelId(channel_proto.id));
|
self.insert_root(ChannelId(channel_proto.id));
|
||||||
@@ -103,18 +100,17 @@ impl Drop for ChannelPathsInsertGuard<'_> {
|
|||||||
fn channel_path_sorting_key(
|
fn channel_path_sorting_key(
|
||||||
id: ChannelId,
|
id: ChannelId,
|
||||||
channels_by_id: &BTreeMap<ChannelId, Arc<Channel>>,
|
channels_by_id: &BTreeMap<ChannelId, Arc<Channel>>,
|
||||||
) -> impl Iterator<Item = (i32, ChannelId)> {
|
) -> impl Iterator<Item = (&str, ChannelId)> {
|
||||||
let (parent_path, order_and_id) =
|
let (parent_path, name) = channels_by_id
|
||||||
channels_by_id
|
.get(&id)
|
||||||
.get(&id)
|
.map_or((&[] as &[_], None), |channel| {
|
||||||
.map_or((&[] as &[_], None), |channel| {
|
(
|
||||||
(
|
channel.parent_path.as_slice(),
|
||||||
channel.parent_path.as_slice(),
|
Some((channel.name.as_ref(), channel.id)),
|
||||||
Some((channel.channel_order, channel.id)),
|
)
|
||||||
)
|
});
|
||||||
});
|
|
||||||
parent_path
|
parent_path
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|id| Some((channels_by_id.get(id)?.channel_order, *id)))
|
.filter_map(|id| Some((channels_by_id.get(id)?.name.as_ref(), *id)))
|
||||||
.chain(order_and_id)
|
.chain(name)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,14 +21,12 @@ fn test_update_channels(cx: &mut App) {
|
|||||||
name: "b".to_string(),
|
name: "b".to_string(),
|
||||||
visibility: proto::ChannelVisibility::Members as i32,
|
visibility: proto::ChannelVisibility::Members as i32,
|
||||||
parent_path: Vec::new(),
|
parent_path: Vec::new(),
|
||||||
channel_order: 1,
|
|
||||||
},
|
},
|
||||||
proto::Channel {
|
proto::Channel {
|
||||||
id: 2,
|
id: 2,
|
||||||
name: "a".to_string(),
|
name: "a".to_string(),
|
||||||
visibility: proto::ChannelVisibility::Members as i32,
|
visibility: proto::ChannelVisibility::Members as i32,
|
||||||
parent_path: Vec::new(),
|
parent_path: Vec::new(),
|
||||||
channel_order: 2,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -39,8 +37,8 @@ fn test_update_channels(cx: &mut App) {
|
|||||||
&channel_store,
|
&channel_store,
|
||||||
&[
|
&[
|
||||||
//
|
//
|
||||||
(0, "b".to_string()),
|
|
||||||
(0, "a".to_string()),
|
(0, "a".to_string()),
|
||||||
|
(0, "b".to_string()),
|
||||||
],
|
],
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
@@ -54,14 +52,12 @@ fn test_update_channels(cx: &mut App) {
|
|||||||
name: "x".to_string(),
|
name: "x".to_string(),
|
||||||
visibility: proto::ChannelVisibility::Members as i32,
|
visibility: proto::ChannelVisibility::Members as i32,
|
||||||
parent_path: vec![1],
|
parent_path: vec![1],
|
||||||
channel_order: 1,
|
|
||||||
},
|
},
|
||||||
proto::Channel {
|
proto::Channel {
|
||||||
id: 4,
|
id: 4,
|
||||||
name: "y".to_string(),
|
name: "y".to_string(),
|
||||||
visibility: proto::ChannelVisibility::Members as i32,
|
visibility: proto::ChannelVisibility::Members as i32,
|
||||||
parent_path: vec![2],
|
parent_path: vec![2],
|
||||||
channel_order: 1,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -71,111 +67,15 @@ fn test_update_channels(cx: &mut App) {
|
|||||||
assert_channels(
|
assert_channels(
|
||||||
&channel_store,
|
&channel_store,
|
||||||
&[
|
&[
|
||||||
(0, "b".to_string()),
|
|
||||||
(1, "x".to_string()),
|
|
||||||
(0, "a".to_string()),
|
(0, "a".to_string()),
|
||||||
(1, "y".to_string()),
|
(1, "y".to_string()),
|
||||||
|
(0, "b".to_string()),
|
||||||
|
(1, "x".to_string()),
|
||||||
],
|
],
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
fn test_update_channels_order_independent(cx: &mut App) {
|
|
||||||
/// Based on: https://stackoverflow.com/a/59939809
|
|
||||||
fn unique_permutations<T: Clone>(items: Vec<T>) -> Vec<Vec<T>> {
|
|
||||||
if items.len() == 1 {
|
|
||||||
vec![items]
|
|
||||||
} else {
|
|
||||||
let mut output: Vec<Vec<T>> = vec![];
|
|
||||||
|
|
||||||
for (ix, first) in items.iter().enumerate() {
|
|
||||||
let mut remaining_elements = items.clone();
|
|
||||||
remaining_elements.remove(ix);
|
|
||||||
for mut permutation in unique_permutations(remaining_elements) {
|
|
||||||
permutation.insert(0, first.clone());
|
|
||||||
output.push(permutation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
output
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let test_data = vec![
|
|
||||||
proto::Channel {
|
|
||||||
id: 6,
|
|
||||||
name: "β".to_string(),
|
|
||||||
visibility: proto::ChannelVisibility::Members as i32,
|
|
||||||
parent_path: vec![1, 3],
|
|
||||||
channel_order: 1,
|
|
||||||
},
|
|
||||||
proto::Channel {
|
|
||||||
id: 5,
|
|
||||||
name: "α".to_string(),
|
|
||||||
visibility: proto::ChannelVisibility::Members as i32,
|
|
||||||
parent_path: vec![1],
|
|
||||||
channel_order: 2,
|
|
||||||
},
|
|
||||||
proto::Channel {
|
|
||||||
id: 3,
|
|
||||||
name: "x".to_string(),
|
|
||||||
visibility: proto::ChannelVisibility::Members as i32,
|
|
||||||
parent_path: vec![1],
|
|
||||||
channel_order: 1,
|
|
||||||
},
|
|
||||||
proto::Channel {
|
|
||||||
id: 4,
|
|
||||||
name: "y".to_string(),
|
|
||||||
visibility: proto::ChannelVisibility::Members as i32,
|
|
||||||
parent_path: vec![2],
|
|
||||||
channel_order: 1,
|
|
||||||
},
|
|
||||||
proto::Channel {
|
|
||||||
id: 1,
|
|
||||||
name: "b".to_string(),
|
|
||||||
visibility: proto::ChannelVisibility::Members as i32,
|
|
||||||
parent_path: Vec::new(),
|
|
||||||
channel_order: 1,
|
|
||||||
},
|
|
||||||
proto::Channel {
|
|
||||||
id: 2,
|
|
||||||
name: "a".to_string(),
|
|
||||||
visibility: proto::ChannelVisibility::Members as i32,
|
|
||||||
parent_path: Vec::new(),
|
|
||||||
channel_order: 2,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
let channel_store = init_test(cx);
|
|
||||||
let permutations = unique_permutations(test_data);
|
|
||||||
|
|
||||||
for test_instance in permutations {
|
|
||||||
channel_store.update(cx, |channel_store, _| channel_store.reset());
|
|
||||||
|
|
||||||
update_channels(
|
|
||||||
&channel_store,
|
|
||||||
proto::UpdateChannels {
|
|
||||||
channels: test_instance,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_channels(
|
|
||||||
&channel_store,
|
|
||||||
&[
|
|
||||||
(0, "b".to_string()),
|
|
||||||
(1, "x".to_string()),
|
|
||||||
(2, "β".to_string()),
|
|
||||||
(1, "α".to_string()),
|
|
||||||
(0, "a".to_string()),
|
|
||||||
(1, "y".to_string()),
|
|
||||||
],
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_dangling_channel_paths(cx: &mut App) {
|
fn test_dangling_channel_paths(cx: &mut App) {
|
||||||
let channel_store = init_test(cx);
|
let channel_store = init_test(cx);
|
||||||
@@ -189,21 +89,18 @@ fn test_dangling_channel_paths(cx: &mut App) {
|
|||||||
name: "a".to_string(),
|
name: "a".to_string(),
|
||||||
visibility: proto::ChannelVisibility::Members as i32,
|
visibility: proto::ChannelVisibility::Members as i32,
|
||||||
parent_path: vec![],
|
parent_path: vec![],
|
||||||
channel_order: 1,
|
|
||||||
},
|
},
|
||||||
proto::Channel {
|
proto::Channel {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: "b".to_string(),
|
name: "b".to_string(),
|
||||||
visibility: proto::ChannelVisibility::Members as i32,
|
visibility: proto::ChannelVisibility::Members as i32,
|
||||||
parent_path: vec![0],
|
parent_path: vec![0],
|
||||||
channel_order: 1,
|
|
||||||
},
|
},
|
||||||
proto::Channel {
|
proto::Channel {
|
||||||
id: 2,
|
id: 2,
|
||||||
name: "c".to_string(),
|
name: "c".to_string(),
|
||||||
visibility: proto::ChannelVisibility::Members as i32,
|
visibility: proto::ChannelVisibility::Members as i32,
|
||||||
parent_path: vec![0, 1],
|
parent_path: vec![0, 1],
|
||||||
channel_order: 1,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -250,7 +147,6 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
|
|||||||
name: "the-channel".to_string(),
|
name: "the-channel".to_string(),
|
||||||
visibility: proto::ChannelVisibility::Members as i32,
|
visibility: proto::ChannelVisibility::Members as i32,
|
||||||
parent_path: vec![],
|
parent_path: vec![],
|
||||||
channel_order: 1,
|
|
||||||
}],
|
}],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ zed_llm_client.workspace = true
|
|||||||
agent_settings.workspace = true
|
agent_settings.workspace = true
|
||||||
assistant_context_editor.workspace = true
|
assistant_context_editor.workspace = true
|
||||||
assistant_slash_command.workspace = true
|
assistant_slash_command.workspace = true
|
||||||
|
assistant_tool.workspace = true
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
audio.workspace = true
|
audio.workspace = true
|
||||||
buffer_diff.workspace = true
|
buffer_diff.workspace = true
|
||||||
|
|||||||
@@ -266,14 +266,11 @@ CREATE TABLE "channels" (
|
|||||||
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
"visibility" VARCHAR NOT NULL,
|
"visibility" VARCHAR NOT NULL,
|
||||||
"parent_path" TEXT NOT NULL,
|
"parent_path" TEXT NOT NULL,
|
||||||
"requires_zed_cla" BOOLEAN NOT NULL DEFAULT FALSE,
|
"requires_zed_cla" BOOLEAN NOT NULL DEFAULT FALSE
|
||||||
"channel_order" INTEGER NOT NULL DEFAULT 1
|
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX "index_channels_on_parent_path" ON "channels" ("parent_path");
|
CREATE INDEX "index_channels_on_parent_path" ON "channels" ("parent_path");
|
||||||
|
|
||||||
CREATE INDEX "index_channels_on_parent_path_and_order" ON "channels" ("parent_path", "channel_order");
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS "channel_chat_participants" (
|
CREATE TABLE IF NOT EXISTS "channel_chat_participants" (
|
||||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
"user_id" INTEGER NOT NULL REFERENCES users (id),
|
"user_id" INTEGER NOT NULL REFERENCES users (id),
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
-- Add channel_order column to channels table with default value
|
|
||||||
ALTER TABLE channels ADD COLUMN channel_order INTEGER NOT NULL DEFAULT 1;
|
|
||||||
|
|
||||||
-- Update channel_order for existing channels using ROW_NUMBER for deterministic ordering
|
|
||||||
UPDATE channels
|
|
||||||
SET channel_order = (
|
|
||||||
SELECT ROW_NUMBER() OVER (
|
|
||||||
PARTITION BY parent_path
|
|
||||||
ORDER BY name, id
|
|
||||||
)
|
|
||||||
FROM channels c2
|
|
||||||
WHERE c2.id = channels.id
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Create index for efficient ordering queries
|
|
||||||
CREATE INDEX "index_channels_on_parent_path_and_order" ON "channels" ("parent_path", "channel_order");
|
|
||||||
@@ -582,7 +582,6 @@ pub struct Channel {
|
|||||||
pub visibility: ChannelVisibility,
|
pub visibility: ChannelVisibility,
|
||||||
/// parent_path is the channel ids from the root to this one (not including this one)
|
/// parent_path is the channel ids from the root to this one (not including this one)
|
||||||
pub parent_path: Vec<ChannelId>,
|
pub parent_path: Vec<ChannelId>,
|
||||||
pub channel_order: i32,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Channel {
|
impl Channel {
|
||||||
@@ -592,7 +591,6 @@ impl Channel {
|
|||||||
visibility: value.visibility,
|
visibility: value.visibility,
|
||||||
name: value.clone().name,
|
name: value.clone().name,
|
||||||
parent_path: value.ancestors().collect(),
|
parent_path: value.ancestors().collect(),
|
||||||
channel_order: value.channel_order,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -602,13 +600,8 @@ impl Channel {
|
|||||||
name: self.name.clone(),
|
name: self.name.clone(),
|
||||||
visibility: self.visibility.into(),
|
visibility: self.visibility.into(),
|
||||||
parent_path: self.parent_path.iter().map(|c| c.to_proto()).collect(),
|
parent_path: self.parent_path.iter().map(|c| c.to_proto()).collect(),
|
||||||
channel_order: self.channel_order,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn root_id(&self) -> ChannelId {
|
|
||||||
self.parent_path.first().copied().unwrap_or(self.id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use rpc::{
|
|||||||
ErrorCode, ErrorCodeExt,
|
ErrorCode, ErrorCodeExt,
|
||||||
proto::{ChannelBufferVersion, VectorClockEntry, channel_member::Kind},
|
proto::{ChannelBufferVersion, VectorClockEntry, channel_member::Kind},
|
||||||
};
|
};
|
||||||
use sea_orm::{ActiveValue, DbBackend, TryGetableMany};
|
use sea_orm::{DbBackend, TryGetableMany};
|
||||||
|
|
||||||
impl Database {
|
impl Database {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -59,32 +59,16 @@ impl Database {
|
|||||||
parent = Some(parent_channel);
|
parent = Some(parent_channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
let parent_path = parent
|
|
||||||
.as_ref()
|
|
||||||
.map_or(String::new(), |parent| parent.path());
|
|
||||||
|
|
||||||
// Find the maximum channel_order among siblings to set the new channel at the end
|
|
||||||
let max_order = if parent_path.is_empty() {
|
|
||||||
0
|
|
||||||
} else {
|
|
||||||
max_order(&parent_path, &tx).await?
|
|
||||||
};
|
|
||||||
|
|
||||||
log::info!(
|
|
||||||
"Creating channel '{}' with parent_path='{}', max_order={}, new_order={}",
|
|
||||||
name,
|
|
||||||
parent_path,
|
|
||||||
max_order,
|
|
||||||
max_order + 1
|
|
||||||
);
|
|
||||||
|
|
||||||
let channel = channel::ActiveModel {
|
let channel = channel::ActiveModel {
|
||||||
id: ActiveValue::NotSet,
|
id: ActiveValue::NotSet,
|
||||||
name: ActiveValue::Set(name.to_string()),
|
name: ActiveValue::Set(name.to_string()),
|
||||||
visibility: ActiveValue::Set(ChannelVisibility::Members),
|
visibility: ActiveValue::Set(ChannelVisibility::Members),
|
||||||
parent_path: ActiveValue::Set(parent_path),
|
parent_path: ActiveValue::Set(
|
||||||
|
parent
|
||||||
|
.as_ref()
|
||||||
|
.map_or(String::new(), |parent| parent.path()),
|
||||||
|
),
|
||||||
requires_zed_cla: ActiveValue::NotSet,
|
requires_zed_cla: ActiveValue::NotSet,
|
||||||
channel_order: ActiveValue::Set(max_order + 1),
|
|
||||||
}
|
}
|
||||||
.insert(&*tx)
|
.insert(&*tx)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -547,7 +531,11 @@ impl Database {
|
|||||||
.get_channel_descendants_excluding_self(channels.iter(), tx)
|
.get_channel_descendants_excluding_self(channels.iter(), tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
descendants.extend(channels);
|
for channel in channels {
|
||||||
|
if let Err(ix) = descendants.binary_search_by_key(&channel.path(), |c| c.path()) {
|
||||||
|
descendants.insert(ix, channel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let roles_by_channel_id = channel_memberships
|
let roles_by_channel_id = channel_memberships
|
||||||
.iter()
|
.iter()
|
||||||
@@ -964,14 +952,11 @@ impl Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let root_id = channel.root_id();
|
let root_id = channel.root_id();
|
||||||
let new_parent_path = new_parent.path();
|
|
||||||
let old_path = format!("{}{}/", channel.parent_path, channel.id);
|
let old_path = format!("{}{}/", channel.parent_path, channel.id);
|
||||||
let new_path = format!("{}{}/", &new_parent_path, channel.id);
|
let new_path = format!("{}{}/", new_parent.path(), channel.id);
|
||||||
let new_order = max_order(&new_parent_path, &tx).await? + 1;
|
|
||||||
|
|
||||||
let mut model = channel.into_active_model();
|
let mut model = channel.into_active_model();
|
||||||
model.parent_path = ActiveValue::Set(new_parent.path());
|
model.parent_path = ActiveValue::Set(new_parent.path());
|
||||||
model.channel_order = ActiveValue::Set(new_order);
|
|
||||||
let channel = model.update(&*tx).await?;
|
let channel = model.update(&*tx).await?;
|
||||||
|
|
||||||
let descendent_ids =
|
let descendent_ids =
|
||||||
@@ -1001,137 +986,6 @@ impl Database {
|
|||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn reorder_channel(
|
|
||||||
&self,
|
|
||||||
channel_id: ChannelId,
|
|
||||||
direction: proto::reorder_channel::Direction,
|
|
||||||
user_id: UserId,
|
|
||||||
) -> Result<Vec<Channel>> {
|
|
||||||
self.transaction(|tx| async move {
|
|
||||||
let mut channel = self.get_channel_internal(channel_id, &tx).await?;
|
|
||||||
|
|
||||||
if channel.is_root() {
|
|
||||||
log::info!("Skipping reorder of root channel {}", channel.id,);
|
|
||||||
return Ok(vec![]);
|
|
||||||
}
|
|
||||||
|
|
||||||
log::info!(
|
|
||||||
"Reordering channel {} (parent_path: '{}', order: {})",
|
|
||||||
channel.id,
|
|
||||||
channel.parent_path,
|
|
||||||
channel.channel_order
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check if user is admin of the channel
|
|
||||||
self.check_user_is_channel_admin(&channel, user_id, &tx)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Find the sibling channel to swap with
|
|
||||||
let sibling_channel = match direction {
|
|
||||||
proto::reorder_channel::Direction::Up => {
|
|
||||||
log::info!(
|
|
||||||
"Looking for sibling with parent_path='{}' and order < {}",
|
|
||||||
channel.parent_path,
|
|
||||||
channel.channel_order
|
|
||||||
);
|
|
||||||
// Find channel with highest order less than current
|
|
||||||
channel::Entity::find()
|
|
||||||
.filter(
|
|
||||||
channel::Column::ParentPath
|
|
||||||
.eq(&channel.parent_path)
|
|
||||||
.and(channel::Column::ChannelOrder.lt(channel.channel_order)),
|
|
||||||
)
|
|
||||||
.order_by_desc(channel::Column::ChannelOrder)
|
|
||||||
.one(&*tx)
|
|
||||||
.await?
|
|
||||||
}
|
|
||||||
proto::reorder_channel::Direction::Down => {
|
|
||||||
log::info!(
|
|
||||||
"Looking for sibling with parent_path='{}' and order > {}",
|
|
||||||
channel.parent_path,
|
|
||||||
channel.channel_order
|
|
||||||
);
|
|
||||||
// Find channel with lowest order greater than current
|
|
||||||
channel::Entity::find()
|
|
||||||
.filter(
|
|
||||||
channel::Column::ParentPath
|
|
||||||
.eq(&channel.parent_path)
|
|
||||||
.and(channel::Column::ChannelOrder.gt(channel.channel_order)),
|
|
||||||
)
|
|
||||||
.order_by_asc(channel::Column::ChannelOrder)
|
|
||||||
.one(&*tx)
|
|
||||||
.await?
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut sibling_channel = match sibling_channel {
|
|
||||||
Some(sibling) => {
|
|
||||||
log::info!(
|
|
||||||
"Found sibling {} (parent_path: '{}', order: {})",
|
|
||||||
sibling.id,
|
|
||||||
sibling.parent_path,
|
|
||||||
sibling.channel_order
|
|
||||||
);
|
|
||||||
sibling
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
log::warn!("No sibling found to swap with");
|
|
||||||
// No sibling to swap with
|
|
||||||
return Ok(vec![]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let current_order = channel.channel_order;
|
|
||||||
let sibling_order = sibling_channel.channel_order;
|
|
||||||
|
|
||||||
channel::ActiveModel {
|
|
||||||
id: ActiveValue::Unchanged(sibling_channel.id),
|
|
||||||
channel_order: ActiveValue::Set(current_order),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
.update(&*tx)
|
|
||||||
.await?;
|
|
||||||
sibling_channel.channel_order = current_order;
|
|
||||||
|
|
||||||
channel::ActiveModel {
|
|
||||||
id: ActiveValue::Unchanged(channel.id),
|
|
||||||
channel_order: ActiveValue::Set(sibling_order),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
.update(&*tx)
|
|
||||||
.await?;
|
|
||||||
channel.channel_order = sibling_order;
|
|
||||||
|
|
||||||
log::info!(
|
|
||||||
"Reorder complete. Swapped channels {} and {}",
|
|
||||||
channel.id,
|
|
||||||
sibling_channel.id
|
|
||||||
);
|
|
||||||
|
|
||||||
let swapped_channels = vec![
|
|
||||||
Channel::from_model(channel),
|
|
||||||
Channel::from_model(sibling_channel),
|
|
||||||
];
|
|
||||||
|
|
||||||
Ok(swapped_channels)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn max_order(parent_path: &str, tx: &TransactionHandle) -> Result<i32> {
|
|
||||||
let max_order = channel::Entity::find()
|
|
||||||
.filter(channel::Column::ParentPath.eq(parent_path))
|
|
||||||
.select_only()
|
|
||||||
.column_as(channel::Column::ChannelOrder.max(), "max_order")
|
|
||||||
.into_tuple::<Option<i32>>()
|
|
||||||
.one(&**tx)
|
|
||||||
.await?
|
|
||||||
.flatten()
|
|
||||||
.unwrap_or(0);
|
|
||||||
|
|
||||||
Ok(max_order)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
||||||
|
|||||||
@@ -66,87 +66,6 @@ impl Database {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete all channel chat participants from previous servers
|
|
||||||
pub async fn delete_stale_channel_chat_participants(
|
|
||||||
&self,
|
|
||||||
environment: &str,
|
|
||||||
new_server_id: ServerId,
|
|
||||||
) -> Result<()> {
|
|
||||||
self.transaction(|tx| async move {
|
|
||||||
let stale_server_epochs = self
|
|
||||||
.stale_server_ids(environment, new_server_id, &tx)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
channel_chat_participant::Entity::delete_many()
|
|
||||||
.filter(
|
|
||||||
channel_chat_participant::Column::ConnectionServerId
|
|
||||||
.is_in(stale_server_epochs.iter().copied()),
|
|
||||||
)
|
|
||||||
.exec(&*tx)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn clear_old_worktree_entries(&self, server_id: ServerId) -> Result<()> {
|
|
||||||
self.transaction(|tx| async move {
|
|
||||||
use sea_orm::Statement;
|
|
||||||
use sea_orm::sea_query::{Expr, Query};
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let delete_query = Query::delete()
|
|
||||||
.from_table(worktree_entry::Entity)
|
|
||||||
.and_where(
|
|
||||||
Expr::tuple([
|
|
||||||
Expr::col((worktree_entry::Entity, worktree_entry::Column::ProjectId))
|
|
||||||
.into(),
|
|
||||||
Expr::col((worktree_entry::Entity, worktree_entry::Column::WorktreeId))
|
|
||||||
.into(),
|
|
||||||
Expr::col((worktree_entry::Entity, worktree_entry::Column::Id)).into(),
|
|
||||||
])
|
|
||||||
.in_subquery(
|
|
||||||
Query::select()
|
|
||||||
.columns([
|
|
||||||
(worktree_entry::Entity, worktree_entry::Column::ProjectId),
|
|
||||||
(worktree_entry::Entity, worktree_entry::Column::WorktreeId),
|
|
||||||
(worktree_entry::Entity, worktree_entry::Column::Id),
|
|
||||||
])
|
|
||||||
.from(worktree_entry::Entity)
|
|
||||||
.inner_join(
|
|
||||||
project::Entity,
|
|
||||||
Expr::col((project::Entity, project::Column::Id)).equals((
|
|
||||||
worktree_entry::Entity,
|
|
||||||
worktree_entry::Column::ProjectId,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.and_where(project::Column::HostConnectionServerId.ne(server_id))
|
|
||||||
.limit(10000)
|
|
||||||
.to_owned(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.to_owned();
|
|
||||||
|
|
||||||
let statement = Statement::from_sql_and_values(
|
|
||||||
tx.get_database_backend(),
|
|
||||||
delete_query
|
|
||||||
.to_string(sea_orm::sea_query::PostgresQueryBuilder)
|
|
||||||
.as_str(),
|
|
||||||
vec![],
|
|
||||||
);
|
|
||||||
|
|
||||||
let result = tx.execute(statement).await?;
|
|
||||||
if result.rows_affected() == 0 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Deletes any stale servers in the environment that don't match the `new_server_id`.
|
/// Deletes any stale servers in the environment that don't match the `new_server_id`.
|
||||||
pub async fn delete_stale_servers(
|
pub async fn delete_stale_servers(
|
||||||
&self,
|
&self,
|
||||||
@@ -167,7 +86,7 @@ impl Database {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn stale_server_ids(
|
async fn stale_server_ids(
|
||||||
&self,
|
&self,
|
||||||
environment: &str,
|
environment: &str,
|
||||||
new_server_id: ServerId,
|
new_server_id: ServerId,
|
||||||
|
|||||||
@@ -10,9 +10,6 @@ pub struct Model {
|
|||||||
pub visibility: ChannelVisibility,
|
pub visibility: ChannelVisibility,
|
||||||
pub parent_path: String,
|
pub parent_path: String,
|
||||||
pub requires_zed_cla: bool,
|
pub requires_zed_cla: bool,
|
||||||
/// The order of this channel relative to its siblings within the same parent.
|
|
||||||
/// Lower values appear first. Channels are sorted by parent_path first, then by channel_order.
|
|
||||||
pub channel_order: i32,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Model {
|
impl Model {
|
||||||
|
|||||||
@@ -172,40 +172,16 @@ impl Drop for TestDb {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[track_caller]
|
|
||||||
fn assert_channel_tree_matches(actual: Vec<Channel>, expected: Vec<Channel>) {
|
|
||||||
let expected_channels = expected.into_iter().collect::<HashSet<_>>();
|
|
||||||
let actual_channels = actual.into_iter().collect::<HashSet<_>>();
|
|
||||||
pretty_assertions::assert_eq!(expected_channels, actual_channels);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn channel_tree(channels: &[(ChannelId, &[ChannelId], &'static str)]) -> Vec<Channel> {
|
fn channel_tree(channels: &[(ChannelId, &[ChannelId], &'static str)]) -> Vec<Channel> {
|
||||||
use std::collections::HashMap;
|
channels
|
||||||
|
.iter()
|
||||||
let mut result = Vec::new();
|
.map(|(id, parent_path, name)| Channel {
|
||||||
let mut order_by_parent: HashMap<Vec<ChannelId>, i32> = HashMap::new();
|
|
||||||
|
|
||||||
for (id, parent_path, name) in channels {
|
|
||||||
let parent_key = parent_path.to_vec();
|
|
||||||
let order = if parent_key.is_empty() {
|
|
||||||
1
|
|
||||||
} else {
|
|
||||||
*order_by_parent
|
|
||||||
.entry(parent_key.clone())
|
|
||||||
.and_modify(|e| *e += 1)
|
|
||||||
.or_insert(1)
|
|
||||||
};
|
|
||||||
|
|
||||||
result.push(Channel {
|
|
||||||
id: *id,
|
id: *id,
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
visibility: ChannelVisibility::Members,
|
visibility: ChannelVisibility::Members,
|
||||||
parent_path: parent_key,
|
parent_path: parent_path.to_vec(),
|
||||||
channel_order: order,
|
})
|
||||||
});
|
.collect()
|
||||||
}
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static GITHUB_USER_ID: AtomicI32 = AtomicI32::new(5);
|
static GITHUB_USER_ID: AtomicI32 = AtomicI32::new(5);
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
db::{
|
db::{
|
||||||
Channel, ChannelId, ChannelRole, Database, NewUserParams, RoomId, UserId,
|
Channel, ChannelId, ChannelRole, Database, NewUserParams, RoomId, UserId,
|
||||||
tests::{assert_channel_tree_matches, channel_tree, new_test_connection, new_test_user},
|
tests::{channel_tree, new_test_connection, new_test_user},
|
||||||
},
|
},
|
||||||
test_both_dbs,
|
test_both_dbs,
|
||||||
};
|
};
|
||||||
use rpc::{
|
use rpc::{
|
||||||
ConnectionId,
|
ConnectionId,
|
||||||
proto::{self, reorder_channel},
|
proto::{self},
|
||||||
};
|
};
|
||||||
use std::{collections::HashSet, sync::Arc};
|
use std::sync::Arc;
|
||||||
|
|
||||||
test_both_dbs!(test_channels, test_channels_postgres, test_channels_sqlite);
|
test_both_dbs!(test_channels, test_channels_postgres, test_channels_sqlite);
|
||||||
|
|
||||||
@@ -59,28 +59,28 @@ async fn test_channels(db: &Arc<Database>) {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let result = db.get_channels_for_user(a_id).await.unwrap();
|
let result = db.get_channels_for_user(a_id).await.unwrap();
|
||||||
assert_channel_tree_matches(
|
assert_eq!(
|
||||||
result.channels,
|
result.channels,
|
||||||
channel_tree(&[
|
channel_tree(&[
|
||||||
(zed_id, &[], "zed"),
|
(zed_id, &[], "zed"),
|
||||||
(crdb_id, &[zed_id], "crdb"),
|
(crdb_id, &[zed_id], "crdb"),
|
||||||
(livestreaming_id, &[zed_id], "livestreaming"),
|
(livestreaming_id, &[zed_id], "livestreaming",),
|
||||||
(replace_id, &[zed_id], "replace"),
|
(replace_id, &[zed_id], "replace"),
|
||||||
(rust_id, &[], "rust"),
|
(rust_id, &[], "rust"),
|
||||||
(cargo_id, &[rust_id], "cargo"),
|
(cargo_id, &[rust_id], "cargo"),
|
||||||
(cargo_ra_id, &[rust_id, cargo_id], "cargo-ra"),
|
(cargo_ra_id, &[rust_id, cargo_id], "cargo-ra",)
|
||||||
]),
|
],)
|
||||||
);
|
);
|
||||||
|
|
||||||
let result = db.get_channels_for_user(b_id).await.unwrap();
|
let result = db.get_channels_for_user(b_id).await.unwrap();
|
||||||
assert_channel_tree_matches(
|
assert_eq!(
|
||||||
result.channels,
|
result.channels,
|
||||||
channel_tree(&[
|
channel_tree(&[
|
||||||
(zed_id, &[], "zed"),
|
(zed_id, &[], "zed"),
|
||||||
(crdb_id, &[zed_id], "crdb"),
|
(crdb_id, &[zed_id], "crdb"),
|
||||||
(livestreaming_id, &[zed_id], "livestreaming"),
|
(livestreaming_id, &[zed_id], "livestreaming",),
|
||||||
(replace_id, &[zed_id], "replace"),
|
(replace_id, &[zed_id], "replace")
|
||||||
]),
|
],)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update member permissions
|
// Update member permissions
|
||||||
@@ -94,14 +94,14 @@ async fn test_channels(db: &Arc<Database>) {
|
|||||||
assert!(set_channel_admin.is_ok());
|
assert!(set_channel_admin.is_ok());
|
||||||
|
|
||||||
let result = db.get_channels_for_user(b_id).await.unwrap();
|
let result = db.get_channels_for_user(b_id).await.unwrap();
|
||||||
assert_channel_tree_matches(
|
assert_eq!(
|
||||||
result.channels,
|
result.channels,
|
||||||
channel_tree(&[
|
channel_tree(&[
|
||||||
(zed_id, &[], "zed"),
|
(zed_id, &[], "zed"),
|
||||||
(crdb_id, &[zed_id], "crdb"),
|
(crdb_id, &[zed_id], "crdb"),
|
||||||
(livestreaming_id, &[zed_id], "livestreaming"),
|
(livestreaming_id, &[zed_id], "livestreaming",),
|
||||||
(replace_id, &[zed_id], "replace"),
|
(replace_id, &[zed_id], "replace")
|
||||||
]),
|
],)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Remove a single channel
|
// Remove a single channel
|
||||||
@@ -313,8 +313,8 @@ async fn test_channel_renames(db: &Arc<Database>) {
|
|||||||
|
|
||||||
test_both_dbs!(
|
test_both_dbs!(
|
||||||
test_db_channel_moving,
|
test_db_channel_moving,
|
||||||
test_db_channel_moving_postgres,
|
test_channels_moving_postgres,
|
||||||
test_db_channel_moving_sqlite
|
test_channels_moving_sqlite
|
||||||
);
|
);
|
||||||
|
|
||||||
async fn test_db_channel_moving(db: &Arc<Database>) {
|
async fn test_db_channel_moving(db: &Arc<Database>) {
|
||||||
@@ -343,14 +343,16 @@ async fn test_db_channel_moving(db: &Arc<Database>) {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let livestreaming_sub_id = db
|
let livestreaming_dag_id = db
|
||||||
.create_sub_channel("livestreaming_sub", livestreaming_id, a_id)
|
.create_sub_channel("livestreaming_dag", livestreaming_id, a_id)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
// sanity check
|
// sanity check
|
||||||
|
// Initial DAG:
|
||||||
// /- gpui2
|
// /- gpui2
|
||||||
// zed -- crdb - livestreaming - livestreaming_sub
|
// zed -- crdb - livestreaming - livestreaming_dag
|
||||||
let result = db.get_channels_for_user(a_id).await.unwrap();
|
let result = db.get_channels_for_user(a_id).await.unwrap();
|
||||||
assert_channel_tree(
|
assert_channel_tree(
|
||||||
result.channels,
|
result.channels,
|
||||||
@@ -358,242 +360,10 @@ async fn test_db_channel_moving(db: &Arc<Database>) {
|
|||||||
(zed_id, &[]),
|
(zed_id, &[]),
|
||||||
(crdb_id, &[zed_id]),
|
(crdb_id, &[zed_id]),
|
||||||
(livestreaming_id, &[zed_id, crdb_id]),
|
(livestreaming_id, &[zed_id, crdb_id]),
|
||||||
(livestreaming_sub_id, &[zed_id, crdb_id, livestreaming_id]),
|
(livestreaming_dag_id, &[zed_id, crdb_id, livestreaming_id]),
|
||||||
(gpui2_id, &[zed_id]),
|
(gpui2_id, &[zed_id]),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check that we can do a simple leaf -> leaf move
|
|
||||||
db.move_channel(livestreaming_sub_id, crdb_id, a_id)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// /- gpui2
|
|
||||||
// zed -- crdb -- livestreaming
|
|
||||||
// \- livestreaming_sub
|
|
||||||
let result = db.get_channels_for_user(a_id).await.unwrap();
|
|
||||||
assert_channel_tree(
|
|
||||||
result.channels,
|
|
||||||
&[
|
|
||||||
(zed_id, &[]),
|
|
||||||
(crdb_id, &[zed_id]),
|
|
||||||
(livestreaming_id, &[zed_id, crdb_id]),
|
|
||||||
(livestreaming_sub_id, &[zed_id, crdb_id]),
|
|
||||||
(gpui2_id, &[zed_id]),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check that we can move a whole subtree at once
|
|
||||||
db.move_channel(crdb_id, gpui2_id, a_id).await.unwrap();
|
|
||||||
|
|
||||||
// zed -- gpui2 -- crdb -- livestreaming
|
|
||||||
// \- livestreaming_sub
|
|
||||||
let result = db.get_channels_for_user(a_id).await.unwrap();
|
|
||||||
assert_channel_tree(
|
|
||||||
result.channels,
|
|
||||||
&[
|
|
||||||
(zed_id, &[]),
|
|
||||||
(gpui2_id, &[zed_id]),
|
|
||||||
(crdb_id, &[zed_id, gpui2_id]),
|
|
||||||
(livestreaming_id, &[zed_id, gpui2_id, crdb_id]),
|
|
||||||
(livestreaming_sub_id, &[zed_id, gpui2_id, crdb_id]),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
test_both_dbs!(
|
|
||||||
test_channel_reordering,
|
|
||||||
test_channel_reordering_postgres,
|
|
||||||
test_channel_reordering_sqlite
|
|
||||||
);
|
|
||||||
|
|
||||||
async fn test_channel_reordering(db: &Arc<Database>) {
|
|
||||||
let admin_id = db
|
|
||||||
.create_user(
|
|
||||||
"admin@example.com",
|
|
||||||
None,
|
|
||||||
false,
|
|
||||||
NewUserParams {
|
|
||||||
github_login: "admin".into(),
|
|
||||||
github_user_id: 1,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.user_id;
|
|
||||||
|
|
||||||
let user_id = db
|
|
||||||
.create_user(
|
|
||||||
"user@example.com",
|
|
||||||
None,
|
|
||||||
false,
|
|
||||||
NewUserParams {
|
|
||||||
github_login: "user".into(),
|
|
||||||
github_user_id: 2,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.user_id;
|
|
||||||
|
|
||||||
// Create a root channel with some sub-channels
|
|
||||||
let root_id = db.create_root_channel("root", admin_id).await.unwrap();
|
|
||||||
|
|
||||||
// Invite user to root channel so they can see the sub-channels
|
|
||||||
db.invite_channel_member(root_id, user_id, admin_id, ChannelRole::Member)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
db.respond_to_channel_invite(root_id, user_id, true)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let alpha_id = db
|
|
||||||
.create_sub_channel("alpha", root_id, admin_id)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let beta_id = db
|
|
||||||
.create_sub_channel("beta", root_id, admin_id)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let gamma_id = db
|
|
||||||
.create_sub_channel("gamma", root_id, admin_id)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Initial order should be: root, alpha (order=1), beta (order=2), gamma (order=3)
|
|
||||||
let result = db.get_channels_for_user(admin_id).await.unwrap();
|
|
||||||
assert_channel_tree_order(
|
|
||||||
result.channels,
|
|
||||||
&[
|
|
||||||
(root_id, &[], 1),
|
|
||||||
(alpha_id, &[root_id], 1),
|
|
||||||
(beta_id, &[root_id], 2),
|
|
||||||
(gamma_id, &[root_id], 3),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test moving beta up (should swap with alpha)
|
|
||||||
let updated_channels = db
|
|
||||||
.reorder_channel(beta_id, reorder_channel::Direction::Up, admin_id)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Verify that beta and alpha were returned as updated
|
|
||||||
assert_eq!(updated_channels.len(), 2);
|
|
||||||
let updated_ids: std::collections::HashSet<_> = updated_channels.iter().map(|c| c.id).collect();
|
|
||||||
assert!(updated_ids.contains(&alpha_id));
|
|
||||||
assert!(updated_ids.contains(&beta_id));
|
|
||||||
|
|
||||||
// Now order should be: root, beta (order=1), alpha (order=2), gamma (order=3)
|
|
||||||
let result = db.get_channels_for_user(admin_id).await.unwrap();
|
|
||||||
assert_channel_tree_order(
|
|
||||||
result.channels,
|
|
||||||
&[
|
|
||||||
(root_id, &[], 1),
|
|
||||||
(beta_id, &[root_id], 1),
|
|
||||||
(alpha_id, &[root_id], 2),
|
|
||||||
(gamma_id, &[root_id], 3),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test moving gamma down (should be no-op since it's already last)
|
|
||||||
let updated_channels = db
|
|
||||||
.reorder_channel(gamma_id, reorder_channel::Direction::Down, admin_id)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Should return just nothing
|
|
||||||
assert_eq!(updated_channels.len(), 0);
|
|
||||||
|
|
||||||
// Test moving alpha down (should swap with gamma)
|
|
||||||
let updated_channels = db
|
|
||||||
.reorder_channel(alpha_id, reorder_channel::Direction::Down, admin_id)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Verify that alpha and gamma were returned as updated
|
|
||||||
assert_eq!(updated_channels.len(), 2);
|
|
||||||
let updated_ids: std::collections::HashSet<_> = updated_channels.iter().map(|c| c.id).collect();
|
|
||||||
assert!(updated_ids.contains(&alpha_id));
|
|
||||||
assert!(updated_ids.contains(&gamma_id));
|
|
||||||
|
|
||||||
// Now order should be: root, beta (order=1), gamma (order=2), alpha (order=3)
|
|
||||||
let result = db.get_channels_for_user(admin_id).await.unwrap();
|
|
||||||
assert_channel_tree_order(
|
|
||||||
result.channels,
|
|
||||||
&[
|
|
||||||
(root_id, &[], 1),
|
|
||||||
(beta_id, &[root_id], 1),
|
|
||||||
(gamma_id, &[root_id], 2),
|
|
||||||
(alpha_id, &[root_id], 3),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test that non-admin cannot reorder
|
|
||||||
let reorder_result = db
|
|
||||||
.reorder_channel(beta_id, reorder_channel::Direction::Up, user_id)
|
|
||||||
.await;
|
|
||||||
assert!(reorder_result.is_err());
|
|
||||||
|
|
||||||
// Test moving beta up (should be no-op since it's already first)
|
|
||||||
let updated_channels = db
|
|
||||||
.reorder_channel(beta_id, reorder_channel::Direction::Up, admin_id)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Should return nothing
|
|
||||||
assert_eq!(updated_channels.len(), 0);
|
|
||||||
|
|
||||||
// Adding a channel to an existing ordering should add it to the end
|
|
||||||
let delta_id = db
|
|
||||||
.create_sub_channel("delta", root_id, admin_id)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let result = db.get_channels_for_user(admin_id).await.unwrap();
|
|
||||||
assert_channel_tree_order(
|
|
||||||
result.channels,
|
|
||||||
&[
|
|
||||||
(root_id, &[], 1),
|
|
||||||
(beta_id, &[root_id], 1),
|
|
||||||
(gamma_id, &[root_id], 2),
|
|
||||||
(alpha_id, &[root_id], 3),
|
|
||||||
(delta_id, &[root_id], 4),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
// And moving a channel into an existing ordering should add it to the end
|
|
||||||
let eta_id = db
|
|
||||||
.create_sub_channel("eta", delta_id, admin_id)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let result = db.get_channels_for_user(admin_id).await.unwrap();
|
|
||||||
assert_channel_tree_order(
|
|
||||||
result.channels,
|
|
||||||
&[
|
|
||||||
(root_id, &[], 1),
|
|
||||||
(beta_id, &[root_id], 1),
|
|
||||||
(gamma_id, &[root_id], 2),
|
|
||||||
(alpha_id, &[root_id], 3),
|
|
||||||
(delta_id, &[root_id], 4),
|
|
||||||
(eta_id, &[root_id, delta_id], 1),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
db.move_channel(eta_id, root_id, admin_id).await.unwrap();
|
|
||||||
let result = db.get_channels_for_user(admin_id).await.unwrap();
|
|
||||||
assert_channel_tree_order(
|
|
||||||
result.channels,
|
|
||||||
&[
|
|
||||||
(root_id, &[], 1),
|
|
||||||
(beta_id, &[root_id], 1),
|
|
||||||
(gamma_id, &[root_id], 2),
|
|
||||||
(alpha_id, &[root_id], 3),
|
|
||||||
(delta_id, &[root_id], 4),
|
|
||||||
(eta_id, &[root_id], 5),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
test_both_dbs!(
|
test_both_dbs!(
|
||||||
@@ -652,20 +422,6 @@ async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
|
|||||||
(livestreaming_id, &[zed_id, projects_id]),
|
(livestreaming_id, &[zed_id, projects_id]),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Can't un-root a root channel
|
|
||||||
db.move_channel(zed_id, livestreaming_id, user_id)
|
|
||||||
.await
|
|
||||||
.unwrap_err();
|
|
||||||
let result = db.get_channels_for_user(user_id).await.unwrap();
|
|
||||||
assert_channel_tree(
|
|
||||||
result.channels,
|
|
||||||
&[
|
|
||||||
(zed_id, &[]),
|
|
||||||
(projects_id, &[zed_id]),
|
|
||||||
(livestreaming_id, &[zed_id, projects_id]),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
test_both_dbs!(
|
test_both_dbs!(
|
||||||
@@ -989,29 +745,10 @@ fn assert_channel_tree(actual: Vec<Channel>, expected: &[(ChannelId, &[ChannelId
|
|||||||
let actual = actual
|
let actual = actual
|
||||||
.iter()
|
.iter()
|
||||||
.map(|channel| (channel.id, channel.parent_path.as_slice()))
|
.map(|channel| (channel.id, channel.parent_path.as_slice()))
|
||||||
.collect::<HashSet<_>>();
|
.collect::<Vec<_>>();
|
||||||
let expected = expected
|
pretty_assertions::assert_eq!(
|
||||||
.iter()
|
actual,
|
||||||
.map(|(id, parents)| (*id, *parents))
|
expected.to_vec(),
|
||||||
.collect::<HashSet<_>>();
|
"wrong channel ids and parent paths"
|
||||||
pretty_assertions::assert_eq!(actual, expected, "wrong channel ids and parent paths");
|
);
|
||||||
}
|
|
||||||
|
|
||||||
#[track_caller]
|
|
||||||
fn assert_channel_tree_order(actual: Vec<Channel>, expected: &[(ChannelId, &[ChannelId], i32)]) {
|
|
||||||
let actual = actual
|
|
||||||
.iter()
|
|
||||||
.map(|channel| {
|
|
||||||
(
|
|
||||||
channel.id,
|
|
||||||
channel.parent_path.as_slice(),
|
|
||||||
channel.channel_order,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect::<HashSet<_>>();
|
|
||||||
let expected = expected
|
|
||||||
.iter()
|
|
||||||
.map(|(id, parents, order)| (*id, *parents, *order))
|
|
||||||
.collect::<HashSet<_>>();
|
|
||||||
pretty_assertions::assert_eq!(actual, expected, "wrong channel ids and parent paths");
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -312,7 +312,6 @@ impl Server {
|
|||||||
.add_request_handler(
|
.add_request_handler(
|
||||||
forward_read_only_project_request::<proto::LanguageServerIdForName>,
|
forward_read_only_project_request::<proto::LanguageServerIdForName>,
|
||||||
)
|
)
|
||||||
.add_request_handler(forward_read_only_project_request::<proto::GetDocumentDiagnostics>)
|
|
||||||
.add_request_handler(
|
.add_request_handler(
|
||||||
forward_mutating_project_request::<proto::RegisterBufferWithLanguageServers>,
|
forward_mutating_project_request::<proto::RegisterBufferWithLanguageServers>,
|
||||||
)
|
)
|
||||||
@@ -355,9 +354,6 @@ impl Server {
|
|||||||
.add_message_handler(broadcast_project_message_from_host::<proto::BufferReloaded>)
|
.add_message_handler(broadcast_project_message_from_host::<proto::BufferReloaded>)
|
||||||
.add_message_handler(broadcast_project_message_from_host::<proto::BufferSaved>)
|
.add_message_handler(broadcast_project_message_from_host::<proto::BufferSaved>)
|
||||||
.add_message_handler(broadcast_project_message_from_host::<proto::UpdateDiffBases>)
|
.add_message_handler(broadcast_project_message_from_host::<proto::UpdateDiffBases>)
|
||||||
.add_message_handler(
|
|
||||||
broadcast_project_message_from_host::<proto::PullWorkspaceDiagnostics>,
|
|
||||||
)
|
|
||||||
.add_request_handler(get_users)
|
.add_request_handler(get_users)
|
||||||
.add_request_handler(fuzzy_search_users)
|
.add_request_handler(fuzzy_search_users)
|
||||||
.add_request_handler(request_contact)
|
.add_request_handler(request_contact)
|
||||||
@@ -388,7 +384,6 @@ impl Server {
|
|||||||
.add_request_handler(get_notifications)
|
.add_request_handler(get_notifications)
|
||||||
.add_request_handler(mark_notification_as_read)
|
.add_request_handler(mark_notification_as_read)
|
||||||
.add_request_handler(move_channel)
|
.add_request_handler(move_channel)
|
||||||
.add_request_handler(reorder_channel)
|
|
||||||
.add_request_handler(follow)
|
.add_request_handler(follow)
|
||||||
.add_message_handler(unfollow)
|
.add_message_handler(unfollow)
|
||||||
.add_message_handler(update_followers)
|
.add_message_handler(update_followers)
|
||||||
@@ -438,16 +433,6 @@ impl Server {
|
|||||||
tracing::info!("waiting for cleanup timeout");
|
tracing::info!("waiting for cleanup timeout");
|
||||||
timeout.await;
|
timeout.await;
|
||||||
tracing::info!("cleanup timeout expired, retrieving stale rooms");
|
tracing::info!("cleanup timeout expired, retrieving stale rooms");
|
||||||
|
|
||||||
app_state
|
|
||||||
.db
|
|
||||||
.delete_stale_channel_chat_participants(
|
|
||||||
&app_state.config.zed_environment,
|
|
||||||
server_id,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.trace_err();
|
|
||||||
|
|
||||||
if let Some((room_ids, channel_ids)) = app_state
|
if let Some((room_ids, channel_ids)) = app_state
|
||||||
.db
|
.db
|
||||||
.stale_server_resource_ids(&app_state.config.zed_environment, server_id)
|
.stale_server_resource_ids(&app_state.config.zed_environment, server_id)
|
||||||
@@ -569,21 +554,6 @@ impl Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
app_state
|
|
||||||
.db
|
|
||||||
.delete_stale_channel_chat_participants(
|
|
||||||
&app_state.config.zed_environment,
|
|
||||||
server_id,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.trace_err();
|
|
||||||
|
|
||||||
app_state
|
|
||||||
.db
|
|
||||||
.clear_old_worktree_entries(server_id)
|
|
||||||
.await
|
|
||||||
.trace_err();
|
|
||||||
|
|
||||||
app_state
|
app_state
|
||||||
.db
|
.db
|
||||||
.delete_stale_servers(&app_state.config.zed_environment, server_id)
|
.delete_stale_servers(&app_state.config.zed_environment, server_id)
|
||||||
@@ -3225,51 +3195,6 @@ async fn move_channel(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn reorder_channel(
|
|
||||||
request: proto::ReorderChannel,
|
|
||||||
response: Response<proto::ReorderChannel>,
|
|
||||||
session: Session,
|
|
||||||
) -> Result<()> {
|
|
||||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
|
||||||
let direction = request.direction();
|
|
||||||
|
|
||||||
let updated_channels = session
|
|
||||||
.db()
|
|
||||||
.await
|
|
||||||
.reorder_channel(channel_id, direction, session.user_id())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if let Some(root_id) = updated_channels.first().map(|channel| channel.root_id()) {
|
|
||||||
let connection_pool = session.connection_pool().await;
|
|
||||||
for (connection_id, role) in connection_pool.channel_connection_ids(root_id) {
|
|
||||||
let channels = updated_channels
|
|
||||||
.iter()
|
|
||||||
.filter_map(|channel| {
|
|
||||||
if role.can_see_channel(channel.visibility) {
|
|
||||||
Some(channel.to_proto())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
if channels.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let update = proto::UpdateChannels {
|
|
||||||
channels,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
session.peer.send(connection_id, update.clone())?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
response.send(Ack {})?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the list of channel members
|
/// Get the list of channel members
|
||||||
async fn get_channel_members(
|
async fn get_channel_members(
|
||||||
request: proto::GetChannelMembers,
|
request: proto::GetChannelMembers,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use editor::{
|
|||||||
Editor, RowInfo,
|
Editor, RowInfo,
|
||||||
actions::{
|
actions::{
|
||||||
ConfirmCodeAction, ConfirmCompletion, ConfirmRename, ContextMenuFirst,
|
ConfirmCodeAction, ConfirmCompletion, ConfirmRename, ContextMenuFirst,
|
||||||
ExpandMacroRecursively, Redo, Rename, SelectAll, ToggleCodeActions, Undo,
|
ExpandMacroRecursively, Redo, Rename, ToggleCodeActions, Undo,
|
||||||
},
|
},
|
||||||
test::{
|
test::{
|
||||||
editor_test_context::{AssertionContextManager, EditorTestContext},
|
editor_test_context::{AssertionContextManager, EditorTestContext},
|
||||||
@@ -2712,7 +2712,7 @@ async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
|||||||
params.text_document.uri,
|
params.text_document.uri,
|
||||||
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
|
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
|
||||||
);
|
);
|
||||||
assert_eq!(params.position, lsp::Position::new(0, 0));
|
assert_eq!(params.position, lsp::Position::new(0, 0),);
|
||||||
Ok(Some(ExpandedMacro {
|
Ok(Some(ExpandedMacro {
|
||||||
name: "test_macro_name".to_string(),
|
name: "test_macro_name".to_string(),
|
||||||
expansion: "test_macro_expansion on the host".to_string(),
|
expansion: "test_macro_expansion on the host".to_string(),
|
||||||
@@ -2747,11 +2747,7 @@ async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
|||||||
params.text_document.uri,
|
params.text_document.uri,
|
||||||
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
|
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(params.position, lsp::Position::new(0, 0),);
|
||||||
params.position,
|
|
||||||
lsp::Position::new(0, 12),
|
|
||||||
"editor_b has selected the entire text and should query for a different position"
|
|
||||||
);
|
|
||||||
Ok(Some(ExpandedMacro {
|
Ok(Some(ExpandedMacro {
|
||||||
name: "test_macro_name".to_string(),
|
name: "test_macro_name".to_string(),
|
||||||
expansion: "test_macro_expansion on the client".to_string(),
|
expansion: "test_macro_expansion on the client".to_string(),
|
||||||
@@ -2760,7 +2756,6 @@ async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
|||||||
);
|
);
|
||||||
|
|
||||||
editor_b.update_in(cx_b, |editor, window, cx| {
|
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||||
editor.select_all(&SelectAll, window, cx);
|
|
||||||
expand_macro_recursively(editor, &ExpandMacroRecursively, window, cx)
|
expand_macro_recursively(editor, &ExpandMacroRecursively, window, cx)
|
||||||
});
|
});
|
||||||
expand_request_b.next().await.unwrap();
|
expand_request_b.next().await.unwrap();
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ use gpui::{
|
|||||||
UpdateGlobal, px, size,
|
UpdateGlobal, px, size,
|
||||||
};
|
};
|
||||||
use language::{
|
use language::{
|
||||||
Diagnostic, DiagnosticEntry, DiagnosticSourceKind, FakeLspAdapter, Language, LanguageConfig,
|
Diagnostic, DiagnosticEntry, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher,
|
||||||
LanguageMatcher, LineEnding, OffsetRangeExt, Point, Rope,
|
LineEnding, OffsetRangeExt, Point, Rope,
|
||||||
language_settings::{
|
language_settings::{
|
||||||
AllLanguageSettings, Formatter, FormatterList, PrettierSettings, SelectedFormatter,
|
AllLanguageSettings, Formatter, FormatterList, PrettierSettings, SelectedFormatter,
|
||||||
},
|
},
|
||||||
@@ -2624,7 +2624,6 @@ async fn test_git_diff_base_change(
|
|||||||
client_a.fs().set_head_for_repo(
|
client_a.fs().set_head_for_repo(
|
||||||
Path::new("/dir/.git"),
|
Path::new("/dir/.git"),
|
||||||
&[("a.txt".into(), committed_text.clone())],
|
&[("a.txt".into(), committed_text.clone())],
|
||||||
"deadbeef",
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create the buffer
|
// Create the buffer
|
||||||
@@ -2718,7 +2717,6 @@ async fn test_git_diff_base_change(
|
|||||||
client_a.fs().set_head_for_repo(
|
client_a.fs().set_head_for_repo(
|
||||||
Path::new("/dir/.git"),
|
Path::new("/dir/.git"),
|
||||||
&[("a.txt".into(), new_committed_text.clone())],
|
&[("a.txt".into(), new_committed_text.clone())],
|
||||||
"deadbeef",
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Wait for buffer_local_a to receive it
|
// Wait for buffer_local_a to receive it
|
||||||
@@ -3008,7 +3006,6 @@ async fn test_git_status_sync(
|
|||||||
client_a.fs().set_head_for_repo(
|
client_a.fs().set_head_for_repo(
|
||||||
path!("/dir/.git").as_ref(),
|
path!("/dir/.git").as_ref(),
|
||||||
&[("b.txt".into(), "B".into()), ("c.txt".into(), "c".into())],
|
&[("b.txt".into(), "B".into()), ("c.txt".into(), "c".into())],
|
||||||
"deadbeef",
|
|
||||||
);
|
);
|
||||||
client_a.fs().set_index_for_repo(
|
client_a.fs().set_index_for_repo(
|
||||||
path!("/dir/.git").as_ref(),
|
path!("/dir/.git").as_ref(),
|
||||||
@@ -4237,8 +4234,7 @@ async fn test_collaborating_with_diagnostics(
|
|||||||
message: "message 1".to_string(),
|
message: "message 1".to_string(),
|
||||||
severity: lsp::DiagnosticSeverity::ERROR,
|
severity: lsp::DiagnosticSeverity::ERROR,
|
||||||
is_primary: true,
|
is_primary: true,
|
||||||
source_kind: DiagnosticSourceKind::Pushed,
|
..Default::default()
|
||||||
..Diagnostic::default()
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
DiagnosticEntry {
|
DiagnosticEntry {
|
||||||
@@ -4248,8 +4244,7 @@ async fn test_collaborating_with_diagnostics(
|
|||||||
severity: lsp::DiagnosticSeverity::WARNING,
|
severity: lsp::DiagnosticSeverity::WARNING,
|
||||||
message: "message 2".to_string(),
|
message: "message 2".to_string(),
|
||||||
is_primary: true,
|
is_primary: true,
|
||||||
source_kind: DiagnosticSourceKind::Pushed,
|
..Default::default()
|
||||||
..Diagnostic::default()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -4261,7 +4256,7 @@ async fn test_collaborating_with_diagnostics(
|
|||||||
&lsp::PublishDiagnosticsParams {
|
&lsp::PublishDiagnosticsParams {
|
||||||
uri: lsp::Url::from_file_path(path!("/a/a.rs")).unwrap(),
|
uri: lsp::Url::from_file_path(path!("/a/a.rs")).unwrap(),
|
||||||
version: None,
|
version: None,
|
||||||
diagnostics: Vec::new(),
|
diagnostics: vec![],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
executor.run_until_parked();
|
executor.run_until_parked();
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ use language::{
|
|||||||
use project::{Completion, CompletionResponse, CompletionSource, search::SearchQuery};
|
use project::{Completion, CompletionResponse, CompletionSource, search::SearchQuery};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::{
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
ops::Range,
|
ops::Range,
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
sync::{Arc, LazyLock},
|
sync::{Arc, LazyLock},
|
||||||
@@ -72,13 +73,22 @@ impl CompletionProvider for MessageEditorCompletionProvider {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn resolve_completions(
|
||||||
|
&self,
|
||||||
|
_buffer: Entity<Buffer>,
|
||||||
|
_completion_indices: Vec<usize>,
|
||||||
|
_completions: Rc<RefCell<Box<[Completion]>>>,
|
||||||
|
_cx: &mut Context<Editor>,
|
||||||
|
) -> Task<anyhow::Result<bool>> {
|
||||||
|
Task::ready(Ok(false))
|
||||||
|
}
|
||||||
|
|
||||||
fn is_completion_trigger(
|
fn is_completion_trigger(
|
||||||
&self,
|
&self,
|
||||||
_buffer: &Entity<Buffer>,
|
_buffer: &Entity<Buffer>,
|
||||||
_position: language::Anchor,
|
_position: language::Anchor,
|
||||||
text: &str,
|
text: &str,
|
||||||
_trigger_in_words: bool,
|
_trigger_in_words: bool,
|
||||||
_menu_is_open: bool,
|
|
||||||
_cx: &mut Context<Editor>,
|
_cx: &mut Context<Editor>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
text == "@"
|
text == "@"
|
||||||
@@ -244,7 +254,7 @@ impl MessageEditor {
|
|||||||
{
|
{
|
||||||
if !candidates.is_empty() {
|
if !candidates.is_empty() {
|
||||||
return cx.spawn(async move |_, cx| {
|
return cx.spawn(async move |_, cx| {
|
||||||
let completion_response = Self::completions_for_candidates(
|
let completion_response = Self::resolve_completions_for_candidates(
|
||||||
&cx,
|
&cx,
|
||||||
query.as_str(),
|
query.as_str(),
|
||||||
&candidates,
|
&candidates,
|
||||||
@@ -262,7 +272,7 @@ impl MessageEditor {
|
|||||||
{
|
{
|
||||||
if !candidates.is_empty() {
|
if !candidates.is_empty() {
|
||||||
return cx.spawn(async move |_, cx| {
|
return cx.spawn(async move |_, cx| {
|
||||||
let completion_response = Self::completions_for_candidates(
|
let completion_response = Self::resolve_completions_for_candidates(
|
||||||
&cx,
|
&cx,
|
||||||
query.as_str(),
|
query.as_str(),
|
||||||
candidates,
|
candidates,
|
||||||
@@ -281,7 +291,7 @@ impl MessageEditor {
|
|||||||
}]))
|
}]))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn completions_for_candidates(
|
async fn resolve_completions_for_candidates(
|
||||||
cx: &AsyncApp,
|
cx: &AsyncApp,
|
||||||
query: &str,
|
query: &str,
|
||||||
candidates: &[StringMatchCandidate],
|
candidates: &[StringMatchCandidate],
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ use fuzzy::{StringMatchCandidate, match_strings};
|
|||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, AsyncWindowContext, Bounds, ClickEvent, ClipboardItem, Context, DismissEvent,
|
AnyElement, App, AsyncWindowContext, Bounds, ClickEvent, ClipboardItem, Context, DismissEvent,
|
||||||
Div, Entity, EventEmitter, FocusHandle, Focusable, FontStyle, InteractiveElement, IntoElement,
|
Div, Entity, EventEmitter, FocusHandle, Focusable, FontStyle, InteractiveElement, IntoElement,
|
||||||
KeyContext, ListOffset, ListState, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel,
|
ListOffset, ListState, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render,
|
||||||
Render, SharedString, Styled, Subscription, Task, TextStyle, WeakEntity, Window, actions,
|
SharedString, Styled, Subscription, Task, TextStyle, WeakEntity, Window, actions, anchored,
|
||||||
anchored, canvas, deferred, div, fill, list, point, prelude::*, px,
|
canvas, deferred, div, fill, list, point, prelude::*, px,
|
||||||
};
|
};
|
||||||
use menu::{Cancel, Confirm, SecondaryConfirm, SelectNext, SelectPrevious};
|
use menu::{Cancel, Confirm, SecondaryConfirm, SelectNext, SelectPrevious};
|
||||||
use project::{Fs, Project};
|
use project::{Fs, Project};
|
||||||
@@ -52,8 +52,6 @@ actions!(
|
|||||||
StartMoveChannel,
|
StartMoveChannel,
|
||||||
MoveSelected,
|
MoveSelected,
|
||||||
InsertSpace,
|
InsertSpace,
|
||||||
MoveChannelUp,
|
|
||||||
MoveChannelDown,
|
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1963,33 +1961,6 @@ impl CollabPanel {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn move_channel_up(&mut self, _: &MoveChannelUp, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
if let Some(channel) = self.selected_channel() {
|
|
||||||
self.channel_store.update(cx, |store, cx| {
|
|
||||||
store
|
|
||||||
.reorder_channel(channel.id, proto::reorder_channel::Direction::Up, cx)
|
|
||||||
.detach_and_prompt_err("Failed to move channel up", window, cx, |_, _, _| None)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn move_channel_down(
|
|
||||||
&mut self,
|
|
||||||
_: &MoveChannelDown,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
if let Some(channel) = self.selected_channel() {
|
|
||||||
self.channel_store.update(cx, |store, cx| {
|
|
||||||
store
|
|
||||||
.reorder_channel(channel.id, proto::reorder_channel::Direction::Down, cx)
|
|
||||||
.detach_and_prompt_err("Failed to move channel down", window, cx, |_, _, _| {
|
|
||||||
None
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn open_channel_notes(
|
fn open_channel_notes(
|
||||||
&mut self,
|
&mut self,
|
||||||
channel_id: ChannelId,
|
channel_id: ChannelId,
|
||||||
@@ -2003,7 +1974,7 @@ impl CollabPanel {
|
|||||||
|
|
||||||
fn show_inline_context_menu(
|
fn show_inline_context_menu(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: &Secondary,
|
_: &menu::SecondaryConfirm,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
@@ -2032,21 +2003,6 @@ impl CollabPanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_context(&self, window: &Window, cx: &Context<Self>) -> KeyContext {
|
|
||||||
let mut dispatch_context = KeyContext::new_with_defaults();
|
|
||||||
dispatch_context.add("CollabPanel");
|
|
||||||
dispatch_context.add("menu");
|
|
||||||
|
|
||||||
let identifier = if self.channel_name_editor.focus_handle(cx).is_focused(window) {
|
|
||||||
"editing"
|
|
||||||
} else {
|
|
||||||
"not_editing"
|
|
||||||
};
|
|
||||||
|
|
||||||
dispatch_context.add(identifier);
|
|
||||||
dispatch_context
|
|
||||||
}
|
|
||||||
|
|
||||||
fn selected_channel(&self) -> Option<&Arc<Channel>> {
|
fn selected_channel(&self) -> Option<&Arc<Channel>> {
|
||||||
self.selection
|
self.selection
|
||||||
.and_then(|ix| self.entries.get(ix))
|
.and_then(|ix| self.entries.get(ix))
|
||||||
@@ -3009,7 +2965,7 @@ fn render_tree_branch(
|
|||||||
impl Render for CollabPanel {
|
impl Render for CollabPanel {
|
||||||
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 {
|
||||||
v_flex()
|
v_flex()
|
||||||
.key_context(self.dispatch_context(window, cx))
|
.key_context("CollabPanel")
|
||||||
.on_action(cx.listener(CollabPanel::cancel))
|
.on_action(cx.listener(CollabPanel::cancel))
|
||||||
.on_action(cx.listener(CollabPanel::select_next))
|
.on_action(cx.listener(CollabPanel::select_next))
|
||||||
.on_action(cx.listener(CollabPanel::select_previous))
|
.on_action(cx.listener(CollabPanel::select_previous))
|
||||||
@@ -3021,8 +2977,6 @@ impl Render for CollabPanel {
|
|||||||
.on_action(cx.listener(CollabPanel::collapse_selected_channel))
|
.on_action(cx.listener(CollabPanel::collapse_selected_channel))
|
||||||
.on_action(cx.listener(CollabPanel::expand_selected_channel))
|
.on_action(cx.listener(CollabPanel::expand_selected_channel))
|
||||||
.on_action(cx.listener(CollabPanel::start_move_selected_channel))
|
.on_action(cx.listener(CollabPanel::start_move_selected_channel))
|
||||||
.on_action(cx.listener(CollabPanel::move_channel_up))
|
|
||||||
.on_action(cx.listener(CollabPanel::move_channel_down))
|
|
||||||
.track_focus(&self.focus_handle(cx))
|
.track_focus(&self.focus_handle(cx))
|
||||||
.size_full()
|
.size_full()
|
||||||
.child(if self.user_store.read(cx).current_user().is_none() {
|
.child(if self.user_store.read(cx).current_user().is_none() {
|
||||||
|
|||||||
@@ -448,7 +448,7 @@ impl PickerDelegate for CommandPaletteDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn humanize_action_name(name: &str) -> String {
|
fn humanize_action_name(name: &str) -> String {
|
||||||
let capacity = name.len() + name.chars().filter(|c| c.is_uppercase()).count();
|
let capacity = name.len() + name.chars().filter(|c| c.is_uppercase()).count();
|
||||||
let mut result = String::with_capacity(capacity);
|
let mut result = String::with_capacity(capacity);
|
||||||
for char in name.chars() {
|
for char in name.chars() {
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ impl ComponentMetadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Implement this trait to define a UI component. This will allow you to
|
/// Implement this trait to define a UI component. This will allow you to
|
||||||
/// derive `RegisterComponent` on it, in turn allowing you to preview the
|
/// derive `RegisterComponent` on it, in tutn allowing you to preview the
|
||||||
/// contents of the preview fn in `workspace: open component preview`.
|
/// contents of the preview fn in `workspace: open component preview`.
|
||||||
///
|
///
|
||||||
/// This can be useful for visual debugging and testing, documenting UI
|
/// This can be useful for visual debugging and testing, documenting UI
|
||||||
|
|||||||
@@ -11,9 +11,6 @@ workspace = true
|
|||||||
[lib]
|
[lib]
|
||||||
path = "src/context_server.rs"
|
path = "src/context_server.rs"
|
||||||
|
|
||||||
[features]
|
|
||||||
test-support = []
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
pub mod client;
|
pub mod client;
|
||||||
pub mod protocol;
|
pub mod protocol;
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
pub mod test;
|
|
||||||
pub mod transport;
|
pub mod transport;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,10 @@
|
|||||||
//! of messages.
|
//! of messages.
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use collections::HashMap;
|
||||||
|
|
||||||
use crate::client::Client;
|
use crate::client::Client;
|
||||||
use crate::types::{self, Request};
|
use crate::types;
|
||||||
|
|
||||||
pub struct ModelContextProtocol {
|
pub struct ModelContextProtocol {
|
||||||
inner: Client,
|
inner: Client,
|
||||||
@@ -42,7 +43,7 @@ impl ModelContextProtocol {
|
|||||||
|
|
||||||
let response: types::InitializeResponse = self
|
let response: types::InitializeResponse = self
|
||||||
.inner
|
.inner
|
||||||
.request(types::request::Initialize::METHOD, params)
|
.request(types::RequestType::Initialize.as_str(), params)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
anyhow::ensure!(
|
anyhow::ensure!(
|
||||||
@@ -93,7 +94,137 @@ impl InitializedContextServerProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn request<T: Request>(&self, params: T::Params) -> Result<T::Response> {
|
fn check_capability(&self, capability: ServerCapability) -> Result<()> {
|
||||||
self.inner.request(T::METHOD, params).await
|
anyhow::ensure!(
|
||||||
|
self.capable(capability),
|
||||||
|
"Server does not support {capability:?} capability"
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List the MCP prompts.
|
||||||
|
pub async fn list_prompts(&self) -> Result<Vec<types::Prompt>> {
|
||||||
|
self.check_capability(ServerCapability::Prompts)?;
|
||||||
|
|
||||||
|
let response: types::PromptsListResponse = self
|
||||||
|
.inner
|
||||||
|
.request(
|
||||||
|
types::RequestType::PromptsList.as_str(),
|
||||||
|
serde_json::json!({}),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(response.prompts)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List the MCP resources.
|
||||||
|
pub async fn list_resources(&self) -> Result<types::ResourcesListResponse> {
|
||||||
|
self.check_capability(ServerCapability::Resources)?;
|
||||||
|
|
||||||
|
let response: types::ResourcesListResponse = self
|
||||||
|
.inner
|
||||||
|
.request(
|
||||||
|
types::RequestType::ResourcesList.as_str(),
|
||||||
|
serde_json::json!({}),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Executes a prompt with the given arguments and returns the result.
|
||||||
|
pub async fn run_prompt<P: AsRef<str>>(
|
||||||
|
&self,
|
||||||
|
prompt: P,
|
||||||
|
arguments: HashMap<String, String>,
|
||||||
|
) -> Result<types::PromptsGetResponse> {
|
||||||
|
self.check_capability(ServerCapability::Prompts)?;
|
||||||
|
|
||||||
|
let params = types::PromptsGetParams {
|
||||||
|
name: prompt.as_ref().to_string(),
|
||||||
|
arguments: Some(arguments),
|
||||||
|
meta: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let response: types::PromptsGetResponse = self
|
||||||
|
.inner
|
||||||
|
.request(types::RequestType::PromptsGet.as_str(), params)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn completion<P: Into<String>>(
|
||||||
|
&self,
|
||||||
|
reference: types::CompletionReference,
|
||||||
|
argument: P,
|
||||||
|
value: P,
|
||||||
|
) -> Result<types::Completion> {
|
||||||
|
let params = types::CompletionCompleteParams {
|
||||||
|
r#ref: reference,
|
||||||
|
argument: types::CompletionArgument {
|
||||||
|
name: argument.into(),
|
||||||
|
value: value.into(),
|
||||||
|
},
|
||||||
|
meta: None,
|
||||||
|
};
|
||||||
|
let result: types::CompletionCompleteResponse = self
|
||||||
|
.inner
|
||||||
|
.request(types::RequestType::CompletionComplete.as_str(), params)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let completion = types::Completion {
|
||||||
|
values: result.completion.values,
|
||||||
|
total: types::CompletionTotal::from_options(
|
||||||
|
result.completion.has_more,
|
||||||
|
result.completion.total,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(completion)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List MCP tools.
|
||||||
|
pub async fn list_tools(&self) -> Result<types::ListToolsResponse> {
|
||||||
|
self.check_capability(ServerCapability::Tools)?;
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.inner
|
||||||
|
.request::<types::ListToolsResponse>(types::RequestType::ListTools.as_str(), ())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Executes a tool with the given arguments
|
||||||
|
pub async fn run_tool<P: AsRef<str>>(
|
||||||
|
&self,
|
||||||
|
tool: P,
|
||||||
|
arguments: Option<HashMap<String, serde_json::Value>>,
|
||||||
|
) -> Result<types::CallToolResponse> {
|
||||||
|
self.check_capability(ServerCapability::Tools)?;
|
||||||
|
|
||||||
|
let params = types::CallToolParams {
|
||||||
|
name: tool.as_ref().to_string(),
|
||||||
|
arguments,
|
||||||
|
meta: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let response: types::CallToolResponse = self
|
||||||
|
.inner
|
||||||
|
.request(types::RequestType::CallTool.as_str(), params)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InitializedContextServerProtocol {
|
||||||
|
pub async fn request<R: serde::de::DeserializeOwned>(
|
||||||
|
&self,
|
||||||
|
method: &str,
|
||||||
|
params: impl serde::Serialize,
|
||||||
|
) -> Result<R> {
|
||||||
|
self.inner.request(method, params).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,118 +0,0 @@
|
|||||||
use anyhow::Context as _;
|
|
||||||
use collections::HashMap;
|
|
||||||
use futures::{Stream, StreamExt as _, lock::Mutex};
|
|
||||||
use gpui::BackgroundExecutor;
|
|
||||||
use std::{pin::Pin, sync::Arc};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
transport::Transport,
|
|
||||||
types::{Implementation, InitializeResponse, ProtocolVersion, ServerCapabilities},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn create_fake_transport(
|
|
||||||
name: impl Into<String>,
|
|
||||||
executor: BackgroundExecutor,
|
|
||||||
) -> FakeTransport {
|
|
||||||
let name = name.into();
|
|
||||||
FakeTransport::new(executor).on_request::<crate::types::request::Initialize>(move |_params| {
|
|
||||||
create_initialize_response(name.clone())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_initialize_response(server_name: String) -> InitializeResponse {
|
|
||||||
InitializeResponse {
|
|
||||||
protocol_version: ProtocolVersion(crate::types::LATEST_PROTOCOL_VERSION.to_string()),
|
|
||||||
server_info: Implementation {
|
|
||||||
name: server_name,
|
|
||||||
version: "1.0.0".to_string(),
|
|
||||||
},
|
|
||||||
capabilities: ServerCapabilities::default(),
|
|
||||||
meta: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct FakeTransport {
|
|
||||||
request_handlers:
|
|
||||||
HashMap<&'static str, Arc<dyn Fn(serde_json::Value) -> serde_json::Value + Send + Sync>>,
|
|
||||||
tx: futures::channel::mpsc::UnboundedSender<String>,
|
|
||||||
rx: Arc<Mutex<futures::channel::mpsc::UnboundedReceiver<String>>>,
|
|
||||||
executor: BackgroundExecutor,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FakeTransport {
|
|
||||||
pub fn new(executor: BackgroundExecutor) -> Self {
|
|
||||||
let (tx, rx) = futures::channel::mpsc::unbounded();
|
|
||||||
Self {
|
|
||||||
request_handlers: Default::default(),
|
|
||||||
tx,
|
|
||||||
rx: Arc::new(Mutex::new(rx)),
|
|
||||||
executor,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_request<T: crate::types::Request>(
|
|
||||||
mut self,
|
|
||||||
handler: impl Fn(T::Params) -> T::Response + Send + Sync + 'static,
|
|
||||||
) -> Self {
|
|
||||||
self.request_handlers.insert(
|
|
||||||
T::METHOD,
|
|
||||||
Arc::new(move |value| {
|
|
||||||
let params = value.get("params").expect("Missing parameters").clone();
|
|
||||||
let params: T::Params =
|
|
||||||
serde_json::from_value(params).expect("Invalid parameters received");
|
|
||||||
let response = handler(params);
|
|
||||||
serde_json::to_value(response).unwrap()
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
impl Transport for FakeTransport {
|
|
||||||
async fn send(&self, message: String) -> anyhow::Result<()> {
|
|
||||||
if let Ok(msg) = serde_json::from_str::<serde_json::Value>(&message) {
|
|
||||||
let id = msg.get("id").and_then(|id| id.as_u64()).unwrap_or(0);
|
|
||||||
|
|
||||||
if let Some(method) = msg.get("method") {
|
|
||||||
let method = method.as_str().expect("Invalid method received");
|
|
||||||
if let Some(handler) = self.request_handlers.get(method) {
|
|
||||||
let payload = handler(msg);
|
|
||||||
let response = serde_json::json!({
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": id,
|
|
||||||
"result": payload
|
|
||||||
});
|
|
||||||
self.tx
|
|
||||||
.unbounded_send(response.to_string())
|
|
||||||
.context("sending a message")?;
|
|
||||||
} else {
|
|
||||||
log::debug!("No handler registered for MCP request '{method}'");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn receive(&self) -> Pin<Box<dyn Stream<Item = String> + Send>> {
|
|
||||||
let rx = self.rx.clone();
|
|
||||||
let executor = self.executor.clone();
|
|
||||||
Box::pin(futures::stream::unfold(rx, move |rx| {
|
|
||||||
let executor = executor.clone();
|
|
||||||
async move {
|
|
||||||
let mut rx_guard = rx.lock().await;
|
|
||||||
executor.simulate_random_delay().await;
|
|
||||||
if let Some(message) = rx_guard.next().await {
|
|
||||||
drop(rx_guard);
|
|
||||||
Some((message, rx))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn receive_err(&self) -> Pin<Box<dyn Stream<Item = String> + Send>> {
|
|
||||||
Box::pin(futures::stream::empty())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,92 +1,76 @@
|
|||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use serde::de::DeserializeOwned;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
pub const LATEST_PROTOCOL_VERSION: &str = "2024-11-05";
|
pub const LATEST_PROTOCOL_VERSION: &str = "2024-11-05";
|
||||||
|
|
||||||
pub mod request {
|
pub enum RequestType {
|
||||||
use super::*;
|
Initialize,
|
||||||
|
CallTool,
|
||||||
macro_rules! request {
|
ResourcesUnsubscribe,
|
||||||
($method:expr, $name:ident, $params:ty, $response:ty) => {
|
ResourcesSubscribe,
|
||||||
pub struct $name;
|
ResourcesRead,
|
||||||
|
ResourcesList,
|
||||||
impl Request for $name {
|
LoggingSetLevel,
|
||||||
type Params = $params;
|
PromptsGet,
|
||||||
type Response = $response;
|
PromptsList,
|
||||||
const METHOD: &'static str = $method;
|
CompletionComplete,
|
||||||
}
|
Ping,
|
||||||
};
|
ListTools,
|
||||||
}
|
ListResourceTemplates,
|
||||||
|
ListRoots,
|
||||||
request!(
|
|
||||||
"initialize",
|
|
||||||
Initialize,
|
|
||||||
InitializeParams,
|
|
||||||
InitializeResponse
|
|
||||||
);
|
|
||||||
request!("tools/call", CallTool, CallToolParams, CallToolResponse);
|
|
||||||
request!(
|
|
||||||
"resources/unsubscribe",
|
|
||||||
ResourcesUnsubscribe,
|
|
||||||
ResourcesUnsubscribeParams,
|
|
||||||
()
|
|
||||||
);
|
|
||||||
request!(
|
|
||||||
"resources/subscribe",
|
|
||||||
ResourcesSubscribe,
|
|
||||||
ResourcesSubscribeParams,
|
|
||||||
()
|
|
||||||
);
|
|
||||||
request!(
|
|
||||||
"resources/read",
|
|
||||||
ResourcesRead,
|
|
||||||
ResourcesReadParams,
|
|
||||||
ResourcesReadResponse
|
|
||||||
);
|
|
||||||
request!("resources/list", ResourcesList, (), ResourcesListResponse);
|
|
||||||
request!(
|
|
||||||
"logging/setLevel",
|
|
||||||
LoggingSetLevel,
|
|
||||||
LoggingSetLevelParams,
|
|
||||||
()
|
|
||||||
);
|
|
||||||
request!(
|
|
||||||
"prompts/get",
|
|
||||||
PromptsGet,
|
|
||||||
PromptsGetParams,
|
|
||||||
PromptsGetResponse
|
|
||||||
);
|
|
||||||
request!("prompts/list", PromptsList, (), PromptsListResponse);
|
|
||||||
request!(
|
|
||||||
"completion/complete",
|
|
||||||
CompletionComplete,
|
|
||||||
CompletionCompleteParams,
|
|
||||||
CompletionCompleteResponse
|
|
||||||
);
|
|
||||||
request!("ping", Ping, (), ());
|
|
||||||
request!("tools/list", ListTools, (), ListToolsResponse);
|
|
||||||
request!(
|
|
||||||
"resources/templates/list",
|
|
||||||
ListResourceTemplates,
|
|
||||||
(),
|
|
||||||
ListResourceTemplatesResponse
|
|
||||||
);
|
|
||||||
request!("roots/list", ListRoots, (), ListRootsResponse);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Request {
|
impl RequestType {
|
||||||
type Params: DeserializeOwned + Serialize + Send + Sync + 'static;
|
pub fn as_str(&self) -> &'static str {
|
||||||
type Response: DeserializeOwned + Serialize + Send + Sync + 'static;
|
match self {
|
||||||
const METHOD: &'static str;
|
RequestType::Initialize => "initialize",
|
||||||
|
RequestType::CallTool => "tools/call",
|
||||||
|
RequestType::ResourcesUnsubscribe => "resources/unsubscribe",
|
||||||
|
RequestType::ResourcesSubscribe => "resources/subscribe",
|
||||||
|
RequestType::ResourcesRead => "resources/read",
|
||||||
|
RequestType::ResourcesList => "resources/list",
|
||||||
|
RequestType::LoggingSetLevel => "logging/setLevel",
|
||||||
|
RequestType::PromptsGet => "prompts/get",
|
||||||
|
RequestType::PromptsList => "prompts/list",
|
||||||
|
RequestType::CompletionComplete => "completion/complete",
|
||||||
|
RequestType::Ping => "ping",
|
||||||
|
RequestType::ListTools => "tools/list",
|
||||||
|
RequestType::ListResourceTemplates => "resources/templates/list",
|
||||||
|
RequestType::ListRoots => "roots/list",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&str> for RequestType {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn try_from(s: &str) -> Result<Self, Self::Error> {
|
||||||
|
match s {
|
||||||
|
"initialize" => Ok(RequestType::Initialize),
|
||||||
|
"tools/call" => Ok(RequestType::CallTool),
|
||||||
|
"resources/unsubscribe" => Ok(RequestType::ResourcesUnsubscribe),
|
||||||
|
"resources/subscribe" => Ok(RequestType::ResourcesSubscribe),
|
||||||
|
"resources/read" => Ok(RequestType::ResourcesRead),
|
||||||
|
"resources/list" => Ok(RequestType::ResourcesList),
|
||||||
|
"logging/setLevel" => Ok(RequestType::LoggingSetLevel),
|
||||||
|
"prompts/get" => Ok(RequestType::PromptsGet),
|
||||||
|
"prompts/list" => Ok(RequestType::PromptsList),
|
||||||
|
"completion/complete" => Ok(RequestType::CompletionComplete),
|
||||||
|
"ping" => Ok(RequestType::Ping),
|
||||||
|
"tools/list" => Ok(RequestType::ListTools),
|
||||||
|
"resources/templates/list" => Ok(RequestType::ListResourceTemplates),
|
||||||
|
"roots/list" => Ok(RequestType::ListRoots),
|
||||||
|
_ => Err(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
pub struct ProtocolVersion(pub String);
|
pub struct ProtocolVersion(pub String);
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct InitializeParams {
|
pub struct InitializeParams {
|
||||||
pub protocol_version: ProtocolVersion,
|
pub protocol_version: ProtocolVersion,
|
||||||
@@ -96,7 +80,7 @@ pub struct InitializeParams {
|
|||||||
pub meta: Option<HashMap<String, serde_json::Value>>,
|
pub meta: Option<HashMap<String, serde_json::Value>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct CallToolParams {
|
pub struct CallToolParams {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -106,7 +90,7 @@ pub struct CallToolParams {
|
|||||||
pub meta: Option<HashMap<String, serde_json::Value>>,
|
pub meta: Option<HashMap<String, serde_json::Value>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ResourcesUnsubscribeParams {
|
pub struct ResourcesUnsubscribeParams {
|
||||||
pub uri: Url,
|
pub uri: Url,
|
||||||
@@ -114,7 +98,7 @@ pub struct ResourcesUnsubscribeParams {
|
|||||||
pub meta: Option<HashMap<String, serde_json::Value>>,
|
pub meta: Option<HashMap<String, serde_json::Value>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ResourcesSubscribeParams {
|
pub struct ResourcesSubscribeParams {
|
||||||
pub uri: Url,
|
pub uri: Url,
|
||||||
@@ -122,7 +106,7 @@ pub struct ResourcesSubscribeParams {
|
|||||||
pub meta: Option<HashMap<String, serde_json::Value>>,
|
pub meta: Option<HashMap<String, serde_json::Value>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ResourcesReadParams {
|
pub struct ResourcesReadParams {
|
||||||
pub uri: Url,
|
pub uri: Url,
|
||||||
@@ -130,7 +114,7 @@ pub struct ResourcesReadParams {
|
|||||||
pub meta: Option<HashMap<String, serde_json::Value>>,
|
pub meta: Option<HashMap<String, serde_json::Value>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct LoggingSetLevelParams {
|
pub struct LoggingSetLevelParams {
|
||||||
pub level: LoggingLevel,
|
pub level: LoggingLevel,
|
||||||
@@ -138,7 +122,7 @@ pub struct LoggingSetLevelParams {
|
|||||||
pub meta: Option<HashMap<String, serde_json::Value>>,
|
pub meta: Option<HashMap<String, serde_json::Value>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct PromptsGetParams {
|
pub struct PromptsGetParams {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -148,40 +132,37 @@ pub struct PromptsGetParams {
|
|||||||
pub meta: Option<HashMap<String, serde_json::Value>>,
|
pub meta: Option<HashMap<String, serde_json::Value>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct CompletionCompleteParams {
|
pub struct CompletionCompleteParams {
|
||||||
#[serde(rename = "ref")]
|
pub r#ref: CompletionReference,
|
||||||
pub reference: CompletionReference,
|
|
||||||
pub argument: CompletionArgument,
|
pub argument: CompletionArgument,
|
||||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||||
pub meta: Option<HashMap<String, serde_json::Value>>,
|
pub meta: Option<HashMap<String, serde_json::Value>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
pub enum CompletionReference {
|
pub enum CompletionReference {
|
||||||
Prompt(PromptReference),
|
Prompt(PromptReference),
|
||||||
Resource(ResourceReference),
|
Resource(ResourceReference),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct PromptReference {
|
pub struct PromptReference {
|
||||||
#[serde(rename = "type")]
|
pub r#type: PromptReferenceType,
|
||||||
pub ty: PromptReferenceType,
|
|
||||||
pub name: String,
|
pub name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ResourceReference {
|
pub struct ResourceReference {
|
||||||
#[serde(rename = "type")]
|
pub r#type: PromptReferenceType,
|
||||||
pub ty: PromptReferenceType,
|
|
||||||
pub uri: Url,
|
pub uri: Url,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum PromptReferenceType {
|
pub enum PromptReferenceType {
|
||||||
#[serde(rename = "ref/prompt")]
|
#[serde(rename = "ref/prompt")]
|
||||||
@@ -190,7 +171,7 @@ pub enum PromptReferenceType {
|
|||||||
Resource,
|
Resource,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct CompletionArgument {
|
pub struct CompletionArgument {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -207,7 +188,7 @@ pub struct InitializeResponse {
|
|||||||
pub meta: Option<HashMap<String, serde_json::Value>>,
|
pub meta: Option<HashMap<String, serde_json::Value>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ResourcesReadResponse {
|
pub struct ResourcesReadResponse {
|
||||||
pub contents: Vec<ResourceContentsType>,
|
pub contents: Vec<ResourceContentsType>,
|
||||||
@@ -215,14 +196,14 @@ pub struct ResourcesReadResponse {
|
|||||||
pub meta: Option<HashMap<String, serde_json::Value>>,
|
pub meta: Option<HashMap<String, serde_json::Value>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
pub enum ResourceContentsType {
|
pub enum ResourceContentsType {
|
||||||
Text(TextResourceContents),
|
Text(TextResourceContents),
|
||||||
Blob(BlobResourceContents),
|
Blob(BlobResourceContents),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ResourcesListResponse {
|
pub struct ResourcesListResponse {
|
||||||
pub resources: Vec<Resource>,
|
pub resources: Vec<Resource>,
|
||||||
@@ -239,7 +220,7 @@ pub struct SamplingMessage {
|
|||||||
pub content: MessageContent,
|
pub content: MessageContent,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct CreateMessageRequest {
|
pub struct CreateMessageRequest {
|
||||||
pub messages: Vec<SamplingMessage>,
|
pub messages: Vec<SamplingMessage>,
|
||||||
@@ -315,7 +296,7 @@ pub struct MessageAnnotations {
|
|||||||
pub priority: Option<f64>,
|
pub priority: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct PromptsGetResponse {
|
pub struct PromptsGetResponse {
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
@@ -325,7 +306,7 @@ pub struct PromptsGetResponse {
|
|||||||
pub meta: Option<HashMap<String, serde_json::Value>>,
|
pub meta: Option<HashMap<String, serde_json::Value>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct PromptsListResponse {
|
pub struct PromptsListResponse {
|
||||||
pub prompts: Vec<Prompt>,
|
pub prompts: Vec<Prompt>,
|
||||||
@@ -335,7 +316,7 @@ pub struct PromptsListResponse {
|
|||||||
pub meta: Option<HashMap<String, serde_json::Value>>,
|
pub meta: Option<HashMap<String, serde_json::Value>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct CompletionCompleteResponse {
|
pub struct CompletionCompleteResponse {
|
||||||
pub completion: CompletionResult,
|
pub completion: CompletionResult,
|
||||||
@@ -343,7 +324,7 @@ pub struct CompletionCompleteResponse {
|
|||||||
pub meta: Option<HashMap<String, serde_json::Value>>,
|
pub meta: Option<HashMap<String, serde_json::Value>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct CompletionResult {
|
pub struct CompletionResult {
|
||||||
pub values: Vec<String>,
|
pub values: Vec<String>,
|
||||||
@@ -355,7 +336,7 @@ pub struct CompletionResult {
|
|||||||
pub meta: Option<HashMap<String, serde_json::Value>>,
|
pub meta: Option<HashMap<String, serde_json::Value>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Prompt {
|
pub struct Prompt {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -365,7 +346,7 @@ pub struct Prompt {
|
|||||||
pub arguments: Option<Vec<PromptArgument>>,
|
pub arguments: Option<Vec<PromptArgument>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct PromptArgument {
|
pub struct PromptArgument {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -528,7 +509,7 @@ pub struct ModelHint {
|
|||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub enum NotificationType {
|
pub enum NotificationType {
|
||||||
Initialized,
|
Initialized,
|
||||||
@@ -608,7 +589,7 @@ pub struct Completion {
|
|||||||
pub total: CompletionTotal,
|
pub total: CompletionTotal,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct CallToolResponse {
|
pub struct CallToolResponse {
|
||||||
pub content: Vec<ToolResponseContent>,
|
pub content: Vec<ToolResponseContent>,
|
||||||
@@ -639,7 +620,7 @@ pub struct ListToolsResponse {
|
|||||||
pub meta: Option<HashMap<String, serde_json::Value>>,
|
pub meta: Option<HashMap<String, serde_json::Value>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ListResourceTemplatesResponse {
|
pub struct ListResourceTemplatesResponse {
|
||||||
pub resource_templates: Vec<ResourceTemplate>,
|
pub resource_templates: Vec<ResourceTemplate>,
|
||||||
@@ -649,7 +630,7 @@ pub struct ListResourceTemplatesResponse {
|
|||||||
pub meta: Option<HashMap<String, serde_json::Value>>,
|
pub meta: Option<HashMap<String, serde_json::Value>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ListRootsResponse {
|
pub struct ListRootsResponse {
|
||||||
pub roots: Vec<Root>,
|
pub roots: Vec<Root>,
|
||||||
|
|||||||
@@ -520,7 +520,7 @@ impl Copilot {
|
|||||||
|
|
||||||
let server = cx
|
let server = cx
|
||||||
.update(|cx| {
|
.update(|cx| {
|
||||||
let mut params = server.default_initialize_params(false, cx);
|
let mut params = server.default_initialize_params(cx);
|
||||||
params.initialization_options = Some(editor_info_json);
|
params.initialization_options = Some(editor_info_json);
|
||||||
server.initialize(params, configuration.into(), cx)
|
server.initialize(params, configuration.into(), cx)
|
||||||
})?
|
})?
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user