Compare commits

..

10 Commits

Author SHA1 Message Date
Nathan Sobo
d104df955e WIP 2025-06-04 09:50:06 -07:00
Mikayla Maki
45de5c080a remove old comment 2025-05-31 22:17:44 -07:00
Mikayla Maki
03bed065e7 Slightly optimize the noop reordering case 2025-05-31 22:15:51 -07:00
Mikayla Maki
a3e7c4e961 Add a test showing that the channels can be in any order 2025-05-31 21:09:45 -07:00
Mikayla Maki
24ccd8268d Add missing channel moving tests
Fix a bug in the channel reordering when moving a channel into another
2025-05-31 20:38:50 -07:00
Mikayla Maki
9168a94c6a Remove unescessary complexity.
Fix a test that wasn't actually testing anything when ordering
constraints where removed.
2025-05-31 19:55:15 -07:00
Mikayla Maki
b96463e5bb remove unescessary channel ordering 2025-05-31 19:31:44 -07:00
Nathan Sobo
0e12e31edc Fix channel reordering to return only swapped channels
The reorder_channel function was returning all sibling channels instead
of just the two that were swapped, causing test failures that expected
exactly 2 channels to be returned.

This change modifies the function to return only the two channels that
had their order values swapped, which is more efficient and matches
the test expectations.
2025-05-31 15:53:45 -06:00
Nathan Sobo
a8287e4289 Fix CollabPanel channel reordering keybindings
The channel reordering keybindings (Cmd/Ctrl+Up/Down) were non-functional because the CollabPanel wasn't properly setting the key context to distinguish between editing and non-editing states.

## Problem

The keymaps specified 'CollabPanel && not_editing' as the required context for the reordering actions, but CollabPanel was only setting 'CollabPanel' without the editing state modifier. This meant the keybindings were never matched.

## Solution

Added a dispatch_context method to CollabPanel that:
- Checks if the channel_name_editor is focused
- Adds 'editing' context when renaming/creating channels
- Adds 'not_editing' context during normal navigation
- Follows the same pattern used by ProjectPanel and OutlinePanel

This allows the keybindings to work correctly while preventing conflicts between text editing operations (where arrows move the cursor) and channel navigation operations (where arrows reorder channels).
2025-05-31 14:49:51 -06:00
Nathan Sobo
8019a3925d Add channel reordering functionality
This change introduces the ability for channel administrators to reorder channels within their parent context, providing better organizational control over channel hierarchies. Channels can now be moved up or down relative to their siblings through keyboard shortcuts.

## Problem

Previously, channels were displayed in alphabetical order with no way to customize their arrangement. This made it difficult for teams to organize channels in a logical order that reflected their workflow or importance, forcing users to prefix channel names with numbers or special characters as a workaround.

## Solution

The implementation adds a persistent `channel_order` field to channels that determines their display order within their parent. Channels with the same parent are sorted by this field rather than alphabetically.

## Technical Implementation

**Database Schema:**
- Added `channel_order` INTEGER column to the channels table with a default value of 1
- Created a compound index on `(parent_path, channel_order)` for efficient ordering queries
- Migration updates existing channels with deterministic ordering based on their current alphabetical position

**Core Changes:**
- Extended the `Channel` struct across the codebase to include the `channel_order` field
- Modified `ChannelIndex` sorting logic to use channel_order instead of alphabetical name comparison
- Channels now maintain their relative position among siblings

**RPC Protocol:**
- Added `ReorderChannel` RPC message with channel_id and direction (Up/Down)
- Extended existing Channel proto message to include the channel_order field
- Server validates that users have admin permissions before allowing reordering

**Reordering Logic:**
- When moving a channel up/down, the server finds the adjacent sibling in that direction
- Swaps the `channel_order` values between the two channels
- Broadcasts the updated channel list to all affected users
- Handles edge cases: channels at boundaries, channels with gaps in ordering

**User Interface:**
- Added keyboard shortcuts:
  - macOS: `Cmd+Up/Down` to move channels
  - Linux: `Ctrl+Up/Down` to move channels
- Added `MoveChannelUp` and `MoveChannelDown` actions to the collab panel
- Reordering is only available when a channel is selected (not during editing)

**Error Handling:**
The change also improves error handling practices throughout the codebase by ensuring that errors from async operations propagate correctly to the UI layer instead of being silently discarded with `let _ =`.

**Testing:**
Comprehensive tests were added to verify:
- Basic reordering functionality up and down
- Boundary conditions (first/last channels)
- Permission checks (non-admins cannot reorder)
- Ordering persistence across server restarts
- Correct broadcasting of changes to channel members

**Known Issue:**
The keybindings are currently non-functional due to missing context setup in CollabPanel. The keymaps expect 'CollabPanel && not_editing' context, but the panel only provides 'CollabPanel'. This will be fixed in a follow-up commit.
2025-05-31 14:45:13 -06:00
697 changed files with 20390 additions and 45095 deletions

View File

@@ -1,4 +1,4 @@
name: Bug Report (AI)
name: Bug Report (AI Related)
description: Zed Agent Panel Bugs
type: "Bug"
labels: ["ai"]
@@ -19,14 +19,15 @@ body:
2.
3.
**Expected Behavior**:
**Actual Behavior**:
Actual Behavior:
Expected Behavior:
### Model Provider Details
- Provider: (Anthropic via ZedPro, Anthropic via API key, Copilot Chat, Mistral, OpenAI, etc)
- Model Name:
- Mode: (Agent Panel, Inline Assistant, Terminal Assistant or Text Threads)
- Other Details (MCPs, other settings, etc):
- MCP Servers in-use:
- Other Details:
validations:
required: true

View File

@@ -0,0 +1,36 @@
name: Bug Report (Edit Predictions)
description: Zed Edit Predictions bugs
type: "Bug"
labels: ["ai", "inline completion", "zeta"]
title: "Edit Predictions: <a short description of the Edit Prediction bug>"
body:
- type: textarea
attributes:
label: Summary
description: Describe the bug with a one line summary, and provide detailed reproduction steps
value: |
<!-- Please insert a one line summary of the issue below -->
SUMMARY_SENTENCE_HERE
### Description
<!-- Describe with sufficient detail to reproduce from a clean Zed install. -->
<!-- Please include the LLM provider and model name you are using -->
Steps to trigger the problem:
1.
2.
3.
Actual Behavior:
Expected Behavior:
validations:
required: true
- type: textarea
id: environment
attributes:
label: Zed Version and System Specs
description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"'
placeholder: |
Output of "zed: copy system specs into clipboard"
validations:
required: true

35
.github/ISSUE_TEMPLATE/03_bug_git.yml vendored Normal file
View File

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

View File

@@ -19,8 +19,8 @@ body:
2.
3.
**Expected Behavior**:
**Actual Behavior**:
Actual Behavior:
Expected Behavior:
validations:
required: true

View File

@@ -18,16 +18,14 @@ body:
- Issues with insufficient detail may be summarily closed.
-->
DESCRIPTION_HERE
Steps to reproduce:
1.
2.
3.
4.
**Expected Behavior**:
**Actual Behavior**:
Expected Behavior:
Actual Behavior:
<!-- Before Submitting, did you:
1. Include settings.json, keymap.json, .editorconfig if relevant?

View File

@@ -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 --exclude '^http' './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/

View File

@@ -10,8 +10,8 @@ inputs:
runs:
using: "composite"
steps:
- name: Install test runner
shell: powershell
- name: Install Rust
shell: pwsh
working-directory: ${{ inputs.working-directory }}
run: cargo install cargo-nextest --locked
@@ -21,6 +21,6 @@ runs:
node-version: "18"
- name: Run tests
shell: powershell
shell: pwsh
working-directory: ${{ inputs.working-directory }}
run: cargo nextest run --workspace --no-fail-fast
run: cargo nextest run --workspace --no-fail-fast --config='profile.dev.debug="limited"'

View File

@@ -73,7 +73,7 @@ jobs:
timeout-minutes: 60
runs-on:
- self-hosted
- macOS
- test
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@@ -183,9 +183,6 @@ jobs:
- name: Check for todo! and FIXME comments
run: script/check-todos
- name: Check modifier use in keymaps
run: script/check-keymaps
- name: Run style checks
uses: ./.github/actions/check_style
@@ -194,27 +191,6 @@ jobs:
with:
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:
timeout-minutes: 60
name: (macOS) Run Clippy and tests
@@ -224,7 +200,7 @@ jobs:
needs.job_spec.outputs.run_tests == 'true'
runs-on:
- self-hosted
- macOS
- test
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@@ -373,6 +349,64 @@ jobs:
if: always()
run: rm -rf ./../.cargo
windows_clippy:
timeout-minutes: 60
name: (Windows) Run Clippy
needs: [job_spec]
if: |
github.repository_owner == 'zed-industries' &&
needs.job_spec.outputs.run_tests == 'true'
runs-on: windows-2025-16
steps:
# more info here:- https://github.com/rust-lang/cargo/issues/13020
- name: Enable longer pathnames for git
run: git config --system core.longpaths true
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
clean: false
- name: Create Dev Drive using ReFS
run: ./script/setup-dev-driver.ps1
# actions/checkout does not let us clone into anywhere outside ${{ github.workspace }}, so we have to copy the clone...
- name: Copy Git Repo to Dev Drive
run: |
Copy-Item -Path "${{ github.workspace }}" -Destination "${{ env.ZED_WORKSPACE }}" -Recurse
- name: Cache dependencies
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
workspaces: ${{ env.ZED_WORKSPACE }}
cache-provider: "github"
- name: Configure CI
run: |
mkdir -p ${{ env.CARGO_HOME }} -ErrorAction Ignore
cp ./.cargo/ci-config.toml ${{ env.CARGO_HOME }}/config.toml
- name: cargo clippy
working-directory: ${{ env.ZED_WORKSPACE }}
run: ./script/clippy.ps1
- name: Check dev drive space
working-directory: ${{ env.ZED_WORKSPACE }}
# `setup-dev-driver.ps1` creates a 100GB drive, with CI taking up ~45GB of the drive.
run: ./script/exit-ci-if-dev-drive-is-full.ps1 95
# Since the Windows runners are stateful, so we need to remove the config file to prevent potential bug.
- name: Clean CI config file
if: always()
run: |
if (Test-Path "${{ env.CARGO_HOME }}/config.toml") {
Remove-Item -Path "${{ env.CARGO_HOME }}/config.toml" -Force
}
# Windows CI takes twice as long as our other platforms and fast github hosted runners are expensive.
# But we still want to do CI, so let's only run tests on main and come back to this when we're
# ready to self host our Windows CI (e.g. during the push for full Windows support)
windows_tests:
timeout-minutes: 60
name: (Windows) Run Tests
@@ -380,45 +414,51 @@ jobs:
if: |
github.repository_owner == 'zed-industries' &&
needs.job_spec.outputs.run_tests == 'true'
runs-on: [self-hosted, Windows, X64]
# Use bigger runners for PRs (speed); smaller for async (cost)
runs-on: ${{ github.event_name == 'pull_request' && 'windows-2025-32' || 'windows-2025-16' }}
steps:
- name: Environment Setup
run: |
$RunnerDir = Split-Path -Parent $env:RUNNER_WORKSPACE
Write-Output `
"RUSTUP_HOME=$RunnerDir\.rustup" `
"CARGO_HOME=$RunnerDir\.cargo" `
"PATH=$RunnerDir\.cargo\bin;$env:PATH" `
>> $env:GITHUB_ENV
# more info here:- https://github.com/rust-lang/cargo/issues/13020
- name: Enable longer pathnames for git
run: git config --system core.longpaths true
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
clean: false
- name: Setup Cargo and Rustup
- name: Create Dev Drive using ReFS
run: ./script/setup-dev-driver.ps1
# actions/checkout does not let us clone into anywhere outside ${{ github.workspace }}, so we have to copy the clone...
- name: Copy Git Repo to Dev Drive
run: |
Copy-Item -Path "${{ github.workspace }}" -Destination "${{ env.ZED_WORKSPACE }}" -Recurse
- name: Cache dependencies
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
workspaces: ${{ env.ZED_WORKSPACE }}
cache-provider: "github"
- name: Configure CI
run: |
mkdir -p ${{ env.CARGO_HOME }} -ErrorAction Ignore
cp ./.cargo/ci-config.toml ${{ env.CARGO_HOME }}/config.toml
.\script\install-rustup.ps1
- name: cargo clippy
run: |
.\script\clippy.ps1
- name: Run tests
uses: ./.github/actions/run_tests_windows
with:
working-directory: ${{ env.ZED_WORKSPACE }}
- name: Build Zed
working-directory: ${{ env.ZED_WORKSPACE }}
run: cargo build
- name: Limit target directory size
run: ./script/clear-target-dir-if-larger-than.ps1 250
# - name: Check dev drive space
# working-directory: ${{ env.ZED_WORKSPACE }}
# # `setup-dev-driver.ps1` creates a 100GB drive, with CI taking up ~45GB of the drive.
# run: ./script/exit-ci-if-dev-drive-is-full.ps1 95
- name: Check dev drive space
working-directory: ${{ env.ZED_WORKSPACE }}
# `setup-dev-driver.ps1` creates a 100GB drive, with CI taking up ~45GB of the drive.
run: ./script/exit-ci-if-dev-drive-is-full.ps1 95
# Since the Windows runners are stateful, so we need to remove the config file to prevent potential bug.
- name: Clean CI config file
@@ -434,25 +474,22 @@ jobs:
needs:
- job_spec
- style
- check_docs
- migration_checks
# run_tests: If adding required tests, add them here and to script below.
- workspace_hack
- linux_tests
- build_remote_server
- macos_tests
- windows_clippy
- windows_tests
if: |
github.repository_owner == 'zed-industries' &&
always()
if: always()
steps:
- name: Check all tests passed
run: |
# Check dependent jobs...
RET_CODE=0
# Always check style
[[ "${{ needs.style.result }}" != 'success' ]] && { RET_CODE=1; echo "style tests failed"; }
[[ "${{ needs.check_docs.result }}" != 'success' ]] && { RET_CODE=1; echo "docs checks failed"; }
[[ "${{ needs.style.result }}" != 'success' ]] && { RET_CODE=1; echo "style tests failed"; }
# Only check test jobs if they were supposed to run
if [[ "${{ needs.job_spec.outputs.run_tests }}" == "true" ]]; then
@@ -460,6 +497,7 @@ jobs:
[[ "${{ needs.macos_tests.result }}" != 'success' ]] && { RET_CODE=1; echo "macOS tests failed"; }
[[ "${{ needs.linux_tests.result }}" != 'success' ]] && { RET_CODE=1; echo "Linux tests failed"; }
[[ "${{ needs.windows_tests.result }}" != 'success' ]] && { RET_CODE=1; echo "Windows tests failed"; }
[[ "${{ needs.windows_clippy.result }}" != 'success' ]] && { RET_CODE=1; echo "Windows clippy failed"; }
[[ "${{ needs.build_remote_server.result }}" != 'success' ]] && { RET_CODE=1; echo "Remote server build failed"; }
# This check is intentionally disabled. See: https://github.com/zed-industries/zed/pull/28431
# [[ "${{ needs.migration_checks.result }}" != 'success' ]] && { RET_CODE=1; echo "Migration Checks failed"; }
@@ -675,69 +713,10 @@ jobs:
env:
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:
name: Build with Nix
uses: ./.github/workflows/nix.yml
if: github.repository_owner == 'zed-industries' && contains(github.event.pull_request.labels.*.name, 'run-nix')
secrets: inherit
with:
flake-output: debug
# excludes the final package to only cache dependencies
@@ -748,12 +727,12 @@ jobs:
if: |
startsWith(github.ref, 'refs/tags/v')
&& 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:
- self-hosted
- bundle
steps:
- name: gh release
run: gh release edit $GITHUB_REF_NAME --draft=false
run: gh release edit $GITHUB_REF_NAME --draft=true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -9,7 +9,7 @@ jobs:
deploy-docs:
name: Deploy Docs
if: github.repository_owner == 'zed-industries'
runs-on: buildjet-16vcpu-ubuntu-2204
runs-on: ubuntu-latest
steps:
- name: Checkout repo
@@ -17,11 +17,24 @@ jobs:
with:
clean: false
- name: Setup mdBook
uses: peaceiris/actions-mdbook@ee69d230fe19748b7abf22df32acaa93833fad08 # v2
with:
mdbook-version: "0.4.37"
- name: Set up default .cargo/config.toml
run: cp ./.cargo/collab-config.toml ./.cargo/config.toml
- name: Build docs
uses: ./.github/actions/build_docs
- name: Install system dependencies
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
uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3

View File

@@ -15,7 +15,7 @@ jobs:
if: github.repository_owner == 'zed-industries'
runs-on:
- self-hosted
- macOS
- test
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@@ -33,7 +33,7 @@ jobs:
name: Run tests
runs-on:
- self-hosted
- macOS
- test
needs: style
steps:
- name: Checkout repo

View File

@@ -30,7 +30,6 @@ jobs:
noop:
name: No-op
runs-on: ubuntu-latest
if: github.repository_owner == 'zed-industries'
steps:
- name: No-op
run: echo "Nothing to do"

View File

@@ -20,7 +20,7 @@ jobs:
if: github.repository_owner == 'zed-industries'
runs-on:
- self-hosted
- macOS
- test
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@@ -40,7 +40,7 @@ jobs:
if: github.repository_owner == 'zed-industries'
runs-on:
- self-hosted
- macOS
- test
needs: style
steps:
- name: Checkout repo
@@ -167,54 +167,9 @@ jobs:
- name: Upload Zed Nightly
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:
name: Build and cache Nix package
needs: tests
secrets: inherit
uses: ./.github/workflows/nix.yml
update-nightly-tag:

View File

@@ -1,86 +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:
if: github.repository_owner == 'zed-industries'
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_)'
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, Ive included the cleanup code here as a precaution.
# While its not strictly necessary at this moment, I believe its better to err on the side of caution.
- name: Clean CI config file
if: always()
run: rm -rf ./../.cargo

View File

@@ -47,7 +47,6 @@
"remove_trailing_whitespace_on_save": true,
"ensure_final_newline_on_save": true,
"file_scan_exclusions": [
"crates/assistant_tools/src/evals/fixtures",
"crates/eval/worktrees/",
"crates/eval/repos/",
"**/.git",

348
Cargo.lock generated
View File

@@ -59,7 +59,7 @@ dependencies = [
"assistant_slash_command",
"assistant_slash_commands",
"assistant_tool",
"assistant_tools",
"async-watch",
"audio",
"buffer_diff",
"chrono",
@@ -99,7 +99,6 @@ dependencies = [
"paths",
"picker",
"postage",
"pretty_assertions",
"project",
"prompt_store",
"proto",
@@ -115,7 +114,6 @@ dependencies = [
"serde_json_lenient",
"settings",
"smol",
"sqlez",
"streaming_diff",
"telemetry",
"telemetry_events",
@@ -131,12 +129,10 @@ dependencies = [
"urlencoding",
"util",
"uuid",
"watch",
"workspace",
"workspace-hack",
"zed_actions",
"zed_llm_client",
"zstd",
]
[[package]]
@@ -149,6 +145,7 @@ dependencies = [
"deepseek",
"fs",
"gpui",
"indexmap",
"language_model",
"lmstudio",
"log",
@@ -491,6 +488,7 @@ dependencies = [
"anyhow",
"futures 0.3.31",
"gpui",
"shlex",
"smol",
"tempfile",
"util",
@@ -527,7 +525,6 @@ dependencies = [
"fuzzy",
"gpui",
"indexed_docs",
"indoc",
"language",
"language_model",
"languages",
@@ -652,7 +649,6 @@ dependencies = [
"settings",
"text",
"util",
"watch",
"workspace",
"workspace-hack",
"zlog",
@@ -665,6 +661,7 @@ dependencies = [
"agent_settings",
"anyhow",
"assistant_tool",
"async-watch",
"buffer_diff",
"chrono",
"client",
@@ -704,7 +701,6 @@ dependencies = [
"serde_json",
"settings",
"smallvec",
"smol",
"streaming_diff",
"strsim",
"task",
@@ -716,7 +712,6 @@ dependencies = [
"ui",
"unindent",
"util",
"watch",
"web_search",
"which 6.0.3",
"workspace",
@@ -1075,6 +1070,15 @@ dependencies = [
"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]]
name = "async_zip"
version = "0.0.17"
@@ -2041,7 +2045,7 @@ dependencies = [
[[package]]
name = "blade-graphics"
version = "0.6.0"
source = "git+https://github.com/kvark/blade?rev=e0ec4e720957edd51b945b64dd85605ea54bcfe5#e0ec4e720957edd51b945b64dd85605ea54bcfe5"
source = "git+https://github.com/kvark/blade?rev=416375211bb0b5826b3584dccdb6a43369e499ad#416375211bb0b5826b3584dccdb6a43369e499ad"
dependencies = [
"ash",
"ash-window",
@@ -2074,7 +2078,7 @@ dependencies = [
[[package]]
name = "blade-macros"
version = "0.3.0"
source = "git+https://github.com/kvark/blade?rev=e0ec4e720957edd51b945b64dd85605ea54bcfe5#e0ec4e720957edd51b945b64dd85605ea54bcfe5"
source = "git+https://github.com/kvark/blade?rev=416375211bb0b5826b3584dccdb6a43369e499ad#416375211bb0b5826b3584dccdb6a43369e499ad"
dependencies = [
"proc-macro2",
"quote",
@@ -2084,7 +2088,7 @@ dependencies = [
[[package]]
name = "blade-util"
version = "0.2.0"
source = "git+https://github.com/kvark/blade?rev=e0ec4e720957edd51b945b64dd85605ea54bcfe5#e0ec4e720957edd51b945b64dd85605ea54bcfe5"
source = "git+https://github.com/kvark/blade?rev=416375211bb0b5826b3584dccdb6a43369e499ad#416375211bb0b5826b3584dccdb6a43369e499ad"
dependencies = [
"blade-graphics",
"bytemuck",
@@ -2822,11 +2826,9 @@ dependencies = [
"collections",
"credentials_provider",
"feature_flags",
"fs",
"futures 0.3.31",
"gpui",
"gpui_tokio",
"hickory-resolver",
"http_client",
"http_client_tls",
"httparse",
@@ -2835,7 +2837,6 @@ dependencies = [
"paths",
"postage",
"rand 0.8.5",
"regex",
"release_channel",
"rpc",
"rustls-pki-types",
@@ -2982,6 +2983,7 @@ dependencies = [
"anyhow",
"assistant_context_editor",
"assistant_slash_command",
"assistant_tool",
"async-stripe",
"async-trait",
"async-tungstenite",
@@ -3163,16 +3165,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "command-fds"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ec1052629a80c28594777d1252efc8a6b005d13f9edfd8c3fc0f44d5b32489a"
dependencies = [
"nix 0.30.1",
"thiserror 2.0.12",
]
[[package]]
name = "command_palette"
version = "0.1.0"
@@ -3542,20 +3534,6 @@ dependencies = [
"coreaudio-sys",
]
[[package]]
name = "coreaudio-rs"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aae284fbaf7d27aa0e292f7677dfbe26503b0d555026f702940805a630eac17"
dependencies = [
"bitflags 1.3.2",
"libc",
"objc2-audio-toolbox",
"objc2-core-audio",
"objc2-core-audio-types",
"objc2-core-foundation",
]
[[package]]
name = "coreaudio-sys"
version = "0.2.16"
@@ -3591,8 +3569,7 @@ dependencies = [
[[package]]
name = "cpal"
version = "0.15.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779"
source = "git+https://github.com/zed-industries/cpal?rev=fd8bc2fd39f1f5fdee5a0690656caff9a26d9d50#fd8bc2fd39f1f5fdee5a0690656caff9a26d9d50"
dependencies = [
"alsa",
"core-foundation-sys",
@@ -3602,7 +3579,7 @@ dependencies = [
"js-sys",
"libc",
"mach2",
"ndk 0.8.0",
"ndk",
"ndk-context",
"oboe",
"wasm-bindgen",
@@ -3611,32 +3588,6 @@ dependencies = [
"windows 0.54.0",
]
[[package]]
name = "cpal"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbd307f43cc2a697e2d1f8bc7a1d824b5269e052209e28883e5bc04d095aaa3f"
dependencies = [
"alsa",
"coreaudio-rs 0.13.0",
"dasp_sample",
"jni",
"js-sys",
"libc",
"mach2",
"ndk 0.9.0",
"ndk-context",
"num-derive",
"num-traits",
"objc2-audio-toolbox",
"objc2-core-audio",
"objc2-core-audio-types",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"windows 0.54.0",
]
[[package]]
name = "cpp_demangle"
version = "0.4.4"
@@ -4070,7 +4021,6 @@ dependencies = [
"gpui",
"http_client",
"language",
"libc",
"log",
"node_runtime",
"parking_lot",
@@ -4094,7 +4044,7 @@ dependencies = [
[[package]]
name = "dap-types"
version = "0.0.1"
source = "git+https://github.com/zed-industries/dap-types?rev=b40956a7f4d1939da67429d941389ee306a3a308#b40956a7f4d1939da67429d941389ee306a3a308"
source = "git+https://github.com/zed-industries/dap-types?rev=68516de327fa1be15214133a0a2e52a12982ce75#68516de327fa1be15214133a0a2e52a12982ce75"
dependencies = [
"schemars",
"serde",
@@ -4107,7 +4057,6 @@ version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"collections",
"dap",
"futures 0.3.31",
"gpui",
@@ -4242,7 +4191,6 @@ dependencies = [
"gpui",
"serde_json",
"task",
"util",
"workspace-hack",
]
@@ -4268,7 +4216,6 @@ dependencies = [
name = "debugger_ui"
version = "0.1.0"
dependencies = [
"alacritty_terminal",
"anyhow",
"client",
"collections",
@@ -4278,11 +4225,11 @@ dependencies = [
"db",
"debugger_tools",
"editor",
"feature_flags",
"file_icons",
"futures 0.3.31",
"fuzzy",
"gpui",
"itertools 0.14.0",
"language",
"log",
"menu",
@@ -4294,7 +4241,6 @@ dependencies = [
"rpc",
"serde",
"serde_json",
"serde_json_lenient",
"settings",
"shlex",
"sysinfo",
@@ -4302,8 +4248,6 @@ dependencies = [
"tasks_ui",
"terminal_view",
"theme",
"tree-sitter",
"tree-sitter-json",
"ui",
"unindent",
"util",
@@ -4595,8 +4539,6 @@ version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"command_palette",
"gpui",
"mdbook",
"regex",
"serde",
@@ -4604,7 +4546,6 @@ dependencies = [
"settings",
"util",
"workspace-hack",
"zed",
]
[[package]]
@@ -4743,11 +4684,13 @@ dependencies = [
"client",
"clock",
"collections",
"command_palette_hooks",
"convert_case 0.8.0",
"ctor",
"dap",
"db",
"emojis",
"feature_flags",
"file_icons",
"fs",
"futures 0.3.31",
@@ -4910,18 +4853,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf"
[[package]]
name = "enum-as-inner"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.101",
]
[[package]]
name = "enumflags2"
version = "0.7.11"
@@ -5075,6 +5006,7 @@ dependencies = [
"assistant_tool",
"assistant_tools",
"async-trait",
"async-watch",
"buffer_diff",
"chrono",
"clap",
@@ -5116,7 +5048,6 @@ dependencies = [
"unindent",
"util",
"uuid",
"watch",
"workspace-hack",
"zed_llm_client",
]
@@ -6179,7 +6110,6 @@ dependencies = [
"anyhow",
"askpass",
"buffer_diff",
"call",
"chrono",
"collections",
"command_palette_hooks",
@@ -6218,7 +6148,6 @@ dependencies = [
"ui",
"unindent",
"util",
"watch",
"windows 0.61.1",
"workspace",
"workspace-hack",
@@ -7141,7 +7070,6 @@ dependencies = [
"image",
"inventory",
"itertools 0.14.0",
"libc",
"log",
"lyon",
"media",
@@ -7501,51 +7429,6 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
[[package]]
name = "hickory-proto"
version = "0.24.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92652067c9ce6f66ce53cc38d1169daa36e6e7eb7dd3b63b5103bd9d97117248"
dependencies = [
"async-trait",
"cfg-if",
"data-encoding",
"enum-as-inner",
"futures-channel",
"futures-io",
"futures-util",
"idna",
"ipnet",
"once_cell",
"rand 0.8.5",
"thiserror 1.0.69",
"tinyvec",
"tokio",
"tracing",
"url",
]
[[package]]
name = "hickory-resolver"
version = "0.24.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbb117a1ca520e111743ab2f6688eddee69db4e0ea242545a604dce8a66fd22e"
dependencies = [
"cfg-if",
"futures-util",
"hickory-proto",
"ipconfig",
"lru-cache",
"once_cell",
"parking_lot",
"rand 0.8.5",
"resolv-conf",
"smallvec",
"thiserror 1.0.69",
"tokio",
"tracing",
]
[[package]]
name = "hidden-trait"
version = "0.1.2"
@@ -8415,18 +8298,6 @@ dependencies = [
"windows 0.58.0",
]
[[package]]
name = "ipconfig"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f"
dependencies = [
"socket2",
"widestring",
"windows-sys 0.48.0",
"winreg 0.50.0",
]
[[package]]
name = "ipnet"
version = "2.11.0"
@@ -8860,6 +8731,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"async-watch",
"clock",
"collections",
"ctor",
@@ -8886,7 +8758,6 @@ dependencies = [
"serde",
"serde_json",
"settings",
"shellexpand 2.1.2",
"smallvec",
"smol",
"streaming-iterator",
@@ -8909,7 +8780,6 @@ dependencies = [
"unicase",
"unindent",
"util",
"watch",
"workspace-hack",
"zlog",
]
@@ -8947,7 +8817,6 @@ dependencies = [
"http_client",
"icons",
"image",
"log",
"parking_lot",
"proto",
"schemars",
@@ -8983,7 +8852,6 @@ dependencies = [
"gpui",
"gpui_tokio",
"http_client",
"language",
"language_model",
"lmstudio",
"log",
@@ -8991,7 +8859,6 @@ dependencies = [
"mistral",
"ollama",
"open_ai",
"open_router",
"partial-json-fixer",
"project",
"proto",
@@ -9122,6 +8989,7 @@ dependencies = [
"tree-sitter-yaml",
"unindent",
"util",
"which 6.0.3",
"workspace",
"workspace-hack",
]
@@ -9313,12 +9181,6 @@ dependencies = [
"cc",
]
[[package]]
name = "linked-hash-map"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "linkify"
version = "0.10.0"
@@ -9445,7 +9307,7 @@ dependencies = [
"core-foundation 0.10.0",
"core-video",
"coreaudio-rs 0.12.1",
"cpal 0.16.0",
"cpal",
"futures 0.3.31",
"gpui",
"gpui_tokio",
@@ -9579,15 +9441,6 @@ dependencies = [
"hashbrown 0.15.3",
]
[[package]]
name = "lru-cache"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c"
dependencies = [
"linked-hash-map",
]
[[package]]
name = "lsp"
version = "0.1.0"
@@ -10224,21 +10077,7 @@ dependencies = [
"bitflags 2.9.0",
"jni-sys",
"log",
"ndk-sys 0.5.0+25.2.9519653",
"num_enum",
"thiserror 1.0.69",
]
[[package]]
name = "ndk"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
dependencies = [
"bitflags 2.9.0",
"jni-sys",
"log",
"ndk-sys 0.6.0+11769913",
"ndk-sys",
"num_enum",
"thiserror 1.0.69",
]
@@ -10258,15 +10097,6 @@ dependencies = [
"jni-sys",
]
[[package]]
name = "ndk-sys"
version = "0.6.0+11769913"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873"
dependencies = [
"jni-sys",
]
[[package]]
name = "new_debug_unreachable"
version = "1.0.6"
@@ -10298,18 +10128,6 @@ dependencies = [
"memoffset",
]
[[package]]
name = "nix"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
dependencies = [
"bitflags 2.9.0",
"cfg-if",
"cfg_aliases 0.2.1",
"libc",
]
[[package]]
name = "node_runtime"
version = "0.1.0"
@@ -10319,6 +10137,7 @@ dependencies = [
"async-std",
"async-tar",
"async-trait",
"async-watch",
"futures 0.3.31",
"http_client",
"log",
@@ -10328,7 +10147,6 @@ dependencies = [
"serde_json",
"smol",
"util",
"watch",
"which 6.0.3",
"workspace-hack",
]
@@ -10377,7 +10195,6 @@ dependencies = [
"util",
"workspace",
"workspace-hack",
"zed_actions",
]
[[package]]
@@ -10680,43 +10497,6 @@ dependencies = [
"objc2-quartz-core",
]
[[package]]
name = "objc2-audio-toolbox"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10cbe18d879e20a4aea544f8befe38bcf52255eb63d3f23eca2842f3319e4c07"
dependencies = [
"bitflags 2.9.0",
"libc",
"objc2",
"objc2-core-audio",
"objc2-core-audio-types",
"objc2-core-foundation",
"objc2-foundation",
]
[[package]]
name = "objc2-core-audio"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca44961e888e19313b808f23497073e3f6b3c22bb485056674c8b49f3b025c82"
dependencies = [
"dispatch2",
"objc2",
"objc2-core-audio-types",
"objc2-core-foundation",
]
[[package]]
name = "objc2-core-audio-types"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0f1cc99bb07ad2ddb6527ddf83db6a15271bb036b3eb94b801cd44fdc666ee1"
dependencies = [
"bitflags 2.9.0",
"objc2",
]
[[package]]
name = "objc2-core-foundation"
version = "0.3.1"
@@ -10822,7 +10602,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb"
dependencies = [
"jni",
"ndk 0.8.0",
"ndk",
"ndk-context",
"num-derive",
"num-traits",
@@ -10923,19 +10703,6 @@ dependencies = [
"workspace-hack",
]
[[package]]
name = "open_router"
version = "0.1.0"
dependencies = [
"anyhow",
"futures 0.3.31",
"http_client",
"schemars",
"serde",
"serde_json",
"workspace-hack",
]
[[package]]
name = "opener"
version = "0.7.2"
@@ -13215,6 +12982,7 @@ dependencies = [
"askpass",
"assistant_tool",
"assistant_tools",
"async-watch",
"backtrace",
"cargo_toml",
"chrono",
@@ -13224,7 +12992,6 @@ dependencies = [
"dap",
"dap_adapters",
"debug_adapter_extension",
"editor",
"env_logger 0.11.8",
"extension",
"extension_host",
@@ -13262,8 +13029,6 @@ dependencies = [
"toml 0.8.20",
"unindent",
"util",
"watch",
"workspace",
"worktree",
"zlog",
]
@@ -13421,7 +13186,6 @@ dependencies = [
"futures-core",
"futures-util",
"h2 0.4.9",
"hickory-resolver",
"http 1.3.1",
"http-body 1.0.1",
"http-body-util",
@@ -13478,12 +13242,6 @@ dependencies = [
"workspace-hack",
]
[[package]]
name = "resolv-conf"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95325155c684b1c89f7765e30bc1c42e4a6da51ca513615660cb8a62ef9a88e3"
[[package]]
name = "resvg"
version = "0.45.1"
@@ -13603,7 +13361,7 @@ version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7ceb6607dd738c99bc8cb28eff249b7cd5c8ec88b9db96c0608c1480d140fb1"
dependencies = [
"cpal 0.15.3",
"cpal",
"hound",
]
@@ -14615,12 +14373,12 @@ dependencies = [
"fs",
"gpui",
"log",
"paths",
"schemars",
"serde",
"settings",
"theme",
"ui",
"util",
"workspace",
"workspace-hack",
]
@@ -15481,7 +15239,6 @@ dependencies = [
"serde",
"serde_json",
"smol",
"util",
"workspace-hack",
]
@@ -15951,8 +15708,6 @@ dependencies = [
"task",
"theme",
"thiserror 2.0.12",
"url",
"urlencoding",
"util",
"windows 0.61.1",
"workspace-hack",
@@ -16734,9 +16489,9 @@ dependencies = [
[[package]]
name = "tree-sitter"
version = "0.25.6"
version = "0.25.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7cf18d43cbf0bfca51f657132cc616a5097edc4424d538bae6fa60142eaf9f0"
checksum = "ac5fff5c47490dfdf473b5228039bfacad9d765d9b6939d26bf7cc064c1c7822"
dependencies = [
"cc",
"regex",
@@ -16749,9 +16504,9 @@ dependencies = [
[[package]]
name = "tree-sitter-bash"
version = "0.25.0"
version = "0.23.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "871b0606e667e98a1237ebdc1b0d7056e0aebfdc3141d12b399865d4cb6ed8a6"
checksum = "329a4d48623ac337d42b1df84e81a1c9dbb2946907c102ca72db158c1964a52e"
dependencies = [
"cc",
"tree-sitter-language",
@@ -17349,25 +17104,21 @@ dependencies = [
"async-fs",
"async_zip",
"collections",
"command-fds",
"dirs 4.0.0",
"dunce",
"futures 0.3.31",
"futures-lite 1.13.0",
"git2",
"globset",
"indoc",
"itertools 0.14.0",
"libc",
"log",
"nix 0.29.0",
"rand 0.8.5",
"regex",
"rust-embed",
"serde",
"serde_json",
"serde_json_lenient",
"shlex",
"smol",
"take-until",
"tempfile",
@@ -18137,19 +17888,6 @@ dependencies = [
"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]]
name = "wayland-backend"
version = "0.3.8"
@@ -18409,12 +18147,6 @@ dependencies = [
"wasite",
]
[[package]]
name = "widestring"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d"
[[package]]
name = "wiggle"
version = "29.0.1"
@@ -19535,7 +19267,6 @@ dependencies = [
"num-rational",
"num-traits",
"objc2",
"objc2-core-foundation",
"objc2-foundation",
"objc2-metal",
"object",
@@ -19956,7 +19687,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.193.0"
version = "0.190.0"
dependencies = [
"activity_indicator",
"agent",
@@ -19968,6 +19699,7 @@ dependencies = [
"assistant_context_editor",
"assistant_tool",
"assistant_tools",
"async-watch",
"audio",
"auto_update",
"auto_update_ui",
@@ -19996,6 +19728,7 @@ dependencies = [
"extension",
"extension_host",
"extensions_ui",
"feature_flags",
"feedback",
"file_finder",
"fs",
@@ -20083,7 +19816,6 @@ dependencies = [
"uuid",
"vim",
"vim_mode_setting",
"watch",
"web_search",
"web_search_providers",
"welcome",

View File

@@ -100,7 +100,6 @@ members = [
"crates/notifications",
"crates/ollama",
"crates/open_ai",
"crates/open_router",
"crates/outline",
"crates/outline_panel",
"crates/panel",
@@ -165,7 +164,6 @@ members = [
"crates/util_macros",
"crates/vim",
"crates/vim_mode_setting",
"crates/watch",
"crates/web_search",
"crates/web_search_providers",
"crates/welcome",
@@ -309,7 +307,6 @@ node_runtime = { path = "crates/node_runtime" }
notifications = { path = "crates/notifications" }
ollama = { path = "crates/ollama" }
open_ai = { path = "crates/open_ai" }
open_router = { path = "crates/open_router", features = ["schemars"] }
outline = { path = "crates/outline" }
outline_panel = { path = "crates/outline_panel" }
panel = { path = "crates/panel" }
@@ -374,7 +371,6 @@ util = { path = "crates/util" }
util_macros = { path = "crates/util_macros" }
vim = { path = "crates/vim" }
vim_mode_setting = { path = "crates/vim_mode_setting" }
watch = { path = "crates/watch" }
web_search = { path = "crates/web_search" }
web_search_providers = { path = "crates/web_search_providers" }
welcome = { path = "crates/welcome" }
@@ -405,6 +401,7 @@ async-recursion = "1.0.0"
async-tar = "0.5.0"
async-trait = "0.1"
async-tungstenite = "0.29.1"
async-watch = "0.3.1"
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
aws-config = { version = "1.6.1", features = ["behavior-version-latest"] }
aws-credential-types = { version = "1.2.2", features = [
@@ -417,9 +414,9 @@ aws-smithy-runtime-api = { version = "1.7.4", features = ["http-1x", "client"] }
aws-smithy-types = { version = "1.3.0", features = ["http-body-1-x"] }
base64 = "0.22"
bitflags = "2.6.0"
blade-graphics = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
blade-util = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
blade-graphics = { git = "https://github.com/kvark/blade", rev = "416375211bb0b5826b3584dccdb6a43369e499ad" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "416375211bb0b5826b3584dccdb6a43369e499ad" }
blade-util = { git = "https://github.com/kvark/blade", rev = "416375211bb0b5826b3584dccdb6a43369e499ad" }
blake3 = "1.5.3"
bytes = "1.0"
cargo_metadata = "0.19"
@@ -433,10 +430,9 @@ convert_case = "0.8.0"
core-foundation = "0.10.0"
core-foundation-sys = "0.8.6"
core-video = { version = "0.4.3", features = ["metal"] }
cpal = "0.16"
criterion = { version = "0.5", features = ["html_reports"] }
ctor = "0.4.0"
dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "b40956a7f4d1939da67429d941389ee306a3a308" }
dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "68516de327fa1be15214133a0a2e52a12982ce75" }
dashmap = "6.0"
derive_more = "0.99.17"
dirs = "4.0"
@@ -524,7 +520,6 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "951c77
"rustls-tls-native-roots",
"socks",
"stream",
"hickory-dns",
] }
rsa = "0.9.6"
runtimelib = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734", default-features = false, features = [
@@ -577,8 +572,8 @@ tokio = { version = "1" }
tokio-tungstenite = { version = "0.26", features = ["__rustls-tls"] }
toml = "0.8"
tower-http = "0.4.4"
tree-sitter = { version = "0.25.6", features = ["wasm"] }
tree-sitter-bash = "0.25.0"
tree-sitter = { version = "0.25.5", features = ["wasm"] }
tree-sitter-bash = "0.23"
tree-sitter-c = "0.23"
tree-sitter-cpp = "0.23"
tree-sitter-css = "0.23"
@@ -684,7 +679,9 @@ features = [
"Win32_UI_WindowsAndMessaging",
]
# TODO livekit https://github.com/RustAudio/cpal/pull/891
[patch.crates-io]
cpal = { git = "https://github.com/zed-industries/cpal", rev = "fd8bc2fd39f1f5fdee5a0690656caff9a26d9d50" }
notify = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" }
notify-types = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" }
@@ -698,8 +695,6 @@ codegen-units = 16
[profile.dev.package]
taffy = { opt-level = 3 }
cranelift-codegen = { opt-level = 3 }
cranelift-codegen-meta = { opt-level = 3 }
cranelift-codegen-shared = { opt-level = 3 }
resvg = { opt-level = 3 }
rustybuzz = { opt-level = 3 }
ttf-parser = { opt-level = 3 }

View File

@@ -1,8 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor" stroke="currentColor">
<g clip-path="url(#clip0_205_3)">
<path d="M0.094 7.78c0.469 0 2.281 -0.405 3.219 -0.936s0.938 -0.531 2.875 -1.906c2.453 -1.741 4.188 -1.158 7.031 -1.158" stroke-width="2.8125" />
<path d="m15.969 3.797 -4.805 2.774V1.023z" />
<path d="M0 7.781c0.469 0 2.281 0.405 3.219 0.936s0.938 0.531 2.875 1.906C8.547 12.364 10.281 11.781 13.125 11.781" stroke-width="2.8125" />
<path d="m15.875 11.764 -4.805 -2.774v5.548z" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 575 B

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-blocks-icon lucide-blocks"><rect width="7" height="7" x="14" y="3" rx="1"/><path d="M10 21V8a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1H3"/></svg>
<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-blocks"><rect width="7" height="7" x="14" y="3" rx="1"/><path d="M10 21V8a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1H3"/></svg>

Before

Width:  |  Height:  |  Size: 386 B

After

Width:  |  Height:  |  Size: 368 B

View File

@@ -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-circle-help-icon lucide-circle-help"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg>

Before

Width:  |  Height:  |  Size: 348 B

View File

@@ -1,4 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 13H10.4C9.76346 13 9.15302 12.7893 8.70296 12.4142C8.25284 12.0391 8 11.5304 8 11V5C8 4.46957 8.25284 3.96086 8.70296 3.58579C9.15302 3.21071 9.76346 3 10.4 3H11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 13H5.6C6.23654 13 6.84698 12.7893 7.29704 12.4142C7.74716 12.0391 8 11.5304 8 11V5C8 4.46957 7.74716 3.96086 7.29704 3.58579C6.84698 3.21071 6.23654 3 5.6 3H5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17 20H16C14.9391 20 13.9217 19.6629 13.1716 19.0627C12.4214 18.4626 12 17.6487 12 16.8V7.2C12 6.35131 12.4214 5.53737 13.1716 4.93726C13.9217 4.33714 14.9391 4 16 4H17" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 20H8C9.06087 20 10.0783 19.5786 10.8284 18.8284C11.5786 18.0783 12 17.0609 12 16V15" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 4H8C9.06087 4 10.0783 4.42143 10.8284 5.17157C11.5786 5.92172 12 6.93913 12 8V9" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 617 B

After

Width:  |  Height:  |  Size: 715 B

View File

@@ -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

View File

@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 3L13 8L4 13V3Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 214 B

View File

@@ -1,8 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 12C2.35977 11.85 1 10.575 1 9" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M1.00875 15.2C1.00875 13.625 0.683456 12.275 4.00001 12.2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 9C7 10.575 5.62857 11.85 4 12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4 12.2C6.98117 12.2 7 13.625 7 15.2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="2.5" y="9" width="3" height="6" rx="1.5" fill="black"/>
<path d="M9 10L13 8L4 3V7.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 813 B

View File

@@ -1,8 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 5H4" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<path d="M8 5L14 5" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<path d="M12 11L14 11" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<path d="M2 11H8" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<circle cx="6" cy="5" r="2" fill="black" fill-opacity="0.1" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<circle cx="10" cy="11" r="2" fill="black" fill-opacity="0.1" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.36667 3.79167C5.53364 3.79167 4.85833 4.46697 4.85833 5.3C4.85833 6.13303 5.53364 6.80833 6.36667 6.80833C7.1997 6.80833 7.875 6.13303 7.875 5.3C7.875 4.46697 7.1997 3.79167 6.36667 3.79167ZM2.1 5.925H3.67944C3.9626 7.14732 5.05824 8.05833 6.36667 8.05833C7.67509 8.05833 8.77073 7.14732 9.05389 5.925H14.9C15.2452 5.925 15.525 5.64518 15.525 5.3C15.525 4.95482 15.2452 4.675 14.9 4.675H9.05389C8.77073 3.45268 7.67509 2.54167 6.36667 2.54167C5.05824 2.54167 3.9626 3.45268 3.67944 4.675H2.1C1.75482 4.675 1.475 4.95482 1.475 5.3C1.475 5.64518 1.75482 5.925 2.1 5.925ZM13.3206 12.325C13.0374 13.5473 11.9418 14.4583 10.6333 14.4583C9.32491 14.4583 8.22927 13.5473 7.94611 12.325H2.1C1.75482 12.325 1.475 12.0452 1.475 11.7C1.475 11.3548 1.75482 11.075 2.1 11.075H7.94611C8.22927 9.85268 9.32491 8.94167 10.6333 8.94167C11.9418 8.94167 13.0374 9.85268 13.3206 11.075H14.9C15.2452 11.075 15.525 11.3548 15.525 11.7C15.525 12.0452 15.2452 12.325 14.9 12.325H13.3206ZM9.125 11.7C9.125 10.867 9.8003 10.1917 10.6333 10.1917C11.4664 10.1917 12.1417 10.867 12.1417 11.7C12.1417 12.533 11.4664 13.2083 10.6333 13.2083C9.8003 13.2083 9.125 12.533 9.125 11.7Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 657 B

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,3 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.68323 1.53C7.71245 1.47097 7.75758 1.42129 7.81353 1.38655C7.86949 1.35181 7.93404 1.3334 7.9999 1.3334C8.06576 1.3334 8.13031 1.35181 8.18626 1.38655C8.24222 1.42129 8.28735 1.47097 8.31656 1.53L9.85656 4.64933C9.95802 4.85465 10.1078 5.03227 10.293 5.16697C10.4782 5.30167 10.6933 5.38941 10.9199 5.42267L14.3639 5.92667C14.4292 5.93612 14.4905 5.96365 14.5409 6.00613C14.5913 6.04862 14.6289 6.10437 14.6492 6.16707C14.6696 6.22978 14.6721 6.29694 14.6563 6.36096C14.6405 6.42498 14.6071 6.4833 14.5599 6.52933L12.0692 8.95467C11.905 9.11473 11.7821 9.31232 11.7111 9.53042C11.6402 9.74852 11.6233 9.98059 11.6619 10.2067L12.2499 13.6333C12.2614 13.6986 12.2544 13.7657 12.2296 13.8271C12.2048 13.8885 12.1632 13.9417 12.1096 13.9807C12.056 14.0196 11.9926 14.0427 11.9265 14.0473C11.8604 14.0519 11.7944 14.0378 11.7359 14.0067L8.65723 12.388C8.45438 12.2815 8.22868 12.2258 7.99956 12.2258C7.77044 12.2258 7.54475 12.2815 7.3419 12.388L4.2639 14.0067C4.20545 14.0376 4.1395 14.0515 4.07353 14.0468C4.00757 14.0421 3.94424 14.019 3.89076 13.9801C3.83728 13.9413 3.79579 13.8881 3.771 13.8268C3.74622 13.7655 3.73914 13.6985 3.75056 13.6333L4.3379 10.2073C4.3767 9.98116 4.35989 9.74893 4.28892 9.5307C4.21796 9.31246 4.09497 9.11477 3.93056 8.95467L1.4399 6.53C1.39229 6.48402 1.35856 6.4256 1.34254 6.36138C1.32652 6.29717 1.32886 6.22975 1.34928 6.16679C1.36971 6.10384 1.40741 6.04789 1.45808 6.00532C1.50876 5.96275 1.57037 5.93527 1.6359 5.926L5.07923 5.42267C5.30607 5.38967 5.52149 5.30204 5.70695 5.16733C5.89242 5.03261 6.04237 4.85485 6.1439 4.64933L7.68323 1.53Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6.97942 1.25171L6.9585 1.30199L5.58662 4.60039C5.54342 4.70426 5.44573 4.77523 5.3336 4.78422L1.7727 5.0697L1.71841 5.07405L1.38687 5.10063L1.08608 5.12475C0.820085 5.14607 0.712228 5.47802 0.914889 5.65162L1.14406 5.84793L1.39666 6.06431L1.43802 6.09974L4.15105 8.42374C4.23648 8.49692 4.2738 8.61176 4.24769 8.72118L3.41882 12.196L3.40618 12.249L3.32901 12.5725L3.25899 12.866C3.19708 13.1256 3.47945 13.3308 3.70718 13.1917L3.9647 13.0344L4.24854 12.861L4.29502 12.8326L7.34365 10.9705C7.43965 10.9119 7.5604 10.9119 7.6564 10.9705L10.705 12.8326L10.7515 12.861L11.0354 13.0344L11.2929 13.1917C11.5206 13.3308 11.803 13.1256 11.7411 12.866L11.671 12.5725L11.5939 12.249L11.5812 12.196L10.7524 8.72118C10.7263 8.61176 10.7636 8.49692 10.849 8.42374L13.562 6.09974L13.6034 6.06431L13.856 5.84793L14.0852 5.65162C14.2878 5.47802 14.18 5.14607 13.914 5.12475L13.6132 5.10063L13.2816 5.07405L13.2274 5.0697L9.66645 4.78422C9.55432 4.77523 9.45663 4.70426 9.41343 4.60039L8.04155 1.30199L8.02064 1.25171L7.89291 0.944609L7.77702 0.665992C7.67454 0.419604 7.32551 0.419604 7.22303 0.665992L7.10715 0.944609L6.97942 1.25171ZM7.50003 2.60397L6.50994 4.98442C6.32273 5.43453 5.89944 5.74207 5.41351 5.78103L2.84361 5.98705L4.8016 7.66428C5.17183 7.98142 5.33351 8.47903 5.2204 8.95321L4.62221 11.461L6.8224 10.1171C7.23842 9.86302 7.76164 9.86302 8.17766 10.1171L10.3778 11.461L9.77965 8.95321C9.66654 8.47903 9.82822 7.98142 10.1984 7.66428L12.1564 5.98705L9.58654 5.78103C9.10061 5.74207 8.67732 5.43453 8.49011 4.98442L7.50003 2.60397Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,3 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.68323 1.53C7.71245 1.47097 7.75758 1.42129 7.81353 1.38655C7.86949 1.35181 7.93404 1.3334 7.9999 1.3334C8.06576 1.3334 8.13031 1.35181 8.18626 1.38655C8.24222 1.42129 8.28735 1.47097 8.31656 1.53L9.85656 4.64933C9.95802 4.85465 10.1078 5.03227 10.293 5.16697C10.4782 5.30167 10.6933 5.38941 10.9199 5.42267L14.3639 5.92667C14.4292 5.93612 14.4905 5.96365 14.5409 6.00613C14.5913 6.04862 14.6289 6.10437 14.6492 6.16707C14.6696 6.22978 14.6721 6.29694 14.6563 6.36096C14.6405 6.42498 14.6071 6.4833 14.5599 6.52933L12.0692 8.95467C11.905 9.11473 11.7821 9.31232 11.7111 9.53042C11.6402 9.74852 11.6233 9.98059 11.6619 10.2067L12.2499 13.6333C12.2614 13.6986 12.2544 13.7657 12.2296 13.8271C12.2048 13.8885 12.1632 13.9417 12.1096 13.9807C12.056 14.0196 11.9926 14.0427 11.9265 14.0473C11.8604 14.0519 11.7944 14.0378 11.7359 14.0067L8.65723 12.388C8.45438 12.2815 8.22868 12.2258 7.99956 12.2258C7.77044 12.2258 7.54475 12.2815 7.3419 12.388L4.2639 14.0067C4.20545 14.0376 4.1395 14.0515 4.07353 14.0468C4.00757 14.0421 3.94424 14.019 3.89076 13.9801C3.83728 13.9413 3.79579 13.8881 3.771 13.8268C3.74622 13.7655 3.73914 13.6985 3.75056 13.6333L4.3379 10.2073C4.3767 9.98116 4.35989 9.74893 4.28892 9.5307C4.21796 9.31246 4.09497 9.11477 3.93056 8.95467L1.4399 6.53C1.39229 6.48402 1.35856 6.4256 1.34254 6.36138C1.32652 6.29717 1.32886 6.22975 1.34928 6.16679C1.36971 6.10384 1.40741 6.04789 1.45808 6.00532C1.50876 5.96275 1.57037 5.93527 1.6359 5.926L5.07923 5.42267C5.30607 5.38967 5.52149 5.30204 5.70695 5.16733C5.89242 5.03261 6.04237 4.85485 6.1439 4.64933L7.68323 1.53Z" fill="black" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7.22303 0.665992C7.32551 0.419604 7.67454 0.419604 7.77702 0.665992L9.41343 4.60039C9.45663 4.70426 9.55432 4.77523 9.66645 4.78422L13.914 5.12475C14.18 5.14607 14.2878 5.47802 14.0852 5.65162L10.849 8.42374C10.7636 8.49692 10.7263 8.61176 10.7524 8.72118L11.7411 12.866C11.803 13.1256 11.5206 13.3308 11.2929 13.1917L7.6564 10.9705C7.5604 10.9119 7.43965 10.9119 7.34365 10.9705L3.70718 13.1917C3.47945 13.3308 3.19708 13.1256 3.25899 12.866L4.24769 8.72118C4.2738 8.61176 4.23648 8.49692 4.15105 8.42374L0.914889 5.65162C0.712228 5.47802 0.820086 5.14607 1.08608 5.12475L5.3336 4.78422C5.44573 4.77523 5.54342 4.70426 5.58662 4.60039L7.22303 0.665992Z" fill="currentColor"></path></svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 794 B

View File

@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 2L6.72534 5.87534C6.6601 6.07367 6.5492 6.25392 6.40155 6.40155C6.25392 6.5492 6.07367 6.6601 5.87534 6.72534L2 8L5.87534 9.27466C6.07367 9.3399 6.25392 9.4508 6.40155 9.59845C6.5492 9.74608 6.6601 9.92633 6.72534 10.1247L8 14L9.27466 10.1247C9.3399 9.92633 9.4508 9.74608 9.59845 9.59845C9.74608 9.4508 9.92633 9.3399 10.1247 9.27466L14 8L10.1247 6.72534C9.92633 6.6601 9.74608 6.5492 9.59845 6.40155C9.4508 6.25392 9.3399 6.07367 9.27466 5.87534L8 2Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.33334 2V4.66666M2 3.33334H4.66666" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.6665 11.3333V14M11.3333 12.6666H13.9999" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 1.75L5.88467 5.14092C5.82759 5.31446 5.73055 5.47218 5.60136 5.60136C5.47218 5.73055 5.31446 5.82759 5.14092 5.88467L1.75 7L5.14092 8.11533C5.31446 8.17241 5.47218 8.26945 5.60136 8.39864C5.73055 8.52782 5.82759 8.68554 5.88467 8.85908L7 12.25L8.11533 8.85908C8.17241 8.68554 8.26945 8.52782 8.39864 8.39864C8.52782 8.26945 8.68554 8.17241 8.85908 8.11533L12.25 7L8.85908 5.88467C8.68554 5.82759 8.52782 5.73055 8.39864 5.60136C8.26945 5.47218 8.17241 5.31446 8.11533 5.14092L7 1.75Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2.91667 1.75V4.08333M1.75 2.91667H4.08333" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.0833 9.91667V12.25M9.91667 11.0833H12.25" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 998 B

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -31,11 +31,13 @@
"ctrl-,": "zed::OpenSettings",
"ctrl-q": "zed::Quit",
"f4": "debugger::Start",
"alt-f4": "debugger::RerunLastSession",
"f5": "debugger::Continue",
"shift-f5": "debugger::Stop",
"ctrl-shift-f5": "debugger::Restart",
"f6": "debugger::Pause",
"f7": "debugger::StepOver",
"ctrl-f11": "debugger::StepInto",
"cmd-f11": "debugger::StepInto",
"shift-f11": "debugger::StepOut",
"f11": "zed::ToggleFullScreen",
"ctrl-alt-z": "edit_prediction::RateCompletions",
@@ -59,6 +61,7 @@
"tab": "editor::Tab",
"shift-tab": "editor::Backtab",
"ctrl-k": "editor::CutToEndOfLine",
// "ctrl-t": "editor::Transpose",
"ctrl-k ctrl-q": "editor::Rewrap",
"ctrl-k q": "editor::Rewrap",
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
@@ -99,23 +102,27 @@
"shift-down": "editor::SelectDown",
"shift-left": "editor::SelectLeft",
"shift-right": "editor::SelectRight",
"ctrl-shift-left": "editor::SelectToPreviousWordStart",
"ctrl-shift-right": "editor::SelectToNextWordEnd",
"ctrl-shift-left": "editor::SelectToPreviousWordStart", // cursorWordLeftSelect
"ctrl-shift-right": "editor::SelectToNextWordEnd", // cursorWordRightSelect
"ctrl-shift-home": "editor::SelectToBeginning",
"ctrl-shift-end": "editor::SelectToEnd",
"ctrl-a": "editor::SelectAll",
"ctrl-l": "editor::SelectLine",
"ctrl-shift-i": "editor::Format",
"alt-shift-o": "editor::OrganizeImports",
// "cmd-shift-left": ["editor::SelectToBeginningOfLine", {"stop_at_soft_wraps": true, "stop_at_indent": true }],
// "ctrl-shift-a": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
"shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
// "cmd-shift-right": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
// "ctrl-shift-e": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
"shift-end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
// "alt-v": ["editor::MovePageUp", { "center_cursor": true }],
"ctrl-alt-space": "editor::ShowCharacterPalette",
"ctrl-;": "editor::ToggleLineNumbers",
"ctrl-'": "editor::ToggleSelectedDiffHunks",
"ctrl-\"": "editor::ExpandAllDiffHunks",
"ctrl-i": "editor::ShowSignatureHelp",
"alt-g b": "git::Blame",
"alt-g m": "git::OpenModifiedFiles",
"alt-g b": "editor::ToggleGitBlame",
"menu": "editor::OpenContextMenu",
"shift-f10": "editor::OpenContextMenu",
"ctrl-shift-e": "editor::ToggleEditPrediction",
@@ -135,6 +142,7 @@
"find": "buffer_search::Deploy",
"ctrl-f": "buffer_search::Deploy",
"ctrl-h": "buffer_search::DeployReplace",
// "cmd-e": ["buffer_search::Deploy", { "focus": false }],
"ctrl->": "assistant::QuoteSelection",
"ctrl-<": "assistant::InsertIntoEditor",
"ctrl-alt-e": "editor::SelectEnclosingSymbol",
@@ -147,7 +155,8 @@
"context": "Editor && mode == full && edit_prediction",
"bindings": {
"alt-]": "editor::NextEditPrediction",
"alt-[": "editor::PreviousEditPrediction"
"alt-[": "editor::PreviousEditPrediction",
"alt-right": "editor::AcceptPartialEditPrediction"
}
},
{
@@ -212,6 +221,7 @@
"ctrl-enter": "assistant::Assist",
"ctrl-s": "workspace::Save",
"save": "workspace::Save",
"ctrl->": "assistant::QuoteSelection",
"ctrl-<": "assistant::InsertIntoEditor",
"shift-enter": "assistant::Split",
"ctrl-r": "assistant::CycleMessageRole",
@@ -237,7 +247,6 @@
"ctrl-shift-j": "agent::ToggleNavigationMenu",
"ctrl-shift-i": "agent::ToggleOptionsMenu",
"shift-alt-escape": "agent::ExpandMessageEditor",
"ctrl->": "assistant::QuoteSelection",
"ctrl-alt-e": "agent::RemoveAllContext",
"ctrl-shift-e": "project_panel::ToggleFocus",
"ctrl-shift-enter": "agent::ContinueThread",
@@ -261,8 +270,8 @@
{
"context": "AgentPanel && prompt_editor",
"bindings": {
"ctrl-n": "agent::NewTextThread",
"ctrl-alt-t": "agent::NewThread"
"cmd-n": "agent::NewTextThread",
"cmd-alt-t": "agent::NewThread"
}
},
{
@@ -271,9 +280,7 @@
"enter": "agent::Chat",
"ctrl-enter": "agent::ChatWithFollow",
"ctrl-i": "agent::ToggleProfileSelector",
"shift-ctrl-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll"
"shift-ctrl-r": "agent::OpenAgentDiff"
}
},
{
@@ -505,14 +512,14 @@
{
"context": "Workspace",
"bindings": {
"alt-open": ["projects::OpenRecent", { "create_new_window": false }],
// 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": false }],
"alt-shift-open": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }],
"alt-open": "projects::OpenRecent",
"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
// "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-shift-enter": "toast::RunAction",
"ctrl-~": "workspace::NewTerminal",
@@ -576,24 +583,11 @@
"ctrl-alt-r": "task::Rerun",
"alt-t": "task::Rerun",
"alt-shift-t": "task::Spawn",
"alt-shift-r": ["task::Spawn", { "reveal_target": "center" }],
"alt-shift-r": ["task::Spawn", { "reveal_target": "center" }]
// also possible to spawn tasks by name:
// "foo-bar": ["task::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }]
// or by tag:
// "foo-bar": ["task::Spawn", { "task_tag": "MyTag" }],
"f5": "debugger::RerunLastSession"
}
},
{
"context": "Workspace && debugger_running",
"bindings": {
"f5": "zed::NoAction"
}
},
{
"context": "Workspace && debugger_stopped",
"bindings": {
"f5": "debugger::Continue"
}
},
{
@@ -655,16 +649,14 @@
"bindings": {
"alt-tab": "editor::AcceptEditPrediction",
"alt-l": "editor::AcceptEditPrediction",
"tab": "editor::AcceptEditPrediction",
"alt-right": "editor::AcceptPartialEditPrediction"
"tab": "editor::AcceptEditPrediction"
}
},
{
"context": "Editor && edit_prediction_conflict",
"bindings": {
"alt-tab": "editor::AcceptEditPrediction",
"alt-l": "editor::AcceptEditPrediction",
"alt-right": "editor::AcceptPartialEditPrediction"
"alt-l": "editor::AcceptEditPrediction"
}
},
{
@@ -881,8 +873,7 @@
"context": "DebugPanel",
"bindings": {
"ctrl-t": "debugger::ToggleThreadPicker",
"ctrl-i": "debugger::ToggleSessionPicker",
"shift-alt-escape": "debugger::ToggleExpandItem"
"ctrl-i": "debugger::ToggleSessionPicker"
}
},
{
@@ -939,13 +930,6 @@
"tab": "channel_modal::ToggleMode"
}
},
{
"context": "FileFinder",
"bindings": {
"ctrl-shift-a": "file_finder::ToggleSplitMenu",
"ctrl-shift-i": "file_finder::ToggleFilterMenu"
}
},
{
"context": "FileFinder || (FileFinder > Picker > Editor) || (FileFinder > Picker > menu)",
"bindings": {

View File

@@ -4,6 +4,8 @@
"use_key_equivalents": true,
"bindings": {
"f4": "debugger::Start",
"alt-f4": "debugger::RerunLastSession",
"f5": "debugger::Continue",
"shift-f5": "debugger::Stop",
"shift-cmd-f5": "debugger::Restart",
"f6": "debugger::Pause",
@@ -138,8 +140,7 @@
"cmd-;": "editor::ToggleLineNumbers",
"cmd-'": "editor::ToggleSelectedDiffHunks",
"cmd-\"": "editor::ExpandAllDiffHunks",
"cmd-alt-g b": "git::Blame",
"cmd-alt-g m": "git::OpenModifiedFiles",
"cmd-alt-g b": "editor::ToggleGitBlame",
"cmd-i": "editor::ShowSignatureHelp",
"f9": "editor::ToggleBreakpoint",
"shift-f9": "editor::EditLogBreakpoint",
@@ -182,7 +183,8 @@
"use_key_equivalents": true,
"bindings": {
"alt-tab": "editor::NextEditPrediction",
"alt-shift-tab": "editor::PreviousEditPrediction"
"alt-shift-tab": "editor::PreviousEditPrediction",
"ctrl-cmd-right": "editor::AcceptPartialEditPrediction"
}
},
{
@@ -253,6 +255,7 @@
"bindings": {
"cmd-enter": "assistant::Assist",
"cmd-s": "workspace::Save",
"cmd->": "assistant::QuoteSelection",
"cmd-<": "assistant::InsertIntoEditor",
"shift-enter": "assistant::Split",
"ctrl-r": "assistant::CycleMessageRole",
@@ -279,7 +282,6 @@
"cmd-shift-j": "agent::ToggleNavigationMenu",
"cmd-shift-i": "agent::ToggleOptionsMenu",
"shift-alt-escape": "agent::ExpandMessageEditor",
"cmd->": "assistant::QuoteSelection",
"cmd-alt-e": "agent::RemoveAllContext",
"cmd-shift-e": "project_panel::ToggleFocus",
"cmd-shift-enter": "agent::ContinueThread",
@@ -315,9 +317,7 @@
"enter": "agent::Chat",
"cmd-enter": "agent::ChatWithFollow",
"cmd-i": "agent::ToggleProfileSelector",
"shift-ctrl-r": "agent::OpenAgentDiff",
"cmd-shift-y": "agent::KeepAll",
"cmd-shift-n": "agent::RejectAll"
"shift-ctrl-r": "agent::OpenAgentDiff"
}
},
{
@@ -584,9 +584,9 @@
"bindings": {
// 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": false }],
"ctrl-cmd-o": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }],
"ctrl-cmd-shift-o": ["projects::OpenRemote", { "from_existing_connection": true, "create_new_window": false }],
"alt-cmd-o": "projects::OpenRecent",
"ctrl-cmd-o": "projects::OpenRemote",
"ctrl-cmd-shift-o": ["projects::OpenRemote", { "from_existing_connection": true }],
"alt-cmd-b": "branches::OpenRecent",
"ctrl-~": "workspace::NewTerminal",
"cmd-s": "workspace::Save",
@@ -635,8 +635,7 @@
"cmd-k shift-right": "workspace::SwapPaneRight",
"cmd-k shift-up": "workspace::SwapPaneUp",
"cmd-k shift-down": "workspace::SwapPaneDown",
"cmd-shift-x": "zed::Extensions",
"f5": "debugger::RerunLastSession"
"cmd-shift-x": "zed::Extensions"
}
},
{
@@ -653,20 +652,6 @@
// "foo-bar": ["task::Spawn", { "task_tag": "MyTag" }],
}
},
{
"context": "Workspace && debugger_running",
"use_key_equivalents": true,
"bindings": {
"f5": "zed::NoAction"
}
},
{
"context": "Workspace && debugger_stopped",
"use_key_equivalents": true,
"bindings": {
"f5": "debugger::Continue"
}
},
// Bindings from Sublime Text
{
"context": "Editor",
@@ -719,16 +704,14 @@
"context": "Editor && edit_prediction",
"bindings": {
"alt-tab": "editor::AcceptEditPrediction",
"tab": "editor::AcceptEditPrediction",
"ctrl-cmd-right": "editor::AcceptPartialEditPrediction"
"tab": "editor::AcceptEditPrediction"
}
},
{
"context": "Editor && edit_prediction_conflict",
"use_key_equivalents": true,
"bindings": {
"alt-tab": "editor::AcceptEditPrediction",
"ctrl-cmd-right": "editor::AcceptPartialEditPrediction"
"alt-tab": "editor::AcceptEditPrediction"
}
},
{
@@ -953,8 +936,7 @@
"context": "DebugPanel",
"bindings": {
"cmd-t": "debugger::ToggleThreadPicker",
"cmd-i": "debugger::ToggleSessionPicker",
"shift-alt-escape": "debugger::ToggleExpandItem"
"cmd-i": "debugger::ToggleSessionPicker"
}
},
{
@@ -1007,14 +989,6 @@
"tab": "channel_modal::ToggleMode"
}
},
{
"context": "FileFinder",
"use_key_equivalents": true,
"bindings": {
"cmd-shift-a": "file_finder::ToggleSplitMenu",
"cmd-shift-i": "file_finder::ToggleFilterMenu"
}
},
{
"context": "FileFinder || (FileFinder > Picker > Editor) || (FileFinder > Picker > menu)",
"use_key_equivalents": true,

View File

@@ -13,9 +13,9 @@
}
},
{
"context": "Editor && vim_mode == insert && !menu",
"context": "Editor",
"bindings": {
// "j k": "vim::SwitchToNormalMode"
// "j k": ["workspace::SendKeystrokes", "escape"]
}
}
]

View File

@@ -38,7 +38,7 @@
"ctrl-shift-d": "editor::DuplicateSelection",
"alt-f3": "editor::SelectAllMatches", // find_all_under
// "ctrl-f3": "", // find_under (cancels any selections)
// "ctrl-alt-shift-g": "" // find_under_prev (cancels any selections)
// "cmd-alt-shift-g": "" // find_under_prev (cancels any selections)
"f9": "editor::SortLinesCaseSensitive",
"ctrl-f9": "editor::SortLinesCaseInsensitive",
"f12": "editor::GoToDefinition",
@@ -51,11 +51,7 @@
"ctrl-k ctrl-l": "editor::ConvertToLowerCase",
"shift-alt-m": "markdown::OpenPreviewToTheSide",
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-delete": "editor::DeleteToNextWordEnd",
"alt-right": "editor::MoveToNextSubwordEnd",
"alt-left": "editor::MoveToPreviousSubwordStart",
"alt-shift-right": "editor::SelectToNextSubwordEnd",
"alt-shift-left": "editor::SelectToPreviousSubwordStart"
"ctrl-delete": "editor::DeleteToNextWordEnd"
}
},
{

View File

@@ -28,8 +28,7 @@
"context": "InlineAssistEditor",
"use_key_equivalents": true,
"bindings": {
"cmd-shift-backspace": "editor::Cancel",
"cmd-enter": "menu::Confirm"
"cmd-shift-backspace": "editor::Cancel"
// "alt-enter": // Quick Question
// "cmd-shift-enter": // Full File Context
// "cmd-shift-k": // Toggle input focus (editor <> inline assist)

View File

@@ -53,11 +53,7 @@
"cmd-shift-j": "editor::JoinLines",
"shift-alt-m": "markdown::OpenPreviewToTheSide",
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-delete": "editor::DeleteToNextWordEnd",
"ctrl-right": "editor::MoveToNextSubwordEnd",
"ctrl-left": "editor::MoveToPreviousSubwordStart",
"ctrl-shift-right": "editor::SelectToNextSubwordEnd",
"ctrl-shift-left": "editor::SelectToPreviousSubwordStart"
"ctrl-delete": "editor::DeleteToNextWordEnd"
}
},
{

View File

@@ -56,9 +56,6 @@
"[ shift-b": ["pane::ActivateItem", 0],
"] space": "vim::InsertEmptyLineBelow",
"[ space": "vim::InsertEmptyLineAbove",
"[ e": "editor::MoveLineUp",
"] e": "editor::MoveLineDown",
// Word motions
"w": "vim::NextWordStart",
"e": "vim::NextWordEnd",
@@ -187,8 +184,6 @@
"z f": "editor::FoldSelectedRanges",
"z shift-m": "editor::FoldAll",
"z shift-r": "editor::UnfoldAll",
"z l": "vim::ColumnRight",
"z h": "vim::ColumnLeft",
"shift-z shift-q": ["pane::CloseActiveItem", { "save_intent": "skip" }],
"shift-z shift-z": ["pane::CloseActiveItem", { "save_intent": "save_all" }],
// Count support
@@ -203,8 +198,6 @@
"9": ["vim::Number", 9],
"ctrl-w 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 g shift-d": "editor::GoToTypeDefinitionSplit",
"ctrl-w space": "editor::OpenExcerptsSplit",
@@ -400,8 +393,6 @@
"ctrl-pagedown": "pane::ActivateNextItem",
"ctrl-pageup": "pane::ActivatePreviousItem",
"insert": "vim::InsertBefore",
".": "vim::Repeat",
"alt-.": "vim::RepeatFind",
// tree-sitter related commands
"[ x": "editor::SelectLargerSyntaxNode",
"] x": "editor::SelectSmallerSyntaxNode",
@@ -428,7 +419,6 @@
"x": "editor::SelectLine",
"shift-x": "editor::SelectLine",
"%": "editor::SelectAll",
// Window mode
"space w h": "workspace::ActivatePaneLeft",
"space w l": "workspace::ActivatePaneRight",
@@ -458,8 +448,7 @@
"ctrl-c": "editor::ToggleComments",
"d": "vim::HelixDelete",
"c": "vim::Substitute",
"shift-c": "editor::AddSelectionBelow",
"alt-shift-c": "editor::AddSelectionAbove"
"shift-c": "editor::AddSelectionBelow"
}
},
{
@@ -720,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": {
// window related commands (ctrl-w X)
"ctrl-w": null,
@@ -849,19 +838,6 @@
"tab": "editor::AcceptEditPrediction"
}
},
{
"context": "MessageEditor > Editor && VimControl",
"bindings": {
"enter": "agent::Chat",
// TODO: Implement search
"/": null,
"?": null,
"#": null,
"*": null,
"n": null,
"shift-n": null
}
},
{
"context": "os != macos && Editor && edit_prediction_conflict",
"bindings": {

View File

@@ -17,27 +17,28 @@ You are a highly skilled software engineer with extensive knowledge in many prog
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.
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
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 }}
{{#if has_tools}}
If appropriate, use tool calls to explore the current project, which contains the following root directories:
{{#each worktrees}}
- `{{abs_path}}`
- `{{root_name}}`
{{/each}}
- Bias towards not asking the user for help if you can find the answer yourself.
- When providing paths to tools, the path should always start with the name of a project root directory listed above.
- When providing paths to tools, the path should always begin with a path that starts with a project root directory listed above.
- Before you read or edit a file, you must first find the full path. DO NOT ever guess a file path!
{{# if (has_tool 'grep') }}
- When looking for symbols in the project, prefer the `grep` tool.
- 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.
{{/if}}
{{/if}}
{{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).

View File

@@ -73,6 +73,9 @@
"unnecessary_code_fade": 0.3,
// Active pane styling settings.
"active_pane_modifiers": {
// The factor to grow the active pane by. Defaults to 1.0
// which gives the same size as all other panes.
"magnification": 1.0,
// Inset border size of the active pane, in pixels.
"border_size": 0.0,
// Opacity of the inactive panes. 0 means transparent, 1 means opaque.
@@ -101,12 +104,9 @@
// The second option is decimal.
"unit": "binary"
},
// Determines the modifier to be used to add multiple cursors with the mouse. The open hover link mouse gestures will adapt such that it do not conflict with the multicursor modifier.
//
// 1. Maps to `Alt` on Linux and Windows and to `Option` on MacOS:
// "alt"
// 2. Maps `Control` on Linux and Windows and to `Command` on MacOS:
// "cmd_or_ctrl" (alias: "cmd", "ctrl")
// The key to use for adding multiple cursors
// Currently "alt" or "cmd_or_ctrl" (also aliased as
// "cmd" and "ctrl") are supported.
"multi_cursor_modifier": "alt",
// Whether to enable vim modes and key bindings.
"vim_mode": false,
@@ -128,8 +128,6 @@
//
// Default: true
"restore_on_file_reopen": true,
// Whether to automatically close files that have been deleted on disk.
"close_on_file_delete": false,
// Size of the drop target in the editor.
"drop_target_size": 0.2,
// Whether the window should be closed when using 'close active item' on a window with no tabs.
@@ -217,8 +215,6 @@
"show_signature_help_after_edits": false,
// Whether to show code action button at start of buffer line.
"inline_code_actions": true,
// Whether to allow drag and drop text selection in buffer.
"drag_and_drop_selection": true,
// What to do when go to definition yields no results.
//
// 1. Do nothing: `none`
@@ -307,8 +303,6 @@
// "all"
// 4. Draw whitespaces at boundaries only:
// "boundary"
// 5. Draw whitespaces only after non-whitespace characters:
// "trailing"
// For a whitespace to be on a boundary, any of the following conditions need to be met:
// - It is a tab
// - It is adjacent to an edge (start or end)
@@ -400,13 +394,6 @@
// 3. Never show the minimap:
// "never" (default)
"show": "never",
// Where to show the minimap in the editor.
// This setting can take two values:
// 1. Show the minimap on the focused editor only:
// "active_editor" (default)
// 2. Show the minimap on all open editors:
// "all_editors"
"display_in": "active_editor",
// When to show the minimap thumb.
// This setting can take two values:
// 1. Show the minimap thumb if the mouse is over the minimap:
@@ -454,9 +441,7 @@
// Whether to show breakpoints in the gutter.
"breakpoints": true,
// Whether to show fold buttons in the gutter.
"folds": true,
// Minimum number of characters to reserve space for in the gutter.
"min_line_number_digits": 4
"folds": true
},
"indent_guides": {
// Whether to show indent guides in the editor.
@@ -549,9 +534,6 @@
"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": {
// Whether to show the project panel button in the status bar
"button": true,
@@ -615,9 +597,7 @@
// 2. Never show indent guides:
// "never"
"show": "always"
},
// Whether to hide the root entry when only one folder is open in the window.
"hide_root": false
}
},
"outline_panel": {
// Whether to show the outline panel button in the status bar
@@ -751,6 +731,13 @@
// The model to use.
"model": "claude-sonnet-4"
},
// The model to use when applying edits from the agent.
"editor_model": {
// The provider to use.
"provider": "zed.dev",
// The model to use.
"model": "claude-sonnet-4"
},
// Additional parameters for language model requests. When making a request to a model, parameters will be taken
// from the last entry in this list that matches the model's provider and name. In each entry, both provider
// and model are optional, so that you can specify parameters for either one.
@@ -789,6 +776,7 @@
"tools": {
"copy_path": true,
"create_directory": true,
"create_file": true,
"delete_path": true,
"diagnostics": true,
"edit_file": true,
@@ -1045,33 +1033,12 @@
// Automatically update Zed. This setting may be ignored on Linux if
// installed through a package manager.
"auto_update": true,
// How to render LSP `textDocument/documentColor` colors in the editor.
//
// Possible values:
//
// 1. Do not query and render document colors.
// "lsp_document_colors": "none",
// 2. Render document colors as inlay hints near the color text (default).
// "lsp_document_colors": "inlay",
// 3. Draw a border around the color text.
// "lsp_document_colors": "border",
// 4. Draw a background behind the color text..
// "lsp_document_colors": "background",
"lsp_document_colors": "inlay",
// Diagnostics configuration.
"diagnostics": {
// Whether to show the project diagnostics button in the status bar.
"button": true,
// Whether to show warnings or not by default.
"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
"inline": {
// Whether to show diagnostics inline or not
@@ -1495,15 +1462,12 @@
"language_servers": ["erlang-ls", "!elp", "..."]
},
"Git Commit": {
"allow_rewrap": "anywhere",
"soft_wrap": "editor_width",
"preferred_line_length": 72
"allow_rewrap": "anywhere"
},
"Go": {
"code_actions_on_format": {
"source.organizeImports": true
},
"debuggers": ["Delve"]
}
},
"GraphQL": {
"prettier": {
@@ -1541,11 +1505,11 @@
}
},
"LaTeX": {
"format_on_save": "on",
"formatter": "language_server",
"language_servers": ["texlab", "..."],
"prettier": {
"allowed": true,
"plugins": ["prettier-plugin-latex"]
"allowed": false
}
},
"Markdown": {
@@ -1568,20 +1532,20 @@
"Plain Text": {
"allow_rewrap": "anywhere"
},
"Python": {
"debuggers": ["Debugpy"]
},
"Ruby": {
"language_servers": ["solargraph", "!ruby-lsp", "!rubocop", "!sorbet", "!steep", "..."]
},
"Rust": {
"debuggers": ["CodeLLDB"]
"language_servers": ["solargraph", "!ruby-lsp", "!rubocop", "..."]
},
"SCSS": {
"prettier": {
"allowed": true
}
},
"SQL": {
"prettier": {
"allowed": true,
"plugins": ["prettier-plugin-sql"]
}
},
"Starlark": {
"language_servers": ["starpls", "!buck2-lsp", "..."]
},
@@ -1646,9 +1610,6 @@
"version": "1",
"api_url": "https://api.openai.com/v1"
},
"open_router": {
"api_url": "https://openrouter.ai/api/v1"
},
"lmstudio": {
"api_url": "http://localhost:1234/api/v0"
},

View File

@@ -1,7 +1,3 @@
// Some example tasks for common languages.
//
// For more documentation on how to configure debug tasks,
// see: https://zed.dev/docs/debugger
[
{
"label": "Debug active PHP file",

View File

@@ -1,5 +0,0 @@
// Project-local debug tasks
//
// For more documentation on how to configure debug tasks,
// see: https://zed.dev/docs/debugger
[]

View File

@@ -1,4 +1,4 @@
// Project tasks configuration. See https://zed.dev/docs/tasks for documentation.
// Static tasks configuration.
//
// Example:
[

View File

@@ -261,11 +261,6 @@
"font_style": null,
"font_weight": null
},
"namespace": {
"color": "#bfbdb6ff",
"font_style": null,
"font_weight": null
},
"number": {
"color": "#d2a6ffff",
"font_style": null,
@@ -321,16 +316,6 @@
"font_style": null,
"font_weight": null
},
"selector": {
"color": "#d2a6ffff",
"font_style": null,
"font_weight": null
},
"selector.pseudo": {
"color": "#5ac1feff",
"font_style": null,
"font_weight": null
},
"string": {
"color": "#a9d94bff",
"font_style": null,
@@ -457,9 +442,9 @@
"terminal.foreground": "#5c6166ff",
"terminal.bright_foreground": "#5c6166ff",
"terminal.dim_foreground": "#fcfcfcff",
"terminal.ansi.black": "#5c6166ff",
"terminal.ansi.bright_black": "#3b9ee5ff",
"terminal.ansi.dim_black": "#9c9fa2ff",
"terminal.ansi.black": "#fcfcfcff",
"terminal.ansi.bright_black": "#bcbec0ff",
"terminal.ansi.dim_black": "#5c6166ff",
"terminal.ansi.red": "#ef7271ff",
"terminal.ansi.bright_red": "#febab6ff",
"terminal.ansi.dim_red": "#833538ff",
@@ -478,9 +463,9 @@
"terminal.ansi.cyan": "#4dbf99ff",
"terminal.ansi.bright_cyan": "#ace0cbff",
"terminal.ansi.dim_cyan": "#2a5f4aff",
"terminal.ansi.white": "#fcfcfcff",
"terminal.ansi.bright_white": "#fcfcfcff",
"terminal.ansi.dim_white": "#bcbec0ff",
"terminal.ansi.white": "#5c6166ff",
"terminal.ansi.bright_white": "#5c6166ff",
"terminal.ansi.dim_white": "#9c9fa2ff",
"link_text.hover": "#3b9ee5ff",
"conflict": "#f1ad49ff",
"conflict.background": "#ffeedaff",
@@ -647,11 +632,6 @@
"font_style": null,
"font_weight": null
},
"namespace": {
"color": "#5c6166ff",
"font_style": null,
"font_weight": null
},
"number": {
"color": "#a37accff",
"font_style": null,
@@ -707,16 +687,6 @@
"font_style": null,
"font_weight": null
},
"selector": {
"color": "#a37accff",
"font_style": null,
"font_weight": null
},
"selector.pseudo": {
"color": "#3b9ee5ff",
"font_style": null,
"font_weight": null
},
"string": {
"color": "#86b300ff",
"font_style": null,
@@ -1033,11 +1003,6 @@
"font_style": null,
"font_weight": null
},
"namespace": {
"color": "#cccac2ff",
"font_style": null,
"font_weight": null
},
"number": {
"color": "#dfbfffff",
"font_style": null,
@@ -1093,16 +1058,6 @@
"font_style": null,
"font_weight": null
},
"selector": {
"color": "#dfbfffff",
"font_style": null,
"font_weight": null
},
"selector.pseudo": {
"color": "#72cffeff",
"font_style": null,
"font_weight": null
},
"string": {
"color": "#d4fe7fff",
"font_style": null,

View File

@@ -270,11 +270,6 @@
"font_style": null,
"font_weight": null
},
"namespace": {
"color": "#83a598ff",
"font_style": null,
"font_weight": null
},
"number": {
"color": "#d3869bff",
"font_style": null,
@@ -330,16 +325,6 @@
"font_style": null,
"font_weight": null
},
"selector": {
"color": "#fabd2eff",
"font_style": null,
"font_weight": null
},
"selector.pseudo": {
"color": "#83a598ff",
"font_style": null,
"font_weight": null
},
"string": {
"color": "#b8bb25ff",
"font_style": null,
@@ -670,11 +655,6 @@
"font_style": null,
"font_weight": null
},
"namespace": {
"color": "#83a598ff",
"font_style": null,
"font_weight": null
},
"number": {
"color": "#d3869bff",
"font_style": null,
@@ -730,16 +710,6 @@
"font_style": null,
"font_weight": null
},
"selector": {
"color": "#fabd2eff",
"font_style": null,
"font_weight": null
},
"selector.pseudo": {
"color": "#83a598ff",
"font_style": null,
"font_weight": null
},
"string": {
"color": "#b8bb25ff",
"font_style": null,
@@ -1070,11 +1040,6 @@
"font_style": null,
"font_weight": null
},
"namespace": {
"color": "#83a598ff",
"font_style": null,
"font_weight": null
},
"number": {
"color": "#d3869bff",
"font_style": null,
@@ -1130,16 +1095,6 @@
"font_style": null,
"font_weight": null
},
"selector": {
"color": "#fabd2eff",
"font_style": null,
"font_weight": null
},
"selector.pseudo": {
"color": "#83a598ff",
"font_style": null,
"font_weight": null
},
"string": {
"color": "#b8bb25ff",
"font_style": null,
@@ -1272,9 +1227,9 @@
"terminal.foreground": "#282828ff",
"terminal.bright_foreground": "#282828ff",
"terminal.dim_foreground": "#fbf1c7ff",
"terminal.ansi.black": "#282828ff",
"terminal.ansi.bright_black": "#0b6678ff",
"terminal.ansi.dim_black": "#5f5650ff",
"terminal.ansi.black": "#fbf1c7ff",
"terminal.ansi.bright_black": "#b0a189ff",
"terminal.ansi.dim_black": "#282828ff",
"terminal.ansi.red": "#9d0308ff",
"terminal.ansi.bright_red": "#db8b7aff",
"terminal.ansi.dim_red": "#4e1207ff",
@@ -1293,9 +1248,9 @@
"terminal.ansi.cyan": "#437b59ff",
"terminal.ansi.bright_cyan": "#9fbca8ff",
"terminal.ansi.dim_cyan": "#253e2eff",
"terminal.ansi.white": "#fbf1c7ff",
"terminal.ansi.bright_white": "#fbf1c7ff",
"terminal.ansi.dim_white": "#b0a189ff",
"terminal.ansi.white": "#282828ff",
"terminal.ansi.bright_white": "#282828ff",
"terminal.ansi.dim_white": "#73675eff",
"link_text.hover": "#0b6678ff",
"version_control.added": "#797410ff",
"version_control.modified": "#b57615ff",
@@ -1470,11 +1425,6 @@
"font_style": null,
"font_weight": null
},
"namespace": {
"color": "#066578ff",
"font_style": null,
"font_weight": null
},
"number": {
"color": "#8f3e71ff",
"font_style": null,
@@ -1530,16 +1480,6 @@
"font_style": null,
"font_weight": null
},
"selector": {
"color": "#b57613ff",
"font_style": null,
"font_weight": null
},
"selector.pseudo": {
"color": "#0b6678ff",
"font_style": null,
"font_weight": null
},
"string": {
"color": "#79740eff",
"font_style": null,
@@ -1672,9 +1612,9 @@
"terminal.foreground": "#282828ff",
"terminal.bright_foreground": "#282828ff",
"terminal.dim_foreground": "#f9f5d7ff",
"terminal.ansi.black": "#282828ff",
"terminal.ansi.bright_black": "#73675eff",
"terminal.ansi.dim_black": "#f9f5d7ff",
"terminal.ansi.black": "#f9f5d7ff",
"terminal.ansi.bright_black": "#b0a189ff",
"terminal.ansi.dim_black": "#282828ff",
"terminal.ansi.red": "#9d0308ff",
"terminal.ansi.bright_red": "#db8b7aff",
"terminal.ansi.dim_red": "#4e1207ff",
@@ -1693,9 +1633,9 @@
"terminal.ansi.cyan": "#437b59ff",
"terminal.ansi.bright_cyan": "#9fbca8ff",
"terminal.ansi.dim_cyan": "#253e2eff",
"terminal.ansi.white": "#f9f5d7ff",
"terminal.ansi.bright_white": "#f9f5d7ff",
"terminal.ansi.dim_white": "#b0a189ff",
"terminal.ansi.white": "#282828ff",
"terminal.ansi.bright_white": "#282828ff",
"terminal.ansi.dim_white": "#73675eff",
"link_text.hover": "#0b6678ff",
"version_control.added": "#797410ff",
"version_control.modified": "#b57615ff",
@@ -1870,11 +1810,6 @@
"font_style": null,
"font_weight": null
},
"namespace": {
"color": "#066578ff",
"font_style": null,
"font_weight": null
},
"number": {
"color": "#8f3e71ff",
"font_style": null,
@@ -1930,16 +1865,6 @@
"font_style": null,
"font_weight": null
},
"selector": {
"color": "#b57613ff",
"font_style": null,
"font_weight": null
},
"selector.pseudo": {
"color": "#0b6678ff",
"font_style": null,
"font_weight": null
},
"string": {
"color": "#79740eff",
"font_style": null,
@@ -2072,9 +1997,9 @@
"terminal.foreground": "#282828ff",
"terminal.bright_foreground": "#282828ff",
"terminal.dim_foreground": "#f2e5bcff",
"terminal.ansi.black": "#282828ff",
"terminal.ansi.bright_black": "#73675eff",
"terminal.ansi.dim_black": "#f2e5bcff",
"terminal.ansi.black": "#f2e5bcff",
"terminal.ansi.bright_black": "#b0a189ff",
"terminal.ansi.dim_black": "#282828ff",
"terminal.ansi.red": "#9d0308ff",
"terminal.ansi.bright_red": "#db8b7aff",
"terminal.ansi.dim_red": "#4e1207ff",
@@ -2093,9 +2018,9 @@
"terminal.ansi.cyan": "#437b59ff",
"terminal.ansi.bright_cyan": "#9fbca8ff",
"terminal.ansi.dim_cyan": "#253e2eff",
"terminal.ansi.white": "#f2e5bcff",
"terminal.ansi.bright_white": "#f2e5bcff",
"terminal.ansi.dim_white": "#b0a189ff",
"terminal.ansi.white": "#282828ff",
"terminal.ansi.bright_white": "#282828ff",
"terminal.ansi.dim_white": "#73675eff",
"link_text.hover": "#0b6678ff",
"version_control.added": "#797410ff",
"version_control.modified": "#b57615ff",
@@ -2270,11 +2195,6 @@
"font_style": null,
"font_weight": null
},
"namespace": {
"color": "#066578ff",
"font_style": null,
"font_weight": null
},
"number": {
"color": "#8f3e71ff",
"font_style": null,
@@ -2330,16 +2250,6 @@
"font_style": null,
"font_weight": null
},
"selector": {
"color": "#b57613ff",
"font_style": null,
"font_weight": null
},
"selector.pseudo": {
"color": "#0b6678ff",
"font_style": null,
"font_weight": null
},
"string": {
"color": "#79740eff",
"font_style": null,

View File

@@ -99,8 +99,6 @@
"version_control.added": "#27a657ff",
"version_control.modified": "#d3b020ff",
"version_control.deleted": "#e06c76ff",
"version_control.conflict_marker.ours": "#a1c1811a",
"version_control.conflict_marker.theirs": "#74ade81a",
"conflict": "#dec184ff",
"conflict.background": "#dec1841a",
"conflict.border": "#5d4c2fff",
@@ -266,11 +264,6 @@
"font_style": null,
"font_weight": null
},
"namespace": {
"color": "#dce0e5ff",
"font_style": null,
"font_weight": null
},
"number": {
"color": "#bf956aff",
"font_style": null,
@@ -326,16 +319,6 @@
"font_style": null,
"font_weight": null
},
"selector": {
"color": "#dfc184ff",
"font_style": null,
"font_weight": null
},
"selector.pseudo": {
"color": "#74ade8ff",
"font_style": null,
"font_weight": null
},
"string": {
"color": "#a1c181ff",
"font_style": null,
@@ -467,9 +450,9 @@
"terminal.foreground": "#242529ff",
"terminal.bright_foreground": "#242529ff",
"terminal.dim_foreground": "#fafafaff",
"terminal.ansi.black": "#242529ff",
"terminal.ansi.bright_black": "#242529ff",
"terminal.ansi.dim_black": "#97979aff",
"terminal.ansi.black": "#fafafaff",
"terminal.ansi.bright_black": "#aaaaaaff",
"terminal.ansi.dim_black": "#242529ff",
"terminal.ansi.red": "#d36151ff",
"terminal.ansi.bright_red": "#f0b0a4ff",
"terminal.ansi.dim_red": "#6f312aff",
@@ -488,9 +471,9 @@
"terminal.ansi.cyan": "#3a82b7ff",
"terminal.ansi.bright_cyan": "#a3bedaff",
"terminal.ansi.dim_cyan": "#254058ff",
"terminal.ansi.white": "#fafafaff",
"terminal.ansi.bright_white": "#fafafaff",
"terminal.ansi.dim_white": "#aaaaaaff",
"terminal.ansi.white": "#242529ff",
"terminal.ansi.bright_white": "#242529ff",
"terminal.ansi.dim_white": "#97979aff",
"link_text.hover": "#5c78e2ff",
"version_control.added": "#27a657ff",
"version_control.modified": "#d3b020ff",
@@ -660,11 +643,6 @@
"font_style": null,
"font_weight": null
},
"namespace": {
"color": "#242529ff",
"font_style": null,
"font_weight": null
},
"number": {
"color": "#ad6e25ff",
"font_style": null,
@@ -720,16 +698,6 @@
"font_style": null,
"font_weight": null
},
"selector": {
"color": "#669f59ff",
"font_style": null,
"font_weight": null
},
"selector.pseudo": {
"color": "#5c78e2ff",
"font_style": null,
"font_weight": null
},
"string": {
"color": "#649f57ff",
"font_style": null,

View File

@@ -7,10 +7,7 @@ use gpui::{
InteractiveElement as _, ParentElement as _, Render, SharedString, StatefulInteractiveElement,
Styled, Transformation, Window, actions, percentage,
};
use language::{
BinaryStatus, LanguageRegistry, LanguageServerId, LanguageServerName,
LanguageServerStatusUpdate, ServerHealth,
};
use language::{BinaryStatus, LanguageRegistry, LanguageServerId};
use project::{
EnvironmentErrorMessage, LanguageServerProgress, LspStoreEvent, Project,
ProjectEnvironmentEvent,
@@ -19,7 +16,6 @@ use project::{
use smallvec::SmallVec;
use std::{
cmp::Reverse,
collections::HashSet,
fmt::Write,
path::Path,
sync::Arc,
@@ -34,9 +30,9 @@ const GIT_OPERATION_DELAY: Duration = Duration::from_millis(0);
actions!(activity_indicator, [ShowErrorMessage]);
pub enum Event {
ShowStatus {
server_name: LanguageServerName,
status: SharedString,
ShowError {
server_name: SharedString,
error: String,
},
}
@@ -49,8 +45,8 @@ pub struct ActivityIndicator {
#[derive(Debug)]
struct ServerStatus {
name: LanguageServerName,
status: LanguageServerStatusUpdate,
name: SharedString,
status: BinaryStatus,
}
struct PendingWork<'a> {
@@ -149,19 +145,19 @@ impl ActivityIndicator {
});
cx.subscribe_in(&this, window, move |_, _, event, window, cx| match event {
Event::ShowStatus {
server_name,
status,
} => {
Event::ShowError { server_name, error } => {
let create_buffer = project.update(cx, |project, cx| project.create_buffer(cx));
let project = project.clone();
let status = status.clone();
let error = error.clone();
let server_name = server_name.clone();
cx.spawn_in(window, async move |workspace, cx| {
let buffer = create_buffer.await?;
buffer.update(cx, |buffer, cx| {
buffer.edit(
[(0..0, format!("Language server {server_name}:\n\n{status}"))],
[(
0..0,
format!("Language server error: {}\n\n{}", server_name, error),
)],
None,
cx,
);
@@ -170,10 +166,7 @@ impl ActivityIndicator {
workspace.update_in(cx, |workspace, window, cx| {
workspace.add_item_to_active_pane(
Box::new(cx.new(|cx| {
let mut editor =
Editor::for_buffer(buffer, Some(project.clone()), window, cx);
editor.set_read_only(true);
editor
Editor::for_buffer(buffer, Some(project.clone()), window, cx)
})),
None,
true,
@@ -192,34 +185,19 @@ impl ActivityIndicator {
}
fn show_error_message(&mut self, _: &ShowErrorMessage, _: &mut Window, cx: &mut Context<Self>) {
let mut status_message_shown = false;
self.statuses.retain(|status| match &status.status {
LanguageServerStatusUpdate::Binary(BinaryStatus::Failed { error })
if !status_message_shown =>
{
cx.emit(Event::ShowStatus {
self.statuses.retain(|status| {
if let BinaryStatus::Failed { error } = &status.status {
cx.emit(Event::ShowError {
server_name: status.name.clone(),
status: SharedString::from(error),
error: error.clone(),
});
status_message_shown = true;
false
} else {
true
}
LanguageServerStatusUpdate::Health(
ServerHealth::Error | ServerHealth::Warning,
status_string,
) if !status_message_shown => match status_string {
Some(error) => {
cx.emit(Event::ShowStatus {
server_name: status.name.clone(),
status: error.clone(),
});
status_message_shown = true;
false
}
None => false,
},
_ => true,
});
cx.notify();
}
fn dismiss_error_message(
@@ -289,52 +267,48 @@ impl ActivityIndicator {
});
}
// Show any language server has pending activity.
let mut pending_work = self.pending_language_server_work(cx);
if let Some(PendingWork {
progress_token,
progress,
..
}) = pending_work.next()
{
let mut pending_work = self.pending_language_server_work(cx);
if let Some(PendingWork {
progress_token,
progress,
..
}) = pending_work.next()
{
let mut message = progress
.title
.as_deref()
.unwrap_or(progress_token)
.to_string();
let mut message = progress
.title
.as_deref()
.unwrap_or(progress_token)
.to_string();
if let Some(percentage) = progress.percentage {
write!(&mut message, " ({}%)", percentage).unwrap();
}
if let Some(progress_message) = progress.message.as_ref() {
message.push_str(": ");
message.push_str(progress_message);
}
let additional_work_count = pending_work.count();
if additional_work_count > 0 {
write!(&mut message, " + {} more", additional_work_count).unwrap();
}
return Some(Content {
icon: Some(
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| {
icon.transform(Transformation::rotate(percentage(delta)))
},
)
.into_any_element(),
),
message,
on_click: Some(Arc::new(Self::toggle_language_server_work_context_menu)),
tooltip_message: None,
});
if let Some(percentage) = progress.percentage {
write!(&mut message, " ({}%)", percentage).unwrap();
}
if let Some(progress_message) = progress.message.as_ref() {
message.push_str(": ");
message.push_str(progress_message);
}
let additional_work_count = pending_work.count();
if additional_work_count > 0 {
write!(&mut message, " + {} more", additional_work_count).unwrap();
}
return Some(Content {
icon: Some(
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
)
.into_any_element(),
),
message,
on_click: Some(Arc::new(Self::toggle_language_server_work_context_menu)),
tooltip_message: None,
});
}
if let Some(session) = self
@@ -395,38 +369,14 @@ impl ActivityIndicator {
let mut downloading = SmallVec::<[_; 3]>::new();
let mut checking_for_update = SmallVec::<[_; 3]>::new();
let mut failed = SmallVec::<[_; 3]>::new();
let mut health_messages = SmallVec::<[_; 3]>::new();
let mut servers_to_clear_statuses = HashSet::<LanguageServerName>::default();
for status in &self.statuses {
match &status.status {
LanguageServerStatusUpdate::Binary(BinaryStatus::CheckingForUpdate) => {
checking_for_update.push(status.name.clone());
}
LanguageServerStatusUpdate::Binary(BinaryStatus::Downloading) => {
downloading.push(status.name.clone());
}
LanguageServerStatusUpdate::Binary(BinaryStatus::Failed { .. }) => {
failed.push(status.name.clone());
}
LanguageServerStatusUpdate::Binary(BinaryStatus::None) => {}
LanguageServerStatusUpdate::Health(health, server_status) => match server_status {
Some(server_status) => {
health_messages.push((status.name.clone(), *health, server_status.clone()));
}
None => {
servers_to_clear_statuses.insert(status.name.clone());
}
},
match status.status {
BinaryStatus::CheckingForUpdate => checking_for_update.push(status.name.clone()),
BinaryStatus::Downloading => downloading.push(status.name.clone()),
BinaryStatus::Failed { .. } => failed.push(status.name.clone()),
BinaryStatus::None => {}
}
}
self.statuses
.retain(|status| !servers_to_clear_statuses.contains(&status.name));
health_messages.sort_by_key(|(_, health, _)| match health {
ServerHealth::Error => 2,
ServerHealth::Warning => 1,
ServerHealth::Ok => 0,
});
if !downloading.is_empty() {
return Some(Content {
@@ -507,7 +457,7 @@ impl ActivityIndicator {
}),
),
on_click: Some(Arc::new(|this, window, cx| {
this.show_error_message(&ShowErrorMessage, window, cx)
this.show_error_message(&Default::default(), window, cx)
})),
tooltip_message: None,
});
@@ -521,7 +471,7 @@ impl ActivityIndicator {
.size(IconSize::Small)
.into_any_element(),
),
message: format!("Formatting failed: {failure}. Click to see logs."),
message: format!("Formatting failed: {}. Click to see logs.", failure),
on_click: Some(Arc::new(|indicator, window, cx| {
indicator.project.update(cx, |project, cx| {
project.reset_last_formatting_failure(cx);
@@ -532,56 +482,6 @@ impl ActivityIndicator {
});
}
// Show any health messages for the language servers
if let Some((server_name, health, message)) = health_messages.pop() {
let health_str = match health {
ServerHealth::Ok => format!("({server_name}) "),
ServerHealth::Warning => format!("({server_name}) Warning: "),
ServerHealth::Error => format!("({server_name}) Error: "),
};
let single_line_message = message
.lines()
.filter_map(|line| {
let line = line.trim();
if line.is_empty() { None } else { Some(line) }
})
.collect::<Vec<_>>()
.join(" ");
let mut altered_message = single_line_message != message;
let truncated_message = truncate_and_trailoff(
&single_line_message,
MAX_MESSAGE_LEN.saturating_sub(health_str.len()),
);
altered_message |= truncated_message != single_line_message;
let final_message = format!("{health_str}{truncated_message}");
let tooltip_message = if altered_message {
Some(format!("{health_str}{message}"))
} else {
None
};
return Some(Content {
icon: Some(
Icon::new(IconName::Warning)
.size(IconSize::Small)
.into_any_element(),
),
message: final_message,
tooltip_message,
on_click: Some(Arc::new(move |activity_indicator, window, cx| {
if altered_message {
activity_indicator.show_error_message(&ShowErrorMessage, window, cx)
} else {
activity_indicator
.statuses
.retain(|status| status.name != server_name);
cx.notify();
}
})),
});
}
// Show any application auto-update info.
if let Some(updater) = &self.auto_updater {
return match &updater.read(cx).status() {

View File

@@ -25,6 +25,7 @@ assistant_context_editor.workspace = true
assistant_slash_command.workspace = true
assistant_slash_commands.workspace = true
assistant_tool.workspace = true
async-watch.workspace = true
audio.workspace = true
buffer_diff.workspace = true
chrono.workspace = true
@@ -45,7 +46,6 @@ git.workspace = true
gpui.workspace = true
heed.workspace = true
html_to_markdown.workspace = true
indoc.workspace = true
http_client.workspace = true
indexed_docs.workspace = true
inventory.workspace = true
@@ -78,7 +78,6 @@ serde_json.workspace = true
serde_json_lenient.workspace = true
settings.workspace = true
smol.workspace = true
sqlez.workspace = true
streaming_diff.workspace = true
telemetry.workspace = true
telemetry_events.workspace = true
@@ -94,21 +93,17 @@ ui_input.workspace = true
urlencoding.workspace = true
util.workspace = true
uuid.workspace = true
watch.workspace = true
workspace-hack.workspace = true
workspace.workspace = true
zed_actions.workspace = true
zed_llm_client.workspace = true
zstd.workspace = true
[dev-dependencies]
assistant_tools.workspace = true
buffer_diff = { workspace = true, features = ["test-support"] }
editor = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, "features" = ["test-support"] }
indoc.workspace = true
language = { workspace = true, "features" = ["test-support"] }
language_model = { workspace = true, "features" = ["test-support"] }
pretty_assertions.workspace = true
project = { workspace = true, features = ["test-support"] }
rand.workspace = true

View File

@@ -1,8 +1,9 @@
use crate::AgentPanel;
use crate::context::{AgentContextHandle, RULES_ICON};
use crate::context_picker::{ContextPicker, MentionLink};
use crate::context_store::ContextStore;
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::{
LastRestoreCheckpoint, MessageCrease, MessageId, MessageSegment, Thread, ThreadError,
ThreadEvent, ThreadFeedback, ThreadSummary,
@@ -12,7 +13,6 @@ use crate::tool_use::{PendingToolUseStatus, ToolUse};
use crate::ui::{
AddedContext, AgentNotification, AgentNotificationEvent, AnimatedLabel, ContextPill,
};
use crate::{AgentPanel, ModelUsageContext};
use agent_settings::{AgentSettings, NotifyWhenAgentWaiting};
use anyhow::Context as _;
use assistant_tool::ToolUseStatus;
@@ -750,7 +750,7 @@ struct EditingMessageState {
editor: Entity<Editor>,
context_strip: Entity<ContextStrip>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
last_estimated_token_count: Option<u64>,
last_estimated_token_count: Option<usize>,
_subscriptions: [Subscription; 2],
_update_token_count_task: Option<Task<()>>,
}
@@ -857,7 +857,7 @@ impl ActiveThread {
}
/// Returns the editing message id and the estimated token count in the content
pub fn editing_message_id(&self) -> Option<(MessageId, u64)> {
pub fn editing_message_id(&self) -> Option<(MessageId, usize)> {
self.editing_message
.as_ref()
.map(|(id, state)| (*id, state.last_estimated_token_count.unwrap_or(0)))
@@ -999,7 +999,7 @@ impl ActiveThread {
ThreadEvent::Stopped(reason) => match reason {
Ok(StopReason::EndTurn | StopReason::MaxTokens) => {
let used_tools = self.thread.read(cx).used_tools_since_last_user_message();
self.play_notification_sound(window, cx);
self.play_notification_sound(cx);
self.show_notification(
if used_tools {
"Finished running tools"
@@ -1014,18 +1014,9 @@ impl ActiveThread {
_ => {}
},
ThreadEvent::ToolConfirmationNeeded => {
self.play_notification_sound(window, cx);
self.play_notification_sound(cx);
self.show_notification("Waiting for tool confirmation", IconName::Info, window, cx);
}
ThreadEvent::ToolUseLimitReached => {
self.play_notification_sound(window, cx);
self.show_notification(
"Consecutive tool use limit reached.",
IconName::Warning,
window,
cx,
);
}
ThreadEvent::StreamedAssistantText(message_id, text) => {
if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(&message_id) {
rendered_message.append_text(text, cx);
@@ -1144,10 +1135,6 @@ impl ActiveThread {
cx,
);
}
ThreadEvent::ProfileChanged => {
self.save_thread(cx);
cx.notify();
}
}
}
@@ -1164,9 +1151,9 @@ impl ActiveThread {
cx.notify();
}
fn play_notification_sound(&self, window: &Window, cx: &mut App) {
fn play_notification_sound(&self, cx: &mut App) {
let settings = AgentSettings::get_global(cx);
if settings.play_sound_when_agent_done && !window.is_window_active() {
if settings.play_sound_when_agent_done {
Audio::play_sound(Sound::AgentDone, cx);
}
}
@@ -1352,7 +1339,6 @@ impl ActiveThread {
Some(self.text_thread_store.downgrade()),
context_picker_menu_handle.clone(),
SuggestContextKind::File,
ModelUsageContext::Thread(self.thread.clone()),
window,
cx,
)
@@ -1522,7 +1508,31 @@ impl ActiveThread {
}
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(
@@ -1567,8 +1577,6 @@ impl ActiveThread {
let edited_text = state.editor.read(cx).text(cx);
let creases = state.editor.update(cx, extract_message_creases);
let new_context = self
.context_store
.read(cx)
@@ -1593,7 +1601,6 @@ impl ActiveThread {
message_id,
Role::User,
vec![MessageSegment::Text(edited_text)],
creases,
Some(context.loaded_context),
checkpoint.ok(),
cx,
@@ -1605,7 +1612,6 @@ impl ActiveThread {
this.thread.update(cx, |thread, cx| {
thread.advance_prompt_id();
thread.cancel_last_completion(Some(window.window_handle()), cx);
thread.send_to_model(
model.model,
CompletionIntent::UserPrompt,
@@ -1681,10 +1687,7 @@ impl ActiveThread {
let editor = cx.new(|cx| {
let mut editor = Editor::new(
editor::EditorMode::AutoHeight {
min_lines: 1,
max_lines: 4,
},
editor::EditorMode::AutoHeight { max_lines: 4 },
buffer,
None,
window,
@@ -1792,31 +1795,12 @@ impl ActiveThread {
fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
let message_id = self.messages[ix];
let workspace = self.workspace.clone();
let thread = self.thread.read(cx);
let is_first_message = ix == 0;
let is_last_message = ix == self.messages.len() - 1;
let Some(message) = thread.message(message_id) else {
let Some(message) = self.thread.read(cx).message(message_id) else {
return Empty.into_any();
};
let is_generating = thread.is_generating();
let is_generating_stale = thread.is_generation_stale().unwrap_or(false);
let loading_dots = (is_generating && is_last_message).then(|| {
h_flex()
.h_8()
.my_3()
.mx_5()
.when(is_generating_stale || message.is_hidden, |this| {
this.child(AnimatedLabel::new("").size(LabelSize::Small))
})
});
if message.is_hidden {
return div().children(loading_dots).into_any();
return Empty.into_any();
}
let message_creases = message.creases.clone();
@@ -1825,16 +1809,26 @@ impl ActiveThread {
return Empty.into_any();
};
let workspace = self.workspace.clone();
let thread = self.thread.read(cx);
// Get all the data we need from thread before we start using it in closures
let checkpoint = thread.checkpoint_for_message(message_id);
let configured_model = thread.configured_model().map(|m| m.model);
let added_context = thread
.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<_>>();
let tool_uses = thread.tool_uses_for_message(message_id, cx);
let has_tool_uses = !tool_uses.is_empty();
let is_generating = thread.is_generating();
let is_generating_stale = thread.is_generation_stale().unwrap_or(false);
let is_first_message = ix == 0;
let is_last_message = ix == self.messages.len() - 1;
let loading_dots = (is_generating_stale && is_last_message)
.then(|| AnimatedLabel::new("").size(LabelSize::Small));
let editing_message_state = self
.editing_message
@@ -2250,7 +2244,17 @@ impl ActiveThread {
parent.child(self.render_rules_item(cx))
})
.child(styled_message)
.children(loading_dots)
.when(is_generating && is_last_message, |this| {
this.child(
h_flex()
.h_8()
.mt_2()
.mb_4()
.ml_4()
.py_1p5()
.when_some(loading_dots, |this, loading_dots| this.child(loading_dots)),
)
})
.when(show_feedback, move |parent| {
parent.child(feedback_items).when_some(
self.open_feedback_editors.get(&message_id),
@@ -3635,38 +3639,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(
project_path: project::ProjectPath,
target_position: Point,
@@ -3696,13 +3668,10 @@ fn open_editor_at_position(
#[cfg(test)]
mod tests {
use assistant_tool::{ToolRegistry, ToolWorkingSet};
use editor::{EditorSettings, display_map::CreaseMetadata};
use editor::EditorSettings;
use fs::FakeFs;
use gpui::{AppContext, TestAppContext, VisualTestContext};
use language_model::{
ConfiguredModel, LanguageModel, LanguageModelRegistry,
fake_provider::{FakeLanguageModel, FakeLanguageModelProvider},
};
use language_model::{LanguageModel, fake_provider::FakeLanguageModel};
use project::Project;
use prompt_store::PromptBuilder;
use serde_json::json;
@@ -3710,7 +3679,7 @@ mod tests {
use util::path;
use workspace::CollaboratorId;
use crate::{ContextLoadResult, thread::MessageSegment, thread_store};
use crate::{ContextLoadResult, thread_store};
use super::*;
@@ -3763,195 +3732,6 @@ mod tests {
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");
});
}
#[gpui::test]
async fn test_editing_message_cancels_previous_completion(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: model.clone(),
}),
cx,
);
});
});
// Track thread events to verify cancellation
let cancellation_events = Arc::new(std::sync::Mutex::new(Vec::new()));
let new_request_events = Arc::new(std::sync::Mutex::new(Vec::new()));
let _subscription = cx.update(|_, cx| {
let cancellation_events = cancellation_events.clone();
let new_request_events = new_request_events.clone();
cx.subscribe(
&thread,
move |_thread, event: &ThreadEvent, _cx| match event {
ThreadEvent::CompletionCanceled => {
cancellation_events.lock().unwrap().push(());
}
ThreadEvent::NewRequest => {
new_request_events.lock().unwrap().push(());
}
_ => {}
},
)
});
// Insert a user message and start streaming a response
let message = thread.update(cx, |thread, cx| {
let message_id = thread.insert_user_message(
"Hello, how are you?",
ContextLoadResult::default(),
None,
vec![],
cx,
);
thread.advance_prompt_id();
thread.send_to_model(
model.clone(),
CompletionIntent::UserPrompt,
cx.active_window(),
cx,
);
thread.message(message_id).cloned().unwrap()
});
cx.run_until_parked();
// Verify that a completion is in progress
assert!(cx.read(|cx| thread.read(cx).is_generating()));
assert_eq!(new_request_events.lock().unwrap().len(), 1);
// Edit the message while the completion is still running
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.set_text("What is the weather like?", window, cx);
});
active_thread.confirm_editing_message(&Default::default(), window, cx);
});
cx.run_until_parked();
// Verify that the previous completion was cancelled
assert_eq!(cancellation_events.lock().unwrap().len(), 1);
// Verify that a new request was started after cancellation
assert_eq!(new_request_events.lock().unwrap().len(), 2);
// Verify that the edited message contains the new text
let edited_message =
thread.update(cx, |thread, _| thread.message(message.id).cloned().unwrap());
match &edited_message.segments[0] {
MessageSegment::Text(text) => {
assert_eq!(text, "What is the weather like?");
}
_ => panic!("Expected text segment"),
}
}
fn init_test_settings(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);

View File

@@ -3,7 +3,6 @@ mod agent_configuration;
mod agent_diff;
mod agent_model_selector;
mod agent_panel;
mod agent_profile;
mod buffer_codegen;
mod context;
mod context_picker;
@@ -34,11 +33,9 @@ use assistant_slash_command::SlashCommandRegistry;
use client::Client;
use feature_flags::FeatureFlagAppExt as _;
use fs::Fs;
use gpui::{App, Entity, actions, impl_actions};
use gpui::{App, actions, impl_actions};
use language::LanguageRegistry;
use language_model::{
ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry,
};
use language_model::{LanguageModelId, LanguageModelProviderId, LanguageModelRegistry};
use prompt_store::PromptBuilder;
use schemars::JsonSchema;
use serde::Deserialize;
@@ -118,28 +115,6 @@ impl 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.
pub fn init(
fs: Arc<dyn Fs>,
@@ -162,7 +137,7 @@ pub fn init(
assistant_slash_command::init(cx);
thread_store::init(cx);
agent_panel::init(cx);
context_server_configuration::init(language_registry, fs.clone(), cx);
context_server_configuration::init(language_registry, cx);
register_slash_commands(cx);
inline_assistant::init(

View File

@@ -12,7 +12,7 @@ use context_server::ContextServerId;
use fs::Fs;
use gpui::{
Action, Animation, AnimationExt as _, AnyView, App, Entity, EventEmitter, FocusHandle,
Focusable, ScrollHandle, Subscription, Transformation, percentage,
Focusable, ScrollHandle, Subscription, pulsating_between,
};
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
use project::context_server_store::{ContextServerStatus, ContextServerStore};
@@ -475,6 +475,7 @@ impl AgentConfiguration {
.get(&context_server_id)
.copied()
.unwrap_or_default();
let tools = tools_by_source
.get(&ToolSource::ContextServer {
id: context_server_id.0.clone().into(),
@@ -483,23 +484,25 @@ impl AgentConfiguration {
let tool_count = tools.len();
let border_color = cx.theme().colors().border.opacity(0.6);
let success_color = Color::Success.color(cx);
let (status_indicator, tooltip_text) = match server_status {
ContextServerStatus::Starting => (
Icon::new(IconName::LoadCircle)
.size(IconSize::XSmall)
.color(Color::Accent)
Indicator::dot()
.color(Color::Success)
.with_animation(
SharedString::from(format!("{}-starting", context_server_id.0.clone(),)),
Animation::new(Duration::from_secs(3)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.4, 1.)),
move |this, delta| this.color(success_color.alpha(delta).into()),
)
.into_any_element(),
"Server is starting.",
),
ContextServerStatus::Running => (
Indicator::dot().color(Color::Success).into_any_element(),
"Server is active.",
"Server is running.",
),
ContextServerStatus::Error(_) => (
Indicator::dot().color(Color::Error).into_any_element(),
@@ -523,11 +526,12 @@ impl AgentConfiguration {
.p_1()
.justify_between()
.when(
error.is_some() || are_tools_expanded && tool_count >= 1,
error.is_some() || are_tools_expanded && tool_count > 1,
|element| element.border_b_1().border_color(border_color),
)
.child(
h_flex()
.gap_1p5()
.child(
Disclosure::new(
"tool-list-disclosure",
@@ -547,16 +551,12 @@ impl AgentConfiguration {
})),
)
.child(
h_flex()
.id(SharedString::from(format!("tooltip-{}", item_id)))
.h_full()
.w_3()
.mx_1()
.justify_center()
div()
.id(item_id.clone())
.tooltip(Tooltip::text(tooltip_text))
.child(status_indicator),
)
.child(Label::new(item_id).ml_0p5().mr_1p5())
.child(Label::new(context_server_id.0.clone()).ml_0p5())
.when(is_running, |this| {
this.child(
Label::new(if tool_count == 1 {
@@ -586,7 +586,7 @@ impl AgentConfiguration {
if let Some(server) =
this.get_server(&context_server_id)
{
this.start_server(server, cx);
this.start_server(server, cx).log_err();
}
})
}

View File

@@ -1,6 +1,7 @@
use context_server::ContextServerCommand;
use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity, prelude::*};
use project::project_settings::{ContextServerSettings, ProjectSettings};
use project::project_settings::{ContextServerConfiguration, ProjectSettings};
use serde_json::json;
use settings::update_settings_file;
use ui::{KeyBinding, Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*};
use ui_input::SingleLineInput;
@@ -80,12 +81,13 @@ impl AddContextServerModal {
update_settings_file::<ProjectSettings>(fs.clone(), cx, |settings, _| {
settings.context_servers.insert(
name.into(),
ContextServerSettings::Custom {
command: ContextServerCommand {
ContextServerConfiguration {
command: Some(ContextServerCommand {
path,
args,
env: None,
},
}),
settings: Some(json!({})),
},
);
});

View File

@@ -15,7 +15,7 @@ use markdown::{Markdown, MarkdownElement, MarkdownStyle};
use notifications::status_toast::{StatusToast, ToastIcon};
use project::{
context_server_store::{ContextServerStatus, ContextServerStore},
project_settings::{ContextServerSettings, ProjectSettings},
project_settings::{ContextServerConfiguration, ProjectSettings},
};
use settings::{Settings as _, update_settings_file};
use theme::ThemeSettings;
@@ -89,7 +89,7 @@ impl ConfigureContextServerModal {
}),
settings_validator,
settings_editor: cx.new(|cx| {
let mut editor = Editor::auto_height(1, 16, window, cx);
let mut editor = Editor::auto_height(16, window, cx);
editor.set_text(config.default_settings.trim(), window, cx);
editor.set_show_gutter(false, cx);
editor.set_soft_wrap_mode(
@@ -175,9 +175,8 @@ impl ConfigureContextServerModal {
let settings_changed = ProjectSettings::get_global(cx)
.context_servers
.get(&id.0)
.map_or(true, |settings| match settings {
ContextServerSettings::Custom { .. } => false,
ContextServerSettings::Extension { settings } => settings != &settings_value,
.map_or(true, |config| {
config.settings.as_ref() != Some(&settings_value)
});
let is_running = self.context_server_store.read(cx).status_for_server(&id)
@@ -222,12 +221,17 @@ impl ConfigureContextServerModal {
update_settings_file::<ProjectSettings>(workspace.read(cx).app_state().fs.clone(), cx, {
let id = id.clone();
|settings, _| {
settings.context_servers.insert(
id.0,
ContextServerSettings::Extension {
settings: settings_value,
},
);
if let Some(server_config) = settings.context_servers.get_mut(&id.0) {
server_config.settings = Some(settings_value);
} else {
settings.context_servers.insert(
id.0,
ContextServerConfiguration {
settings: Some(settings_value),
..Default::default()
},
);
}
}
});
}

View File

@@ -2,21 +2,25 @@ mod profile_modal_header;
use std::sync::Arc;
use agent_settings::{AgentProfileId, AgentSettings, builtin_profiles};
use agent_settings::{AgentProfile, AgentProfileId, AgentSettings, builtin_profiles};
use assistant_tool::ToolWorkingSet;
use convert_case::{Case, Casing as _};
use editor::Editor;
use fs::Fs;
use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription, prelude::*};
use settings::Settings as _;
use gpui::{
DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription, WeakEntity,
prelude::*,
};
use settings::{Settings as _, update_settings_file};
use ui::{
KeyBinding, ListItem, ListItemSpacing, ListSeparator, Navigable, NavigableEntry, prelude::*,
};
use util::ResultExt as _;
use workspace::{ModalView, Workspace};
use crate::agent_configuration::manage_profiles_modal::profile_modal_header::ProfileModalHeader;
use crate::agent_configuration::tool_picker::{ToolPicker, ToolPickerDelegate};
use crate::agent_profile::AgentProfile;
use crate::{AgentPanel, ManageProfiles};
use crate::{AgentPanel, ManageProfiles, ThreadStore};
use super::tool_picker::ToolPickerMode;
@@ -99,6 +103,7 @@ pub struct NewProfileMode {
pub struct ManageProfilesModal {
fs: Arc<dyn Fs>,
tools: Entity<ToolWorkingSet>,
thread_store: WeakEntity<ThreadStore>,
focus_handle: FocusHandle,
mode: Mode,
}
@@ -114,8 +119,9 @@ impl ManageProfilesModal {
let fs = workspace.app_state().fs.clone();
let thread_store = panel.read(cx).thread_store();
let tools = thread_store.read(cx).tools();
let thread_store = thread_store.downgrade();
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() {
this.configure_builtin_tools(profile_id, window, cx);
@@ -130,6 +136,7 @@ impl ManageProfilesModal {
pub fn new(
fs: Arc<dyn Fs>,
tools: Entity<ToolWorkingSet>,
thread_store: WeakEntity<ThreadStore>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
@@ -138,6 +145,7 @@ impl ManageProfilesModal {
Self {
fs,
tools,
thread_store,
focus_handle,
mode: Mode::choose_profile(window, cx),
}
@@ -198,6 +206,7 @@ impl ManageProfilesModal {
ToolPickerMode::McpTools,
self.fs.clone(),
self.tools.clone(),
self.thread_store.clone(),
profile_id.clone(),
profile,
cx,
@@ -235,6 +244,7 @@ impl ManageProfilesModal {
ToolPickerMode::BuiltinTools,
self.fs.clone(),
self.tools.clone(),
self.thread_store.clone(),
profile_id.clone(),
profile,
cx,
@@ -260,10 +270,32 @@ impl ManageProfilesModal {
match &self.mode {
Mode::ChooseProfile { .. } => {}
Mode::NewProfile(mode) => {
let name = mode.name_editor.read(cx).text(cx);
let settings = AgentSettings::get_global(cx);
let profile_id =
AgentProfile::create(name, mode.base_profile_id.clone(), self.fs.clone(), cx);
let base_profile = mode
.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);
}
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 {}
@@ -475,13 +520,14 @@ impl ManageProfilesModal {
) -> impl IntoElement {
let settings = AgentSettings::get_global(cx);
let profile_id = &settings.default_profile;
let profile_name = settings
.profiles
.get(&mode.profile_id)
.map(|profile| profile.name.clone())
.unwrap_or_else(|| "Unknown".into());
let icon = match mode.profile_id.as_str() {
let icon = match profile_id.as_str() {
"write" => IconName::Pencil,
"ask" => IconName::MessageBubbles,
_ => IconName::UserRoundPen,

View File

@@ -1,17 +1,19 @@
use std::{collections::BTreeMap, sync::Arc};
use agent_settings::{
AgentProfileContent, AgentProfileId, AgentProfileSettings, AgentSettings, AgentSettingsContent,
AgentProfile, AgentProfileContent, AgentProfileId, AgentSettings, AgentSettingsContent,
ContextServerPresetContent,
};
use assistant_tool::{ToolSource, ToolWorkingSet};
use fs::Fs;
use gpui::{App, Context, DismissEvent, Entity, EventEmitter, Focusable, Task, WeakEntity, Window};
use picker::{Picker, PickerDelegate};
use settings::update_settings_file;
use settings::{Settings as _, update_settings_file};
use ui::{ListItem, ListItemSpacing, prelude::*};
use util::ResultExt as _;
use crate::ThreadStore;
pub struct ToolPicker {
picker: Entity<Picker<ToolPickerDelegate>>,
}
@@ -69,10 +71,11 @@ pub enum PickerItem {
pub struct ToolPickerDelegate {
tool_picker: WeakEntity<ToolPicker>,
thread_store: WeakEntity<ThreadStore>,
fs: Arc<dyn Fs>,
items: Arc<Vec<PickerItem>>,
profile_id: AgentProfileId,
profile_settings: AgentProfileSettings,
profile: AgentProfile,
filtered_items: Vec<PickerItem>,
selected_index: usize,
mode: ToolPickerMode,
@@ -83,18 +86,20 @@ impl ToolPickerDelegate {
mode: ToolPickerMode,
fs: Arc<dyn Fs>,
tool_set: Entity<ToolWorkingSet>,
thread_store: WeakEntity<ThreadStore>,
profile_id: AgentProfileId,
profile_settings: AgentProfileSettings,
profile: AgentProfile,
cx: &mut Context<ToolPicker>,
) -> Self {
let items = Arc::new(Self::resolve_items(mode, &tool_set, cx));
Self {
tool_picker: cx.entity().downgrade(),
thread_store,
fs,
items,
profile_id,
profile_settings,
profile,
filtered_items: Vec::new(),
selected_index: 0,
mode,
@@ -244,31 +249,28 @@ impl PickerDelegate for ToolPickerDelegate {
};
let is_currently_enabled = if let Some(server_id) = server_id.clone() {
let preset = self
.profile_settings
.context_servers
.entry(server_id)
.or_default();
let preset = self.profile.context_servers.entry(server_id).or_default();
let is_enabled = *preset.tools.entry(tool_name.clone()).or_default();
*preset.tools.entry(tool_name.clone()).or_default() = !is_enabled;
is_enabled
} else {
let is_enabled = *self
.profile_settings
.tools
.entry(tool_name.clone())
.or_default();
*self
.profile_settings
.tools
.entry(tool_name.clone())
.or_default() = !is_enabled;
let is_enabled = *self.profile.tools.entry(tool_name.clone()).or_default();
*self.profile.tools.entry(tool_name.clone()).or_default() = !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, {
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 tool_name = tool_name.clone();
move |settings: &mut AgentSettingsContent, _cx| {
@@ -346,18 +348,14 @@ impl PickerDelegate for ToolPickerDelegate {
),
PickerItem::Tool { name, server_id } => {
let is_enabled = if let Some(server_id) = server_id {
self.profile_settings
self.profile
.context_servers
.get(server_id.as_ref())
.and_then(|preset| preset.tools.get(name))
.copied()
.unwrap_or(self.profile_settings.enable_all_context_servers)
.unwrap_or(self.profile.enable_all_context_servers)
} else {
self.profile_settings
.tools
.get(name)
.copied()
.unwrap_or(false)
self.profile.tools.get(name).copied().unwrap_or(false)
};
Some(

View File

@@ -31,7 +31,7 @@ use util::ResultExt;
use workspace::{
Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
Workspace,
item::{BreadcrumbText, ItemEvent, SaveOptions, TabContentParams},
item::{BreadcrumbText, ItemEvent, TabContentParams},
searchable::SearchableItemHandle,
};
use zed_actions::assistant::ToggleFocus;
@@ -532,12 +532,12 @@ impl Item for AgentDiffPane {
fn save(
&mut self,
options: SaveOptions,
format: bool,
project: Entity<Project>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
self.editor.save(options, project, window, cx)
self.editor.save(format, project, window, cx)
}
fn save_as(
@@ -1086,7 +1086,7 @@ impl Render for AgentDiffToolbar {
.child(vertical_divider())
.when_some(editor.read(cx).workspace(), |this, _workspace| {
this.child(
IconButton::new("review", IconName::ListTodo)
IconButton::new("review", IconName::ListCollapse)
.icon_size(IconSize::Small)
.tooltip(Tooltip::for_action_title_in(
"Review All Files",
@@ -1116,13 +1116,8 @@ impl Render for AgentDiffToolbar {
return Empty.into_any();
};
let has_pending_edit_tool_use = agent_diff
.read(cx)
.thread
.read(cx)
.has_pending_edit_tool_uses();
if has_pending_edit_tool_use {
let is_generating = agent_diff.read(cx).thread.read(cx).is_generating();
if is_generating {
return div().px_2().child(spinner_icon).into_any();
}
@@ -1377,9 +1372,7 @@ impl AgentDiff {
| ThreadEvent::ToolFinished { .. }
| ThreadEvent::CheckpointChanged
| ThreadEvent::ToolConfirmationNeeded
| ThreadEvent::ToolUseLimitReached
| ThreadEvent::CancelEditing
| ThreadEvent::ProfileChanged => {}
| ThreadEvent::CancelEditing => {}
}
}
@@ -1471,10 +1464,7 @@ impl AgentDiff {
if !AgentSettings::get_global(cx).single_file_review {
for (editor, _) in self.reviewing_editors.drain() {
editor
.update(cx, |editor, cx| {
editor.end_temporary_diff_override(cx);
editor.unregister_addon::<EditorAgentDiffAddon>();
})
.update(cx, |editor, cx| editor.end_temporary_diff_override(cx))
.ok();
}
return;
@@ -1570,10 +1560,7 @@ impl AgentDiff {
if in_workspace {
editor
.update(cx, |editor, cx| {
editor.end_temporary_diff_override(cx);
editor.unregister_addon::<EditorAgentDiffAddon>();
})
.update(cx, |editor, cx| editor.end_temporary_diff_override(cx))
.ok();
self.reviewing_editors.remove(&editor);
}

View File

@@ -3,7 +3,7 @@ use fs::Fs;
use gpui::{Entity, FocusHandle, SharedString};
use picker::popover_menu::PickerPopoverMenu;
use crate::ModelUsageContext;
use crate::Thread;
use assistant_context_editor::language_model_selector::{
LanguageModelSelector, ToggleModelSelector, language_model_selector,
};
@@ -12,6 +12,12 @@ use settings::update_settings_file;
use std::sync::Arc;
use ui::{PopoverMenuHandle, Tooltip, prelude::*};
#[derive(Clone)]
pub enum ModelType {
Default(Entity<Thread>),
InlineAssistant,
}
pub struct AgentModelSelector {
selector: Entity<LanguageModelSelector>,
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
@@ -23,7 +29,7 @@ impl AgentModelSelector {
fs: Arc<dyn Fs>,
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
focus_handle: FocusHandle,
model_usage_context: ModelUsageContext,
model_type: ModelType,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
@@ -32,14 +38,19 @@ impl AgentModelSelector {
let fs = fs.clone();
language_model_selector(
{
let model_context = model_usage_context.clone();
move |cx| model_context.configured_model(cx)
let model_type = model_type.clone();
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| {
let provider = model.provider_id().0.to_string();
let model_id = model.id().0.to_string();
match &model_usage_context {
ModelUsageContext::Thread(thread) => {
match &model_type {
ModelType::Default(thread) => {
thread.update(cx, |thread, cx| {
let registry = LanguageModelRegistry::read_global(cx);
if let Some(provider) = registry.provider(&model.provider_id())
@@ -61,7 +72,7 @@ impl AgentModelSelector {
},
);
}
ModelUsageContext::InlineAssistant => {
ModelType::InlineAssistant => {
update_settings_file::<AgentSettings>(
fs.clone(),
cx,
@@ -91,13 +102,12 @@ impl AgentModelSelector {
impl Render for AgentModelSelector {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let focus_handle = self.focus_handle.clone();
let model = self.selector.read(cx).delegate.active_model(cx);
let model_name = model
.map(|model| model.model.name().0)
.unwrap_or_else(|| SharedString::from("No model selected"));
let focus_handle = self.focus_handle.clone();
PickerPopoverMenu::new(
self.selector.clone(),
Button::new("active-model", model_name)

View File

@@ -10,9 +10,9 @@ use serde::{Deserialize, Serialize};
use agent_settings::{AgentDockPosition, AgentSettings, CompletionMode, DefaultView};
use anyhow::{Result, anyhow};
use assistant_context_editor::{
AgentPanelDelegate, AssistantContext, ContextEditor, ContextEvent, ContextSummary,
SlashCommandCompletionProvider, humanize_token_count, make_lsp_adapter_delegate,
render_remaining_tokens,
AgentPanelDelegate, AssistantContext, ConfigurationError, ContextEditor, ContextEvent,
ContextSummary, SlashCommandCompletionProvider, humanize_token_count,
make_lsp_adapter_delegate, render_remaining_tokens,
};
use assistant_slash_command::SlashCommandWorkingSet;
use assistant_tool::ToolWorkingSet;
@@ -29,8 +29,7 @@ use gpui::{
};
use language::LanguageRegistry;
use language_model::{
ConfigurationError, LanguageModelProviderTosView, LanguageModelRegistry, RequestUsage,
ZED_CLOUD_PROVIDER_ID,
LanguageModelProviderTosView, LanguageModelRegistry, RequestUsage, ZED_CLOUD_PROVIDER_ID,
};
use project::{Project, ProjectPath, Worktree};
use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
@@ -58,7 +57,7 @@ use zed_llm_client::{CompletionIntent, UsageLimit};
use crate::active_thread::{self, ActiveThread, ActiveThreadEvent};
use crate::agent_configuration::{AgentConfiguration, AssistantConfigurationEvent};
use crate::agent_diff::AgentDiff;
use crate::history_store::{HistoryEntryId, HistoryStore};
use crate::history_store::{HistoryStore, RecentEntry};
use crate::message_editor::{MessageEditor, MessageEditorEvent};
use crate::thread::{Thread, ThreadError, ThreadId, ThreadSummary, TokenUsageRatio};
use crate::thread_history::{HistoryEntryElement, ThreadHistory};
@@ -258,7 +257,6 @@ impl ActiveView {
pub fn prompt_editor(
context_editor: Entity<ContextEditor>,
history_store: Entity<HistoryStore>,
language_registry: Arc<LanguageRegistry>,
window: &mut Window,
cx: &mut App,
@@ -324,19 +322,6 @@ impl ActiveView {
editor.set_text(summary, window, cx);
})
}
ContextEvent::PathChanged { old_path, new_path } => {
history_store.update(cx, |history_store, cx| {
if let Some(old_path) = old_path {
history_store
.replace_recently_opened_text_thread(old_path, new_path, cx);
} else {
history_store.push_recently_opened_entry(
HistoryEntryId::Context(new_path.clone()),
cx,
);
}
});
}
_ => {}
}
}),
@@ -520,15 +505,10 @@ impl AgentPanel {
});
let message_editor_subscription =
cx.subscribe(&message_editor, |this, _, event, cx| match event {
cx.subscribe(&message_editor, |_, _, event, cx| match event {
MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
cx.notify();
}
MessageEditorEvent::ScrollThreadToBottom => {
this.thread.update(cx, |thread, cx| {
thread.scroll_to_bottom(cx);
});
}
});
let thread_id = thread.read(cx).id().clone();
@@ -536,7 +516,8 @@ impl AgentPanel {
HistoryStore::new(
thread_store.clone(),
context_store.clone(),
[HistoryEntryId::Thread(thread_id)],
[RecentEntry::Thread(thread_id, thread.clone())],
window,
cx,
)
});
@@ -563,13 +544,7 @@ impl AgentPanel {
editor.insert_default_prompt(window, cx);
editor
});
ActiveView::prompt_editor(
context_editor,
history_store.clone(),
language_registry.clone(),
window,
cx,
)
ActiveView::prompt_editor(context_editor, language_registry.clone(), window, cx)
}
};
@@ -606,9 +581,86 @@ impl AgentPanel {
let panel = weak_panel.clone();
let assistant_navigation_menu =
ContextMenu::build_persistent(window, cx, move |mut menu, _window, cx| {
if let Some(panel) = panel.upgrade() {
menu = Self::populate_recently_opened_menu_section(menu, panel, cx);
let recently_opened = panel
.update(cx, |this, cx| {
this.history_store.update(cx, |history_store, cx| {
history_store.recently_opened_entries(cx)
})
})
.unwrap_or_default();
if !recently_opened.is_empty() {
menu = menu.header("Recently Opened");
for entry in recently_opened.iter() {
if let RecentEntry::Context(context) = entry {
if context.read(cx).path().is_none() {
log::error!(
"bug: text thread in recent history list was never saved"
);
continue;
}
}
let summary = entry.summary(cx);
menu = menu.entry_with_end_slot_on_hover(
summary,
None,
{
let panel = panel.clone();
let entry = entry.clone();
move |window, cx| {
panel
.update(cx, {
let entry = entry.clone();
move |this, cx| match entry {
RecentEntry::Thread(_, thread) => {
this.open_thread(thread, window, cx)
}
RecentEntry::Context(context) => {
let Some(path) = context.read(cx).path()
else {
return;
};
this.open_saved_prompt_editor(
path.clone(),
window,
cx,
)
.detach_and_log_err(cx)
}
}
})
.ok();
}
},
IconName::Close,
"Close Entry".into(),
{
let panel = panel.clone();
let entry = entry.clone();
move |_window, cx| {
panel
.update(cx, |this, cx| {
this.history_store.update(
cx,
|history_store, cx| {
history_store.remove_recently_opened_entry(
&entry, cx,
);
},
);
})
.ok();
}
},
);
}
menu = menu.separator();
}
menu.action("View All", Box::new(OpenHistory))
.end_slot_action(DeleteRecentlyOpenThread.boxed_clone())
.fixed_width(px(320.).into())
@@ -808,15 +860,10 @@ impl AgentPanel {
self.message_editor.focus_handle(cx).focus(window);
let message_editor_subscription =
cx.subscribe(&self.message_editor, |this, _, event, cx| match event {
cx.subscribe(&self.message_editor, |_, _, event, cx| match event {
MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
cx.notify();
}
MessageEditorEvent::ScrollThreadToBottom => {
this.thread.update(cx, |thread, cx| {
thread.scroll_to_bottom(cx);
});
}
});
self._active_thread_subscriptions = vec![
@@ -851,7 +898,6 @@ impl AgentPanel {
self.set_active_view(
ActiveView::prompt_editor(
context_editor.clone(),
self.history_store.clone(),
self.language_registry.clone(),
window,
cx,
@@ -938,13 +984,7 @@ impl AgentPanel {
)
});
self.set_active_view(
ActiveView::prompt_editor(
editor.clone(),
self.history_store.clone(),
self.language_registry.clone(),
window,
cx,
),
ActiveView::prompt_editor(editor.clone(), self.language_registry.clone(), window, cx),
window,
cx,
);
@@ -1028,15 +1068,10 @@ impl AgentPanel {
self.message_editor.focus_handle(cx).focus(window);
let message_editor_subscription =
cx.subscribe(&self.message_editor, |this, _, event, cx| match event {
cx.subscribe(&self.message_editor, |_, _, event, cx| match event {
MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
cx.notify();
}
MessageEditorEvent::ScrollThreadToBottom => {
this.thread.update(cx, |thread, cx| {
thread.scroll_to_bottom(cx);
});
}
});
self._active_thread_subscriptions = vec![
@@ -1348,6 +1383,16 @@ impl AgentPanel {
}
}
}
ActiveView::TextThread { context_editor, .. } => {
let context = context_editor.read(cx).context();
// When switching away from an unsaved text thread, delete its entry.
if context.read(cx).path().is_none() {
let context = context.clone();
self.history_store.update(cx, |store, cx| {
store.remove_recently_opened_entry(&RecentEntry::Context(context), cx);
});
}
}
_ => {}
}
@@ -1355,14 +1400,13 @@ impl AgentPanel {
ActiveView::Thread { thread, .. } => self.history_store.update(cx, |store, cx| {
if let Some(thread) = thread.upgrade() {
let id = thread.read(cx).id().clone();
store.push_recently_opened_entry(HistoryEntryId::Thread(id), cx);
store.push_recently_opened_entry(RecentEntry::Thread(id, thread), cx);
}
}),
ActiveView::TextThread { context_editor, .. } => {
self.history_store.update(cx, |store, cx| {
if let Some(path) = context_editor.read(cx).context().read(cx).path() {
store.push_recently_opened_entry(HistoryEntryId::Context(path.clone()), cx)
}
let context = context_editor.read(cx).context().clone();
store.push_recently_opened_entry(RecentEntry::Context(context), cx)
})
}
_ => {}
@@ -1381,70 +1425,6 @@ impl AgentPanel {
self.focus_handle(cx).focus(window);
}
fn populate_recently_opened_menu_section(
mut menu: ContextMenu,
panel: Entity<Self>,
cx: &mut Context<ContextMenu>,
) -> ContextMenu {
let entries = panel
.read(cx)
.history_store
.read(cx)
.recently_opened_entries(cx);
if entries.is_empty() {
return menu;
}
menu = menu.header("Recently Opened");
for entry in entries {
let title = entry.title().clone();
let id = entry.id();
menu = menu.entry_with_end_slot_on_hover(
title,
None,
{
let panel = panel.downgrade();
let id = id.clone();
move |window, cx| {
let id = id.clone();
panel
.update(cx, move |this, cx| match id {
HistoryEntryId::Thread(id) => this
.open_thread_by_id(&id, window, cx)
.detach_and_log_err(cx),
HistoryEntryId::Context(path) => this
.open_saved_prompt_editor(path.clone(), window, cx)
.detach_and_log_err(cx),
})
.ok();
}
},
IconName::Close,
"Close Entry".into(),
{
let panel = panel.downgrade();
let id = id.clone();
move |_window, cx| {
panel
.update(cx, |this, cx| {
this.history_store.update(cx, |history_store, cx| {
history_store.remove_recently_opened_entry(&id, cx);
});
})
.ok();
}
},
);
}
menu = menu.separator();
menu
}
}
impl Focusable for AgentPanel {
@@ -2369,6 +2349,24 @@ impl AgentPanel {
self.thread.clone().into_any_element()
}
fn configuration_error(&self, cx: &App) -> Option<ConfigurationError> {
let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
return Some(ConfigurationError::NoProvider);
};
if !model.provider.is_authenticated(cx) {
return Some(ConfigurationError::ProviderNotAuthenticated);
}
if model.provider.must_accept_terms(cx) {
return Some(ConfigurationError::ProviderPendingTermsAcceptance(
model.provider,
));
}
None
}
fn render_thread_empty_state(
&self,
window: &mut Window,
@@ -2378,9 +2376,7 @@ impl AgentPanel {
.history_store
.update(cx, |this, cx| this.recent_entries(6, cx));
let model_registry = LanguageModelRegistry::read_global(cx);
let configuration_error =
model_registry.configuration_error(model_registry.default_model(), cx);
let configuration_error = self.configuration_error(cx);
let no_error = configuration_error.is_none();
let focus_handle = self.focus_handle(cx);
@@ -2397,7 +2393,11 @@ impl AgentPanel {
.justify_center()
.items_center()
.gap_1()
.child(h_flex().child(Headline::new("Welcome to the Agent Panel")))
.child(
h_flex().child(
Headline::new("Welcome to the Agent Panel")
),
)
.when(no_error, |parent| {
parent
.child(
@@ -2421,10 +2421,7 @@ impl AgentPanel {
cx,
))
.on_click(|_event, window, cx| {
window.dispatch_action(
NewThread::default().boxed_clone(),
cx,
)
window.dispatch_action(NewThread::default().boxed_clone(), cx)
}),
)
.child(
@@ -2441,10 +2438,7 @@ impl AgentPanel {
cx,
))
.on_click(|_event, window, cx| {
window.dispatch_action(
ToggleContextPicker.boxed_clone(),
cx,
)
window.dispatch_action(ToggleContextPicker.boxed_clone(), cx)
}),
)
.child(
@@ -2461,10 +2455,7 @@ impl AgentPanel {
cx,
))
.on_click(|_event, window, cx| {
window.dispatch_action(
ToggleModelSelector.boxed_clone(),
cx,
)
window.dispatch_action(ToggleModelSelector.boxed_clone(), cx)
}),
)
.child(
@@ -2481,50 +2472,51 @@ impl AgentPanel {
cx,
))
.on_click(|_event, window, cx| {
window.dispatch_action(
OpenConfiguration.boxed_clone(),
cx,
)
window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
}),
)
})
.map(|parent| match configuration_error_ref {
Some(
err @ (ConfigurationError::ModelNotFound
| ConfigurationError::ProviderNotAuthenticated(_)
| ConfigurationError::NoProvider),
) => parent
.child(h_flex().child(
Label::new(err.to_string()).color(Color::Muted).mb_2p5(),
))
.child(
Button::new("settings", "Configure a Provider")
.icon(IconName::Settings)
.icon_position(IconPosition::Start)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.full_width()
.key_binding(KeyBinding::for_action_in(
&OpenConfiguration,
&focus_handle,
window,
cx,
))
.on_click(|_event, window, cx| {
window.dispatch_action(
OpenConfiguration.boxed_clone(),
cx,
.map(|parent| {
match configuration_error_ref {
Some(ConfigurationError::ProviderNotAuthenticated)
| Some(ConfigurationError::NoProvider) => {
parent
.child(
h_flex().child(
Label::new("To start using the agent, configure at least one LLM provider.")
.color(Color::Muted)
.mb_2p5()
)
}),
),
Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
parent.children(provider.render_accept_terms(
LanguageModelProviderTosView::ThreadFreshStart,
cx,
))
)
.child(
Button::new("settings", "Configure a Provider")
.icon(IconName::Settings)
.icon_position(IconPosition::Start)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.full_width()
.key_binding(KeyBinding::for_action_in(
&OpenConfiguration,
&focus_handle,
window,
cx,
))
.on_click(|_event, window, cx| {
window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
}),
)
}
Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
parent.children(
provider.render_accept_terms(
LanguageModelProviderTosView::ThreadFreshStart,
cx,
),
)
}
None => parent,
}
None => parent,
}),
})
)
})
.when(!recent_history.is_empty(), |parent| {
@@ -2559,8 +2551,7 @@ impl AgentPanel {
&self.focus_handle(cx),
window,
cx,
)
.map(|kb| kb.size(rems_from_px(12.))),
).map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(move |_event, window, cx| {
window.dispatch_action(OpenHistory.boxed_clone(), cx);
@@ -2570,68 +2561,79 @@ impl AgentPanel {
.child(
v_flex()
.gap_1()
.children(recent_history.into_iter().enumerate().map(
|(index, entry)| {
.children(
recent_history.into_iter().enumerate().map(|(index, entry)| {
// TODO: Add keyboard navigation.
let is_hovered =
self.hovered_recent_history_item == Some(index);
let is_hovered = self.hovered_recent_history_item == Some(index);
HistoryEntryElement::new(entry.clone(), cx.entity().downgrade())
.hovered(is_hovered)
.on_hover(cx.listener(
move |this, is_hovered, _window, cx| {
if *is_hovered {
this.hovered_recent_history_item = Some(index);
} else if this.hovered_recent_history_item
== Some(index)
{
this.hovered_recent_history_item = None;
}
cx.notify();
},
))
.on_hover(cx.listener(move |this, is_hovered, _window, cx| {
if *is_hovered {
this.hovered_recent_history_item = Some(index);
} else if this.hovered_recent_history_item == Some(index) {
this.hovered_recent_history_item = None;
}
cx.notify();
}))
.into_any_element()
},
)),
}),
)
)
.map(|parent| match configuration_error_ref {
Some(
err @ (ConfigurationError::ModelNotFound
| ConfigurationError::ProviderNotAuthenticated(_)
| ConfigurationError::NoProvider),
) => parent.child(
Banner::new()
.severity(ui::Severity::Warning)
.child(Label::new(err.to_string()).size(LabelSize::Small))
.action_slot(
Button::new("settings", "Configure Provider")
.style(ButtonStyle::Tinted(ui::TintColor::Warning))
.label_size(LabelSize::Small)
.key_binding(
KeyBinding::for_action_in(
&OpenConfiguration,
&focus_handle,
window,
cx,
.map(|parent| {
match configuration_error_ref {
Some(ConfigurationError::ProviderNotAuthenticated)
| Some(ConfigurationError::NoProvider) => {
parent
.child(
Banner::new()
.severity(ui::Severity::Warning)
.child(
Label::new(
"Configure at least one LLM provider to start using the panel.",
)
.size(LabelSize::Small),
)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(|_event, window, cx| {
window.dispatch_action(
OpenConfiguration.boxed_clone(),
cx,
)
}),
),
),
Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
parent.child(Banner::new().severity(ui::Severity::Warning).child(
h_flex().w_full().children(provider.render_accept_terms(
LanguageModelProviderTosView::ThreadtEmptyState,
cx,
)),
))
.action_slot(
Button::new("settings", "Configure Provider")
.style(ButtonStyle::Tinted(ui::TintColor::Warning))
.label_size(LabelSize::Small)
.key_binding(
KeyBinding::for_action_in(
&OpenConfiguration,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(|_event, window, cx| {
window.dispatch_action(
OpenConfiguration.boxed_clone(),
cx,
)
}),
),
)
}
Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
parent
.child(
Banner::new()
.severity(ui::Severity::Warning)
.child(
h_flex()
.w_full()
.children(
provider.render_accept_terms(
LanguageModelProviderTosView::ThreadtEmptyState,
cx,
),
),
),
)
}
None => parent,
}
None => parent,
})
})
}

View File

@@ -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!()
}
}
}

View File

@@ -386,10 +386,8 @@ impl CodegenAlternative {
async { Ok(LanguageModelTextStream::default()) }.boxed_local()
} else {
let request = self.build_request(&model, user_prompt, cx)?;
cx.spawn(async move |_, cx| {
Ok(model.stream_completion_text(request.await, &cx).await?)
})
.boxed_local()
cx.spawn(async move |_, cx| model.stream_completion_text(request.await, &cx).await)
.boxed_local()
};
self.handle_stream(telemetry_id, provider_id.to_string(), api_key, stream, cx);
Ok(())

View File

@@ -734,7 +734,6 @@ impl Display for RulesContext {
#[derive(Debug, Clone)]
pub struct ImageContext {
pub project_path: Option<ProjectPath>,
pub full_path: Option<Arc<Path>>,
pub original_image: Arc<gpui::Image>,
// TODO: handle this elsewhere and remove `ignore-interior-mutability` opt-out in clippy.toml
// needed due to a false positive of `clippy::mutable_key_type`.
@@ -745,7 +744,6 @@ pub struct ImageContext {
pub enum ImageStatus {
Loading,
Error,
Warning,
Ready,
}
@@ -762,17 +760,11 @@ impl ImageContext {
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() {
None => ImageStatus::Loading,
Some(None) => ImageStatus::Error,
Some(Some(_)) => {
if model.is_some_and(|model| !model.supports_images()) {
ImageStatus::Warning
} else {
ImageStatus::Ready
}
}
Some(Some(_)) => ImageStatus::Ready,
}
}

View File

@@ -1,5 +1,7 @@
use std::cell::RefCell;
use std::ops::Range;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
@@ -12,7 +14,7 @@ use http_client::HttpClientWithUrl;
use itertools::Itertools;
use language::{Buffer, CodeLabel, HighlightId};
use lsp::CompletionContext;
use project::{Completion, CompletionIntent, CompletionResponse, ProjectPath, Symbol, WorktreeId};
use project::{Completion, CompletionIntent, ProjectPath, Symbol, WorktreeId};
use prompt_store::PromptStore;
use rope::Point;
use text::{Anchor, OffsetRangeExt, ToPoint};
@@ -214,7 +216,6 @@ fn search(
&entry_candidates,
&query,
false,
true,
100,
&Arc::new(AtomicBool::default()),
executor,
@@ -745,7 +746,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
_trigger: CompletionContext,
_window: &mut Window,
cx: &mut Context<Editor>,
) -> Task<Result<Vec<CompletionResponse>>> {
) -> Task<Result<Option<Vec<Completion>>>> {
let state = buffer.update(cx, |buffer, _cx| {
let position = buffer_position.to_point(buffer);
let line_start = Point::new(position.row, 0);
@@ -755,18 +756,18 @@ impl CompletionProvider for ContextPickerCompletionProvider {
MentionCompletion::try_parse(line, offset_to_line)
});
let Some(state) = state else {
return Task::ready(Ok(Vec::new()));
return Task::ready(Ok(None));
};
let Some((workspace, context_store)) =
self.workspace.upgrade().zip(self.context_store.upgrade())
else {
return Task::ready(Ok(Vec::new()));
return Task::ready(Ok(None));
};
let snapshot = buffer.read(cx).snapshot();
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 text_thread_store = self.text_thread_store.clone();
@@ -814,10 +815,10 @@ impl CompletionProvider for ContextPickerCompletionProvider {
cx.spawn(async move |_, cx| {
let matches = search_task.await;
let Some(editor) = editor.upgrade() else {
return Ok(Vec::new());
return Ok(None);
};
let completions = cx.update(|cx| {
Ok(Some(cx.update(|cx| {
matches
.into_iter()
.filter_map(|mat| match mat {
@@ -900,24 +901,26 @@ impl CompletionProvider for ContextPickerCompletionProvider {
),
})
.collect()
})?;
Ok(vec![CompletionResponse {
completions,
// Since this does its own filtering (see `filter_completions()` returns false),
// there is no benefit to computing whether this set of completions is incomplete.
is_incomplete: true,
}])
})?))
})
}
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(
&self,
buffer: &Entity<language::Buffer>,
position: language::Anchor,
_text: &str,
_trigger_in_words: bool,
_menu_is_open: bool,
_: &str,
_: bool,
cx: &mut Context<Editor>,
) -> bool {
let buffer = buffer.read(cx);
@@ -1066,8 +1069,8 @@ mod tests {
use project::{Project, ProjectPath};
use serde_json::json;
use settings::SettingsStore;
use std::{ops::Deref, rc::Rc};
use util::path;
use std::ops::Deref;
use util::{path, separator};
use workspace::{AppState, Item};
#[test]
@@ -1218,14 +1221,14 @@ mod tests {
let mut cx = VisualTestContext::from_window(*window.deref(), cx);
let paths = vec![
path!("a/one.txt"),
path!("a/two.txt"),
path!("a/three.txt"),
path!("a/four.txt"),
path!("b/five.txt"),
path!("b/six.txt"),
path!("b/seven.txt"),
path!("b/eight.txt"),
separator!("a/one.txt"),
separator!("a/two.txt"),
separator!("a/three.txt"),
separator!("a/four.txt"),
separator!("b/five.txt"),
separator!("b/six.txt"),
separator!("b/seven.txt"),
separator!("b/eight.txt"),
];
let mut opened_editors = Vec::new();

View File

@@ -307,7 +307,6 @@ pub(crate) fn search_symbols(
&visible_match_candidates,
&query,
false,
true,
MAX_MATCHES,
&cancellation_flag,
cx.background_executor().clone(),
@@ -316,7 +315,6 @@ pub(crate) fn search_symbols(
&external_match_candidates,
&query,
false,
true,
MAX_MATCHES - visible_matches.len().min(MAX_MATCHES),
&cancellation_flag,
cx.background_executor().clone(),

View File

@@ -282,18 +282,15 @@ pub fn unordered_thread_entries(
text_thread_store: Entity<TextThreadStore>,
cx: &App,
) -> impl Iterator<Item = (DateTime<Utc>, ThreadContextEntry)> {
let threads = thread_store
.read(cx)
.reverse_chronological_threads()
.map(|thread| {
(
thread.updated_at,
ThreadContextEntry::Thread {
id: thread.id.clone(),
title: thread.summary.clone(),
},
)
});
let threads = thread_store.read(cx).unordered_threads().map(|thread| {
(
thread.updated_at,
ThreadContextEntry::Thread {
id: thread.id.clone(),
title: thread.summary.clone(),
},
)
});
let text_threads = text_thread_store
.read(cx)
@@ -303,7 +300,7 @@ pub fn unordered_thread_entries(
context.mtime.to_utc(),
ThreadContextEntry::Context {
path: context.path.clone(),
title: context.title.clone(),
title: context.title.clone().into(),
},
)
});
@@ -342,7 +339,6 @@ pub(crate) fn search_threads(
&candidates,
&query,
false,
true,
100,
&cancellation_flag,
executor,

View File

@@ -3,21 +3,16 @@ use std::sync::Arc;
use anyhow::Context as _;
use context_server::ContextServerId;
use extension::{ContextServerConfiguration, ExtensionManifest};
use fs::Fs;
use gpui::Task;
use language::LanguageRegistry;
use project::{
context_server_store::registry::ContextServerDescriptorRegistry,
project_settings::ProjectSettings,
};
use settings::update_settings_file;
use project::context_server_store::registry::ContextServerDescriptorRegistry;
use ui::prelude::*;
use util::ResultExt;
use workspace::Workspace;
use crate::agent_configuration::ConfigureContextServerModal;
pub(crate) fn init(language_registry: Arc<LanguageRegistry>, fs: Arc<dyn Fs>, cx: &mut App) {
pub(crate) fn init(language_registry: Arc<LanguageRegistry>, cx: &mut App) {
cx.observe_new(move |_: &mut Workspace, window, cx| {
let Some(window) = window else {
return;
@@ -26,7 +21,6 @@ pub(crate) fn init(language_registry: Arc<LanguageRegistry>, fs: Arc<dyn Fs>, cx
if let Some(extension_events) = extension::ExtensionEvents::try_global(cx).as_ref() {
cx.subscribe_in(extension_events, window, {
let language_registry = language_registry.clone();
let fs = fs.clone();
move |workspace, _, event, window, cx| match event {
extension::Event::ExtensionInstalled(manifest) => {
show_configure_mcp_modal(
@@ -37,13 +31,6 @@ pub(crate) fn init(language_registry: Arc<LanguageRegistry>, fs: Arc<dyn Fs>, cx
cx,
);
}
extension::Event::ExtensionUninstalled(manifest) => {
remove_context_server_settings(
manifest.context_servers.keys().cloned().collect(),
fs.clone(),
cx,
);
}
extension::Event::ConfigureExtensionRequested(manifest) => {
if !manifest.context_servers.is_empty() {
show_configure_mcp_modal(
@@ -68,18 +55,6 @@ pub(crate) fn init(language_registry: Arc<LanguageRegistry>, fs: Arc<dyn Fs>, cx
.detach();
}
fn remove_context_server_settings(
context_server_ids: Vec<Arc<str>>,
fs: Arc<dyn Fs>,
cx: &mut App,
) {
update_settings_file::<ProjectSettings>(fs, cx, move |settings, _| {
settings
.context_servers
.retain(|server_id, _| !context_server_ids.contains(server_id));
});
}
pub enum Configuration {
NotAvailable(ContextServerId, Option<SharedString>),
Required(
@@ -96,10 +71,6 @@ fn show_configure_mcp_modal(
window: &mut Window,
cx: &mut Context<'_, Workspace>,
) {
if !window.is_window_active() {
return;
}
let context_server_store = workspace.project().read(cx).context_server_store();
let repository: Option<SharedString> = manifest.repository.as_ref().map(|s| s.clone().into());

View File

@@ -51,10 +51,6 @@ impl Tool for ContextServerTool {
true
}
fn may_perform_edits(&self) -> bool {
true
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
let mut schema = self.tool.input_schema.clone();
assistant_tool::adapt_schema_to_format(&mut schema, format)?;
@@ -104,15 +100,7 @@ impl Tool for ContextServerTool {
tool_name,
arguments
);
let response = protocol
.request::<context_server::types::requests::CallTool>(
context_server::types::CallToolParams {
name: tool_name,
arguments,
meta: None,
},
)
.await?;
let response = protocol.run_tool(tool_name, arguments).await?;
let mut result = String::new();
for content in response.content {
@@ -123,9 +111,6 @@ impl Tool for ContextServerTool {
types::ToolResponseContent::Image { .. } => {
log::warn!("Ignoring image content from tool response");
}
types::ToolResponseContent::Audio { .. } => {
log::warn!("Ignoring audio content from tool response");
}
types::ToolResponseContent::Resource { .. } => {
log::warn!("Ignoring resource content from tool response");
}

View File

@@ -7,7 +7,7 @@ use assistant_context_editor::AssistantContext;
use collections::{HashSet, IndexSet};
use futures::{self, FutureExt};
use gpui::{App, Context, Entity, EventEmitter, Image, SharedString, Task, WeakEntity};
use language::{Buffer, File as _};
use language::Buffer;
use language_model::LanguageModelImage;
use project::image_store::is_image_file;
use project::{Project, ProjectItem, ProjectPath, Symbol};
@@ -304,13 +304,11 @@ impl ContextStore {
project.open_image(project_path.clone(), cx)
})?;
let image_item = open_image_task.await?;
let image = image_item.read_with(cx, |image_item, _| image_item.image.clone())?;
this.update(cx, |this, cx| {
let item = image_item.read(cx);
this.insert_image(
Some(item.project_path(cx)),
Some(item.file.full_path(cx).into()),
item.image.clone(),
Some(image_item.read(cx).project_path(cx)),
image,
remove_if_exists,
cx,
)
@@ -319,13 +317,12 @@ impl ContextStore {
}
pub fn add_image_instance(&mut self, image: Arc<Image>, cx: &mut Context<ContextStore>) {
self.insert_image(None, None, image, false, cx);
self.insert_image(None, image, false, cx);
}
fn insert_image(
&mut self,
project_path: Option<ProjectPath>,
full_path: Option<Arc<Path>>,
image: Arc<Image>,
remove_if_exists: bool,
cx: &mut Context<ContextStore>,
@@ -333,7 +330,6 @@ impl ContextStore {
let image_task = LanguageModelImage::from_image(image.clone(), cx).shared();
let context = AgentContextHandle::Image(ImageContext {
project_path,
full_path,
original_image: image,
image_task,
context_id: self.next_context_id.post_inc(),

View File

@@ -23,7 +23,7 @@ use crate::thread_store::{TextThreadStore, ThreadStore};
use crate::ui::{AddedContext, ContextPill};
use crate::{
AcceptSuggestedContext, AgentPanel, FocusDown, FocusLeft, FocusRight, FocusUp,
ModelUsageContext, RemoveAllContext, RemoveFocusedContext, ToggleContextPicker,
RemoveAllContext, RemoveFocusedContext, ToggleContextPicker,
};
pub struct ContextStrip {
@@ -37,7 +37,6 @@ pub struct ContextStrip {
_subscriptions: Vec<Subscription>,
focused_index: Option<usize>,
children_bounds: Option<Vec<Bounds<Pixels>>>,
model_usage_context: ModelUsageContext,
}
impl ContextStrip {
@@ -48,7 +47,6 @@ impl ContextStrip {
text_thread_store: Option<WeakEntity<TextThreadStore>>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
suggest_context_kind: SuggestContextKind,
model_usage_context: ModelUsageContext,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
@@ -83,7 +81,6 @@ impl ContextStrip {
_subscriptions: subscriptions,
focused_index: None,
children_bounds: None,
model_usage_context,
}
}
@@ -101,20 +98,11 @@ impl ContextStrip {
.as_ref()
.and_then(|thread_store| thread_store.upgrade())
.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
.read(cx)
.context()
.flat_map(|context| {
AddedContext::new_pending(
context.clone(),
prompt_store,
project,
current_model.as_ref(),
cx,
)
AddedContext::new_pending(context.clone(), prompt_store, project, cx)
})
.collect::<Vec<_>>()
} else {

View File

@@ -1,17 +1,18 @@
use std::{collections::VecDeque, path::Path, sync::Arc};
use anyhow::{Context as _, Result};
use assistant_context_editor::SavedContextMetadata;
use anyhow::Context as _;
use assistant_context_editor::{AssistantContext, SavedContextMetadata};
use chrono::{DateTime, Utc};
use gpui::{AsyncApp, Entity, SharedString, Task, prelude::*};
use itertools::Itertools;
use paths::contexts_dir;
use futures::future::{TryFutureExt as _, join_all};
use gpui::{Entity, Task, prelude::*};
use serde::{Deserialize, Serialize};
use smol::future::FutureExt;
use std::time::Duration;
use ui::App;
use ui::{App, SharedString, Window};
use util::ResultExt as _;
use crate::{
Thread,
thread::ThreadId,
thread_store::{SerializedThreadMetadata, ThreadStore},
};
@@ -40,34 +41,52 @@ impl HistoryEntry {
HistoryEntry::Context(context) => HistoryEntryId::Context(context.path.clone()),
}
}
pub fn title(&self) -> &SharedString {
match self {
HistoryEntry::Thread(thread) => &thread.summary,
HistoryEntry::Context(context) => &context.title,
}
}
}
/// Generic identifier for a history entry.
#[derive(Clone, PartialEq, Eq, Debug)]
#[derive(Clone, PartialEq, Eq)]
pub enum HistoryEntryId {
Thread(ThreadId),
Context(Arc<Path>),
}
#[derive(Clone, Debug)]
pub(crate) enum RecentEntry {
Thread(ThreadId, Entity<Thread>),
Context(Entity<AssistantContext>),
}
impl PartialEq for RecentEntry {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Thread(l0, _), Self::Thread(r0, _)) => l0 == r0,
(Self::Context(l0), Self::Context(r0)) => l0 == r0,
_ => false,
}
}
}
impl Eq for RecentEntry {}
impl RecentEntry {
pub(crate) fn summary(&self, cx: &App) -> SharedString {
match self {
RecentEntry::Thread(_, thread) => thread.read(cx).summary().or_default(),
RecentEntry::Context(context) => context.read(cx).summary().or_default(),
}
}
}
#[derive(Serialize, Deserialize)]
enum SerializedRecentOpen {
enum SerializedRecentEntry {
Thread(String),
ContextName(String),
/// Old format which stores the full path
Context(String),
}
pub struct HistoryStore {
thread_store: Entity<ThreadStore>,
context_store: Entity<assistant_context_editor::ContextStore>,
recently_opened_entries: VecDeque<HistoryEntryId>,
recently_opened_entries: VecDeque<RecentEntry>,
_subscriptions: Vec<gpui::Subscription>,
_save_recently_opened_entries_task: Task<()>,
}
@@ -76,7 +95,8 @@ impl HistoryStore {
pub fn new(
thread_store: Entity<ThreadStore>,
context_store: Entity<assistant_context_editor::ContextStore>,
initial_recent_entries: impl IntoIterator<Item = HistoryEntryId>,
initial_recent_entries: impl IntoIterator<Item = RecentEntry>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let subscriptions = vec![
@@ -84,20 +104,68 @@ impl HistoryStore {
cx.observe(&context_store, |_, _, cx| cx.notify()),
];
cx.spawn(async move |this, cx| {
let entries = Self::load_recently_opened_entries(cx).await.log_err()?;
this.update(cx, |this, _| {
this.recently_opened_entries
.extend(
entries.into_iter().take(
MAX_RECENTLY_OPENED_ENTRIES
.saturating_sub(this.recently_opened_entries.len()),
),
);
window
.spawn(cx, {
let thread_store = thread_store.downgrade();
let context_store = context_store.downgrade();
let this = cx.weak_entity();
async move |cx| {
let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH);
let contents = cx
.background_spawn(async move { std::fs::read_to_string(path) })
.await
.ok()?;
let entries = serde_json::from_str::<Vec<SerializedRecentEntry>>(&contents)
.context("deserializing persisted agent panel navigation history")
.log_err()?
.into_iter()
.take(MAX_RECENTLY_OPENED_ENTRIES)
.map(|serialized| match serialized {
SerializedRecentEntry::Thread(id) => thread_store
.update_in(cx, |thread_store, window, cx| {
let thread_id = ThreadId::from(id.as_str());
thread_store
.open_thread(&thread_id, window, cx)
.map_ok(|thread| RecentEntry::Thread(thread_id, thread))
.boxed()
})
.unwrap_or_else(|_| {
async {
anyhow::bail!("no thread store");
}
.boxed()
}),
SerializedRecentEntry::Context(id) => context_store
.update(cx, |context_store, cx| {
context_store
.open_local_context(Path::new(&id).into(), cx)
.map_ok(RecentEntry::Context)
.boxed()
})
.unwrap_or_else(|_| {
async {
anyhow::bail!("no context store");
}
.boxed()
}),
});
let entries = join_all(entries)
.await
.into_iter()
.filter_map(|result| result.log_err())
.collect::<VecDeque<_>>();
this.update(cx, |this, _| {
this.recently_opened_entries.extend(entries);
this.recently_opened_entries
.truncate(MAX_RECENTLY_OPENED_ENTRIES);
})
.ok();
Some(())
}
})
.ok()
})
.detach();
.detach();
Self {
thread_store,
@@ -116,20 +184,19 @@ impl HistoryStore {
return history_entries;
}
history_entries.extend(
self.thread_store
.read(cx)
.reverse_chronological_threads()
.cloned()
.map(HistoryEntry::Thread),
);
history_entries.extend(
self.context_store
.read(cx)
.unordered_contexts()
.cloned()
.map(HistoryEntry::Context),
);
for thread in self
.thread_store
.update(cx, |this, _cx| this.reverse_chronological_threads())
{
history_entries.push(HistoryEntry::Thread(thread));
}
for context in self
.context_store
.update(cx, |this, _cx| this.reverse_chronological_contexts())
{
history_entries.push(HistoryEntry::Context(context));
}
history_entries.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.updated_at()));
history_entries
@@ -139,62 +206,15 @@ impl HistoryStore {
self.entries(cx).into_iter().take(limit).collect()
}
pub fn recently_opened_entries(&self, cx: &App) -> Vec<HistoryEntry> {
#[cfg(debug_assertions)]
if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
return Vec::new();
}
let thread_entries = self
.thread_store
.read(cx)
.reverse_chronological_threads()
.flat_map(|thread| {
self.recently_opened_entries
.iter()
.enumerate()
.flat_map(|(index, entry)| match entry {
HistoryEntryId::Thread(id) if &thread.id == id => {
Some((index, HistoryEntry::Thread(thread.clone())))
}
_ => None,
})
});
let context_entries =
self.context_store
.read(cx)
.unordered_contexts()
.flat_map(|context| {
self.recently_opened_entries
.iter()
.enumerate()
.flat_map(|(index, entry)| match entry {
HistoryEntryId::Context(path) if &context.path == path => {
Some((index, HistoryEntry::Context(context.clone())))
}
_ => None,
})
});
thread_entries
.chain(context_entries)
// optimization to halt iteration early
.take(self.recently_opened_entries.len())
.sorted_unstable_by_key(|(index, _)| *index)
.map(|(_, entry)| entry)
.collect()
}
fn save_recently_opened_entries(&mut self, cx: &mut Context<Self>) {
let serialized_entries = self
.recently_opened_entries
.iter()
.filter_map(|entry| match entry {
HistoryEntryId::Context(path) => path.file_name().map(|file| {
SerializedRecentOpen::ContextName(file.to_string_lossy().to_string())
}),
HistoryEntryId::Thread(id) => Some(SerializedRecentOpen::Thread(id.to_string())),
RecentEntry::Context(context) => Some(SerializedRecentEntry::Context(
context.read(cx).path()?.to_str()?.to_owned(),
)),
RecentEntry::Thread(id, _) => Some(SerializedRecentEntry::Thread(id.to_string())),
})
.collect::<Vec<_>>();
@@ -213,33 +233,7 @@ impl HistoryStore {
});
}
fn load_recently_opened_entries(cx: &AsyncApp) -> Task<Result<Vec<HistoryEntryId>>> {
cx.background_spawn(async move {
let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH);
let contents = smol::fs::read_to_string(path).await?;
let entries = serde_json::from_str::<Vec<SerializedRecentOpen>>(&contents)
.context("deserializing persisted agent panel navigation history")?
.into_iter()
.take(MAX_RECENTLY_OPENED_ENTRIES)
.flat_map(|entry| match entry {
SerializedRecentOpen::Thread(id) => {
Some(HistoryEntryId::Thread(id.as_str().into()))
}
SerializedRecentOpen::ContextName(file_name) => Some(HistoryEntryId::Context(
contexts_dir().join(file_name).into(),
)),
SerializedRecentOpen::Context(path) => {
Path::new(&path).file_name().map(|file_name| {
HistoryEntryId::Context(contexts_dir().join(file_name).into())
})
}
})
.collect::<Vec<_>>();
Ok(entries)
})
}
pub fn push_recently_opened_entry(&mut self, entry: HistoryEntryId, cx: &mut Context<Self>) {
pub fn push_recently_opened_entry(&mut self, entry: RecentEntry, cx: &mut Context<Self>) {
self.recently_opened_entries
.retain(|old_entry| old_entry != &entry);
self.recently_opened_entries.push_front(entry);
@@ -250,33 +244,24 @@ impl HistoryStore {
pub fn remove_recently_opened_thread(&mut self, id: ThreadId, cx: &mut Context<Self>) {
self.recently_opened_entries.retain(|entry| match entry {
HistoryEntryId::Thread(thread_id) if thread_id == &id => false,
RecentEntry::Thread(thread_id, _) if thread_id == &id => false,
_ => true,
});
self.save_recently_opened_entries(cx);
}
pub fn replace_recently_opened_text_thread(
&mut self,
old_path: &Path,
new_path: &Arc<Path>,
cx: &mut Context<Self>,
) {
for entry in &mut self.recently_opened_entries {
match entry {
HistoryEntryId::Context(path) if path.as_ref() == old_path => {
*entry = HistoryEntryId::Context(new_path.clone());
break;
}
_ => {}
}
}
self.save_recently_opened_entries(cx);
}
pub fn remove_recently_opened_entry(&mut self, entry: &HistoryEntryId, cx: &mut Context<Self>) {
pub fn remove_recently_opened_entry(&mut self, entry: &RecentEntry, cx: &mut Context<Self>) {
self.recently_opened_entries
.retain(|old_entry| old_entry != entry);
self.save_recently_opened_entries(cx);
}
pub fn recently_opened_entries(&self, _cx: &mut Context<Self>) -> VecDeque<RecentEntry> {
#[cfg(debug_assertions)]
if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
return VecDeque::new();
}
self.recently_opened_entries.clone()
}
}

View File

@@ -24,7 +24,6 @@ use gpui::{
WeakEntity, Window, point,
};
use language::{Buffer, Point, Selection, TransactionId};
use language_model::ConfigurationError;
use language_model::ConfiguredModel;
use language_model::{LanguageModelRegistry, report_assistant_event};
use multi_buffer::MultiBufferRow;
@@ -39,7 +38,8 @@ use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
use text::{OffsetRangeExt, ToPoint as _};
use ui::prelude::*;
use util::{RangeExt, ResultExt, maybe};
use util::RangeExt;
use util::ResultExt;
use workspace::{ItemHandle, Toast, Workspace, dock::Panel, notifications::NotificationId};
use zed_actions::agent::OpenConfiguration;
@@ -233,9 +233,10 @@ impl InlineAssistant {
return;
};
let configuration_error = || {
let model_registry = LanguageModelRegistry::read_global(cx);
model_registry.configuration_error(model_registry.inline_assistant_model(), cx)
let is_authenticated = || {
LanguageModelRegistry::read_global(cx)
.inline_assistant_model()
.map_or(false, |model| model.provider.is_authenticated(cx))
};
let Some(agent_panel) = workspace.panel::<AgentPanel>(cx) else {
@@ -283,23 +284,20 @@ impl InlineAssistant {
}
};
if let Some(error) = configuration_error() {
if let ConfigurationError::ProviderNotAuthenticated(provider) = error {
cx.spawn(async move |_, cx| {
cx.update(|cx| provider.authenticate(cx))?.await?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
if configuration_error().is_none() {
handle_assist(window, cx);
}
} else {
cx.spawn_in(window, async move |_, cx| {
if is_authenticated() {
handle_assist(window, cx);
} else {
cx.spawn_in(window, async move |_workspace, cx| {
let Some(task) = cx.update(|_, cx| {
LanguageModelRegistry::read_global(cx)
.inline_assistant_model()
.map_or(None, |model| Some(model.provider.authenticate(cx)))
})?
else {
let answer = cx
.prompt(
gpui::PromptLevel::Warning,
&error.to_string(),
"No language model provider configured",
None,
&["Configure", "Cancel"],
)
@@ -313,12 +311,17 @@ impl InlineAssistant {
.ok();
}
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
return Ok(());
};
task.await?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
if is_authenticated() {
handle_assist(window, cx);
}
} else {
handle_assist(window, cx);
}
}
@@ -765,6 +768,9 @@ impl InlineAssistant {
PromptEditorEvent::CancelRequested => {
self.finish_assist(assist_id, true, window, cx);
}
PromptEditorEvent::DismissRequested => {
self.dismiss_assist(assist_id, window, cx);
}
PromptEditorEvent::Resized { .. } => {
// This only matters for the terminal inline assistant
}
@@ -1005,7 +1011,7 @@ impl InlineAssistant {
self.update_editor_highlights(&editor, cx);
}
} else {
entry.get_mut().highlight_updates.send(()).ok();
entry.get().highlight_updates.send(()).ok();
}
}
@@ -1165,31 +1171,27 @@ impl InlineAssistant {
selections.select_anchor_ranges([position..position])
});
let mut scroll_target_range = None;
let mut scroll_target_top;
let mut scroll_target_bottom;
if let Some(decorations) = assist.decorations.as_ref() {
scroll_target_range = maybe!({
let top = editor.row_for_block(decorations.prompt_block_id, cx)?.0 as f32;
let bottom = editor.row_for_block(decorations.end_block_id, cx)?.0 as f32;
Some((top, bottom))
});
if scroll_target_range.is_none() {
log::error!("bug: failed to find blocks for scrolling to inline assist");
}
}
let scroll_target_range = scroll_target_range.unwrap_or_else(|| {
scroll_target_top = editor
.row_for_block(decorations.prompt_block_id, cx)
.unwrap()
.0 as f32;
scroll_target_bottom = editor
.row_for_block(decorations.end_block_id, cx)
.unwrap()
.0 as f32;
} else {
let snapshot = editor.snapshot(window, cx);
let start_row = assist
.range
.start
.to_display_point(&snapshot.display_snapshot)
.row();
let top = start_row.0 as f32;
let bottom = top + 1.0;
(top, bottom)
});
let mut scroll_target_top = scroll_target_range.0;
let mut scroll_target_bottom = scroll_target_range.1;
scroll_target_top = start_row.0 as f32;
scroll_target_bottom = scroll_target_top + 1.;
}
scroll_target_top -= editor.vertical_scroll_margin() as f32;
scroll_target_bottom += editor.vertical_scroll_margin() as f32;
@@ -1329,7 +1331,7 @@ impl InlineAssistant {
editor.clear_gutter_highlights::<GutterPendingRange>(cx);
} else {
editor.highlight_gutter::<GutterPendingRange>(
gutter_pending_ranges,
&gutter_pending_ranges,
|cx| cx.theme().status().info_background,
cx,
)
@@ -1340,7 +1342,7 @@ impl InlineAssistant {
editor.clear_gutter_highlights::<GutterTransformedRange>(cx);
} else {
editor.highlight_gutter::<GutterTransformedRange>(
gutter_transformed_ranges,
&gutter_transformed_ranges,
|cx| cx.theme().status().info,
cx,
)
@@ -1517,7 +1519,7 @@ impl InlineAssistant {
struct EditorInlineAssists {
assist_ids: Vec<InlineAssistId>,
scroll_lock: Option<InlineAssistScrollLock>,
highlight_updates: watch::Sender<()>,
highlight_updates: async_watch::Sender<()>,
_update_highlights: Task<Result<()>>,
_subscriptions: Vec<gpui::Subscription>,
}
@@ -1529,7 +1531,7 @@ struct InlineAssistScrollLock {
impl EditorInlineAssists {
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 {
assist_ids: Vec::new(),
scroll_lock: None,
@@ -1687,7 +1689,7 @@ impl InlineAssist {
if let Some(editor) = editor.upgrade() {
InlineAssistant::update_global(cx, |this, cx| {
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();
}

View File

@@ -1,4 +1,4 @@
use crate::agent_model_selector::AgentModelSelector;
use crate::agent_model_selector::{AgentModelSelector, ModelType};
use crate::buffer_codegen::BufferCodegen;
use crate::context::ContextCreasesAddon;
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::terminal_codegen::TerminalCodegen;
use crate::thread_store::{TextThreadStore, ThreadStore};
use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext};
use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist};
use crate::{RemoveAllContext, ToggleContextPicker};
use assistant_context_editor::language_model_selector::ToggleModelSelector;
use client::ErrorExt;
use collections::VecDeque;
use db::kvp::Dismissable;
use editor::actions::Paste;
use editor::display_map::EditorMargins;
use editor::{
ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
@@ -100,7 +99,6 @@ impl<T: 'static> Render for PromptEditor<T> {
v_flex()
.key_context("PromptEditor")
.capture_action(cx.listener(Self::paste))
.bg(cx.theme().colors().editor_background)
.block_mouse_except_scroll()
.gap_0p5()
@@ -261,7 +259,7 @@ impl<T: 'static> PromptEditor<T> {
let focus = self.editor.focus_handle(cx).contains_focused(window, cx);
self.editor = cx.new(|cx| {
let mut editor = Editor::auto_height(1, Self::MAX_LINES as usize, window, cx);
let mut editor = Editor::auto_height(Self::MAX_LINES as usize, window, cx);
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
editor.set_placeholder_text("Add a prompt…", cx);
editor.set_text(prompt, window, cx);
@@ -305,10 +303,6 @@ impl<T: 'static> PromptEditor<T> {
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(
&mut self,
_: &ClickEvent,
@@ -403,7 +397,9 @@ impl<T: 'static> PromptEditor<T> {
CodegenStatus::Idle => {
cx.emit(PromptEditorEvent::StartRequested);
}
CodegenStatus::Pending => {}
CodegenStatus::Pending => {
cx.emit(PromptEditorEvent::DismissRequested);
}
CodegenStatus::Done => {
if self.edited_since_done {
cx.emit(PromptEditorEvent::StartRequested);
@@ -829,6 +825,7 @@ pub enum PromptEditorEvent {
StopRequested,
ConfirmRequested { execute: bool },
CancelRequested,
DismissRequested,
Resized { height_in_lines: u8 },
}
@@ -869,7 +866,6 @@ impl PromptEditor<BufferCodegen> {
let prompt_editor = cx.new(|cx| {
let mut editor = Editor::new(
EditorMode::AutoHeight {
min_lines: 1,
max_lines: Self::MAX_LINES as usize,
},
prompt_buffer,
@@ -916,7 +912,6 @@ impl PromptEditor<BufferCodegen> {
text_thread_store.clone(),
context_picker_menu_handle.clone(),
SuggestContextKind::Thread,
ModelUsageContext::InlineAssistant,
window,
cx,
)
@@ -935,7 +930,7 @@ impl PromptEditor<BufferCodegen> {
fs,
model_selector_menu_handle,
prompt_editor.focus_handle(cx),
ModelUsageContext::InlineAssistant,
ModelType::InlineAssistant,
window,
cx,
)
@@ -1048,7 +1043,6 @@ impl PromptEditor<TerminalCodegen> {
let prompt_editor = cx.new(|cx| {
let mut editor = Editor::new(
EditorMode::AutoHeight {
min_lines: 1,
max_lines: Self::MAX_LINES as usize,
},
prompt_buffer,
@@ -1089,7 +1083,6 @@ impl PromptEditor<TerminalCodegen> {
text_thread_store.clone(),
context_picker_menu_handle.clone(),
SuggestContextKind::Thread,
ModelUsageContext::InlineAssistant,
window,
cx,
)
@@ -1108,7 +1101,7 @@ impl PromptEditor<TerminalCodegen> {
fs,
model_selector_menu_handle.clone(),
prompt_editor.focus_handle(cx),
ModelUsageContext::InlineAssistant,
ModelType::InlineAssistant,
window,
cx,
)

View File

@@ -2,11 +2,11 @@ use std::collections::BTreeMap;
use std::rc::Rc;
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::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip};
use crate::ui::{
MaxModeTooltip,
AnimatedLabel, MaxModeTooltip,
preview::{AgentPreview, UsageCallout},
};
use agent_settings::{AgentSettings, CompletionMode};
@@ -24,10 +24,10 @@ use fs::Fs;
use futures::future::Shared;
use futures::{FutureExt as _, future};
use gpui::{
Animation, AnimationExt, App, Entity, EventEmitter, Focusable, Subscription, Task, TextStyle,
WeakEntity, linear_color_stop, linear_gradient, point, pulsating_between,
Animation, AnimationExt, App, ClipboardEntry, Entity, EventEmitter, Focusable, Subscription,
Task, TextStyle, WeakEntity, linear_color_stop, linear_gradient, point, pulsating_between,
};
use language::{Buffer, Language, Point};
use language::{Buffer, Language};
use language_model::{
ConfiguredModel, LanguageModelRequestMessage, MessageContent, RequestUsage,
ZED_CLOUD_PROVIDER_ID,
@@ -39,9 +39,7 @@ use proto::Plan;
use settings::Settings;
use std::time::Duration;
use theme::ThemeSettings;
use ui::{
Callout, Disclosure, Divider, DividerColor, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*,
};
use ui::{Disclosure, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*};
use util::{ResultExt as _, maybe};
use workspace::{CollaboratorId, Workspace};
use zed_llm_client::CompletionIntent;
@@ -53,9 +51,9 @@ use crate::profile_selector::ProfileSelector;
use crate::thread::{MessageCrease, Thread, TokenUsageRatio};
use crate::thread_store::{TextThreadStore, ThreadStore};
use crate::{
ActiveThread, AgentDiffPane, Chat, ChatWithFollow, ExpandMessageEditor, Follow, KeepAll,
ModelUsageContext, NewThread, OpenAgentDiff, RejectAll, RemoveAllContext, ToggleBurnMode,
ToggleContextPicker, ToggleProfileSelector, register_agent_preview,
ActiveThread, AgentDiffPane, Chat, ChatWithFollow, ExpandMessageEditor, Follow, NewThread,
OpenAgentDiff, RemoveAllContext, ToggleBurnMode, ToggleContextPicker, ToggleProfileSelector,
register_agent_preview,
};
#[derive(RegisterComponent)]
@@ -76,12 +74,11 @@ pub struct MessageEditor {
profile_selector: Entity<ProfileSelector>,
edits_expanded: bool,
editor_is_expanded: bool,
last_estimated_token_count: Option<u64>,
last_estimated_token_count: Option<usize>,
update_token_count_task: Option<Task<()>>,
_subscriptions: Vec<Subscription>,
}
const MIN_EDITOR_LINES: usize = 4;
const MAX_EDITOR_LINES: usize = 8;
pub(crate) fn create_editor(
@@ -105,7 +102,6 @@ pub(crate) fn create_editor(
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
let mut editor = Editor::new(
editor::EditorMode::AutoHeight {
min_lines: MIN_EDITOR_LINES,
max_lines: MAX_EDITOR_LINES,
},
buffer,
@@ -116,7 +112,6 @@ pub(crate) fn create_editor(
editor.set_placeholder_text("Message the agent @ to include context", cx);
editor.set_show_indent_guides(false, cx);
editor.set_soft_wrap();
editor.set_use_modal_editing(true);
editor.set_context_menu_options(ContextMenuOptions {
min_entries_visible: 12,
max_entries_visible: 12,
@@ -173,13 +168,13 @@ impl MessageEditor {
Some(text_thread_store.clone()),
context_picker_menu_handle.clone(),
SuggestContextKind::File,
ModelUsageContext::Thread(thread.clone()),
window,
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![
cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event),
@@ -201,14 +196,21 @@ impl MessageEditor {
fs.clone(),
model_selector_menu_handle,
editor.focus_handle(cx),
ModelUsageContext::Thread(thread.clone()),
ModelType::Default(thread.clone()),
window,
cx,
)
});
let profile_selector =
cx.new(|cx| ProfileSelector::new(fs, thread.clone(), editor.focus_handle(cx), cx));
let profile_selector = cx.new(|cx| {
ProfileSelector::new(
fs,
thread.clone(),
thread_store,
editor.focus_handle(cx),
cx,
)
});
Self {
editor: editor.clone(),
@@ -257,7 +259,6 @@ impl MessageEditor {
})
} else {
editor.set_mode(EditorMode::AutoHeight {
min_lines: MIN_EDITOR_LINES,
max_lines: MAX_EDITOR_LINES,
})
}
@@ -301,7 +302,6 @@ impl MessageEditor {
self.set_editor_is_expanded(false, cx);
self.send_to_model(window, cx);
cx.emit(MessageEditorEvent::ScrollThreadToBottom);
cx.notify();
}
@@ -430,7 +430,31 @@ impl MessageEditor {
}
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>) {
@@ -439,11 +463,6 @@ impl MessageEditor {
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(
&self,
buffer: Entity<Buffer>,
@@ -474,81 +493,7 @@ 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 handle_reject_file_changes(
&mut self,
buffer: Entity<Buffer>,
_window: &mut Window,
cx: &mut Context<Self>,
) {
if self.thread.read(cx).has_pending_edit_tool_uses() {
return;
}
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 handle_accept_file_changes(
&mut self,
buffer: Entity<Buffer>,
_window: &mut Window,
cx: &mut Context<Self>,
) {
if self.thread.read(cx).has_pending_edit_tool_uses() {
return;
}
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.keep_edits_in_range(buffer, start..end, cx);
});
cx.notify();
}
fn render_burn_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 model = thread.configured_model();
if !model?.model.supports_max_mode() {
@@ -669,12 +614,6 @@ impl MessageEditor {
.on_action(cx.listener(Self::move_up))
.on_action(cx.listener(Self::expand_message_editor))
.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))
.gap_2()
.p_2()
@@ -683,87 +622,96 @@ impl MessageEditor {
.border_color(cx.theme().colors().border)
.child(
h_flex()
.items_start()
.justify_between()
.child(self.context_strip.clone())
.when(focus_handle.is_focused(window), |this| {
this.child(
IconButton::new("toggle-height", expand_icon)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
let expand_label = if is_editor_expanded {
"Minimize Message Editor".to_string()
} else {
"Expand Message Editor".to_string()
};
.child(
h_flex()
.gap_1()
.when(focus_handle.is_focused(window), |this| {
this.child(
IconButton::new("toggle-height", expand_icon)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
let expand_label = if is_editor_expanded {
"Minimize Message Editor".to_string()
} else {
"Expand Message Editor".to_string()
};
Tooltip::for_action_in(
expand_label,
&ExpandMessageEditor,
&focus_handle,
window,
cx,
)
}
})
.on_click(cx.listener(|_, _, window, cx| {
window.dispatch_action(Box::new(ExpandMessageEditor), cx);
})),
)
}),
Tooltip::for_action_in(
expand_label,
&ExpandMessageEditor,
&focus_handle,
window,
cx,
)
}
})
.on_click(cx.listener(|_, _, window, cx| {
window
.dispatch_action(Box::new(ExpandMessageEditor), cx);
})),
)
}),
),
)
.child(
v_flex()
.size_full()
.gap_1()
.gap_4()
.when(is_editor_expanded, |this| {
this.h(vh(0.8, window)).justify_between()
})
.child({
let settings = ThemeSettings::get_global(cx);
let font_size = TextSize::Small
.rems(cx)
.to_pixels(settings.agent_font_size(cx));
let line_height = settings.buffer_line_height.value() * font_size;
.child(
v_flex()
.min_h_16()
.when(is_editor_expanded, |this| this.h_full())
.child({
let settings = ThemeSettings::get_global(cx);
let font_size = TextSize::Small
.rems(cx)
.to_pixels(settings.agent_font_size(cx));
let line_height = settings.buffer_line_height.value() * font_size;
let text_style = TextStyle {
color: cx.theme().colors().text,
font_family: settings.buffer_font.family.clone(),
font_fallbacks: settings.buffer_font.fallbacks.clone(),
font_features: settings.buffer_font.features.clone(),
font_size: font_size.into(),
line_height: line_height.into(),
..Default::default()
};
let text_style = TextStyle {
color: cx.theme().colors().text,
font_family: settings.buffer_font.family.clone(),
font_fallbacks: settings.buffer_font.fallbacks.clone(),
font_features: settings.buffer_font.features.clone(),
font_size: font_size.into(),
line_height: line_height.into(),
..Default::default()
};
EditorElement::new(
&self.editor,
EditorStyle {
background: editor_bg_color,
local_player: cx.theme().players().local(),
text: text_style,
syntax: cx.theme().syntax().clone(),
..Default::default()
},
)
.into_any()
})
EditorElement::new(
&self.editor,
EditorStyle {
background: editor_bg_color,
local_player: cx.theme().players().local(),
text: text_style,
syntax: cx.theme().syntax().clone(),
..Default::default()
},
)
.into_any()
}),
)
.child(
h_flex()
.flex_none()
.flex_wrap()
.justify_between()
.child(
h_flex()
.child(self.render_follow_toggle(cx))
.children(self.render_burn_mode_toggle(cx)),
.children(self.render_max_mode_toggle(cx)),
)
.child(
h_flex()
.gap_1()
.flex_wrap()
.when(!incompatible_tools.is_empty(), |this| {
this.child(
IconButton::new(
@@ -907,7 +855,7 @@ impl MessageEditor {
)
}
fn render_edits_bar(
fn render_changed_buffers(
&self,
changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
window: &mut Window,
@@ -921,10 +869,7 @@ impl MessageEditor {
let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
let is_edit_changes_expanded = self.edits_expanded;
let thread = self.thread.read(cx);
let pending_edits = thread.has_pending_edit_tool_uses();
const EDIT_NOT_READY_TOOLTIP_LABEL: &str = "Wait until file edits are complete.";
let is_generating = self.thread.read(cx).is_generating();
v_flex()
.mt_1()
@@ -942,28 +887,31 @@ impl MessageEditor {
}])
.child(
h_flex()
.p_1()
.id("edits-container")
.cursor_pointer()
.p_1p5()
.justify_between()
.when(is_edit_changes_expanded, |this| {
this.border_b_1().border_color(border_color)
})
.on_click(
cx.listener(|this, _, window, cx| this.handle_review_click(window, cx)),
)
.child(
h_flex()
.id("edits-container")
.cursor_pointer()
.w_full()
.gap_1()
.child(
Disclosure::new("edits-disclosure", is_edit_changes_expanded)
.on_click(cx.listener(|this, _, _, cx| {
this.handle_edit_bar_expand(cx)
.on_click(cx.listener(|this, _ev, _window, cx| {
this.edits_expanded = !this.edits_expanded;
cx.notify();
})),
)
.map(|this| {
if pending_edits {
if is_generating {
this.child(
Label::new(format!(
"Editing {} {}",
AnimatedLabel::new(format!(
"Editing {} {}",
changed_buffers.len(),
if changed_buffers.len() == 1 {
"file"
@@ -971,15 +919,7 @@ impl MessageEditor {
"files"
}
))
.color(Color::Muted)
.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),
),
.size(LabelSize::Small),
)
} else {
this.child(
@@ -1004,74 +944,23 @@ impl MessageEditor {
.color(Color::Muted),
)
}
})
.on_click(
cx.listener(|this, _, _, cx| this.handle_edit_bar_expand(cx)),
),
}),
)
.child(
h_flex()
.gap_1()
.child(
IconButton::new("review-changes", IconName::ListTodo)
.icon_size(IconSize::Small)
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(
"Review Changes",
&OpenAgentDiff,
&focus_handle,
window,
cx,
)
}
})
.on_click(cx.listener(|this, _, window, cx| {
this.handle_review_click(window, cx)
})),
Button::new("review", "Review Changes")
.label_size(LabelSize::Small)
.key_binding(
KeyBinding::for_action_in(
&OpenAgentDiff,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(12.))),
)
.child(Divider::vertical().color(DividerColor::Border))
.child(
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)
})),
),
.on_click(cx.listener(|this, _, window, cx| {
this.handle_review_click(window, cx)
})),
),
)
.when(is_edit_changes_expanded, |parent| {
@@ -1081,7 +970,7 @@ impl MessageEditor {
let file = buffer.read(cx).file()?;
let path = file.path();
let file_path = path.parent().and_then(|parent| {
let parent_label = path.parent().and_then(|parent| {
let parent_str = parent.to_string_lossy();
if parent_str.is_empty() {
@@ -1100,7 +989,7 @@ impl MessageEditor {
}
});
let file_name = path.file_name().map(|name| {
let name_label = path.file_name().map(|name| {
Label::new(name.to_string_lossy().to_string())
.size(LabelSize::XSmall)
.buffer_font(cx)
@@ -1115,22 +1004,36 @@ impl MessageEditor {
.size(IconSize::Small)
});
let hover_color = cx
.theme()
.colors()
.element_background
.blend(cx.theme().colors().editor_foreground.opacity(0.025));
let overlay_gradient = linear_gradient(
90.,
linear_color_stop(editor_bg_color, 1.),
linear_color_stop(editor_bg_color.opacity(0.2), 0.),
);
let overlay_gradient_hover = linear_gradient(
90.,
linear_color_stop(hover_color, 1.),
linear_color_stop(hover_color.opacity(0.2), 0.),
);
let element = h_flex()
.group("edited-code")
.id(("file-container", index))
.cursor_pointer()
.relative()
.py_1()
.pl_2()
.pr_1()
.gap_2()
.justify_between()
.bg(editor_bg_color)
.bg(cx.theme().colors().editor_background)
.hover(|style| style.bg(hover_color))
.when(index < changed_buffers.len() - 1, |parent| {
parent.border_color(border_color).border_b_1()
})
@@ -1145,75 +1048,47 @@ impl MessageEditor {
.child(
h_flex()
.gap_0p5()
.children(file_name)
.children(file_path),
.children(name_label)
.children(parent_label),
), // TODO: Implement line diff
// .child(Label::new("+").color(Color::Created))
// .child(Label::new("-").color(Color::Deleted)),
)
.child(
h_flex()
.gap_1()
.visible_on_hover("edited-code")
.child(
Button::new("review", "Review")
.label_size(LabelSize::Small)
.on_click({
let buffer = buffer.clone();
cx.listener(move |this, _, window, cx| {
this.handle_file_click(
buffer.clone(),
window,
cx,
);
})
}),
)
.child(
Divider::vertical().color(DividerColor::BorderVariant),
)
.child(
Button::new("reject-file", "Reject")
.label_size(LabelSize::Small)
.disabled(pending_edits)
.on_click({
let buffer = buffer.clone();
cx.listener(move |this, _, window, cx| {
this.handle_reject_file_changes(
buffer.clone(),
window,
cx,
);
})
}),
)
.child(
Button::new("accept-file", "Accept")
.label_size(LabelSize::Small)
.disabled(pending_edits)
.on_click({
let buffer = buffer.clone();
cx.listener(move |this, _, window, cx| {
this.handle_accept_file_changes(
buffer.clone(),
window,
cx,
);
})
}),
),
div().visible_on_hover("edited-code").child(
Button::new("review", "Review")
.label_size(LabelSize::Small)
.on_click({
let buffer = buffer.clone();
cx.listener(move |this, _, window, cx| {
this.handle_file_click(
buffer.clone(),
window,
cx,
);
})
}),
),
)
.child(
div()
.id("gradient-overlay")
.absolute()
.h_full()
.h_5_6()
.w_12()
.top_0()
.bottom_0()
.right(px(152.))
.bg(overlay_gradient),
);
.right(px(52.))
.bg(overlay_gradient)
.group_hover("edited-code", |style| {
style.bg(overlay_gradient_hover)
}),
)
.on_click({
let buffer = buffer.clone();
cx.listener(move |this, _, window, cx| {
this.handle_file_click(buffer.clone(), window, cx);
})
});
Some(element)
},
@@ -1230,7 +1105,6 @@ impl MessageEditor {
.map_or(false, |model| {
model.provider.id().0 == ZED_CLOUD_PROVIDER_ID
});
if !is_using_zed_provider {
return None;
}
@@ -1285,6 +1159,14 @@ impl MessageEditor {
token_usage_ratio: TokenUsageRatio,
cx: &mut Context<Self>,
) -> Option<Div> {
let title = if token_usage_ratio == TokenUsageRatio::Exceeded {
"Thread reached the token limit"
} else {
"Thread reaching the token limit soon"
};
let message = "Start a new thread from a summary to continue the conversation.";
let icon = if token_usage_ratio == TokenUsageRatio::Exceeded {
Icon::new(IconName::X)
.color(Color::Error)
@@ -1295,47 +1177,23 @@ impl MessageEditor {
.size(IconSize::XSmall)
};
let title = if token_usage_ratio == TokenUsageRatio::Exceeded {
"Thread reached the token limit"
} else {
"Thread reaching the token limit soon"
};
Some(
div()
.border_t_1()
.border_color(cx.theme().colors().border)
.child(
Callout::new()
.line_height(line_height)
.icon(icon)
.title(title)
.description(
"To continue, start a new thread from a summary or turn burn mode on.",
)
.primary_action(
Button::new("start-new-thread", "Start New Thread")
.label_size(LabelSize::Small)
.on_click(cx.listener(|this, _, window, cx| {
let from_thread_id = Some(this.thread.read(cx).id().clone());
window.dispatch_action(
Box::new(NewThread { from_thread_id }),
cx,
);
})),
)
.secondary_action(
IconButton::new("burn-mode-callout", IconName::ZedBurnMode)
.icon_size(IconSize::XSmall)
.on_click(cx.listener(|this, _event, window, cx| {
this.toggle_burn_mode(&ToggleBurnMode, window, cx);
})),
),
),
.child(ui::Callout::multi_line(
title,
message,
icon,
"Start New Thread",
Box::new(cx.listener(|this, _, window, cx| {
let from_thread_id = Some(this.thread.read(cx).id().clone());
window.dispatch_action(Box::new(NewThread { from_thread_id }), cx);
})),
))
.line_height(line_height),
)
}
pub fn last_estimated_token_count(&self) -> Option<u64> {
pub fn last_estimated_token_count(&self) -> Option<usize> {
self.last_estimated_token_count
}
@@ -1511,7 +1369,6 @@ impl EventEmitter<MessageEditorEvent> for MessageEditor {}
pub enum MessageEditorEvent {
EstimatedTokenCount,
Changed,
ScrollThreadToBottom,
}
impl Focusable for MessageEditor {
@@ -1529,8 +1386,6 @@ impl Render for MessageEditor {
total_token_usage.ratio()
});
let burn_mode_enabled = thread.completion_mode() == CompletionMode::Burn;
let action_log = self.thread.read(cx).action_log();
let changed_buffers = action_log.read(cx).changed_buffers(cx);
@@ -1539,7 +1394,7 @@ impl Render for MessageEditor {
v_flex()
.size_full()
.when(changed_buffers.len() > 0, |parent| {
parent.child(self.render_edits_bar(&changed_buffers, window, cx))
parent.child(self.render_changed_buffers(&changed_buffers, window, cx))
})
.child(self.render_editor(window, cx))
.children({
@@ -1547,7 +1402,7 @@ impl Render for MessageEditor {
if usage_callout.is_some() {
usage_callout
} else if token_usage_ratio != TokenUsageRatio::Normal && !burn_mode_enabled {
} else if token_usage_ratio != TokenUsageRatio::Normal {
self.render_token_limit_callout(line_height, token_usage_ratio, cx)
} else {
None

View File

@@ -1,24 +1,26 @@
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 gpui::{Action, Empty, Entity, FocusHandle, Subscription, prelude::*};
use gpui::{Action, Empty, Entity, FocusHandle, Subscription, WeakEntity, prelude::*};
use language_model::LanguageModelRegistry;
use settings::{Settings as _, SettingsStore, update_settings_file};
use ui::{
ContextMenu, ContextMenuEntry, DocumentationSide, PopoverMenu, PopoverMenuHandle, Tooltip,
prelude::*,
};
use util::ResultExt as _;
use crate::{
ManageProfiles, Thread, ToggleProfileSelector,
agent_profile::{AgentProfile, AvailableProfiles},
};
use crate::{ManageProfiles, Thread, ThreadStore, ToggleProfileSelector};
pub struct ProfileSelector {
profiles: AvailableProfiles,
profiles: GroupedAgentProfiles,
fs: Arc<dyn Fs>,
thread: Entity<Thread>,
thread_store: WeakEntity<ThreadStore>,
menu_handle: PopoverMenuHandle<ContextMenu>,
focus_handle: FocusHandle,
_subscriptions: Vec<Subscription>,
@@ -28,6 +30,7 @@ impl ProfileSelector {
pub fn new(
fs: Arc<dyn Fs>,
thread: Entity<Thread>,
thread_store: WeakEntity<ThreadStore>,
focus_handle: FocusHandle,
cx: &mut Context<Self>,
) -> Self {
@@ -36,9 +39,10 @@ impl ProfileSelector {
});
Self {
profiles: AgentProfile::available_profiles(cx),
profiles: GroupedAgentProfiles::from_settings(AgentSettings::get_global(cx)),
fs,
thread,
thread_store,
menu_handle: PopoverMenuHandle::default(),
focus_handle,
_subscriptions: vec![settings_subscription],
@@ -50,7 +54,7 @@ impl ProfileSelector {
}
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(
@@ -60,30 +64,21 @@ impl ProfileSelector {
) -> Entity<ContextMenu> {
ContextMenu::build(window, cx, |mut menu, _window, cx| {
let settings = AgentSettings::get_global(cx);
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;
}
for (profile_id, profile) in self.profiles.builtin.iter() {
menu = menu.item(self.menu_entry_for_profile(
profile_id.clone(),
profile_name,
profile,
settings,
cx,
));
}
if found_non_builtin {
if !self.profiles.custom.is_empty() {
menu = menu.separator().header("Custom Profiles");
for (profile_id, profile_name) in self.profiles.iter() {
if builtin_profiles::is_builtin(profile_id) {
continue;
}
for (profile_id, profile) in self.profiles.custom.iter() {
menu = menu.item(self.menu_entry_for_profile(
profile_id.clone(),
profile_name,
profile,
settings,
cx,
));
@@ -104,20 +99,19 @@ impl ProfileSelector {
fn menu_entry_for_profile(
&self,
profile_id: AgentProfileId,
profile_name: &SharedString,
profile: &AgentProfile,
settings: &AgentSettings,
cx: &App,
_cx: &App,
) -> 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::ASK => Some("Chat about your codebase."),
builtin_profiles::MINIMAL => Some("Chat about anything with no tools."),
_ => None,
};
let thread_profile_id = self.thread.read(cx).profile().id();
let entry = ContextMenuEntry::new(profile_name.clone())
.toggleable(IconPosition::End, &profile_id == thread_profile_id);
let entry = ContextMenuEntry::new(profile.name.clone())
.toggleable(IconPosition::End, profile_id == settings.default_profile);
let entry = if let Some(doc_text) = documentation {
entry.documentation_aside(documentation_side(settings.dock), move |_| {
@@ -129,7 +123,7 @@ impl ProfileSelector {
entry.handler({
let fs = self.fs.clone();
let thread = self.thread.clone();
let thread_store = self.thread_store.clone();
let profile_id = profile_id.clone();
move |_window, cx| {
update_settings_file::<AgentSettings>(fs.clone(), cx, {
@@ -139,9 +133,11 @@ impl ProfileSelector {
}
});
thread.update(cx, |this, cx| {
this.set_profile(profile_id.clone(), cx);
});
thread_store
.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 {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
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 selected_profile = profile

View File

@@ -1,3 +1 @@
[The following is an auto-generated notification; do not reply]
These files have changed since the last read:
These files changed since last read:

View File

@@ -179,17 +179,18 @@ impl TerminalTransaction {
// Ensure that the assistant cannot accidentally execute commands that are streamed into the terminal
let input = Self::sanitize_input(hunk);
self.terminal
.update(cx, |terminal, _| terminal.input(input.into_bytes()));
.update(cx, |terminal, _| terminal.input(input));
}
pub fn undo(&self, cx: &mut App) {
self.terminal
.update(cx, |terminal, _| terminal.input(CLEAR_INPUT.as_bytes()));
.update(cx, |terminal, _| terminal.input(CLEAR_INPUT.to_string()));
}
pub fn complete(&self, cx: &mut App) {
self.terminal
.update(cx, |terminal, _| terminal.input(CARRIAGE_RETURN.as_bytes()));
self.terminal.update(cx, |terminal, _| {
terminal.input(CARRIAGE_RETURN.to_string())
});
}
fn sanitize_input(mut input: String) -> String {

View File

@@ -106,7 +106,7 @@ impl TerminalInlineAssistant {
});
let prompt_editor_render = prompt_editor.clone();
let block = terminal_view::BlockProperties {
height: 4,
height: 2,
render: Box::new(move |_| prompt_editor_render.clone().into_any_element()),
};
terminal_view.update(cx, |terminal_view, cx| {
@@ -167,6 +167,9 @@ impl TerminalInlineAssistant {
PromptEditorEvent::CancelRequested => {
self.finish_assist(assist_id, true, false, window, cx);
}
PromptEditorEvent::DismissRequested => {
self.dismiss_assist(assist_id, window, cx);
}
PromptEditorEvent::Resized { height_in_lines } => {
self.insert_prompt_editor_into_terminal(assist_id, *height_in_lines, window, cx);
}
@@ -199,7 +202,7 @@ impl TerminalInlineAssistant {
.update(cx, |terminal, cx| {
terminal
.terminal()
.update(cx, |terminal, _| terminal.input(CLEAR_INPUT.as_bytes()));
.update(cx, |terminal, _| terminal.input(CLEAR_INPUT.to_string()));
})
.log_err();

View File

@@ -4,7 +4,7 @@ use std::ops::Range;
use std::sync::Arc;
use std::time::Instant;
use agent_settings::{AgentProfileId, AgentSettings, CompletionMode};
use agent_settings::{AgentSettings, CompletionMode};
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet};
use chrono::{DateTime, Utc};
@@ -41,7 +41,6 @@ use uuid::Uuid;
use zed_llm_client::{CompletionIntent, CompletionRequestStatus};
use crate::ThreadStore;
use crate::agent_profile::AgentProfile;
use crate::context::{AgentContext, AgentContextHandle, ContextLoadResult, LoadedContext};
use crate::thread_store::{
SerializedCrease, SerializedLanguageModel, SerializedMessage, SerializedMessageSegment,
@@ -195,20 +194,20 @@ impl MessageSegment {
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProjectSnapshot {
pub worktree_snapshots: Vec<WorktreeSnapshot>,
pub unsaved_buffer_paths: Vec<String>,
pub timestamp: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorktreeSnapshot {
pub worktree_path: String,
pub git_state: Option<GitState>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GitState {
pub remote_url: Option<String>,
pub head_sha: Option<String>,
@@ -247,7 +246,7 @@ impl LastRestoreCheckpoint {
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub enum DetailedSummaryState {
#[default]
NotGenerated,
@@ -272,8 +271,8 @@ impl DetailedSummaryState {
#[derive(Default, Debug)]
pub struct TotalTokenUsage {
pub total: u64,
pub max: u64,
pub total: usize,
pub max: usize,
}
impl TotalTokenUsage {
@@ -299,7 +298,7 @@ impl TotalTokenUsage {
}
}
pub fn add(&self, tokens: u64) -> TotalTokenUsage {
pub fn add(&self, tokens: usize) -> TotalTokenUsage {
TotalTokenUsage {
total: self.total + tokens,
max: self.max,
@@ -361,7 +360,6 @@ pub struct Thread {
>,
remaining_turns: u32,
configured_model: Option<ConfiguredModel>,
profile: AgentProfile,
}
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -391,12 +389,12 @@ impl ThreadSummary {
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExceededWindowError {
/// Model used when last message exceeded context window
model_id: LanguageModelId,
/// Token count including last message
token_count: u64,
token_count: usize,
}
impl Thread {
@@ -409,7 +407,6 @@ impl Thread {
) -> Self {
let (detailed_summary_tx, detailed_summary_rx) = postage::watch::channel();
let configured_model = LanguageModelRegistry::read_global(cx).default_model();
let profile_id = AgentSettings::get_global(cx).default_profile.clone();
Self {
id: ThreadId::new(),
@@ -452,7 +449,6 @@ impl Thread {
request_callback: None,
remaining_turns: u32::MAX,
configured_model,
profile: AgentProfile::new(profile_id, tools),
}
}
@@ -499,9 +495,6 @@ impl Thread {
let completion_mode = serialized
.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 {
id,
@@ -561,7 +554,7 @@ impl Thread {
pending_checkpoint: None,
project: project.clone(),
prompt_builder,
tools: tools.clone(),
tools,
tool_use,
action_log: cx.new(|_| ActionLog::new(project)),
initial_project_snapshot: Task::ready(serialized.initial_project_snapshot).shared(),
@@ -577,7 +570,6 @@ impl Thread {
request_callback: None,
remaining_turns: u32::MAX,
configured_model,
profile: AgentProfile::new(profile_id, tools),
}
}
@@ -593,17 +585,6 @@ impl Thread {
&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 {
self.messages.is_empty()
}
@@ -890,16 +871,7 @@ impl Thread {
self.tool_use
.pending_tool_uses()
.iter()
.all(|pending_tool_use| pending_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)
.all(|tool_use| tool_use.status.is_error())
}
pub fn tool_uses_for_message(&self, id: MessageId, cx: &App) -> Vec<ToolUse> {
@@ -938,7 +910,8 @@ impl Thread {
model: Arc<dyn LanguageModel>,
) -> Vec<LanguageModelRequestTool> {
if model.supports_tools() {
self.profile
self.tools()
.read(cx)
.enabled_tools(cx)
.into_iter()
.filter_map(|tool| {
@@ -1050,7 +1023,6 @@ impl Thread {
id: MessageId,
new_role: Role,
new_segments: Vec<MessageSegment>,
creases: Vec<MessageCrease>,
loaded_context: Option<LoadedContext>,
checkpoint: Option<GitStoreCheckpoint>,
cx: &mut Context<Self>,
@@ -1060,7 +1032,6 @@ impl Thread {
};
message.role = new_role;
message.segments = new_segments;
message.creases = creases;
if let Some(context) = loaded_context {
message.loaded_context = context;
}
@@ -1198,7 +1169,6 @@ impl Thread {
}),
completion_mode: Some(this.completion_mode),
tool_use_limit_reached: this.tool_use_limit_reached,
profile: Some(this.profile.id().clone()),
})
})
}
@@ -1389,7 +1359,7 @@ impl Thread {
request.messages[message_ix_to_cache].cache = true;
}
self.attach_tracked_files_state(&mut request.messages, cx);
self.attached_tracked_files_state(&mut request.messages, cx);
request.tools = available_tools;
request.mode = if model.supports_max_mode() {
@@ -1453,57 +1423,43 @@ impl Thread {
request
}
fn attach_tracked_files_state(
fn attached_tracked_files_state(
&self,
messages: &mut Vec<LanguageModelRequestMessage>,
cx: &App,
) {
let mut stale_files = String::new();
const STALE_FILES_HEADER: &str = include_str!("./prompts/stale_files_prompt_header.txt");
let mut stale_message = String::new();
let action_log = self.action_log.read(cx);
for stale_file in action_log.stale_buffers(cx) {
if let Some(file) = stale_file.read(cx).file() {
writeln!(&mut stale_files, "- {}", file.path().display()).ok();
let Some(file) = stale_file.read(cx).file() else {
continue;
};
if stale_message.is_empty() {
write!(&mut stale_message, "{}\n", STALE_FILES_HEADER.trim()).ok();
}
writeln!(&mut stale_message, "- {}", file.path().display()).ok();
}
if stale_files.is_empty() {
return;
let mut content = Vec::with_capacity(2);
if !stale_message.is_empty() {
content.push(stale_message.into());
}
// NOTE: Changes to this prompt require a symmetric update in the LLM Worker
const STALE_FILES_HEADER: &str = include_str!("./prompts/stale_files_prompt_header.txt");
let content = MessageContent::Text(
format!("{STALE_FILES_HEADER}{stale_files}").replace("\r\n", "\n"),
);
if !content.is_empty() {
let context_message = LanguageModelRequestMessage {
role: Role::User,
content,
cache: false,
};
// Insert our message before the last Assistant message.
// Inserting it to the tail distracts the agent too much
let insert_position = messages
.iter()
.enumerate()
.rfind(|(_, message)| message.role == Role::Assistant)
.map_or(messages.len(), |(i, _)| i);
let request_message = LanguageModelRequestMessage {
role: Role::User,
content: vec![content],
cache: false,
};
messages.insert(insert_position, request_message);
// It makes no sense to cache messages after this one because
// the cache is invalidated when this message is gone.
// Move the cache marker before this message.
let has_cached_messages_after = messages
.iter()
.skip(insert_position + 1)
.any(|message| message.cache);
if has_cached_messages_after {
messages[insert_position - 1].cache = true;
messages.push(context_message);
}
}
@@ -1577,9 +1533,6 @@ impl Thread {
Err(LanguageModelCompletionError::Other(error)) => {
return Err(error);
}
Err(err @ LanguageModelCompletionError::RateLimit(..)) => {
return Err(err.into());
}
};
match event {
@@ -1720,7 +1673,6 @@ impl Thread {
}
CompletionRequestStatus::ToolUseLimitReached => {
thread.tool_use_limit_reached = true;
cx.emit(ThreadEvent::ToolUseLimitReached);
}
}
}
@@ -2157,7 +2109,7 @@ impl Thread {
window: Option<AnyWindowHandle>,
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
.iter()
@@ -2249,15 +2201,19 @@ impl Thread {
) -> Task<()> {
let tool_name: Arc<str> = tool.name().into();
let tool_result = tool.run(
input,
request,
self.project.clone(),
self.action_log.clone(),
model,
window,
cx,
);
let tool_result = if self.tools.read(cx).is_disabled(&tool.source(), &tool_name) {
Task::ready(Err(anyhow!("tool is disabled: {tool_name}"))).into()
} else {
tool.run(
input,
request,
self.project.clone(),
self.action_log.clone(),
model,
window,
cx,
)
};
// Store the card separately if it exists
if let Some(card) = tool_result.card.clone() {
@@ -2376,7 +2332,8 @@ impl Thread {
let client = self.project.read(cx).client();
let enabled_tool_names: Vec<String> = self
.profile
.tools()
.read(cx)
.enabled_tools(cx)
.iter()
.map(|tool| tool.name())
@@ -2769,7 +2726,7 @@ impl Thread {
.unwrap_or_default();
TotalTokenUsage {
total: token_usage.total_tokens(),
total: token_usage.total_tokens() as usize,
max,
}
}
@@ -2791,7 +2748,7 @@ impl Thread {
let total = self
.token_usage_at_last_message()
.unwrap_or_default()
.total_tokens();
.total_tokens() as usize;
Some(TotalTokenUsage { total, max })
}
@@ -2886,10 +2843,8 @@ pub enum ThreadEvent {
},
CheckpointChanged,
ToolConfirmationNeeded,
ToolUseLimitReached,
CancelEditing,
CompletionCanceled,
ProfileChanged,
}
impl EventEmitter<ThreadEvent> for Thread {}
@@ -2904,7 +2859,7 @@ struct PendingCompletion {
mod tests {
use super::*;
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 editor::EditorSettings;
use gpui::TestAppContext;
@@ -3309,89 +3264,12 @@ fn main() {{
assert_eq!(last_message.role, Role::User);
// Check the exact content of the message
let expected_content = "[The following is an auto-generated notification; do not reply]
These files have changed since the last read:
- code.rs
";
let expected_content = "These files changed since last read:\n- code.rs\n";
assert_eq!(
last_message.string_contents(),
expected_content,
"Last message should be exactly the stale buffer notification"
);
// The message before the notification should be cached
let index = new_request.messages.len() - 2;
let previous_message = new_request.messages.get(index).unwrap();
assert!(
previous_message.cache,
"Message before the stale buffer notification should be cached"
);
}
#[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]

View File

@@ -224,7 +224,6 @@ impl ThreadHistory {
&candidates,
&query,
false,
true,
MAX_MATCHES,
&Default::default(),
executor,
@@ -595,11 +594,10 @@ impl Render for ThreadHistory {
view.pr_5()
.child(
uniform_list(
cx.entity().clone(),
"thread-history",
self.list_item_count(),
cx.processor(|this, range: Range<usize>, window, cx| {
this.list_items(range, window, cx)
}),
Self::list_items,
)
.p_1()
.track_scroll(self.scroll_handle.clone())
@@ -673,7 +671,7 @@ impl RenderOnce for HistoryEntryElement {
),
HistoryEntry::Context(context) => (
context.path.to_string_lossy().to_string(),
context.title.clone(),
context.title.clone().into(),
context.mtime.timestamp(),
),
};

View File

@@ -1,11 +1,12 @@
use std::borrow::Cow;
use std::cell::{Ref, RefCell};
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use std::sync::Arc;
use agent_settings::{AgentProfileId, CompletionMode};
use agent_settings::{AgentProfile, AgentProfileId, AgentSettings, CompletionMode};
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ToolId, ToolWorkingSet};
use assistant_tool::{ToolId, ToolSource, ToolWorkingSet};
use chrono::{DateTime, Utc};
use collections::HashMap;
use context_server::ContextServerId;
@@ -16,7 +17,8 @@ use gpui::{
App, BackgroundExecutor, Context, Entity, EventEmitter, Global, ReadGlobal, SharedString,
Subscription, Task, prelude::*,
};
use heed::Database;
use heed::types::SerdeBincode;
use language_model::{LanguageModelToolResultContent, LanguageModelToolUseId, Role, TokenUsage};
use project::context_server_store::{ContextServerStatus, ContextServerStore};
use project::{Project, ProjectItem, ProjectPath, Worktree};
@@ -25,6 +27,7 @@ use prompt_store::{
UserRulesContext, WorktreeContext,
};
use serde::{Deserialize, Serialize};
use settings::{Settings as _, SettingsStore};
use ui::Window;
use util::ResultExt as _;
@@ -32,52 +35,14 @@ use crate::context_server_tool::ContextServerTool;
use crate::thread::{
DetailedSummaryState, ExceededWindowError, MessageId, ProjectSnapshot, Thread, ThreadId,
};
use indoc::indoc;
use sqlez::{
bindable::{Bind, Column},
connection::Connection,
statement::Statement,
};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DataType {
#[serde(rename = "json")]
Json,
#[serde(rename = "zstd")]
Zstd,
}
impl Bind for DataType {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
let value = match self {
DataType::Json => "json",
DataType::Zstd => "zstd",
};
value.bind(statement, start_index)
}
}
impl Column for DataType {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let (value, next_index) = String::column(statement, start_index)?;
let data_type = match value.as_str() {
"json" => DataType::Json,
"zstd" => DataType::Zstd,
_ => anyhow::bail!("Unknown data type: {}", value),
};
Ok((data_type, next_index))
}
}
const RULES_FILE_NAMES: [&'static str; 8] = [
const RULES_FILE_NAMES: [&'static str; 6] = [
".rules",
".cursorrules",
".windsurfrules",
".clinerules",
".github/copilot-instructions.md",
"CLAUDE.md",
"AGENT.md",
"AGENTS.md",
];
pub fn init(cx: &mut App) {
@@ -89,7 +54,7 @@ pub fn init(cx: &mut App) {
pub struct SharedProjectContext(Rc<RefCell<Option<ProjectContext>>>);
impl SharedProjectContext {
pub fn borrow(&self) -> Ref<'_, Option<ProjectContext>> {
pub fn borrow(&self) -> Ref<Option<ProjectContext>> {
self.0.borrow()
}
}
@@ -146,7 +111,12 @@ impl ThreadStore {
prompt_store: Option<Entity<PromptStore>>,
cx: &mut Context<Self>,
) -> (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() {
subscriptions.push(cx.subscribe(
@@ -194,6 +164,7 @@ impl ThreadStore {
_reload_system_prompt_task: reload_system_prompt_task,
_subscriptions: subscriptions,
};
this.load_default_profile(cx);
this.register_context_server_handlers(cx);
this.reload(cx).detach_and_log_err(cx);
(this, ready_rx)
@@ -305,19 +276,17 @@ impl ThreadStore {
project: Entity<Project>,
cx: &mut App,
) -> Task<(WorktreeContext, Option<RulesLoadingError>)> {
let tree = worktree.read(cx);
let root_name = tree.root_name().into();
let abs_path = tree.abs_path();
let mut context = WorktreeContext {
root_name,
abs_path,
rules_file: None,
};
let root_name = worktree.read(cx).root_name().into();
let rules_task = Self::load_worktree_rules_file(worktree, project, cx);
let Some(rules_task) = rules_task else {
return Task::ready((context, None));
return Task::ready((
WorktreeContext {
root_name,
rules_file: None,
},
None,
));
};
cx.spawn(async move |_| {
@@ -330,8 +299,11 @@ impl ThreadStore {
}),
),
};
context.rules_file = rules_file;
(context, rules_file_error)
let worktree_info = WorktreeContext {
root_name,
rules_file,
};
(worktree_info, rules_file_error)
})
}
@@ -340,12 +312,12 @@ impl ThreadStore {
project: Entity<Project>,
cx: &mut App,
) -> Option<Task<Result<RulesFileContext>>> {
let worktree = worktree.read(cx);
let worktree_id = worktree.id();
let worktree_ref = worktree.read(cx);
let worktree_id = worktree_ref.id();
let selected_rules_file = RULES_FILE_NAMES
.into_iter()
.filter_map(|name| {
worktree
worktree_ref
.entry_for_path(name)
.filter(|entry| entry.is_file())
.map(|entry| entry.path.clone())
@@ -392,11 +364,16 @@ impl ThreadStore {
self.threads.len()
}
pub fn reverse_chronological_threads(&self) -> impl Iterator<Item = &SerializedThreadMetadata> {
// ordering is from "ORDER BY" in `list_threads`
pub fn unordered_threads(&self) -> impl Iterator<Item = &SerializedThreadMetadata> {
self.threads.iter()
}
pub fn reverse_chronological_threads(&self) -> Vec<SerializedThreadMetadata> {
let mut threads = self.threads.iter().cloned().collect::<Vec<_>>();
threads.sort_unstable_by_key(|thread| std::cmp::Reverse(thread.updated_at));
threads
}
pub fn create_thread(&mut self, cx: &mut Context<Self>) -> Entity<Thread> {
cx.new(|cx| {
Thread::new(
@@ -507,17 +484,94 @@ impl ThreadStore {
})
}
fn register_context_server_handlers(&self, cx: &mut Context<Self>) {
let context_server_store = self.project.read(cx).context_server_store();
cx.subscribe(&context_server_store, Self::handle_context_server_event)
.detach();
fn load_default_profile(&self, cx: &mut Context<Self>) {
let assistant_settings = AgentSettings::get_global(cx);
// Check for any servers that were already running before the handler was registered
for server in context_server_store.read(cx).running_servers() {
self.load_context_server_tools(server.id(), context_server_store.clone(), cx);
self.load_profile_by_id(assistant_settings.default_profile.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(
&mut self,
context_server_store: Entity<ContextServerStore>,
@@ -528,71 +582,71 @@ impl ThreadStore {
match event {
project::context_server_store::Event::ServerStatusChanged { server_id, status } => {
match status {
ContextServerStatus::Starting => {}
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(_) => {
if let Some(tool_ids) = self.context_server_tool_ids.remove(server_id) {
tool_working_set.update(cx, |tool_working_set, _| {
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::requests::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)]
@@ -602,7 +656,7 @@ pub struct SerializedThreadMetadata {
pub updated_at: DateTime<Utc>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[derive(Serialize, Deserialize, Debug)]
pub struct SerializedThread {
pub version: String,
pub summary: SharedString,
@@ -624,11 +678,9 @@ pub struct SerializedThread {
pub completion_mode: Option<CompletionMode>,
#[serde(default)]
pub tool_use_limit_reached: bool,
#[serde(default)]
pub profile: Option<AgentProfileId>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[derive(Serialize, Deserialize, Debug)]
pub struct SerializedLanguageModel {
pub provider: String,
pub model: String,
@@ -689,15 +741,11 @@ impl SerializedThreadV0_1_0 {
messages.push(message);
}
SerializedThread {
messages,
version: SerializedThread::VERSION.to_string(),
..self.0
}
SerializedThread { messages, ..self.0 }
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Serialize, Deserialize)]
pub struct SerializedMessage {
pub id: MessageId,
pub role: Role,
@@ -715,7 +763,7 @@ pub struct SerializedMessage {
pub is_hidden: bool,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum SerializedMessageSegment {
#[serde(rename = "text")]
@@ -733,14 +781,14 @@ pub enum SerializedMessageSegment {
},
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Serialize, Deserialize)]
pub struct SerializedToolUse {
pub id: LanguageModelToolUseId,
pub name: SharedString,
pub input: serde_json::Value,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Serialize, Deserialize)]
pub struct SerializedToolResult {
pub tool_use_id: LanguageModelToolUseId,
pub is_error: bool,
@@ -772,7 +820,6 @@ impl LegacySerializedThread {
model: None,
completion_mode: None,
tool_use_limit_reached: false,
profile: None,
}
}
}
@@ -803,7 +850,7 @@ impl LegacySerializedMessage {
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Serialize, Deserialize)]
pub struct SerializedCrease {
pub start: usize,
pub end: usize,
@@ -819,27 +866,25 @@ impl Global for GlobalThreadsDatabase {}
pub(crate) struct ThreadsDatabase {
executor: BackgroundExecutor,
connection: Arc<Mutex<Connection>>,
env: heed::Env,
threads: Database<SerdeBincode<ThreadId>, SerializedThread>,
}
impl ThreadsDatabase {
fn connection(&self) -> Arc<Mutex<Connection>> {
self.connection.clone()
}
impl heed::BytesEncode<'_> for SerializedThread {
type EItem = SerializedThread;
const COMPRESSION_LEVEL: i32 = 3;
}
impl Bind for ThreadId {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
self.to_string().bind(statement, start_index)
fn bytes_encode(item: &Self::EItem) -> Result<Cow<[u8]>, heed::BoxedError> {
serde_json::to_vec(item).map(Cow::Owned).map_err(Into::into)
}
}
impl Column for ThreadId {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let (id_str, next_index) = String::column(statement, start_index)?;
Ok((ThreadId::from(id_str.as_str()), next_index))
impl<'a> heed::BytesDecode<'a> for SerializedThread {
type DItem = SerializedThread;
fn bytes_decode(bytes: &'a [u8]) -> Result<Self::DItem, heed::BoxedError> {
// We implement this type manually because we want to call `SerializedThread::from_json`,
// instead of the Deserialize trait implementation for `SerializedThread`.
SerializedThread::from_json(bytes).map_err(Into::into)
}
}
@@ -855,8 +900,8 @@ impl ThreadsDatabase {
let database_future = executor
.spawn({
let executor = executor.clone();
let threads_dir = paths::data_dir().join("threads");
async move { ThreadsDatabase::new(threads_dir, executor) }
let database_path = paths::data_dir().join("threads/threads-db.1.mdb");
async move { ThreadsDatabase::new(database_path, executor) }
})
.then(|result| future::ready(result.map(Arc::new).map_err(Arc::new)))
.boxed()
@@ -865,144 +910,41 @@ impl ThreadsDatabase {
cx.set_global(GlobalThreadsDatabase(database_future));
}
pub fn new(threads_dir: PathBuf, executor: BackgroundExecutor) -> Result<Self> {
std::fs::create_dir_all(&threads_dir)?;
let sqlite_path = threads_dir.join("threads.db");
let mdb_path = threads_dir.join("threads-db.1.mdb");
let needs_migration_from_heed = mdb_path.exists();
let connection = Connection::open_file(&sqlite_path.to_string_lossy());
connection.exec(indoc! {"
CREATE TABLE IF NOT EXISTS threads (
id TEXT PRIMARY KEY,
summary TEXT NOT NULL,
updated_at TEXT NOT NULL,
data_type TEXT NOT NULL,
data BLOB NOT NULL
)
"})?()
.map_err(|e| anyhow!("Failed to create threads table: {}", e))?;
let db = Self {
executor: executor.clone(),
connection: Arc::new(Mutex::new(connection)),
};
if needs_migration_from_heed {
let db_connection = db.connection();
let executor_clone = executor.clone();
executor
.spawn(async move {
log::info!("Starting threads.db migration");
Self::migrate_from_heed(&mdb_path, db_connection, executor_clone)?;
std::fs::remove_dir_all(mdb_path)?;
log::info!("threads.db migrated to sqlite");
Ok::<(), anyhow::Error>(())
})
.detach();
}
Ok(db)
}
// Remove this migration after 2025-09-01
fn migrate_from_heed(
mdb_path: &Path,
connection: Arc<Mutex<Connection>>,
_executor: BackgroundExecutor,
) -> Result<()> {
use heed::types::SerdeBincode;
struct SerializedThreadHeed(SerializedThread);
impl heed::BytesEncode<'_> for SerializedThreadHeed {
type EItem = SerializedThreadHeed;
fn bytes_encode(
item: &Self::EItem,
) -> Result<std::borrow::Cow<'_, [u8]>, heed::BoxedError> {
serde_json::to_vec(&item.0)
.map(std::borrow::Cow::Owned)
.map_err(Into::into)
}
}
impl<'a> heed::BytesDecode<'a> for SerializedThreadHeed {
type DItem = SerializedThreadHeed;
fn bytes_decode(bytes: &'a [u8]) -> Result<Self::DItem, heed::BoxedError> {
SerializedThread::from_json(bytes)
.map(SerializedThreadHeed)
.map_err(Into::into)
}
}
pub fn new(path: PathBuf, executor: BackgroundExecutor) -> Result<Self> {
std::fs::create_dir_all(&path)?;
const ONE_GB_IN_BYTES: usize = 1024 * 1024 * 1024;
let env = unsafe {
heed::EnvOpenOptions::new()
.map_size(ONE_GB_IN_BYTES)
.max_dbs(1)
.open(mdb_path)?
.open(path)?
};
let txn = env.write_txn()?;
let threads: heed::Database<SerdeBincode<ThreadId>, SerializedThreadHeed> = env
.open_database(&txn, Some("threads"))?
.ok_or_else(|| anyhow!("threads database not found"))?;
let mut txn = env.write_txn()?;
let threads = env.create_database(&mut txn, Some("threads"))?;
txn.commit()?;
for result in threads.iter(&txn)? {
let (thread_id, thread_heed) = result?;
Self::save_thread_sync(&connection, thread_id, thread_heed.0)?;
}
Ok(())
}
fn save_thread_sync(
connection: &Arc<Mutex<Connection>>,
id: ThreadId,
thread: SerializedThread,
) -> Result<()> {
let json_data = serde_json::to_string(&thread)?;
let summary = thread.summary.to_string();
let updated_at = thread.updated_at.to_rfc3339();
let connection = connection.lock().unwrap();
let compressed = zstd::encode_all(json_data.as_bytes(), Self::COMPRESSION_LEVEL)?;
let data_type = DataType::Zstd;
let data = compressed;
let mut insert = connection.exec_bound::<(ThreadId, String, String, DataType, Vec<u8>)>(indoc! {"
INSERT OR REPLACE INTO threads (id, summary, updated_at, data_type, data) VALUES (?, ?, ?, ?, ?)
"})?;
insert((id, summary, updated_at, data_type, data))?;
Ok(())
Ok(Self {
executor,
env,
threads,
})
}
pub fn list_threads(&self) -> Task<Result<Vec<SerializedThreadMetadata>>> {
let connection = self.connection.clone();
let env = self.env.clone();
let threads = self.threads;
self.executor.spawn(async move {
let connection = connection.lock().unwrap();
let mut select =
connection.select_bound::<(), (ThreadId, String, String)>(indoc! {"
SELECT id, summary, updated_at FROM threads ORDER BY updated_at DESC
"})?;
let rows = select(())?;
let txn = env.read_txn()?;
let mut iter = threads.iter(&txn)?;
let mut threads = Vec::new();
for (id, summary, updated_at) in rows {
while let Some((key, value)) = iter.next().transpose()? {
threads.push(SerializedThreadMetadata {
id,
summary: summary.into(),
updated_at: DateTime::parse_from_rfc3339(&updated_at)?.with_timezone(&Utc),
id: key,
summary: value.summary,
updated_at: value.updated_at,
});
}
@@ -1011,230 +953,37 @@ impl ThreadsDatabase {
}
pub fn try_find_thread(&self, id: ThreadId) -> Task<Result<Option<SerializedThread>>> {
let connection = self.connection.clone();
let env = self.env.clone();
let threads = self.threads;
self.executor.spawn(async move {
let connection = connection.lock().unwrap();
let mut select = connection.select_bound::<ThreadId, (DataType, Vec<u8>)>(indoc! {"
SELECT data_type, data FROM threads WHERE id = ? LIMIT 1
"})?;
let rows = select(id)?;
if let Some((data_type, data)) = rows.into_iter().next() {
let json_data = match data_type {
DataType::Zstd => {
let decompressed = zstd::decode_all(&data[..])?;
String::from_utf8(decompressed)?
}
DataType::Json => String::from_utf8(data)?,
};
let thread = SerializedThread::from_json(json_data.as_bytes())?;
Ok(Some(thread))
} else {
Ok(None)
}
let txn = env.read_txn()?;
let thread = threads.get(&txn, &id)?;
Ok(thread)
})
}
pub fn save_thread(&self, id: ThreadId, thread: SerializedThread) -> Task<Result<()>> {
let connection = self.connection.clone();
let env = self.env.clone();
let threads = self.threads;
self.executor
.spawn(async move { Self::save_thread_sync(&connection, id, thread) })
self.executor.spawn(async move {
let mut txn = env.write_txn()?;
threads.put(&mut txn, &id, &thread)?;
txn.commit()?;
Ok(())
})
}
pub fn delete_thread(&self, id: ThreadId) -> Task<Result<()>> {
let connection = self.connection.clone();
let env = self.env.clone();
let threads = self.threads;
self.executor.spawn(async move {
let connection = connection.lock().unwrap();
let mut delete = connection.exec_bound::<ThreadId>(indoc! {"
DELETE FROM threads WHERE id = ?
"})?;
delete(id)?;
let mut txn = env.write_txn()?;
threads.delete(&mut txn, &id)?;
txn.commit()?;
Ok(())
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::thread::{DetailedSummaryState, MessageId};
use chrono::Utc;
use language_model::{Role, TokenUsage};
use pretty_assertions::assert_eq;
#[test]
fn test_legacy_serialized_thread_upgrade() {
let updated_at = Utc::now();
let legacy_thread = LegacySerializedThread {
summary: "Test conversation".into(),
updated_at,
messages: vec![LegacySerializedMessage {
id: MessageId(1),
role: Role::User,
text: "Hello, world!".to_string(),
tool_uses: vec![],
tool_results: vec![],
}],
initial_project_snapshot: None,
};
let upgraded = legacy_thread.upgrade();
assert_eq!(
upgraded,
SerializedThread {
summary: "Test conversation".into(),
updated_at,
messages: vec![SerializedMessage {
id: MessageId(1),
role: Role::User,
segments: vec![SerializedMessageSegment::Text {
text: "Hello, world!".to_string()
}],
tool_uses: vec![],
tool_results: vec![],
context: "".to_string(),
creases: vec![],
is_hidden: false
}],
version: SerializedThread::VERSION.to_string(),
initial_project_snapshot: None,
cumulative_token_usage: TokenUsage::default(),
request_token_usage: vec![],
detailed_summary_state: DetailedSummaryState::default(),
exceeded_window_error: None,
model: None,
completion_mode: None,
tool_use_limit_reached: false,
profile: None
}
)
}
#[test]
fn test_serialized_threadv0_1_0_upgrade() {
let updated_at = Utc::now();
let thread_v0_1_0 = SerializedThreadV0_1_0(SerializedThread {
summary: "Test conversation".into(),
updated_at,
messages: vec![
SerializedMessage {
id: MessageId(1),
role: Role::User,
segments: vec![SerializedMessageSegment::Text {
text: "Use tool_1".to_string(),
}],
tool_uses: vec![],
tool_results: vec![],
context: "".to_string(),
creases: vec![],
is_hidden: false,
},
SerializedMessage {
id: MessageId(2),
role: Role::Assistant,
segments: vec![SerializedMessageSegment::Text {
text: "I want to use a tool".to_string(),
}],
tool_uses: vec![SerializedToolUse {
id: "abc".into(),
name: "tool_1".into(),
input: serde_json::Value::Null,
}],
tool_results: vec![],
context: "".to_string(),
creases: vec![],
is_hidden: false,
},
SerializedMessage {
id: MessageId(1),
role: Role::User,
segments: vec![SerializedMessageSegment::Text {
text: "Here is the tool result".to_string(),
}],
tool_uses: vec![],
tool_results: vec![SerializedToolResult {
tool_use_id: "abc".into(),
is_error: false,
content: LanguageModelToolResultContent::Text("abcdef".into()),
output: Some(serde_json::Value::Null),
}],
context: "".to_string(),
creases: vec![],
is_hidden: false,
},
],
version: SerializedThreadV0_1_0::VERSION.to_string(),
initial_project_snapshot: None,
cumulative_token_usage: TokenUsage::default(),
request_token_usage: vec![],
detailed_summary_state: DetailedSummaryState::default(),
exceeded_window_error: None,
model: None,
completion_mode: None,
tool_use_limit_reached: false,
profile: None,
});
let upgraded = thread_v0_1_0.upgrade();
assert_eq!(
upgraded,
SerializedThread {
summary: "Test conversation".into(),
updated_at,
messages: vec![
SerializedMessage {
id: MessageId(1),
role: Role::User,
segments: vec![SerializedMessageSegment::Text {
text: "Use tool_1".to_string()
}],
tool_uses: vec![],
tool_results: vec![],
context: "".to_string(),
creases: vec![],
is_hidden: false
},
SerializedMessage {
id: MessageId(2),
role: Role::Assistant,
segments: vec![SerializedMessageSegment::Text {
text: "I want to use a tool".to_string(),
}],
tool_uses: vec![SerializedToolUse {
id: "abc".into(),
name: "tool_1".into(),
input: serde_json::Value::Null,
}],
tool_results: vec![SerializedToolResult {
tool_use_id: "abc".into(),
is_error: false,
content: LanguageModelToolResultContent::Text("abcdef".into()),
output: Some(serde_json::Value::Null),
}],
context: "".to_string(),
creases: vec![],
is_hidden: false,
},
],
version: SerializedThread::VERSION.to_string(),
initial_project_snapshot: None,
cumulative_token_usage: TokenUsage::default(),
request_token_usage: vec![],
detailed_summary_state: DetailedSummaryState::default(),
exceeded_window_error: None,
model: None,
completion_mode: None,
tool_use_limit_reached: false,
profile: None
}
)
}
}

View File

@@ -1,33 +1,30 @@
use std::sync::Arc;
use assistant_tool::{Tool, ToolSource};
use assistant_tool::{Tool, ToolSource, ToolWorkingSet, ToolWorkingSetEvent};
use collections::HashMap;
use gpui::{App, Context, Entity, IntoElement, Render, Subscription, Window};
use language_model::{LanguageModel, LanguageModelToolSchemaFormat};
use ui::prelude::*;
use crate::{Thread, ThreadEvent};
pub struct IncompatibleToolsState {
cache: HashMap<LanguageModelToolSchemaFormat, Vec<Arc<dyn Tool>>>,
thread: Entity<Thread>,
_thread_subscription: Subscription,
tool_working_set: Entity<ToolWorkingSet>,
_tool_working_set_subscription: Subscription,
}
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 =
cx.subscribe(&thread, |this, _, event, _| match event {
ThreadEvent::ProfileChanged => {
cx.subscribe(&tool_working_set, |this, _, event, _| match event {
ToolWorkingSetEvent::EnabledToolsChanged => {
this.cache.clear();
}
_ => {}
});
Self {
cache: HashMap::default(),
thread,
_thread_subscription: _tool_working_set_subscription,
tool_working_set,
_tool_working_set_subscription,
}
}
@@ -39,9 +36,8 @@ impl IncompatibleToolsState {
self.cache
.entry(model.tool_input_format())
.or_insert_with(|| {
self.thread
self.tool_working_set
.read(cx)
.profile()
.enabled_tools(cx)
.iter()
.filter(|tool| tool.input_schema(model.tool_input_format()).is_err())

View File

@@ -337,12 +337,6 @@ impl ToolUseState {
)
.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(
tool_use.id.clone(),
PendingToolUse {
@@ -351,7 +345,6 @@ impl ToolUseState {
name: tool_use.name.clone(),
ui_text: ui_text.clone(),
input: tool_use.input,
may_perform_edits,
status,
},
);
@@ -427,7 +420,7 @@ impl ToolUseState {
// Protect from overly large output
let tool_output_limit = configured_model
.map(|model| model.model.max_token_count() as usize * BYTES_PER_TOKEN_ESTIMATE)
.map(|model| model.model.max_token_count() * BYTES_PER_TOKEN_ESTIMATE)
.unwrap_or(usize::MAX);
let content = match tool_result {
@@ -525,7 +518,6 @@ pub struct PendingToolUse {
pub ui_text: Arc<str>,
pub input: serde_json::Value,
pub status: PendingToolUseStatus,
pub may_perform_edits: bool,
}
#[derive(Debug, Clone)]

View File

@@ -93,9 +93,20 @@ impl ContextPill {
Self::Suggested {
icon_path: Some(icon_path),
..
}
| Self::Added {
context:
AddedContext {
icon_path: Some(icon_path),
..
},
..
} => Icon::from_path(icon_path),
Self::Suggested { kind, .. } => Icon::new(kind.icon()),
Self::Added { context, .. } => context.icon(),
Self::Suggested { kind, .. }
| Self::Added {
context: AddedContext { kind, .. },
..
} => Icon::new(kind.icon()),
}
}
}
@@ -122,7 +133,6 @@ impl RenderOnce for ContextPill {
on_click,
} => {
let status_is_error = matches!(context.status, ContextStatus::Error { .. });
let status_is_warning = matches!(context.status, ContextStatus::Warning { .. });
base_pill
.pr(if on_remove.is_some() { px(2.) } else { px(4.) })
@@ -130,9 +140,6 @@ impl RenderOnce for ContextPill {
if status_is_error {
pill.bg(cx.theme().status().error_background)
.border_color(cx.theme().status().error_border)
} else if status_is_warning {
pill.bg(cx.theme().status().warning_background)
.border_color(cx.theme().status().warning_border)
} else if *focused {
pill.bg(color.element_background)
.border_color(color.border_focused)
@@ -188,8 +195,7 @@ impl RenderOnce for ContextPill {
|label, delta| label.opacity(delta),
)
.into_any_element(),
ContextStatus::Warning { message }
| ContextStatus::Error { message } => element
ContextStatus::Error { message } => element
.tooltip(ui::Tooltip::text(message.clone()))
.into_any_element(),
}),
@@ -264,7 +270,6 @@ pub enum ContextStatus {
Ready,
Loading { message: SharedString },
Error { message: SharedString },
Warning { message: SharedString },
}
#[derive(RegisterComponent)]
@@ -280,19 +285,6 @@ pub struct 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
/// `None` if `DirectoryContext` or `RulesContext` no longer exist.
///
@@ -301,7 +293,6 @@ impl AddedContext {
handle: AgentContextHandle,
prompt_store: Option<&Entity<PromptStore>>,
project: &Project,
model: Option<&Arc<dyn language_model::LanguageModel>>,
cx: &App,
) -> Option<AddedContext> {
match handle {
@@ -313,15 +304,11 @@ impl AddedContext {
AgentContextHandle::Thread(handle) => Some(Self::pending_thread(handle, cx)),
AgentContextHandle::TextThread(handle) => Some(Self::pending_text_thread(handle, cx)),
AgentContextHandle::Rules(handle) => Self::pending_rules(handle, prompt_store, cx),
AgentContextHandle::Image(handle) => Some(Self::image(handle, model, cx)),
AgentContextHandle::Image(handle) => Some(Self::image(handle)),
}
}
pub fn new_attached(
context: &AgentContext,
model: Option<&Arc<dyn language_model::LanguageModel>>,
cx: &App,
) -> AddedContext {
pub fn new_attached(context: &AgentContext, cx: &App) -> AddedContext {
match context {
AgentContext::File(context) => Self::attached_file(context, cx),
AgentContext::Directory(context) => Self::attached_directory(context),
@@ -331,7 +318,7 @@ impl AddedContext {
AgentContext::Thread(context) => Self::attached_thread(context),
AgentContext::TextThread(context) => Self::attached_text_thread(context),
AgentContext::Rules(context) => Self::attached_rules(context),
AgentContext::Image(context) => Self::image(context.clone(), model, cx),
AgentContext::Image(context) => Self::image(context.clone()),
}
}
@@ -346,8 +333,14 @@ impl AddedContext {
fn file(handle: FileContextHandle, full_path: &Path, cx: &App) -> AddedContext {
let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into();
let (name, parent) =
extract_file_name_and_directory_from_full_path(full_path, &full_path_string);
let name = full_path
.file_name()
.map(|n| n.to_string_lossy().into_owned().into())
.unwrap_or_else(|| full_path_string.clone());
let parent = full_path
.parent()
.and_then(|p| p.file_name())
.map(|n| n.to_string_lossy().into_owned().into());
AddedContext {
kind: ContextKind::File,
name,
@@ -377,8 +370,14 @@ impl AddedContext {
fn directory(handle: DirectoryContextHandle, full_path: &Path) -> AddedContext {
let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into();
let (name, parent) =
extract_file_name_and_directory_from_full_path(full_path, &full_path_string);
let name = full_path
.file_name()
.map(|n| n.to_string_lossy().into_owned().into())
.unwrap_or_else(|| full_path_string.clone());
let parent = full_path
.parent()
.and_then(|p| p.file_name())
.map(|n| n.to_string_lossy().into_owned().into());
AddedContext {
kind: ContextKind::Directory,
name,
@@ -606,45 +605,22 @@ impl AddedContext {
}
}
fn image(
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 full_path_string: SharedString = full_path.to_string_lossy().into_owned().into();
let (name, parent) =
extract_file_name_and_directory_from_full_path(full_path, &full_path_string);
let icon_path = FileIcons::get_icon(&full_path, cx);
(name, parent, icon_path)
} else {
("Image".into(), None, None)
};
let status = match context.status(model) {
ImageStatus::Loading => ContextStatus::Loading {
message: "Loading…".into(),
},
ImageStatus::Error => ContextStatus::Error {
message: "Failed to load Image".into(),
},
ImageStatus::Warning => ContextStatus::Warning {
message: format!(
"{} doesn't support attaching Images as Context",
model.map(|m| m.name().0).unwrap_or_else(|| "Model".into())
)
.into(),
},
ImageStatus::Ready => ContextStatus::Ready,
};
fn image(context: ImageContext) -> AddedContext {
AddedContext {
kind: ContextKind::Image,
name,
parent,
name: "Image".into(),
parent: None,
tooltip: None,
icon_path,
status,
icon_path: None,
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({
let image = context.original_image.clone();
move |_, cx| {
@@ -663,22 +639,6 @@ impl AddedContext {
}
}
fn extract_file_name_and_directory_from_full_path(
path: &Path,
name_fallback: &SharedString,
) -> (SharedString, Option<SharedString>) {
let name = path
.file_name()
.map(|n| n.to_string_lossy().into_owned().into())
.unwrap_or_else(|| name_fallback.clone());
let parent = path
.parent()
.and_then(|p| p.file_name())
.map(|n| n.to_string_lossy().into_owned().into());
(name, parent)
}
#[derive(Debug, Clone)]
struct ContextFileExcerpt {
pub file_name_and_range: SharedString,
@@ -805,52 +765,37 @@ impl Component for AddedContext {
let mut next_context_id = ContextId::zero();
let image_ready = (
"Ready",
AddedContext::image(
ImageContext {
context_id: next_context_id.post_inc(),
project_path: None,
full_path: None,
original_image: Arc::new(Image::empty()),
image_task: Task::ready(Some(LanguageModelImage::empty())).shared(),
},
None,
cx,
),
AddedContext::image(ImageContext {
context_id: next_context_id.post_inc(),
project_path: None,
original_image: Arc::new(Image::empty()),
image_task: Task::ready(Some(LanguageModelImage::empty())).shared(),
}),
);
let image_loading = (
"Loading",
AddedContext::image(
ImageContext {
context_id: next_context_id.post_inc(),
project_path: None,
full_path: None,
original_image: Arc::new(Image::empty()),
image_task: cx
.background_spawn(async move {
smol::Timer::after(Duration::from_secs(60 * 5)).await;
Some(LanguageModelImage::empty())
})
.shared(),
},
None,
cx,
),
AddedContext::image(ImageContext {
context_id: next_context_id.post_inc(),
project_path: None,
original_image: Arc::new(Image::empty()),
image_task: cx
.background_spawn(async move {
smol::Timer::after(Duration::from_secs(60 * 5)).await;
Some(LanguageModelImage::empty())
})
.shared(),
}),
);
let image_error = (
"Error",
AddedContext::image(
ImageContext {
context_id: next_context_id.post_inc(),
project_path: None,
full_path: None,
original_image: Arc::new(Image::empty()),
image_task: Task::ready(None).shared(),
},
None,
cx,
),
AddedContext::image(ImageContext {
context_id: next_context_id.post_inc(),
project_path: None,
original_image: Arc::new(Image::empty()),
image_task: Task::ready(None).shared(),
}),
);
Some(
@@ -870,60 +815,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());
}
}

View File

@@ -2,7 +2,7 @@ use client::zed_urls;
use component::{empty_example, example_group_with_title, single_example};
use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
use language_model::RequestUsage;
use ui::{Callout, prelude::*};
use ui::{Callout, Color, Icon, IconName, IconSize, prelude::*};
use zed_llm_client::{Plan, UsageLimit};
#[derive(IntoElement, RegisterComponent)]
@@ -91,23 +91,16 @@ impl RenderOnce for UsageCallout {
.size(IconSize::XSmall)
};
div()
.border_t_1()
.border_color(cx.theme().colors().border)
.child(
Callout::new()
.icon(icon)
.title(title)
.description(message)
.primary_action(
Button::new("upgrade", button_text)
.label_size(LabelSize::Small)
.on_click(move |_, _, cx| {
cx.open_url(&url);
}),
),
)
.into_any_element()
Callout::multi_line(
title,
message,
icon,
button_text,
Box::new(move |_, _, cx| {
cx.open_url(&url);
}),
)
.into_any_element()
}
}
@@ -196,8 +189,10 @@ impl Component for UsageCallout {
);
Some(
v_flex()
div()
.p_4()
.flex()
.flex_col()
.gap_4()
.child(free_examples)
.child(trial_examples)

View File

@@ -16,6 +16,7 @@ anthropic = { workspace = true, features = ["schemars"] }
anyhow.workspace = true
collections.workspace = true
gpui.workspace = true
indexmap.workspace = true
language_model.workspace = true
lmstudio = { workspace = true, features = ["schemars"] }
log.workspace = true

View File

@@ -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)]
pub struct AgentProfileId(pub Arc<str>);
@@ -40,7 +63,7 @@ impl Default for AgentProfileId {
/// A profile for the Zed Agent that controls its behavior.
#[derive(Debug, Clone)]
pub struct AgentProfileSettings {
pub struct AgentProfile {
/// The name of the profile.
pub name: SharedString,
pub tools: IndexMap<Arc<str>, bool>,

View File

@@ -102,7 +102,7 @@ pub struct AgentSettings {
pub using_outdated_settings_version: bool,
pub default_profile: AgentProfileId,
pub default_view: DefaultView,
pub profiles: IndexMap<AgentProfileId, AgentProfileSettings>,
pub profiles: IndexMap<AgentProfileId, AgentProfile>,
pub always_allow_tool_actions: bool,
pub notify_when_agent_waiting: NotifyWhenAgentWaiting,
pub play_sound_when_agent_done: bool,
@@ -372,8 +372,6 @@ impl AgentSettingsContent {
None,
None,
Some(language_model.supports_tools()),
Some(language_model.supports_images()),
None,
)),
api_url,
});
@@ -386,9 +384,7 @@ impl AgentSettingsContent {
_ => None,
};
settings.provider = Some(AgentProviderContentV1::LmStudio {
default_model: Some(lmstudio::Model::new(
&model, None, None, false, false,
)),
default_model: Some(lmstudio::Model::new(&model, None, None, false)),
api_url,
});
}
@@ -533,7 +529,7 @@ impl AgentSettingsContent {
pub fn create_profile(
&mut self,
profile_id: AgentProfileId,
profile_settings: AgentProfileSettings,
profile: AgentProfile,
) -> Result<()> {
self.v2_setting(|settings| {
let profiles = settings.profiles.get_or_insert_default();
@@ -544,10 +540,10 @@ impl AgentSettingsContent {
profiles.insert(
profile_id,
AgentProfileContent {
name: profile_settings.name.into(),
tools: profile_settings.tools,
enable_all_context_servers: Some(profile_settings.enable_all_context_servers),
context_servers: profile_settings
name: profile.name.into(),
tools: profile.tools,
enable_all_context_servers: Some(profile.enable_all_context_servers),
context_servers: profile
.context_servers
.into_iter()
.map(|(server_id, preset)| {
@@ -693,7 +689,6 @@ pub struct AgentSettingsContentV2 {
pub enum CompletionMode {
#[default]
Normal,
#[serde(alias = "max")]
Burn,
}
@@ -732,7 +727,6 @@ impl JsonSchema for LanguageModelProviderSetting {
"zed.dev".into(),
"copilot_chat".into(),
"deepseek".into(),
"openrouter".into(),
"mistral".into(),
]),
..Default::default()
@@ -912,7 +906,7 @@ impl Settings for AgentSettings {
.extend(profiles.into_iter().map(|(id, profile)| {
(
id,
AgentProfileSettings {
AgentProfile {
name: profile.name.into(),
tools: profile.tools,
enable_all_context_servers: profile

View File

@@ -1,5 +1,4 @@
use std::str::FromStr;
use std::time::Duration;
use anyhow::{Context as _, Result, anyhow};
use chrono::{DateTime, Utc};
@@ -15,7 +14,7 @@ pub const ANTHROPIC_API_URL: &str = "https://api.anthropic.com";
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
pub struct AnthropicModelCacheConfiguration {
pub min_total_token: u64,
pub min_total_token: usize,
pub should_speculate: bool,
pub max_cache_anchors: usize,
}
@@ -33,6 +32,15 @@ pub enum AnthropicModelMode {
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)]
pub enum Model {
#[serde(rename = "claude-3-5-sonnet", alias = "claude-3-5-sonnet-latest")]
Claude3_5Sonnet,
#[serde(rename = "claude-3-7-sonnet", alias = "claude-3-7-sonnet-latest")]
Claude3_7Sonnet,
#[serde(
rename = "claude-3-7-sonnet-thinking",
alias = "claude-3-7-sonnet-thinking-latest"
)]
Claude3_7SonnetThinking,
#[serde(rename = "claude-opus-4", alias = "claude-opus-4-latest")]
ClaudeOpus4,
#[serde(
@@ -48,15 +56,6 @@ pub enum Model {
alias = "claude-sonnet-4-thinking-latest"
)]
ClaudeSonnet4Thinking,
#[serde(rename = "claude-3-7-sonnet", alias = "claude-3-7-sonnet-latest")]
Claude3_7Sonnet,
#[serde(
rename = "claude-3-7-sonnet-thinking",
alias = "claude-3-7-sonnet-thinking-latest"
)]
Claude3_7SonnetThinking,
#[serde(rename = "claude-3-5-sonnet", alias = "claude-3-5-sonnet-latest")]
Claude3_5Sonnet,
#[serde(rename = "claude-3-5-haiku", alias = "claude-3-5-haiku-latest")]
Claude3_5Haiku,
#[serde(rename = "claude-3-opus", alias = "claude-3-opus-latest")]
@@ -68,14 +67,14 @@ pub enum Model {
#[serde(rename = "custom")]
Custom {
name: String,
max_tokens: u64,
max_tokens: usize,
/// The name displayed in the UI, such as in the assistant panel model dropdown menu.
display_name: Option<String>,
/// Override this model with a different Anthropic model for tool calls.
tool_override: Option<String>,
/// Indicates whether this custom model supports caching.
cache_configuration: Option<AnthropicModelCacheConfiguration>,
max_output_tokens: Option<u64>,
max_output_tokens: Option<u32>,
default_temperature: Option<f32>,
#[serde(default)]
extra_beta_headers: Vec<String>,
@@ -90,66 +89,46 @@ impl Model {
}
pub fn from_id(id: &str) -> Result<Self> {
if id.starts_with("claude-opus-4-thinking") {
return Ok(Self::ClaudeOpus4Thinking);
}
if id.starts_with("claude-opus-4") {
return Ok(Self::ClaudeOpus4);
}
if id.starts_with("claude-sonnet-4-thinking") {
return Ok(Self::ClaudeSonnet4Thinking);
}
if id.starts_with("claude-sonnet-4") {
return Ok(Self::ClaudeSonnet4);
}
if id.starts_with("claude-3-7-sonnet-thinking") {
return Ok(Self::Claude3_7SonnetThinking);
}
if id.starts_with("claude-3-7-sonnet") {
return Ok(Self::Claude3_7Sonnet);
}
if id.starts_with("claude-3-5-sonnet") {
return Ok(Self::Claude3_5Sonnet);
Ok(Self::Claude3_5Sonnet)
} else if id.starts_with("claude-3-7-sonnet-thinking") {
Ok(Self::Claude3_7SonnetThinking)
} else if id.starts_with("claude-3-7-sonnet") {
Ok(Self::Claude3_7Sonnet)
} else if id.starts_with("claude-3-5-haiku") {
Ok(Self::Claude3_5Haiku)
} else if id.starts_with("claude-3-opus") {
Ok(Self::Claude3Opus)
} else if id.starts_with("claude-3-sonnet") {
Ok(Self::Claude3Sonnet)
} else if id.starts_with("claude-3-haiku") {
Ok(Self::Claude3Haiku)
} else if id.starts_with("claude-opus-4-thinking") {
Ok(Self::ClaudeOpus4Thinking)
} else if id.starts_with("claude-opus-4") {
Ok(Self::ClaudeOpus4)
} else if id.starts_with("claude-sonnet-4-thinking") {
Ok(Self::ClaudeSonnet4Thinking)
} else if id.starts_with("claude-sonnet-4") {
Ok(Self::ClaudeSonnet4)
} else {
anyhow::bail!("invalid model id {id}");
}
if id.starts_with("claude-3-5-haiku") {
return Ok(Self::Claude3_5Haiku);
}
if id.starts_with("claude-3-opus") {
return Ok(Self::Claude3Opus);
}
if id.starts_with("claude-3-sonnet") {
return Ok(Self::Claude3Sonnet);
}
if id.starts_with("claude-3-haiku") {
return Ok(Self::Claude3Haiku);
}
Err(anyhow!("invalid model ID: {id}"))
}
pub fn id(&self) -> &str {
match self {
Self::ClaudeOpus4 => "claude-opus-4-latest",
Self::ClaudeOpus4Thinking => "claude-opus-4-thinking-latest",
Self::ClaudeSonnet4 => "claude-sonnet-4-latest",
Self::ClaudeSonnet4Thinking => "claude-sonnet-4-thinking-latest",
Self::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
Self::Claude3_7Sonnet => "claude-3-7-sonnet-latest",
Self::Claude3_7SonnetThinking => "claude-3-7-sonnet-thinking-latest",
Self::Claude3_5Haiku => "claude-3-5-haiku-latest",
Self::Claude3Opus => "claude-3-opus-latest",
Self::Claude3Sonnet => "claude-3-sonnet-20240229",
Self::Claude3Haiku => "claude-3-haiku-20240307",
Model::ClaudeOpus4 => "claude-opus-4-latest",
Model::ClaudeOpus4Thinking => "claude-opus-4-thinking-latest",
Model::ClaudeSonnet4 => "claude-sonnet-4-latest",
Model::ClaudeSonnet4Thinking => "claude-sonnet-4-thinking-latest",
Model::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
Model::Claude3_7Sonnet => "claude-3-7-sonnet-latest",
Model::Claude3_7SonnetThinking => "claude-3-7-sonnet-thinking-latest",
Model::Claude3_5Haiku => "claude-3-5-haiku-latest",
Model::Claude3Opus => "claude-3-opus-latest",
Model::Claude3Sonnet => "claude-3-sonnet-20240229",
Model::Claude3Haiku => "claude-3-haiku-20240307",
Self::Custom { name, .. } => name,
}
}
@@ -157,24 +136,24 @@ impl Model {
/// The id of the model that should be used for making API requests
pub fn request_id(&self) -> &str {
match self {
Self::ClaudeOpus4 | Self::ClaudeOpus4Thinking => "claude-opus-4-20250514",
Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking => "claude-sonnet-4-20250514",
Self::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => "claude-3-7-sonnet-latest",
Self::Claude3_5Haiku => "claude-3-5-haiku-latest",
Self::Claude3Opus => "claude-3-opus-latest",
Self::Claude3Sonnet => "claude-3-sonnet-20240229",
Self::Claude3Haiku => "claude-3-haiku-20240307",
Model::ClaudeOpus4 | Model::ClaudeOpus4Thinking => "claude-opus-4-20250514",
Model::ClaudeSonnet4 | Model::ClaudeSonnet4Thinking => "claude-sonnet-4-20250514",
Model::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
Model::Claude3_7Sonnet | Model::Claude3_7SonnetThinking => "claude-3-7-sonnet-latest",
Model::Claude3_5Haiku => "claude-3-5-haiku-latest",
Model::Claude3Opus => "claude-3-opus-latest",
Model::Claude3Sonnet => "claude-3-sonnet-20240229",
Model::Claude3Haiku => "claude-3-haiku-20240307",
Self::Custom { name, .. } => name,
}
}
pub fn display_name(&self) -> &str {
match self {
Self::ClaudeOpus4 => "Claude Opus 4",
Self::ClaudeOpus4Thinking => "Claude Opus 4 Thinking",
Self::ClaudeSonnet4 => "Claude Sonnet 4",
Self::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking",
Model::ClaudeOpus4 => "Claude Opus 4",
Model::ClaudeOpus4Thinking => "Claude Opus 4 Thinking",
Model::ClaudeSonnet4 => "Claude Sonnet 4",
Model::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking",
Self::Claude3_7Sonnet => "Claude 3.7 Sonnet",
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
Self::Claude3_7SonnetThinking => "Claude 3.7 Sonnet Thinking",
@@ -211,7 +190,7 @@ impl Model {
}
}
pub fn max_token_count(&self) -> u64 {
pub fn max_token_count(&self) -> usize {
match self {
Self::ClaudeOpus4
| Self::ClaudeOpus4Thinking
@@ -228,17 +207,17 @@ impl Model {
}
}
pub fn max_output_tokens(&self) -> u64 {
pub fn max_output_tokens(&self) -> u32 {
match self {
Self::ClaudeOpus4
| Self::ClaudeOpus4Thinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking
| Self::Claude3_5Sonnet
Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3Haiku => 4_096,
Self::Claude3_5Sonnet
| Self::Claude3_7Sonnet
| Self::Claude3_7SonnetThinking
| Self::Claude3_5Haiku => 8_192,
Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3Haiku => 4_096,
| Self::Claude3_5Haiku
| Self::ClaudeOpus4
| Self::ClaudeOpus4Thinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking => 8_192,
Self::Custom {
max_output_tokens, ..
} => max_output_tokens.unwrap_or(4_096),
@@ -267,17 +246,17 @@ impl Model {
pub fn mode(&self) -> AnthropicModelMode {
match self {
Self::ClaudeOpus4
| Self::ClaudeSonnet4
| Self::Claude3_5Sonnet
Self::Claude3_5Sonnet
| Self::Claude3_7Sonnet
| Self::Claude3_5Haiku
| Self::ClaudeOpus4
| Self::ClaudeSonnet4
| Self::Claude3Opus
| Self::Claude3Sonnet
| Self::Claude3Haiku => AnthropicModelMode::Default,
Self::ClaudeOpus4Thinking
| Self::ClaudeSonnet4Thinking
| Self::Claude3_7SonnetThinking => AnthropicModelMode::Thinking {
Self::Claude3_7SonnetThinking
| Self::ClaudeOpus4Thinking
| Self::ClaudeSonnet4Thinking => AnthropicModelMode::Thinking {
budget_tokens: Some(4_096),
},
Self::Custom { mode, .. } => mode.clone(),
@@ -288,7 +267,7 @@ impl Model {
pub fn beta_headers(&self) -> String {
let mut headers = Self::DEFAULT_BETA_HEADERS
.iter()
.into_iter()
.map(|header| header.to_string())
.collect::<Vec<_>>();
@@ -427,7 +406,6 @@ impl RateLimit {
/// <https://docs.anthropic.com/en/api/rate-limits#response-headers>
#[derive(Debug)]
pub struct RateLimitInfo {
pub retry_after: Option<Duration>,
pub requests: Option<RateLimit>,
pub tokens: Option<RateLimit>,
pub input_tokens: Option<RateLimit>,
@@ -439,11 +417,10 @@ impl RateLimitInfo {
// Check if any rate limit headers exist
let has_rate_limit_headers = headers
.keys()
.any(|k| k == "retry-after" || k.as_str().starts_with("anthropic-ratelimit-"));
.any(|k| k.as_str().starts_with("anthropic-ratelimit-"));
if !has_rate_limit_headers {
return Self {
retry_after: None,
requests: None,
tokens: None,
input_tokens: None,
@@ -452,11 +429,6 @@ impl RateLimitInfo {
}
Self {
retry_after: headers
.get("retry-after")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse::<u64>().ok())
.map(Duration::from_secs),
requests: RateLimit::from_headers("requests", headers).ok(),
tokens: RateLimit::from_headers("tokens", headers).ok(),
input_tokens: RateLimit::from_headers("input-tokens", headers).ok(),
@@ -509,8 +481,8 @@ pub async fn stream_completion_with_rate_limit_info(
.send(request)
.await
.context("failed to send request to Anthropic")?;
let rate_limits = RateLimitInfo::from_headers(response.headers());
if response.status().is_success() {
let rate_limits = RateLimitInfo::from_headers(response.headers());
let reader = BufReader::new(response.into_body());
let stream = reader
.lines()
@@ -528,8 +500,6 @@ pub async fn stream_completion_with_rate_limit_info(
})
.boxed();
Ok((stream, Some(rate_limits)))
} else if let Some(retry_after) = rate_limits.retry_after {
Err(AnthropicError::RateLimit(retry_after))
} else {
let mut body = Vec::new();
response
@@ -693,7 +663,7 @@ pub enum StringOrContents {
#[derive(Debug, Serialize, Deserialize)]
pub struct Request {
pub model: String,
pub max_tokens: u64,
pub max_tokens: u32,
pub messages: Vec<Message>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tools: Vec<Tool>,
@@ -730,13 +700,13 @@ pub struct Metadata {
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct Usage {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub input_tokens: Option<u64>,
pub input_tokens: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output_tokens: Option<u64>,
pub output_tokens: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_creation_input_tokens: Option<u64>,
pub cache_creation_input_tokens: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_read_input_tokens: Option<u64>,
pub cache_read_input_tokens: Option<u32>,
}
#[derive(Debug, Serialize, Deserialize)]
@@ -799,8 +769,6 @@ pub struct MessageDelta {
#[derive(Error, Debug)]
pub enum AnthropicError {
#[error("rate limit exceeded, retry after {0:?}")]
RateLimit(Duration),
#[error("an error occurred while interacting with the Anthropic API: {error_type}: {message}", error_type = .0.error_type, message = .0.message)]
ApiError(ApiError),
#[error("{0}")]
@@ -846,7 +814,7 @@ impl ApiError {
matches!(self.error_type.as_str(), "rate_limit_error")
}
pub fn match_window_exceeded(&self) -> Option<u64> {
pub fn match_window_exceeded(&self) -> Option<usize> {
let Some(ApiErrorCode::InvalidRequestError) = self.code() else {
return None;
};
@@ -855,12 +823,12 @@ impl ApiError {
}
}
pub fn parse_prompt_too_long(message: &str) -> Option<u64> {
pub fn parse_prompt_too_long(message: &str) -> Option<usize> {
message
.strip_prefix("prompt is too long: ")?
.split_once(" tokens")?
.0
.parse()
.parse::<usize>()
.ok()
}

View File

@@ -15,6 +15,7 @@ path = "src/askpass.rs"
anyhow.workspace = true
futures.workspace = true
gpui.workspace = true
shlex.workspace = true
smol.workspace = true
tempfile.workspace = true
util.workspace = true

View File

@@ -13,9 +13,9 @@ use gpui::{AsyncApp, BackgroundExecutor, Task};
#[cfg(unix)]
use smol::fs;
#[cfg(unix)]
use smol::net::unix::UnixListener;
use smol::{fs::unix::PermissionsExt as _, net::unix::UnixListener};
#[cfg(unix)]
use util::{ResultExt as _, fs::make_file_executable, get_shell_safe_zed_path};
use util::ResultExt as _;
#[derive(PartialEq, Eq)]
pub enum AskPassResult {
@@ -120,7 +120,7 @@ impl AskPassSession {
shebang = "#!/bin/sh",
);
fs::write(&askpass_script_path, askpass_script).await?;
make_file_executable(&askpass_script_path).await?;
fs::set_permissions(&askpass_script_path, std::fs::Permissions::from_mode(0o755)).await?;
Ok(Self {
script_path: askpass_script_path,
@@ -160,6 +160,38 @@ impl AskPassSession {
}
}
#[cfg(unix)]
fn get_shell_safe_zed_path() -> anyhow::Result<String> {
let zed_path = std::env::current_exe()
.context("Failed to determine current executable path for use in askpass")?
.to_string_lossy()
// see https://github.com/rust-lang/rust/issues/69343
.trim_end_matches(" (deleted)")
.to_string();
// NOTE: this was previously enabled, however, it caused errors when it shouldn't have
// (see https://github.com/zed-industries/zed/issues/29819)
// The zed path failing to execute within the askpass script results in very vague ssh
// authentication failed errors, so this was done to try and surface a better error
//
// use std::os::unix::fs::MetadataExt;
// let metadata = std::fs::metadata(&zed_path)
// .context("Failed to check metadata of Zed executable path for use in askpass")?;
// let is_executable = metadata.is_file() && metadata.mode() & 0o111 != 0;
// anyhow::ensure!(
// is_executable,
// "Failed to verify Zed executable path for use in askpass"
// );
// As of writing, this can only be fail if the path contains a null byte, which shouldn't be possible
// but shlex has annotated the error as #[non_exhaustive] so we can't make it a compile error if other
// errors are introduced in the future :(
let zed_path_escaped = shlex::try_quote(&zed_path)
.context("Failed to shell-escape Zed executable path for use in askpass")?;
return Ok(zed_path_escaped.to_string());
}
/// The main function for when Zed is running in netcat mode for use in askpass.
/// Called from both the remote server binary and the zed binary in their respective main functions.
#[cfg(unix)]

View File

@@ -60,7 +60,6 @@ zed_actions.workspace = true
zed_llm_client.workspace = true
[dev-dependencies]
indoc.workspace = true
language_model = { workspace = true, features = ["test-support"] }
languages = { workspace = true, features = ["test-support"] }
pretty_assertions.workspace = true

View File

@@ -11,7 +11,7 @@ use assistant_slash_commands::FileCommandMetadata;
use client::{self, proto, telemetry::Telemetry};
use clock::ReplicaId;
use collections::{HashMap, HashSet};
use fs::{Fs, RenameOptions};
use fs::{Fs, RemoveOptions};
use futures::{FutureExt, StreamExt, future::Shared};
use gpui::{
App, AppContext as _, Context, Entity, EventEmitter, RenderImage, SharedString, Subscription,
@@ -452,10 +452,6 @@ pub enum ContextEvent {
MessagesEdited,
SummaryChanged,
SummaryGenerated,
PathChanged {
old_path: Option<Arc<Path>>,
new_path: Arc<Path>,
},
StreamedCompletion,
StartedThoughtProcess(Range<language::Anchor>),
EndedThoughtProcess(language::Anchor),
@@ -678,7 +674,7 @@ pub struct AssistantContext {
summary_task: Task<Option<()>>,
completion_count: usize,
pending_completions: Vec<PendingCompletion>,
token_count: Option<u64>,
token_count: Option<usize>,
pending_token_count: Task<Option<()>>,
pending_save: Task<Result<()>>,
pending_cache_warming_task: Task<Option<()>>,
@@ -1250,7 +1246,7 @@ impl AssistantContext {
}
}
pub fn token_count(&self) -> Option<u64> {
pub fn token_count(&self) -> Option<usize> {
self.token_count
}
@@ -2898,34 +2894,22 @@ impl AssistantContext {
}
fs.create_dir(contexts_dir().as_ref()).await?;
// rename before write ensures that only one file exists
if let Some(old_path) = old_path.as_ref() {
fs.atomic_write(new_path.clone(), serde_json::to_string(&context).unwrap())
.await?;
if let Some(old_path) = old_path {
if new_path.as_path() != old_path.as_ref() {
fs.rename(
fs.remove_file(
&old_path,
&new_path,
RenameOptions {
overwrite: true,
ignore_if_exists: true,
RemoveOptions {
recursive: false,
ignore_if_not_exists: true,
},
)
.await?;
}
}
// update path before write in case it fails
this.update(cx, {
let new_path: Arc<Path> = new_path.clone().into();
move |this, cx| {
this.path = Some(new_path.clone());
cx.emit(ContextEvent::PathChanged { old_path, new_path });
}
})
.ok();
fs.atomic_write(new_path, serde_json::to_string(&context).unwrap())
.await?;
this.update(cx, |this, _| this.path = Some(new_path.into()))?;
}
Ok(())
@@ -3293,7 +3277,7 @@ impl SavedContextV0_1_0 {
#[derive(Debug, Clone)]
pub struct SavedContextMetadata {
pub title: SharedString,
pub title: String,
pub path: Arc<Path>,
pub mtime: chrono::DateTime<chrono::Local>,
}

View File

@@ -39,7 +39,7 @@ use language::{
language_settings::{SoftWrap, all_language_settings},
};
use language_model::{
ConfigurationError, LanguageModelImage, LanguageModelProviderTosView, LanguageModelRegistry,
LanguageModelImage, LanguageModelProvider, LanguageModelProviderTosView, LanguageModelRegistry,
Role,
};
use multi_buffer::MultiBufferRow;
@@ -580,7 +580,6 @@ impl ContextEditor {
});
}
ContextEvent::SummaryGenerated => {}
ContextEvent::PathChanged { .. } => {}
ContextEvent::StartedThoughtProcess(range) => {
let creases = self.insert_thought_process_output_sections(
[(
@@ -1647,35 +1646,34 @@ impl ContextEditor {
let context = self.context.read(cx);
let mut text = String::new();
// If selection is empty, we want to copy the entire line
if selection.range().is_empty() {
let snapshot = context.buffer().read(cx).snapshot();
let point = snapshot.offset_to_point(selection.range().start);
selection.start = snapshot.point_to_offset(Point::new(point.row, 0));
selection.end = snapshot
.point_to_offset(cmp::min(Point::new(point.row + 1, 0), snapshot.max_point()));
for chunk in context.buffer().read(cx).text_for_range(selection.range()) {
text.push_str(chunk);
}
} else {
for message in context.messages(cx) {
if message.offset_range.start >= selection.range().end {
break;
} else if message.offset_range.end >= selection.range().start {
let range = cmp::max(message.offset_range.start, selection.range().start)
..cmp::min(message.offset_range.end, selection.range().end);
if !range.is_empty() {
for chunk in context.buffer().read(cx).text_for_range(range) {
text.push_str(chunk);
}
if message.offset_range.end < selection.range().end {
text.push('\n');
}
for message in context.messages(cx) {
if message.offset_range.start >= selection.range().end {
break;
} else if message.offset_range.end >= selection.range().start {
let range = cmp::max(message.offset_range.start, selection.range().start)
..cmp::min(message.offset_range.end, selection.range().end);
if range.is_empty() {
let snapshot = context.buffer().read(cx).snapshot();
let point = snapshot.offset_to_point(range.start);
selection.start = snapshot.point_to_offset(Point::new(point.row, 0));
selection.end = snapshot.point_to_offset(cmp::min(
Point::new(point.row + 1, 0),
snapshot.max_point(),
));
for chunk in context.buffer().read(cx).text_for_range(selection.range()) {
text.push_str(chunk);
}
} else {
for chunk in context.buffer().read(cx).text_for_range(range) {
text.push_str(chunk);
}
if message.offset_range.end < selection.range().end {
text.push('\n');
}
}
}
}
(text, CopyMetadata { creases }, vec![selection])
}
@@ -1887,8 +1885,6 @@ impl ContextEditor {
// value to not show the nudge.
let nudge = Some(false);
let model_registry = LanguageModelRegistry::read_global(cx);
if nudge.map_or(false, |value| value) {
Some(
h_flex()
@@ -1937,9 +1933,14 @@ impl ContextEditor {
)
.into_any_element(),
)
} else if let Some(configuration_error) =
model_registry.configuration_error(model_registry.default_model(), cx)
{
} else if let Some(configuration_error) = configuration_error(cx) {
let label = match configuration_error {
ConfigurationError::NoProvider => "No LLM provider selected.",
ConfigurationError::ProviderNotAuthenticated => "LLM provider is not configured.",
ConfigurationError::ProviderPendingTermsAcceptance(_) => {
"LLM provider requires accepting the Terms of Service."
}
};
Some(
h_flex()
.px_3()
@@ -1956,7 +1957,7 @@ impl ContextEditor {
.size(IconSize::Small)
.color(Color::Warning),
)
.child(Label::new(configuration_error.to_string())),
.child(Label::new(label)),
)
.child(
Button::new("open-configuration", "Configure Providers")
@@ -2031,19 +2032,14 @@ impl ContextEditor {
/// Will return false if the selected provided has a configuration error or
/// if the user has not accepted the terms of service for this provider.
fn sending_disabled(&self, cx: &mut Context<'_, ContextEditor>) -> bool {
let model_registry = LanguageModelRegistry::read_global(cx);
let Some(configuration_error) =
model_registry.configuration_error(model_registry.default_model(), cx)
else {
return false;
};
let model = LanguageModelRegistry::read_global(cx).default_model();
match configuration_error {
ConfigurationError::NoProvider
| ConfigurationError::ModelNotFound
| ConfigurationError::ProviderNotAuthenticated(_) => true,
ConfigurationError::ProviderPendingTermsAcceptance(_) => self.show_accept_terms,
}
let has_configuration_error = configuration_error(cx).is_some();
let needs_to_accept_terms = self.show_accept_terms
&& model
.as_ref()
.map_or(false, |model| model.provider.must_accept_terms(cx));
has_configuration_error || needs_to_accept_terms
}
fn render_inject_context_menu(&self, cx: &mut Context<Self>) -> impl IntoElement {
@@ -3121,12 +3117,12 @@ fn invoked_slash_command_fold_placeholder(
enum TokenState {
NoTokensLeft {
max_token_count: u64,
token_count: u64,
max_token_count: usize,
token_count: usize,
},
HasMoreTokens {
max_token_count: u64,
token_count: u64,
max_token_count: usize,
token_count: usize,
over_warn_threshold: bool,
},
}
@@ -3139,7 +3135,9 @@ fn token_state(context: &Entity<AssistantContext>, cx: &App) -> Option<TokenStat
.model;
let token_count = context.read(cx).token_count()?;
let max_token_count = model.max_token_count();
let token_state = if max_token_count.saturating_sub(token_count) == 0 {
let remaining_tokens = max_token_count as isize - token_count as isize;
let token_state = if remaining_tokens <= 0 {
TokenState::NoTokensLeft {
max_token_count,
token_count,
@@ -3180,7 +3178,34 @@ fn size_for_image(data: &RenderImage, max_size: Size<Pixels>) -> Size<Pixels> {
}
}
pub fn humanize_token_count(count: u64) -> String {
pub enum ConfigurationError {
NoProvider,
ProviderNotAuthenticated,
ProviderPendingTermsAcceptance(Arc<dyn LanguageModelProvider>),
}
fn configuration_error(cx: &App) -> Option<ConfigurationError> {
let model = LanguageModelRegistry::read_global(cx).default_model();
let is_authenticated = model
.as_ref()
.map_or(false, |model| model.provider.is_authenticated(cx));
if model.is_some() && is_authenticated {
return None;
}
if model.is_none() {
return Some(ConfigurationError::NoProvider);
}
if !is_authenticated {
return Some(ConfigurationError::ProviderNotAuthenticated);
}
None
}
pub fn humanize_token_count(count: usize) -> String {
match count {
0..=999 => count.to_string(),
1000..=9999 => {
@@ -3239,92 +3264,74 @@ mod tests {
use super::*;
use fs::FakeFs;
use gpui::{App, TestAppContext, VisualTestContext};
use indoc::indoc;
use language::{Buffer, LanguageRegistry};
use pretty_assertions::assert_eq;
use prompt_store::PromptBuilder;
use text::OffsetRangeExt;
use unindent::Unindent;
use util::path;
#[gpui::test]
async fn test_copy_paste_whole_message(cx: &mut TestAppContext) {
let (context, context_editor, mut cx) = setup_context_editor_text(vec![
(Role::User, "What is the Zed editor?"),
(
Role::Assistant,
"Zed is a modern, high-performance code editor designed from the ground up for speed and collaboration.",
),
(Role::User, ""),
],cx).await;
// Select & Copy whole user message
assert_copy_paste_context_editor(
&context_editor,
message_range(&context, 0, &mut cx),
indoc! {"
What is the Zed editor?
Zed is a modern, high-performance code editor designed from the ground up for speed and collaboration.
What is the Zed editor?
"},
&mut cx,
);
// Select & Copy whole assistant message
assert_copy_paste_context_editor(
&context_editor,
message_range(&context, 1, &mut cx),
indoc! {"
What is the Zed editor?
Zed is a modern, high-performance code editor designed from the ground up for speed and collaboration.
What is the Zed editor?
Zed is a modern, high-performance code editor designed from the ground up for speed and collaboration.
"},
&mut cx,
);
}
#[gpui::test]
async fn test_copy_paste_no_selection(cx: &mut TestAppContext) {
let (context, context_editor, mut cx) = setup_context_editor_text(
vec![
(Role::User, "user1"),
(Role::Assistant, "assistant1"),
(Role::Assistant, "assistant2"),
(Role::User, ""),
],
cx,
)
.await;
cx.update(init_test);
// Copy and paste first assistant message
let message_2_range = message_range(&context, 1, &mut cx);
assert_copy_paste_context_editor(
&context_editor,
message_2_range.start..message_2_range.start,
indoc! {"
user1
assistant1
assistant2
assistant1
"},
&mut cx,
);
let fs = FakeFs::new(cx.executor());
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context = cx.new(|cx| {
AssistantContext::local(
registry,
None,
None,
prompt_builder.clone(),
Arc::new(SlashCommandWorkingSet::default()),
cx,
)
});
let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let workspace = window.root(cx).unwrap();
let cx = &mut VisualTestContext::from_window(*window, cx);
// Copy and cut second assistant message
let message_3_range = message_range(&context, 2, &mut cx);
assert_copy_paste_context_editor(
&context_editor,
message_3_range.start..message_3_range.start,
indoc! {"
user1
assistant1
assistant2
assistant1
assistant2
"},
&mut cx,
);
let context_editor = window
.update(cx, |_, window, cx| {
cx.new(|cx| {
ContextEditor::for_context(
context,
fs,
workspace.downgrade(),
project,
None,
window,
cx,
)
})
})
.unwrap();
context_editor.update_in(cx, |context_editor, window, cx| {
context_editor.editor.update(cx, |editor, cx| {
editor.set_text("abc\ndef\nghi", window, cx);
editor.move_to_beginning(&Default::default(), window, cx);
})
});
context_editor.update_in(cx, |context_editor, window, cx| {
context_editor.editor.update(cx, |editor, cx| {
editor.copy(&Default::default(), window, cx);
editor.paste(&Default::default(), window, cx);
assert_eq!(editor.text(cx), "abc\nabc\ndef\nghi");
})
});
context_editor.update_in(cx, |context_editor, window, cx| {
context_editor.editor.update(cx, |editor, cx| {
editor.cut(&Default::default(), window, cx);
assert_eq!(editor.text(cx), "abc\ndef\nghi");
editor.paste(&Default::default(), window, cx);
assert_eq!(editor.text(cx), "abc\nabc\ndef\nghi");
})
});
}
#[gpui::test]
@@ -3401,129 +3408,6 @@ mod tests {
}
}
async fn setup_context_editor_text(
messages: Vec<(Role, &str)>,
cx: &mut TestAppContext,
) -> (
Entity<AssistantContext>,
Entity<ContextEditor>,
VisualTestContext,
) {
cx.update(init_test);
let fs = FakeFs::new(cx.executor());
let context = create_context_with_messages(messages, cx);
let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let workspace = window.root(cx).unwrap();
let mut cx = VisualTestContext::from_window(*window, cx);
let context_editor = window
.update(&mut cx, |_, window, cx| {
cx.new(|cx| {
let editor = ContextEditor::for_context(
context.clone(),
fs,
workspace.downgrade(),
project,
None,
window,
cx,
);
editor
})
})
.unwrap();
(context, context_editor, cx)
}
fn message_range(
context: &Entity<AssistantContext>,
message_ix: usize,
cx: &mut TestAppContext,
) -> Range<usize> {
context.update(cx, |context, cx| {
context
.messages(cx)
.nth(message_ix)
.unwrap()
.anchor_range
.to_offset(&context.buffer().read(cx).snapshot())
})
}
fn assert_copy_paste_context_editor<T: editor::ToOffset>(
context_editor: &Entity<ContextEditor>,
range: Range<T>,
expected_text: &str,
cx: &mut VisualTestContext,
) {
context_editor.update_in(cx, |context_editor, window, cx| {
context_editor.editor.update(cx, |editor, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([range]));
});
context_editor.copy(&Default::default(), window, cx);
context_editor.editor.update(cx, |editor, cx| {
editor.move_to_end(&Default::default(), window, cx);
});
context_editor.paste(&Default::default(), window, cx);
context_editor.editor.update(cx, |editor, cx| {
assert_eq!(editor.text(cx), expected_text);
});
});
}
fn create_context_with_messages(
mut messages: Vec<(Role, &str)>,
cx: &mut TestAppContext,
) -> Entity<AssistantContext> {
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
cx.new(|cx| {
let mut context = AssistantContext::local(
registry,
None,
None,
prompt_builder.clone(),
Arc::new(SlashCommandWorkingSet::default()),
cx,
);
let mut message_1 = context.messages(cx).next().unwrap();
let (role, text) = messages.remove(0);
loop {
if role == message_1.role {
context.buffer().update(cx, |buffer, cx| {
buffer.edit([(message_1.offset_range, text)], None, cx);
});
break;
}
let mut ids = HashSet::default();
ids.insert(message_1.id);
context.cycle_message_roles(ids, cx);
message_1 = context.messages(cx).next().unwrap();
}
let mut last_message_id = message_1.id;
for (role, text) in messages {
context.insert_message_after(last_message_id, role, MessageStatus::Done, cx);
let message = context.messages(cx).last().unwrap();
last_message_id = message.id;
context.buffer().update(cx, |buffer, cx| {
buffer.edit([(message.offset_range, text)], None, cx);
})
}
context
})
}
fn init_test(cx: &mut App) {
let settings_store = SettingsStore::test(cx);
prompt_store::init(cx);

View File

@@ -347,6 +347,12 @@ impl ContextStore {
self.contexts_metadata.iter()
}
pub fn reverse_chronological_contexts(&self) -> Vec<SavedContextMetadata> {
let mut contexts = self.contexts_metadata.iter().cloned().collect::<Vec<_>>();
contexts.sort_unstable_by_key(|thread| std::cmp::Reverse(thread.mtime));
contexts
}
pub fn create(&mut self, cx: &mut Context<Self>) -> Entity<AssistantContext> {
let context = cx.new(|cx| {
AssistantContext::local(
@@ -612,16 +618,6 @@ impl ContextStore {
ContextEvent::SummaryChanged => {
self.advertise_contexts(cx);
}
ContextEvent::PathChanged { old_path, new_path } => {
if let Some(old_path) = old_path.as_ref() {
for metadata in &mut self.contexts_metadata {
if &metadata.path == old_path {
metadata.path = new_path.clone();
break;
}
}
}
}
ContextEvent::Operation(operation) => {
let context_id = context.read(cx).id().to_proto();
let operation = operation.to_proto();
@@ -745,7 +741,6 @@ impl ContextStore {
&candidates,
&query,
false,
true,
100,
&Default::default(),
executor,
@@ -797,7 +792,7 @@ impl ContextStore {
.next()
{
contexts.push(SavedContextMetadata {
title: title.to_string().into(),
title: title.to_string(),
path: path.into(),
mtime: metadata.mtime.timestamp_for_user().into(),
});
@@ -814,37 +809,74 @@ impl ContextStore {
}
fn register_context_server_handlers(&self, cx: &mut Context<Self>) {
let context_server_store = self.project.read(cx).context_server_store();
cx.subscribe(&context_server_store, Self::handle_context_server_event)
.detach();
// Check for any servers that were already running before the handler was registered
for server in context_server_store.read(cx).running_servers() {
self.load_context_server_slash_commands(server.id(), context_server_store.clone(), cx);
}
cx.subscribe(
&self.project.read(cx).context_server_store(),
Self::handle_context_server_event,
)
.detach();
}
fn handle_context_server_event(
&mut self,
context_server_store: Entity<ContextServerStore>,
context_server_manager: Entity<ContextServerStore>,
event: &project::context_server_store::Event,
cx: &mut Context<Self>,
) {
let slash_command_working_set = self.slash_commands.clone();
match event {
project::context_server_store::Event::ServerStatusChanged { server_id, status } => {
match status {
ContextServerStatus::Running => {
self.load_context_server_slash_commands(
server_id.clone(),
context_server_store.clone(),
cx,
);
if let Some(server) = context_server_manager
.read(cx)
.get_running_server(server_id)
{
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(_) => {
if let Some(slash_command_ids) =
self.context_server_slash_command_ids.remove(server_id)
{
self.slash_commands.remove(&slash_command_ids);
slash_command_working_set.remove(&slash_command_ids);
}
}
_ => {}
@@ -852,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::requests::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();
}
}

View File

@@ -310,7 +310,6 @@ impl ModelMatcher {
&self.candidates,
&query,
false,
true,
100,
&Default::default(),
self.bg_executor.clone(),
@@ -665,7 +664,7 @@ mod tests {
format!("{}/{}", self.provider_id.0, self.name.0)
}
fn max_token_count(&self) -> u64 {
fn max_token_count(&self) -> usize {
1000
}
@@ -673,7 +672,7 @@ mod tests {
&self,
_: LanguageModelRequest,
_: &App,
) -> BoxFuture<'static, http_client::Result<u64>> {
) -> BoxFuture<'static, http_client::Result<usize>> {
unimplemented!()
}
@@ -683,12 +682,11 @@ mod tests {
_: &AsyncApp,
) -> BoxFuture<
'static,
Result<
http_client::Result<
BoxStream<
'static,
Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
http_client::Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
>,
LanguageModelCompletionError,
>,
> {
unimplemented!()

View File

@@ -10,7 +10,9 @@ use parking_lot::Mutex;
use project::{CompletionIntent, CompletionSource, lsp_store::CompletionDocumentation};
use rope::Point;
use std::{
cell::RefCell,
ops::Range,
rc::Rc,
sync::{
Arc,
atomic::{AtomicBool, Ordering::SeqCst},
@@ -46,7 +48,7 @@ impl SlashCommandCompletionProvider {
name_range: Range<Anchor>,
window: &mut Window,
cx: &mut App,
) -> Task<Result<Vec<project::CompletionResponse>>> {
) -> Task<Result<Option<Vec<project::Completion>>>> {
let slash_commands = self.slash_commands.clone();
let candidates = slash_commands
.command_names(cx)
@@ -62,7 +64,6 @@ impl SlashCommandCompletionProvider {
&candidates,
&command_name,
true,
true,
usize::MAX,
&Default::default(),
cx.background_executor().clone(),
@@ -70,27 +71,28 @@ impl SlashCommandCompletionProvider {
.await;
cx.update(|_, cx| {
let completions = matches
.into_iter()
.filter_map(|mat| {
let command = slash_commands.command(&mat.string, cx)?;
let mut new_text = mat.string.clone();
let requires_argument = command.requires_argument();
let accepts_arguments = command.accepts_arguments();
if requires_argument || accepts_arguments {
new_text.push(' ');
}
Some(
matches
.into_iter()
.filter_map(|mat| {
let command = slash_commands.command(&mat.string, cx)?;
let mut new_text = mat.string.clone();
let requires_argument = command.requires_argument();
let accepts_arguments = command.accepts_arguments();
if requires_argument || accepts_arguments {
new_text.push(' ');
}
let confirm =
editor
.clone()
.zip(workspace.clone())
.map(|(editor, workspace)| {
let command_name = mat.string.clone();
let command_range = command_range.clone();
let editor = editor.clone();
let workspace = workspace.clone();
Arc::new(
let confirm =
editor
.clone()
.zip(workspace.clone())
.map(|(editor, workspace)| {
let command_name = mat.string.clone();
let command_range = command_range.clone();
let editor = editor.clone();
let workspace = workspace.clone();
Arc::new(
move |intent: CompletionIntent,
window: &mut Window,
cx: &mut App| {
@@ -116,27 +118,22 @@ impl SlashCommandCompletionProvider {
}
},
) as Arc<_>
});
Some(project::Completion {
replace_range: name_range.clone(),
documentation: Some(CompletionDocumentation::SingleLine(
command.description().into(),
)),
new_text,
label: command.label(cx),
icon_path: None,
insert_text_mode: None,
confirm,
source: CompletionSource::Custom,
});
Some(project::Completion {
replace_range: name_range.clone(),
documentation: Some(CompletionDocumentation::SingleLine(
command.description().into(),
)),
new_text,
label: command.label(cx),
icon_path: None,
insert_text_mode: None,
confirm,
source: CompletionSource::Custom,
})
})
})
.collect();
vec![project::CompletionResponse {
completions,
is_incomplete: false,
}]
.collect(),
)
})
})
}
@@ -150,7 +147,7 @@ impl SlashCommandCompletionProvider {
last_argument_range: Range<Anchor>,
window: &mut Window,
cx: &mut App,
) -> Task<Result<Vec<project::CompletionResponse>>> {
) -> Task<Result<Option<Vec<project::Completion>>>> {
let new_cancel_flag = Arc::new(AtomicBool::new(false));
let mut flag = self.cancel_flag.lock();
flag.store(true, SeqCst);
@@ -168,27 +165,28 @@ impl SlashCommandCompletionProvider {
let workspace = self.workspace.clone();
let arguments = arguments.to_vec();
cx.background_spawn(async move {
let completions = completions
.await?
.into_iter()
.map(|new_argument| {
let confirm =
editor
.clone()
.zip(workspace.clone())
.map(|(editor, workspace)| {
Arc::new({
let mut completed_arguments = arguments.clone();
if new_argument.replace_previous_arguments {
completed_arguments.clear();
} else {
completed_arguments.pop();
}
completed_arguments.push(new_argument.new_text.clone());
Ok(Some(
completions
.await?
.into_iter()
.map(|new_argument| {
let confirm =
editor
.clone()
.zip(workspace.clone())
.map(|(editor, workspace)| {
Arc::new({
let mut completed_arguments = arguments.clone();
if new_argument.replace_previous_arguments {
completed_arguments.clear();
} else {
completed_arguments.pop();
}
completed_arguments.push(new_argument.new_text.clone());
let command_range = command_range.clone();
let command_name = command_name.clone();
move |intent: CompletionIntent,
let command_range = command_range.clone();
let command_name = command_name.clone();
move |intent: CompletionIntent,
window: &mut Window,
cx: &mut App| {
if new_argument.after_completion.run()
@@ -212,42 +210,34 @@ impl SlashCommandCompletionProvider {
!new_argument.after_completion.run()
}
}
}) as Arc<_>
});
}) as Arc<_>
});
let mut new_text = new_argument.new_text.clone();
if new_argument.after_completion == AfterCompletion::Continue {
new_text.push(' ');
}
let mut new_text = new_argument.new_text.clone();
if new_argument.after_completion == AfterCompletion::Continue {
new_text.push(' ');
}
project::Completion {
replace_range: if new_argument.replace_previous_arguments {
argument_range.clone()
} else {
last_argument_range.clone()
},
label: new_argument.label,
icon_path: None,
new_text,
documentation: None,
confirm,
insert_text_mode: None,
source: CompletionSource::Custom,
}
})
.collect();
Ok(vec![project::CompletionResponse {
completions,
// TODO: Could have slash commands indicate whether their completions are incomplete.
is_incomplete: true,
}])
project::Completion {
replace_range: if new_argument.replace_previous_arguments {
argument_range.clone()
} else {
last_argument_range.clone()
},
label: new_argument.label,
icon_path: None,
new_text,
documentation: None,
confirm,
insert_text_mode: None,
source: CompletionSource::Custom,
}
})
.collect(),
))
})
} else {
Task::ready(Ok(vec![project::CompletionResponse {
completions: Vec::new(),
is_incomplete: true,
}]))
Task::ready(Ok(Some(Vec::new())))
}
}
}
@@ -261,7 +251,7 @@ impl CompletionProvider for SlashCommandCompletionProvider {
_: editor::CompletionContext,
window: &mut Window,
cx: &mut Context<Editor>,
) -> Task<Result<Vec<project::CompletionResponse>>> {
) -> Task<Result<Option<Vec<project::Completion>>>> {
let Some((name, arguments, command_range, last_argument_range)) =
buffer.update(cx, |buffer, _cx| {
let position = buffer_position.to_point(buffer);
@@ -275,17 +265,17 @@ impl CompletionProvider for SlashCommandCompletionProvider {
position.row,
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);
let name = line[call.name.clone()].to_string();
let (arguments, last_argument_range) = if let Some(argument) = call.arguments.last()
{
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 = buffer
.anchor_before(Point::new(position.row, first_arg_start.start as u32));
let first_arg_start =
buffer.anchor_after(Point::new(position.row, first_arg_start.start as u32));
let arguments = call
.arguments
.into_iter()
@@ -298,17 +288,14 @@ impl CompletionProvider for SlashCommandCompletionProvider {
)
} else {
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)
};
Some((name, arguments, command_range, last_argument_range))
})
else {
return Task::ready(Ok(vec![project::CompletionResponse {
completions: Vec::new(),
is_incomplete: false,
}]));
return Task::ready(Ok(Some(Vec::new())));
};
if let Some((arguments, argument_range)) = arguments {
@@ -326,13 +313,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(
&self,
buffer: &Entity<Buffer>,
position: language::Anchor,
_text: &str,
_trigger_in_words: bool,
_menu_is_open: bool,
cx: &mut Context<Editor>,
) -> bool {
let buffer = buffer.read(cx);

View File

@@ -86,26 +86,20 @@ impl SlashCommand for ContextServerSlashCommand {
cx.foreground_executor().spawn(async move {
let protocol = server.client().context("Context server not initialized")?;
let response = protocol
.request::<context_server::types::requests::CompletionComplete>(
context_server::types::CompletionCompleteParams {
reference: context_server::types::CompletionReference::Prompt(
context_server::types::PromptReference {
ty: context_server::types::PromptReferenceType::Prompt,
name: prompt_name,
},
),
argument: context_server::types::CompletionArgument {
name: arg_name,
value: arg_value,
let completion_result = protocol
.completion(
context_server::types::CompletionReference::Prompt(
context_server::types::PromptReference {
r#type: context_server::types::PromptReferenceType::Prompt,
name: prompt_name,
},
meta: None,
},
),
arg_name,
arg_value,
)
.await?;
let completions = response
.completion
let completions = completion_result
.values
.into_iter()
.map(|value| ArgumentCompletion {
@@ -144,18 +138,10 @@ impl SlashCommand for ContextServerSlashCommand {
if let Some(server) = store.get_running_server(&server_id) {
cx.foreground_executor().spawn(async move {
let protocol = server.client().context("Context server not initialized")?;
let response = protocol
.request::<context_server::types::requests::PromptsGet>(
context_server::types::PromptsGetParams {
name: prompt_name.clone(),
arguments: Some(prompt_args),
meta: None,
},
)
.await?;
let result = protocol.run_prompt(&prompt_name, prompt_args).await?;
anyhow::ensure!(
response
result
.messages
.iter()
.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
let mut prompt = response
let mut prompt = result
.messages
.into_iter()
.filter_map(|msg| match msg.content {
@@ -181,7 +167,7 @@ impl SlashCommand for ContextServerSlashCommand {
range: 0..(prompt.len()),
icon: IconName::ZedAssistant,
label: SharedString::from(
response
result
.description
.unwrap_or(format!("Result from {}", prompt_name)),
),

View File

@@ -147,7 +147,6 @@ impl SlashCommand for DiagnosticsSlashCommand {
&Options::match_candidates_for_args(),
&query,
false,
true,
10,
&cancellation_flag,
executor,

View File

@@ -582,7 +582,7 @@ mod test {
use serde_json::json;
use settings::SettingsStore;
use smol::stream::StreamExt;
use util::path;
use util::{path, separator};
use super::collect_files;
@@ -627,7 +627,7 @@ mod test {
.await
.unwrap();
assert!(result_1.text.starts_with(path!("root/dir")));
assert!(result_1.text.starts_with(separator!("root/dir")));
// 4 files + 2 directories
assert_eq!(result_1.sections.len(), 6);
@@ -643,7 +643,7 @@ mod test {
cx.update(|cx| collect_files(project.clone(), &["root/dir*".to_string()], cx).boxed());
let result = SlashCommandOutput::from_event_stream(result).await.unwrap();
assert!(result.text.starts_with(path!("root/dir")));
assert!(result.text.starts_with(separator!("root/dir")));
// 5 files + 2 directories
assert_eq!(result.sections.len(), 7);
@@ -691,20 +691,24 @@ mod test {
.unwrap();
// Sanity check
assert!(result.text.starts_with(path!("zed/assets/themes\n")));
assert!(result.text.starts_with(separator!("zed/assets/themes\n")));
assert_eq!(result.sections.len(), 7);
// Ensure that full file paths are included in the real output
assert!(
result
.text
.contains(path!("zed/assets/themes/andromeda/LICENSE"))
.contains(separator!("zed/assets/themes/andromeda/LICENSE"))
);
assert!(result.text.contains(path!("zed/assets/themes/ayu/LICENSE")));
assert!(
result
.text
.contains(path!("zed/assets/themes/summercamp/LICENSE"))
.contains(separator!("zed/assets/themes/ayu/LICENSE"))
);
assert!(
result
.text
.contains(separator!("zed/assets/themes/summercamp/LICENSE"))
);
assert_eq!(result.sections[5].label, "summercamp");
@@ -712,17 +716,17 @@ mod test {
// Ensure that things are in descending order, with properly relativized paths
assert_eq!(
result.sections[0].label,
path!("zed/assets/themes/andromeda/LICENSE")
separator!("zed/assets/themes/andromeda/LICENSE")
);
assert_eq!(result.sections[1].label, "andromeda");
assert_eq!(
result.sections[2].label,
path!("zed/assets/themes/ayu/LICENSE")
separator!("zed/assets/themes/ayu/LICENSE")
);
assert_eq!(result.sections[3].label, "ayu");
assert_eq!(
result.sections[4].label,
path!("zed/assets/themes/summercamp/LICENSE")
separator!("zed/assets/themes/summercamp/LICENSE")
);
// Ensure that the project lasts until after the last await
@@ -763,28 +767,31 @@ mod test {
.await
.unwrap();
assert!(result.text.starts_with(path!("zed/assets/themes\n")));
assert_eq!(result.sections[0].label, path!("zed/assets/themes/LICENSE"));
assert!(result.text.starts_with(separator!("zed/assets/themes\n")));
assert_eq!(
result.sections[0].label,
separator!("zed/assets/themes/LICENSE")
);
assert_eq!(
result.sections[1].label,
path!("zed/assets/themes/summercamp/LICENSE")
separator!("zed/assets/themes/summercamp/LICENSE")
);
assert_eq!(
result.sections[2].label,
path!("zed/assets/themes/summercamp/subdir/LICENSE")
separator!("zed/assets/themes/summercamp/subdir/LICENSE")
);
assert_eq!(
result.sections[3].label,
path!("zed/assets/themes/summercamp/subdir/subsubdir/LICENSE")
separator!("zed/assets/themes/summercamp/subdir/subsubdir/LICENSE")
);
assert_eq!(result.sections[4].label, "subsubdir");
assert_eq!(result.sections[5].label, "subdir");
assert_eq!(result.sections[6].label, "summercamp");
assert_eq!(result.sections[7].label, path!("zed/assets/themes"));
assert_eq!(result.sections[7].label, separator!("zed/assets/themes"));
assert_eq!(
result.text,
path!(
separator!(
"zed/assets/themes\n```zed/assets/themes/LICENSE\n1\n```\n\nsummercamp\n```zed/assets/themes/summercamp/LICENSE\n1\n```\n\nsubdir\n```zed/assets/themes/summercamp/subdir/LICENSE\n1\n```\n\nsubsubdir\n```zed/assets/themes/summercamp/subdir/subsubdir/LICENSE\n3\n```\n\n"
)
);

View File

@@ -261,7 +261,6 @@ fn tab_items_for_queries(
&match_candidates,
query,
true,
true,
usize::MAX,
&cancel,
background_executor.clone(),

View File

@@ -29,7 +29,6 @@ serde.workspace = true
serde_json.workspace = true
text.workspace = true
util.workspace = true
watch.workspace = true
workspace.workspace = true
workspace-hack.workspace = true

View File

@@ -1,7 +1,7 @@
use anyhow::{Context as _, Result};
use buffer_diff::BufferDiff;
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 language::{Anchor, Buffer, BufferEvent, DiskState, Point, ToPoint};
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_update_tx, diff_update_rx) = mpsc::unbounded();
let diff_base;
let unreviewed_edits;
let unreviewed_changes;
if is_created {
diff_base = Rope::default();
unreviewed_edits = Patch::new(vec![Edit {
unreviewed_changes = Patch::new(vec![Edit {
old: 0..1,
new: 0..text_snapshot.max_point().row + 1,
}])
} else {
diff_base = buffer.read(cx).as_rope().clone();
unreviewed_edits = Patch::default();
unreviewed_changes = Patch::default();
}
TrackedBuffer {
buffer: buffer.clone(),
diff_base,
unreviewed_edits: unreviewed_edits,
unreviewed_changes,
snapshot: text_snapshot.clone(),
status,
version: buffer.read(cx).version(),
@@ -175,7 +175,7 @@ impl ActionLog {
.map_or(false, |file| file.disk_state() != DiskState::Deleted)
{
// 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.
self.tracked_buffers.remove(&buffer);
self.track_buffer_internal(buffer, false, cx);
@@ -188,286 +188,120 @@ impl ActionLog {
async fn maintain_diff(
this: WeakEntity<Self>,
buffer: Entity<Buffer>,
mut buffer_updates: mpsc::UnboundedReceiver<(ChangeAuthor, text::BufferSnapshot)>,
mut diff_update: mpsc::UnboundedReceiver<(ChangeAuthor, text::BufferSnapshot)>,
cx: &mut AsyncApp,
) -> Result<()> {
let git_store = this.read_with(cx, |this, cx| this.project.read(cx).git_store().clone())?;
let git_diff = this
.update(cx, |this, cx| {
this.project.update(cx, |project, cx| {
project.open_uncommitted_diff(buffer.clone(), cx)
})
})?
.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)
})?;
while let Some((author, buffer_snapshot)) = diff_update.next().await {
let (rebase, diff, language, language_registry) =
this.read_with(cx, |this, cx| {
let tracked_buffer = this
.tracked_buffers
.get(&buffer)
.context("buffer not tracked")?;
let (mut git_diff_updates_tx, mut git_diff_updates_rx) = watch::channel(());
let _repo_subscription =
if let Some((git_diff, (buffer_repo, _))) = git_diff.as_ref().zip(buffer_repo) {
cx.update(|cx| {
let mut old_head = buffer_repo.read(cx).head_commit.clone();
Some(cx.subscribe(git_diff, move |_, event, cx| match event {
buffer_diff::BufferDiffEvent::DiffChanged { .. } => {
let new_head = buffer_repo.read(cx).head_commit.clone();
if new_head != old_head {
old_head = new_head;
git_diff_updates_tx.send(()).ok();
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_changes = tracked_buffer.unreviewed_changes.clone();
async move {
let edits = diff_snapshots(&old_snapshot, &new_snapshot);
if let ChangeAuthor::User = author {
apply_non_conflicting_edits(
&unreviewed_changes,
edits,
&mut base_text,
new_snapshot.as_rope(),
);
}
(Arc::new(base_text.to_string()), base_text)
}
_ => {}
}))
})?
} else {
None
};
});
loop {
futures::select_biased! {
buffer_update = buffer_updates.next() => {
if let Some((author, buffer_snapshot)) = buffer_update {
Self::track_edits(&this, &buffer, author, buffer_snapshot, cx).await?;
} else {
break;
}
}
_ = git_diff_updates_rx.changed().fuse() => {
if let Some(git_diff) = git_diff.as_ref() {
Self::keep_committed_edits(&this, &buffer, &git_diff, cx).await?;
}
}
anyhow::Ok((
rebase,
tracked_buffer.diff.clone(),
tracked_buffer.buffer.read(cx).language().cloned(),
tracked_buffer.buffer.read(cx).language_registry(),
))
})??;
let (new_base_text, new_diff_base) = rebase.await;
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_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(())
}
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 by agent, 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>) {
self.track_buffer_internal(buffer, false, cx);
}
/// Mark a buffer as created by agent, so we can refresh it in the context
/// Mark a buffer as edited, so we can refresh it in the context
pub fn buffer_created(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
self.edited_since_project_diagnostics_check = true;
self.track_buffer_internal(buffer.clone(), true, cx);
}
/// Mark a buffer as edited by agent, so we can refresh it in the context
/// Mark a buffer as edited, so we can refresh it in the context
pub fn buffer_edited(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
self.edited_since_project_diagnostics_check = true;
@@ -516,7 +350,7 @@ impl ActionLog {
buffer_range.start.to_point(buffer)..buffer_range.end.to_point(buffer);
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.end = (edit.old.end as i32 + delta) as u32;
@@ -627,7 +461,7 @@ impl ActionLog {
.project
.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.buffer_read(buffer.clone(), cx);
cx.notify();
@@ -643,7 +477,7 @@ impl ActionLog {
.peekable();
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
.snapshot
.anchor_before(Point::new(edit.new.start, 0))
@@ -695,7 +529,7 @@ impl ActionLog {
.retain(|_buffer, tracked_buffer| match tracked_buffer.status {
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.schedule_diff_update(ChangeAuthor::User, cx);
true
@@ -704,11 +538,11 @@ impl ActionLog {
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>> {
self.tracked_buffers
.iter()
.filter(|(_, tracked)| tracked.has_edits(cx))
.filter(|(_, tracked)| tracked.has_changes(cx))
.map(|(buffer, tracked)| (buffer.clone(), tracked.diff.clone()))
.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,
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 {
old: edit.old.start.row..edit.old.end.row,
new: edit.new.start.row..edit.new.end.row,
@@ -856,7 +694,7 @@ enum TrackedBufferStatus {
struct TrackedBuffer {
buffer: Entity<Buffer>,
diff_base: Rope,
unreviewed_edits: Patch<u32>,
unreviewed_changes: Patch<u32>,
status: TrackedBufferStatus,
version: clock::Global,
diff: Entity<BufferDiff>,
@@ -868,7 +706,7 @@ struct TrackedBuffer {
}
impl TrackedBuffer {
fn has_edits(&self, cx: &App) -> bool {
fn has_changes(&self, cx: &App) -> bool {
self.diff
.read(cx)
.hunks(&self.buffer.read(cx), cx)
@@ -889,6 +727,8 @@ pub struct ChangedBuffer {
#[cfg(test)]
mod tests {
use std::env;
use super::*;
use buffer_diff::DiffHunkStatusKind;
use gpui::TestAppContext;
@@ -897,7 +737,6 @@ mod tests {
use rand::prelude::*;
use serde_json::json;
use settings::SettingsStore;
use std::env;
use util::{RandomCharIter, path};
#[ctor::ctor]
@@ -1912,15 +1751,15 @@ mod tests {
.unwrap();
}
_ => {
let is_agent_edit = rng.gen_bool(0.5);
if is_agent_edit {
let is_agent_change = rng.gen_bool(0.5);
if is_agent_change {
log::info!("agent edit");
} else {
log::info!("user edit");
}
cx.update(|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));
}
});
@@ -1945,7 +1784,7 @@ mod tests {
let tracked_buffer = log.tracked_buffers.get(&buffer).unwrap();
let mut old_text = tracked_buffer.diff_base.clone();
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_end = old_text.point_to_offset(cmp::min(
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)]
struct HunkStatus {
range: Range<Point>,

View File

@@ -214,13 +214,10 @@ pub trait Tool: 'static + Send + Sync {
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.
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.
fn input_schema(&self, _: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
Ok(serde_json::Value::Object(serde_json::Map::default()))

View File

@@ -16,24 +16,11 @@ pub fn adapt_schema_to_format(
}
match format {
LanguageModelToolSchemaFormat::JsonSchema => preprocess_json_schema(json),
LanguageModelToolSchemaFormat::JsonSchema => Ok(()),
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
fn adapt_to_json_schema_subset(json: &mut Value) -> Result<()> {
if let Value::Object(obj) = json {
@@ -46,19 +33,15 @@ fn adapt_to_json_schema_subset(json: &mut Value) -> Result<()> {
);
}
const KEYS_TO_REMOVE: [(&str, fn(&Value) -> bool); 5] = [
("format", |value| value.is_string()),
("additionalProperties", |value| value.is_boolean()),
("exclusiveMinimum", |value| value.is_number()),
("exclusiveMaximum", |value| value.is_number()),
("optional", |value| value.is_boolean()),
const KEYS_TO_REMOVE: [&str; 5] = [
"format",
"additionalProperties",
"exclusiveMinimum",
"exclusiveMaximum",
"optional",
];
for (key, predicate) in KEYS_TO_REMOVE {
if let Some(value) = obj.get(key) {
if predicate(value) {
obj.remove(key);
}
}
for key in KEYS_TO_REMOVE {
obj.remove(key);
}
// If a type is not specified for an input parameter, add a default type
@@ -157,24 +140,6 @@ mod tests {
"type": "integer"
})
);
// Ensure that we do not remove keys that are actually supported (e.g. "format" can just be used as another property)
let mut json = json!({
"description": "A test field",
"type": "integer",
"format": {},
});
adapt_to_json_schema_subset(&mut json).unwrap();
assert_eq!(
json,
json!({
"description": "A test field",
"type": "integer",
"format": {},
})
);
}
#[test]
@@ -272,59 +237,4 @@ mod tests {
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
})
);
}
}

View File

@@ -1,7 +1,7 @@
use std::sync::Arc;
use collections::{HashMap, IndexMap};
use gpui::App;
use collections::{HashMap, HashSet, IndexMap};
use gpui::{App, Context, EventEmitter};
use crate::{Tool, ToolRegistry, ToolSource};
@@ -13,9 +13,17 @@ pub struct ToolId(usize);
pub struct ToolWorkingSet {
context_server_tools_by_id: HashMap<ToolId, 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,
}
pub enum ToolWorkingSetEvent {
EnabledToolsChanged,
}
impl EventEmitter<ToolWorkingSetEvent> for ToolWorkingSet {}
impl ToolWorkingSet {
pub fn tool(&self, name: &str, cx: &App) -> Option<Arc<dyn Tool>> {
self.context_server_tools_by_name
@@ -49,6 +57,42 @@ impl ToolWorkingSet {
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 {
let tool_id = self.next_tool_id;
self.next_tool_id.0 += 1;
@@ -58,6 +102,42 @@ impl ToolWorkingSet {
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]) {
self.context_server_tools_by_id
.retain(|id, _| !tool_ids_to_remove.contains(id));

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