diff --git a/.github/actions/run_tests_windows/action.yml b/.github/actions/run_tests_windows/action.yml
index 5115ef02ac..cbe95e82c1 100644
--- a/.github/actions/run_tests_windows/action.yml
+++ b/.github/actions/run_tests_windows/action.yml
@@ -10,8 +10,8 @@ inputs:
runs:
using: "composite"
steps:
- - name: Install Rust
- shell: pwsh
+ - name: Install test runner
+ shell: powershell
working-directory: ${{ inputs.working-directory }}
run: cargo install cargo-nextest --locked
@@ -21,6 +21,6 @@ runs:
node-version: "18"
- name: Run tests
- shell: pwsh
+ shell: powershell
working-directory: ${{ inputs.working-directory }}
- run: cargo nextest run --workspace --no-fail-fast --config='profile.dev.debug="limited"'
+ run: cargo nextest run --workspace --no-fail-fast
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index eb7a36db5b..600956c379 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -29,6 +29,7 @@ jobs:
outputs:
run_tests: ${{ steps.filter.outputs.run_tests }}
run_license: ${{ steps.filter.outputs.run_license }}
+ run_docs: ${{ steps.filter.outputs.run_docs }}
runs-on:
- ubuntu-latest
steps:
@@ -58,6 +59,11 @@ jobs:
else
echo "run_tests=false" >> $GITHUB_OUTPUT
fi
+ if [[ $(git diff --name-only $COMPARE_REV ${{ github.sha }} | grep '^docs/') ]]; then
+ echo "run_docs=true" >> $GITHUB_OUTPUT
+ else
+ echo "run_docs=false" >> $GITHUB_OUTPUT
+ fi
if [[ $(git diff --name-only $COMPARE_REV ${{ github.sha }} | grep '^Cargo.lock') ]]; then
echo "run_license=true" >> $GITHUB_OUTPUT
else
@@ -198,7 +204,9 @@ jobs:
timeout-minutes: 60
name: Check docs
needs: [job_spec]
- if: github.repository_owner == 'zed-industries'
+ if: |
+ github.repository_owner == 'zed-industries' &&
+ (needs.job_spec.outputs.run_tests == 'true' || needs.job_spec.outputs.run_docs == 'true')
runs-on:
- buildjet-8vcpu-ubuntu-2204
steps:
@@ -373,64 +381,6 @@ 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
@@ -438,51 +388,45 @@ jobs:
if: |
github.repository_owner == 'zed-industries' &&
needs.job_spec.outputs.run_tests == 'true'
- # Use bigger runners for PRs (speed); smaller for async (cost)
- runs-on: ${{ github.event_name == 'pull_request' && 'windows-2025-32' || 'windows-2025-16' }}
+ runs-on: [self-hosted, Windows, X64]
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: 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
- 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
+ - name: Setup Cargo and Rustup
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: 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: 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
# Since the Windows runners are stateful, so we need to remove the config file to prevent potential bug.
- name: Clean CI config file
@@ -498,13 +442,13 @@ 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' &&
@@ -515,7 +459,8 @@ jobs:
# Check dependent jobs...
RET_CODE=0
# Always check style
- [[ "${{ needs.style.result }}" != 'success' ]] && { RET_CODE=1; echo "style tests failed"; }
+ [[ "${{ needs.style.result }}" != 'success' ]] && { RET_CODE=1; echo "style tests failed"; }
+ [[ "${{ needs.check_docs.result }}" != 'success' ]] && { RET_CODE=1; echo "docs checks failed"; }
# Only check test jobs if they were supposed to run
if [[ "${{ needs.job_spec.outputs.run_tests }}" == "true" ]]; then
@@ -523,7 +468,6 @@ 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"; }
diff --git a/.github/workflows/community_delete_comments.yml b/.github/workflows/community_delete_comments.yml
deleted file mode 100644
index 0ebe1ac3ac..0000000000
--- a/.github/workflows/community_delete_comments.yml
+++ /dev/null
@@ -1,34 +0,0 @@
-name: Delete Mediafire Comments
-
-on:
- issue_comment:
- types: [created]
-
-permissions:
- issues: write
-
-jobs:
- delete_comment:
- if: github.repository_owner == 'zed-industries'
- runs-on: ubuntu-latest
- steps:
- - name: Check for specific strings in comment
- id: check_comment
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
- with:
- script: |
- const comment = context.payload.comment.body;
- const triggerStrings = ['www.mediafire.com'];
- return triggerStrings.some(triggerString => comment.includes(triggerString));
-
- - name: Delete comment if it contains any of the specific strings
- if: steps.check_comment.outputs.result == 'true'
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
- with:
- script: |
- const commentId = context.payload.comment.id;
- await github.rest.issues.deleteComment({
- owner: context.repo.owner,
- repo: context.repo.repo,
- comment_id: commentId
- });
diff --git a/.gitignore b/.gitignore
index db2a8139cd..7b40c45adf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,7 @@
.flatpak-builder
.idea
.netrc
+*.pyc
.pytest_cache
.swiftpm
.swiftpm/config/registries.json
diff --git a/.rules b/.rules
index b9eea27b67..da009f1877 100644
--- a/.rules
+++ b/.rules
@@ -100,9 +100,7 @@ Often event handlers will want to update the entity that's in the current `Conte
Actions are dispatched via user keyboard interaction or in code via `window.dispatch_action(SomeAction.boxed_clone(), cx)` or `focus_handle.dispatch_action(&SomeAction, window, cx)`.
-Actions which have no data inside are created and registered with the `actions!(some_namespace, [SomeAction, AnotherAction])` macro call.
-
-Actions that do have data must implement `Clone, Default, PartialEq, Deserialize, JsonSchema` and can be registered with an `impl_actions!(some_namespace, [SomeActionWithData])` macro call.
+Actions with no data defined with the `actions!(some_namespace, [SomeAction, AnotherAction])` macro call. Otherwise the `Action` derive macro is used. Doc comments on actions are displayed to the user.
Action handlers can be registered on an element via the event handler `.on_action(|action, window, cx| ...)`. Like other event handlers, this is often used with `cx.listener`.
diff --git a/.zed/debug.json b/.zed/debug.json
index 2dde32b870..49b8f1a7a6 100644
--- a/.zed/debug.json
+++ b/.zed/debug.json
@@ -2,11 +2,23 @@
{
"label": "Debug Zed (CodeLLDB)",
"adapter": "CodeLLDB",
- "build": { "label": "Build Zed", "command": "cargo", "args": ["build"] }
+ "build": {
+ "label": "Build Zed",
+ "command": "cargo",
+ "args": [
+ "build"
+ ]
+ }
},
{
"label": "Debug Zed (GDB)",
"adapter": "GDB",
- "build": { "label": "Build Zed", "command": "cargo", "args": ["build"] }
- }
+ "build": {
+ "label": "Build Zed",
+ "command": "cargo",
+ "args": [
+ "build"
+ ]
+ }
+ },
]
diff --git a/.zed/settings.json b/.zed/settings.json
index 67677d8d91..1ef6bc28f7 100644
--- a/.zed/settings.json
+++ b/.zed/settings.json
@@ -40,6 +40,7 @@
},
"file_types": {
"Dockerfile": ["Dockerfile*[!dockerignore]"],
+ "JSONC": ["assets/**/*.json", "renovate.json"],
"Git Ignore": ["dockerignore"]
},
"hard_tabs": false,
@@ -47,7 +48,7 @@
"remove_trailing_whitespace_on_save": true,
"ensure_final_newline_on_save": true,
"file_scan_exclusions": [
- "crates/assistant_tools/src/evals/fixtures",
+ "crates/assistant_tools/src/edit_agent/evals/fixtures",
"crates/eval/worktrees/",
"crates/eval/repos/",
"**/.git",
diff --git a/Cargo.lock b/Cargo.lock
index 06c3d1e99a..122d1ef7b7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -14,6 +14,7 @@ dependencies = [
"gpui",
"language",
"project",
+ "proto",
"release_channel",
"smallvec",
"ui",
@@ -55,7 +56,83 @@ version = "0.1.0"
dependencies = [
"agent_settings",
"anyhow",
- "assistant_context_editor",
+ "assistant_context",
+ "assistant_tool",
+ "assistant_tools",
+ "chrono",
+ "client",
+ "collections",
+ "component",
+ "context_server",
+ "convert_case 0.8.0",
+ "feature_flags",
+ "fs",
+ "futures 0.3.31",
+ "git",
+ "gpui",
+ "heed",
+ "http_client",
+ "icons",
+ "indoc",
+ "itertools 0.14.0",
+ "language",
+ "language_model",
+ "log",
+ "paths",
+ "postage",
+ "pretty_assertions",
+ "project",
+ "prompt_store",
+ "proto",
+ "rand 0.8.5",
+ "ref-cast",
+ "rope",
+ "schemars",
+ "serde",
+ "serde_json",
+ "settings",
+ "smol",
+ "sqlez",
+ "telemetry",
+ "text",
+ "theme",
+ "thiserror 2.0.12",
+ "time",
+ "util",
+ "uuid",
+ "workspace",
+ "workspace-hack",
+ "zed_llm_client",
+ "zstd",
+]
+
+[[package]]
+name = "agent_settings"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "collections",
+ "fs",
+ "gpui",
+ "language_model",
+ "paths",
+ "schemars",
+ "serde",
+ "serde_json",
+ "serde_json_lenient",
+ "settings",
+ "workspace-hack",
+ "zed_llm_client",
+]
+
+[[package]]
+name = "agent_ui"
+version = "0.1.0"
+dependencies = [
+ "agent",
+ "agent_settings",
+ "anyhow",
+ "assistant_context",
"assistant_slash_command",
"assistant_slash_commands",
"assistant_tool",
@@ -67,18 +144,16 @@ dependencies = [
"collections",
"component",
"context_server",
- "convert_case 0.8.0",
"db",
"editor",
"extension",
+ "extension_host",
"feature_flags",
"file_icons",
"fs",
"futures 0.3.31",
"fuzzy",
- "git",
"gpui",
- "heed",
"html_to_markdown",
"http_client",
"indexed_docs",
@@ -88,6 +163,7 @@ dependencies = [
"jsonschema",
"language",
"language_model",
+ "languages",
"log",
"lsp",
"markdown",
@@ -98,13 +174,11 @@ dependencies = [
"parking_lot",
"paths",
"picker",
- "postage",
"pretty_assertions",
"project",
"prompt_store",
"proto",
"rand 0.8.5",
- "ref-cast",
"release_channel",
"rope",
"rules_library",
@@ -115,7 +189,6 @@ dependencies = [
"serde_json_lenient",
"settings",
"smol",
- "sqlez",
"streaming_diff",
"telemetry",
"telemetry_events",
@@ -123,11 +196,11 @@ dependencies = [
"terminal_view",
"text",
"theme",
- "thiserror 2.0.12",
"time",
"time_format",
+ "tree-sitter-md",
"ui",
- "ui_input",
+ "unindent",
"urlencoding",
"util",
"uuid",
@@ -136,33 +209,6 @@ dependencies = [
"workspace-hack",
"zed_actions",
"zed_llm_client",
- "zstd",
-]
-
-[[package]]
-name = "agent_settings"
-version = "0.1.0"
-dependencies = [
- "anthropic",
- "anyhow",
- "collections",
- "deepseek",
- "fs",
- "gpui",
- "language_model",
- "lmstudio",
- "log",
- "mistral",
- "ollama",
- "open_ai",
- "paths",
- "schemars",
- "serde",
- "serde_json",
- "serde_json_lenient",
- "settings",
- "workspace-hack",
- "zed_llm_client",
]
[[package]]
@@ -491,7 +537,6 @@ dependencies = [
"anyhow",
"futures 0.3.31",
"gpui",
- "shlex",
"smol",
"tempfile",
"util",
@@ -509,7 +554,7 @@ dependencies = [
]
[[package]]
-name = "assistant_context_editor"
+name = "assistant_context"
version = "0.1.0"
dependencies = [
"agent_settings",
@@ -521,31 +566,23 @@ dependencies = [
"clock",
"collections",
"context_server",
- "editor",
- "feature_flags",
"fs",
"futures 0.3.31",
"fuzzy",
"gpui",
- "indexed_docs",
"indoc",
"language",
"language_model",
- "languages",
"log",
- "multi_buffer",
"open_ai",
- "ordered-float 2.10.1",
"parking_lot",
"paths",
- "picker",
"pretty_assertions",
"project",
"prompt_store",
"proto",
"rand 0.8.5",
"regex",
- "rope",
"rpc",
"serde",
"serde_json",
@@ -554,15 +591,12 @@ dependencies = [
"smol",
"telemetry_events",
"text",
- "theme",
- "tree-sitter-md",
"ui",
"unindent",
"util",
"uuid",
"workspace",
"workspace-hack",
- "zed_actions",
"zed_llm_client",
]
@@ -2042,7 +2076,7 @@ dependencies = [
[[package]]
name = "blade-graphics"
version = "0.6.0"
-source = "git+https://github.com/kvark/blade?rev=416375211bb0b5826b3584dccdb6a43369e499ad#416375211bb0b5826b3584dccdb6a43369e499ad"
+source = "git+https://github.com/kvark/blade?rev=e0ec4e720957edd51b945b64dd85605ea54bcfe5#e0ec4e720957edd51b945b64dd85605ea54bcfe5"
dependencies = [
"ash",
"ash-window",
@@ -2075,7 +2109,7 @@ dependencies = [
[[package]]
name = "blade-macros"
version = "0.3.0"
-source = "git+https://github.com/kvark/blade?rev=416375211bb0b5826b3584dccdb6a43369e499ad#416375211bb0b5826b3584dccdb6a43369e499ad"
+source = "git+https://github.com/kvark/blade?rev=e0ec4e720957edd51b945b64dd85605ea54bcfe5#e0ec4e720957edd51b945b64dd85605ea54bcfe5"
dependencies = [
"proc-macro2",
"quote",
@@ -2085,7 +2119,7 @@ dependencies = [
[[package]]
name = "blade-util"
version = "0.2.0"
-source = "git+https://github.com/kvark/blade?rev=416375211bb0b5826b3584dccdb6a43369e499ad#416375211bb0b5826b3584dccdb6a43369e499ad"
+source = "git+https://github.com/kvark/blade?rev=e0ec4e720957edd51b945b64dd85605ea54bcfe5#e0ec4e720957edd51b945b64dd85605ea54bcfe5"
dependencies = [
"blade-graphics",
"bytemuck",
@@ -2649,6 +2683,7 @@ dependencies = [
"http_client",
"language",
"log",
+ "postage",
"rand 0.8.5",
"release_channel",
"rpc",
@@ -2822,7 +2857,9 @@ dependencies = [
"cocoa 0.26.0",
"collections",
"credentials_provider",
+ "derive_more",
"feature_flags",
+ "fs",
"futures 0.3.31",
"gpui",
"gpui_tokio",
@@ -2834,6 +2871,7 @@ dependencies = [
"paths",
"postage",
"rand 0.8.5",
+ "regex",
"release_channel",
"rpc",
"rustls-pki-types",
@@ -2858,6 +2896,7 @@ dependencies = [
"windows 0.61.1",
"workspace-hack",
"worktree",
+ "zed_llm_client",
]
[[package]]
@@ -2978,7 +3017,7 @@ version = "0.44.0"
dependencies = [
"agent_settings",
"anyhow",
- "assistant_context_editor",
+ "assistant_context",
"assistant_slash_command",
"async-stripe",
"async-trait",
@@ -3345,6 +3384,7 @@ dependencies = [
"collections",
"command_palette_hooks",
"ctor",
+ "dirs 4.0.0",
"editor",
"fs",
"futures 0.3.31",
@@ -3540,6 +3580,20 @@ 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"
@@ -3575,7 +3629,8 @@ dependencies = [
[[package]]
name = "cpal"
version = "0.15.3"
-source = "git+https://github.com/zed-industries/cpal?rev=fd8bc2fd39f1f5fdee5a0690656caff9a26d9d50#fd8bc2fd39f1f5fdee5a0690656caff9a26d9d50"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779"
dependencies = [
"alsa",
"core-foundation-sys",
@@ -3585,7 +3640,7 @@ dependencies = [
"js-sys",
"libc",
"mach2",
- "ndk",
+ "ndk 0.8.0",
"ndk-context",
"oboe",
"wasm-bindgen",
@@ -3594,6 +3649,32 @@ 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"
@@ -4199,6 +4280,7 @@ dependencies = [
"gpui",
"serde_json",
"task",
+ "util",
"workspace-hack",
]
@@ -4224,6 +4306,7 @@ dependencies = [
name = "debugger_ui"
version = "0.1.0"
dependencies = [
+ "alacritty_terminal",
"anyhow",
"client",
"collections",
@@ -4233,7 +4316,6 @@ dependencies = [
"db",
"debugger_tools",
"editor",
- "feature_flags",
"file_icons",
"futures 0.3.31",
"fuzzy",
@@ -4250,13 +4332,18 @@ dependencies = [
"rpc",
"serde",
"serde_json",
+ "serde_json_lenient",
"settings",
"shlex",
"sysinfo",
"task",
"tasks_ui",
+ "telemetry",
"terminal_view",
"theme",
+ "tree-sitter",
+ "tree-sitter-go",
+ "tree-sitter-json",
"ui",
"unindent",
"util",
@@ -4702,7 +4789,6 @@ dependencies = [
"dap",
"db",
"emojis",
- "feature_flags",
"file_icons",
"fs",
"futures 0.3.31",
@@ -5014,6 +5100,7 @@ version = "0.1.0"
dependencies = [
"agent",
"agent_settings",
+ "agent_ui",
"anyhow",
"assistant_tool",
"assistant_tools",
@@ -6122,6 +6209,7 @@ dependencies = [
"anyhow",
"askpass",
"buffer_diff",
+ "call",
"chrono",
"collections",
"command_palette_hooks",
@@ -6160,6 +6248,7 @@ dependencies = [
"ui",
"unindent",
"util",
+ "watch",
"windows 0.61.1",
"workspace",
"workspace-hack",
@@ -8110,12 +8199,11 @@ dependencies = [
name = "inline_completion"
version = "0.1.0"
dependencies = [
- "anyhow",
+ "client",
"gpui",
"language",
"project",
"workspace-hack",
- "zed_llm_client",
]
[[package]]
@@ -8831,6 +8919,7 @@ dependencies = [
"http_client",
"icons",
"image",
+ "log",
"parking_lot",
"proto",
"schemars",
@@ -8866,6 +8955,7 @@ dependencies = [
"gpui",
"gpui_tokio",
"http_client",
+ "language",
"language_model",
"lmstudio",
"log",
@@ -8889,7 +8979,9 @@ dependencies = [
"tiktoken-rs",
"tokio",
"ui",
+ "ui_input",
"util",
+ "vercel",
"workspace-hack",
"zed_llm_client",
]
@@ -8928,6 +9020,7 @@ dependencies = [
"itertools 0.14.0",
"language",
"lsp",
+ "picker",
"project",
"release_channel",
"serde_json",
@@ -9321,7 +9414,7 @@ dependencies = [
"core-foundation 0.10.0",
"core-video",
"coreaudio-rs 0.12.1",
- "cpal",
+ "cpal 0.16.0",
"futures 0.3.31",
"gpui",
"gpui_tokio",
@@ -10091,7 +10184,21 @@ dependencies = [
"bitflags 2.9.0",
"jni-sys",
"log",
- "ndk-sys",
+ "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",
"num_enum",
"thiserror 1.0.69",
]
@@ -10111,6 +10218,15 @@ 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"
@@ -10524,6 +10640,43 @@ 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"
@@ -10629,7 +10782,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb"
dependencies = [
"jni",
- "ndk",
+ "ndk 0.8.0",
"ndk-context",
"num-derive",
"num-traits",
@@ -11079,9 +11232,9 @@ dependencies = [
[[package]]
name = "pathfinder_simd"
-version = "0.5.4"
+version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5cf07ef4804cfa9aea3b04a7bbdd5a40031dbb6b4f2cbaf2b011666c80c5b4f2"
+checksum = "bf9027960355bf3afff9841918474a81a5f972ac6d226d518060bba758b5ad57"
dependencies = [
"rustc_version",
]
@@ -13011,6 +13164,7 @@ dependencies = [
"thiserror 2.0.12",
"urlencoding",
"util",
+ "which 6.0.3",
"workspace-hack",
]
@@ -13031,6 +13185,7 @@ dependencies = [
"dap",
"dap_adapters",
"debug_adapter_extension",
+ "editor",
"env_logger 0.11.8",
"extension",
"extension_host",
@@ -13069,6 +13224,7 @@ dependencies = [
"unindent",
"util",
"watch",
+ "workspace",
"worktree",
"zlog",
]
@@ -13401,7 +13557,7 @@ version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7ceb6607dd738c99bc8cb28eff249b7cd5c8ec88b9db96c0608c1480d140fb1"
dependencies = [
- "cpal",
+ "cpal 0.15.3",
"hound",
]
@@ -13495,6 +13651,7 @@ dependencies = [
"serde",
"settings",
"theme",
+ "title_bar",
"ui",
"util",
"workspace",
@@ -14413,12 +14570,12 @@ dependencies = [
"fs",
"gpui",
"log",
- "paths",
"schemars",
"serde",
"settings",
"theme",
"ui",
+ "util",
"workspace",
"workspace-hack",
]
@@ -15279,6 +15436,7 @@ dependencies = [
"serde",
"serde_json",
"smol",
+ "util",
"workspace-hack",
]
@@ -15749,6 +15907,7 @@ dependencies = [
"theme",
"thiserror 2.0.12",
"url",
+ "urlencoding",
"util",
"windows 0.61.1",
"workspace-hack",
@@ -17156,12 +17315,14 @@ dependencies = [
"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",
@@ -17262,6 +17423,17 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+[[package]]
+name = "vercel"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "schemars",
+ "serde",
+ "strum 0.27.1",
+ "workspace-hack",
+]
+
[[package]]
name = "version-compare"
version = "0.2.0"
@@ -19323,6 +19495,7 @@ dependencies = [
"num-rational",
"num-traits",
"objc2",
+ "objc2-core-foundation",
"objc2-foundation",
"objc2-metal",
"object",
@@ -19743,16 +19916,16 @@ dependencies = [
[[package]]
name = "zed"
-version = "0.191.0"
+version = "0.194.0"
dependencies = [
"activity_indicator",
"agent",
"agent_settings",
+ "agent_ui",
"anyhow",
"ashpd",
"askpass",
"assets",
- "assistant_context_editor",
"assistant_tool",
"assistant_tools",
"audio",
@@ -19783,7 +19956,6 @@ dependencies = [
"extension",
"extension_host",
"extensions_ui",
- "feature_flags",
"feedback",
"file_finder",
"fs",
@@ -19800,6 +19972,7 @@ dependencies = [
"inline_completion_button",
"inspector_ui",
"install_cli",
+ "itertools 0.14.0",
"jj_ui",
"journal",
"language",
@@ -19824,6 +19997,7 @@ dependencies = [
"parking_lot",
"paths",
"picker",
+ "pretty_assertions",
"profiling",
"project",
"project_panel",
diff --git a/Cargo.toml b/Cargo.toml
index fac347056f..da2ed94ac4 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -2,12 +2,13 @@
resolver = "2"
members = [
"crates/activity_indicator",
+ "crates/agent_ui",
"crates/agent",
"crates/agent_settings",
"crates/anthropic",
"crates/askpass",
"crates/assets",
- "crates/assistant_context_editor",
+ "crates/assistant_context",
"crates/assistant_slash_command",
"crates/assistant_slash_commands",
"crates/assistant_tool",
@@ -65,6 +66,7 @@ members = [
"crates/gpui",
"crates/gpui_macros",
"crates/gpui_tokio",
+
"crates/html_to_markdown",
"crates/http_client",
"crates/http_client_tls",
@@ -163,6 +165,7 @@ members = [
"crates/ui_prompt",
"crates/util",
"crates/util_macros",
+ "crates/vercel",
"crates/vim",
"crates/vim_mode_setting",
"crates/watch",
@@ -213,12 +216,13 @@ edition = "2024"
activity_indicator = { path = "crates/activity_indicator" }
agent = { path = "crates/agent" }
+agent_ui = { path = "crates/agent_ui" }
agent_settings = { path = "crates/agent_settings" }
ai = { path = "crates/ai" }
anthropic = { path = "crates/anthropic" }
askpass = { path = "crates/askpass" }
assets = { path = "crates/assets" }
-assistant_context_editor = { path = "crates/assistant_context_editor" }
+assistant_context = { path = "crates/assistant_context" }
assistant_slash_command = { path = "crates/assistant_slash_command" }
assistant_slash_commands = { path = "crates/assistant_slash_commands" }
assistant_tool = { path = "crates/assistant_tool" }
@@ -372,8 +376,10 @@ ui_macros = { path = "crates/ui_macros" }
ui_prompt = { path = "crates/ui_prompt" }
util = { path = "crates/util" }
util_macros = { path = "crates/util_macros" }
+vercel = { path = "crates/vercel" }
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" }
@@ -417,9 +423,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 = "416375211bb0b5826b3584dccdb6a43369e499ad" }
-blade-macros = { git = "https://github.com/kvark/blade", rev = "416375211bb0b5826b3584dccdb6a43369e499ad" }
-blade-util = { git = "https://github.com/kvark/blade", rev = "416375211bb0b5826b3584dccdb6a43369e499ad" }
+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" }
blake3 = "1.5.3"
bytes = "1.0"
cargo_metadata = "0.19"
@@ -433,6 +439,7 @@ 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" }
@@ -452,7 +459,6 @@ futures-batch = "0.6.1"
futures-lite = "1.13"
git2 = { version = "0.20.1", default-features = false }
globset = "0.4"
-hashbrown = "0.15.3"
handlebars = "4.3"
heck = "0.5"
heed = { version = "0.21.0", features = ["read-txn-no-tls"] }
@@ -480,7 +486,6 @@ log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "c9c189f1c5dd53c624a419ce35bc77ad6a908d18" }
markup5ever_rcdom = "0.3.0"
metal = "0.29"
-mlua = { version = "0.10", features = ["lua54", "vendored", "async", "send"] }
moka = { version = "0.12.10", features = ["sync"] }
naga = { version = "25.0", features = ["wgsl-in"] }
nanoid = "0.4"
@@ -515,7 +520,6 @@ rand = "0.8.5"
rayon = "1.8"
ref-cast = "1.0.24"
regex = "1.5"
-repair_json = "0.1.0"
reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "951c770a32f1998d6e999cef3e59e0013e6c4415", default-features = false, features = [
"charset",
"http2",
@@ -547,7 +551,6 @@ serde_repr = "0.1"
sha2 = "0.10"
shellexpand = "2.1.0"
shlex = "1.3.0"
-signal-hook = "0.3.17"
simplelog = "0.12.2"
smallvec = { version = "1.6", features = ["union"] }
smol = "2.0"
@@ -682,9 +685,7 @@ 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" }
diff --git a/assets/icons/ai_v_zero.svg b/assets/icons/ai_v_zero.svg
new file mode 100644
index 0000000000..26d09ea26a
--- /dev/null
+++ b/assets/icons/ai_v_zero.svg
@@ -0,0 +1,16 @@
+
diff --git a/assets/icons/arrow_up_alt.svg b/assets/icons/arrow_up_alt.svg
new file mode 100644
index 0000000000..c8cf286a8c
--- /dev/null
+++ b/assets/icons/arrow_up_alt.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/blocks.svg b/assets/icons/blocks.svg
index 42d44c3f95..588d49abbc 100644
--- a/assets/icons/blocks.svg
+++ b/assets/icons/blocks.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/assets/icons/file_icons/cairo.svg b/assets/icons/file_icons/cairo.svg
new file mode 100644
index 0000000000..dcf77c6fbf
--- /dev/null
+++ b/assets/icons/file_icons/cairo.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/zed_mcp_custom.svg b/assets/icons/zed_mcp_custom.svg
new file mode 100644
index 0000000000..6410a26fca
--- /dev/null
+++ b/assets/icons/zed_mcp_custom.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/zed_mcp_extension.svg b/assets/icons/zed_mcp_extension.svg
new file mode 100644
index 0000000000..996e0c1920
--- /dev/null
+++ b/assets/icons/zed_mcp_extension.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/images/debugger_grid.svg b/assets/images/debugger_grid.svg
new file mode 100644
index 0000000000..8b40dbd707
--- /dev/null
+++ b/assets/images/debugger_grid.svg
@@ -0,0 +1,890 @@
+
diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json
index b85b0626b3..0c4de0e053 100644
--- a/assets/keymaps/default-linux.json
+++ b/assets/keymaps/default-linux.json
@@ -10,8 +10,10 @@
"pagedown": "menu::SelectLast",
"ctrl-n": "menu::SelectNext",
"tab": "menu::SelectNext",
+ "down": "menu::SelectNext",
"ctrl-p": "menu::SelectPrevious",
"shift-tab": "menu::SelectPrevious",
+ "up": "menu::SelectPrevious",
"enter": "menu::Confirm",
"ctrl-enter": "menu::SecondaryConfirm",
"ctrl-escape": "menu::Cancel",
@@ -39,7 +41,8 @@
"shift-f11": "debugger::StepOut",
"f11": "zed::ToggleFullScreen",
"ctrl-alt-z": "edit_prediction::RateCompletions",
- "ctrl-shift-i": "edit_prediction::ToggleMenu"
+ "ctrl-shift-i": "edit_prediction::ToggleMenu",
+ "ctrl-alt-l": "lsp_tool::ToggleMenu"
}
},
{
@@ -115,6 +118,7 @@
"ctrl-\"": "editor::ExpandAllDiffHunks",
"ctrl-i": "editor::ShowSignatureHelp",
"alt-g b": "git::Blame",
+ "alt-g m": "git::OpenModifiedFiles",
"menu": "editor::OpenContextMenu",
"shift-f10": "editor::OpenContextMenu",
"ctrl-shift-e": "editor::ToggleEditPrediction",
@@ -240,8 +244,7 @@
"ctrl-alt-e": "agent::RemoveAllContext",
"ctrl-shift-e": "project_panel::ToggleFocus",
"ctrl-shift-enter": "agent::ContinueThread",
- "alt-enter": "agent::ContinueWithBurnMode",
- "ctrl-alt-b": "agent::ToggleBurnMode"
+ "alt-enter": "agent::ContinueWithBurnMode"
}
},
{
@@ -891,7 +894,10 @@
"right": "variable_list::ExpandSelectedEntry",
"enter": "variable_list::EditVariable",
"ctrl-c": "variable_list::CopyVariableValue",
- "ctrl-alt-c": "variable_list::CopyVariableName"
+ "ctrl-alt-c": "variable_list::CopyVariableName",
+ "delete": "variable_list::RemoveWatch",
+ "backspace": "variable_list::RemoveWatch",
+ "alt-enter": "variable_list::AddWatch"
}
},
{
@@ -939,7 +945,7 @@
}
},
{
- "context": "FileFinder",
+ "context": "FileFinder || (FileFinder > Picker > Editor)",
"bindings": {
"ctrl-shift-a": "file_finder::ToggleSplitMenu",
"ctrl-shift-i": "file_finder::ToggleFilterMenu"
@@ -1034,7 +1040,8 @@
"context": "DebugConsole > Editor",
"use_key_equivalents": true,
"bindings": {
- "enter": "menu::Confirm"
+ "enter": "menu::Confirm",
+ "alt-enter": "console::WatchExpression"
}
},
{
diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json
index 225cddf590..5bd99963bd 100644
--- a/assets/keymaps/default-macos.json
+++ b/assets/keymaps/default-macos.json
@@ -47,7 +47,8 @@
"fn-f": "zed::ToggleFullScreen",
"ctrl-cmd-f": "zed::ToggleFullScreen",
"ctrl-cmd-z": "edit_prediction::RateCompletions",
- "ctrl-cmd-i": "edit_prediction::ToggleMenu"
+ "ctrl-cmd-i": "edit_prediction::ToggleMenu",
+ "ctrl-cmd-l": "lsp_tool::ToggleMenu"
}
},
{
@@ -139,6 +140,7 @@
"cmd-'": "editor::ToggleSelectedDiffHunks",
"cmd-\"": "editor::ExpandAllDiffHunks",
"cmd-alt-g b": "git::Blame",
+ "cmd-alt-g m": "git::OpenModifiedFiles",
"cmd-i": "editor::ShowSignatureHelp",
"f9": "editor::ToggleBreakpoint",
"shift-f9": "editor::EditLogBreakpoint",
@@ -282,8 +284,7 @@
"cmd-alt-e": "agent::RemoveAllContext",
"cmd-shift-e": "project_panel::ToggleFocus",
"cmd-shift-enter": "agent::ContinueThread",
- "alt-enter": "agent::ContinueWithBurnMode",
- "cmd-alt-b": "agent::ToggleBurnMode"
+ "alt-enter": "agent::ContinueWithBurnMode"
}
},
{
@@ -586,7 +587,6 @@
"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-b": "branches::OpenRecent",
"ctrl-~": "workspace::NewTerminal",
"cmd-s": "workspace::Save",
"cmd-k s": "workspace::SaveWithoutFormat",
@@ -863,7 +863,10 @@
"right": "variable_list::ExpandSelectedEntry",
"enter": "variable_list::EditVariable",
"cmd-c": "variable_list::CopyVariableValue",
- "cmd-alt-c": "variable_list::CopyVariableName"
+ "cmd-alt-c": "variable_list::CopyVariableName",
+ "delete": "variable_list::RemoveWatch",
+ "backspace": "variable_list::RemoveWatch",
+ "alt-enter": "variable_list::AddWatch"
}
},
{
@@ -1007,7 +1010,7 @@
}
},
{
- "context": "FileFinder",
+ "context": "FileFinder || (FileFinder > Picker > Editor)",
"use_key_equivalents": true,
"bindings": {
"cmd-shift-a": "file_finder::ToggleSplitMenu",
@@ -1134,7 +1137,8 @@
"context": "DebugConsole > Editor",
"use_key_equivalents": true,
"bindings": {
- "enter": "menu::Confirm"
+ "enter": "menu::Confirm",
+ "alt-enter": "console::WatchExpression"
}
},
{
diff --git a/assets/keymaps/linux/atom.json b/assets/keymaps/linux/atom.json
index d471a54ea5..86ee068b06 100644
--- a/assets/keymaps/linux/atom.json
+++ b/assets/keymaps/linux/atom.json
@@ -9,6 +9,13 @@
},
{
"context": "Editor",
+ "bindings": {
+ "ctrl-k ctrl-u": "editor::ConvertToUpperCase", // editor:upper-case
+ "ctrl-k ctrl-l": "editor::ConvertToLowerCase" // editor:lower-case
+ }
+ },
+ {
+ "context": "Editor && mode == full",
"bindings": {
"ctrl-shift-l": "language_selector::Toggle", // grammar-selector:show
"ctrl-|": "pane::RevealInProjectPanel", // tree-view:reveal-active-file
@@ -19,25 +26,20 @@
"shift-f3": ["editor::SelectPrevious", { "replace_newest": true }], //find-and-replace:find-previous
"alt-shift-down": "editor::AddSelectionBelow", // editor:add-selection-below
"alt-shift-up": "editor::AddSelectionAbove", // editor:add-selection-above
- "ctrl-k ctrl-u": "editor::ConvertToUpperCase", // editor:upper-case
- "ctrl-k ctrl-l": "editor::ConvertToLowerCase", // editor:lower-case
"ctrl-j": "editor::JoinLines", // editor:join-lines
"ctrl-shift-d": "editor::DuplicateLineDown", // editor:duplicate-lines
"ctrl-up": "editor::MoveLineUp", // editor:move-line-up
"ctrl-down": "editor::MoveLineDown", // editor:move-line-down
"ctrl-\\": "workspace::ToggleLeftDock", // tree-view:toggle
- "ctrl-shift-m": "markdown::OpenPreviewToTheSide" // markdown-preview:toggle
- }
- },
- {
- "context": "Editor && mode == full",
- "bindings": {
+ "ctrl-shift-m": "markdown::OpenPreviewToTheSide", // markdown-preview:toggle
"ctrl-r": "outline::Toggle" // symbols-view:toggle-project-symbols
}
},
{
"context": "BufferSearchBar",
"bindings": {
+ "f3": ["editor::SelectNext", { "replace_newest": true }], // find-and-replace:find-next
+ "shift-f3": ["editor::SelectPrevious", { "replace_newest": true }], //find-and-replace:find-previous
"ctrl-f3": "search::SelectNextMatch", // find-and-replace:find-next-selected
"ctrl-shift-f3": "search::SelectPreviousMatch" // find-and-replace:find-previous-selected
}
diff --git a/assets/keymaps/linux/cursor.json b/assets/keymaps/linux/cursor.json
index 14cfcc43ec..347b7885fc 100644
--- a/assets/keymaps/linux/cursor.json
+++ b/assets/keymaps/linux/cursor.json
@@ -8,7 +8,6 @@
"ctrl-shift-i": "agent::ToggleFocus",
"ctrl-l": "agent::ToggleFocus",
"ctrl-shift-l": "agent::ToggleFocus",
- "ctrl-alt-b": "agent::ToggleFocus",
"ctrl-shift-j": "agent::OpenConfiguration"
}
},
@@ -42,7 +41,6 @@
"ctrl-shift-i": "workspace::ToggleRightDock",
"ctrl-l": "workspace::ToggleRightDock",
"ctrl-shift-l": "workspace::ToggleRightDock",
- "ctrl-alt-b": "workspace::ToggleRightDock",
"ctrl-w": "workspace::ToggleRightDock", // technically should close chat
"ctrl-.": "agent::ToggleProfileSelector",
"ctrl-/": "agent::ToggleModelSelector",
diff --git a/assets/keymaps/linux/emacs.json b/assets/keymaps/linux/emacs.json
index 5a5cb6d90c..d1453da485 100755
--- a/assets/keymaps/linux/emacs.json
+++ b/assets/keymaps/linux/emacs.json
@@ -90,6 +90,13 @@
"ctrl-g": "editor::Cancel"
}
},
+ {
+ "context": "Editor && (showing_code_actions || showing_completions)",
+ "bindings": {
+ "ctrl-p": "editor::ContextMenuPrevious",
+ "ctrl-n": "editor::ContextMenuNext"
+ }
+ },
{
"context": "Workspace",
"bindings": {
diff --git a/assets/keymaps/macos/atom.json b/assets/keymaps/macos/atom.json
index 9ddf353810..df48e51767 100644
--- a/assets/keymaps/macos/atom.json
+++ b/assets/keymaps/macos/atom.json
@@ -9,6 +9,14 @@
},
{
"context": "Editor",
+ "bindings": {
+ "cmd-shift-backspace": "editor::DeleteToBeginningOfLine",
+ "cmd-k cmd-u": "editor::ConvertToUpperCase",
+ "cmd-k cmd-l": "editor::ConvertToLowerCase"
+ }
+ },
+ {
+ "context": "Editor && mode == full",
"bindings": {
"ctrl-shift-l": "language_selector::Toggle",
"cmd-|": "pane::RevealInProjectPanel",
@@ -19,26 +27,20 @@
"cmd-shift-g": ["editor::SelectPrevious", { "replace_newest": true }],
"ctrl-shift-down": "editor::AddSelectionBelow",
"ctrl-shift-up": "editor::AddSelectionAbove",
- "cmd-shift-backspace": "editor::DeleteToBeginningOfLine",
- "cmd-k cmd-u": "editor::ConvertToUpperCase",
- "cmd-k cmd-l": "editor::ConvertToLowerCase",
"alt-enter": "editor::Newline",
"cmd-shift-d": "editor::DuplicateLineDown",
"ctrl-cmd-up": "editor::MoveLineUp",
"ctrl-cmd-down": "editor::MoveLineDown",
"cmd-\\": "workspace::ToggleLeftDock",
- "ctrl-shift-m": "markdown::OpenPreviewToTheSide"
- }
- },
- {
- "context": "Editor && mode == full",
- "bindings": {
+ "ctrl-shift-m": "markdown::OpenPreviewToTheSide",
"cmd-r": "outline::Toggle"
}
},
{
"context": "BufferSearchBar",
"bindings": {
+ "cmd-g": ["editor::SelectNext", { "replace_newest": true }],
+ "cmd-shift-g": ["editor::SelectPrevious", { "replace_newest": true }],
"cmd-f3": "search::SelectNextMatch",
"cmd-shift-f3": "search::SelectPreviousMatch"
}
diff --git a/assets/keymaps/macos/cursor.json b/assets/keymaps/macos/cursor.json
index 5d26974f05..b1d39bef9e 100644
--- a/assets/keymaps/macos/cursor.json
+++ b/assets/keymaps/macos/cursor.json
@@ -8,7 +8,6 @@
"cmd-shift-i": "agent::ToggleFocus",
"cmd-l": "agent::ToggleFocus",
"cmd-shift-l": "agent::ToggleFocus",
- "cmd-alt-b": "agent::ToggleFocus",
"cmd-shift-j": "agent::OpenConfiguration"
}
},
@@ -43,7 +42,6 @@
"cmd-shift-i": "workspace::ToggleRightDock",
"cmd-l": "workspace::ToggleRightDock",
"cmd-shift-l": "workspace::ToggleRightDock",
- "cmd-alt-b": "workspace::ToggleRightDock",
"cmd-w": "workspace::ToggleRightDock", // technically should close chat
"cmd-.": "agent::ToggleProfileSelector",
"cmd-/": "agent::ToggleModelSelector",
diff --git a/assets/keymaps/macos/emacs.json b/assets/keymaps/macos/emacs.json
index 5a5cb6d90c..d1453da485 100755
--- a/assets/keymaps/macos/emacs.json
+++ b/assets/keymaps/macos/emacs.json
@@ -90,6 +90,13 @@
"ctrl-g": "editor::Cancel"
}
},
+ {
+ "context": "Editor && (showing_code_actions || showing_completions)",
+ "bindings": {
+ "ctrl-p": "editor::ContextMenuPrevious",
+ "ctrl-n": "editor::ContextMenuNext"
+ }
+ },
{
"context": "Workspace",
"bindings": {
diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json
index f2d50bd13f..6b95839e2a 100644
--- a/assets/keymaps/vim.json
+++ b/assets/keymaps/vim.json
@@ -56,6 +56,9 @@
"[ 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",
@@ -82,10 +85,10 @@
"[ {": ["vim::UnmatchedBackward", { "char": "{" }],
"] )": ["vim::UnmatchedForward", { "char": ")" }],
"[ (": ["vim::UnmatchedBackward", { "char": "(" }],
- "f": ["vim::PushFindForward", { "before": false }],
- "t": ["vim::PushFindForward", { "before": true }],
- "shift-f": ["vim::PushFindBackward", { "after": false }],
- "shift-t": ["vim::PushFindBackward", { "after": true }],
+ "f": ["vim::PushFindForward", { "before": false, "multiline": false }],
+ "t": ["vim::PushFindForward", { "before": true, "multiline": false }],
+ "shift-f": ["vim::PushFindBackward", { "after": false, "multiline": false }],
+ "shift-t": ["vim::PushFindBackward", { "after": true, "multiline": false }],
"m": "vim::PushMark",
"'": ["vim::PushJump", { "line": true }],
"`": ["vim::PushJump", { "line": false }],
@@ -184,6 +187,8 @@
"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
@@ -363,6 +368,10 @@
"escape": "editor::Cancel",
"ctrl-[": "editor::Cancel",
":": "command_palette::Toggle",
+ "left": "vim::WrappingLeft",
+ "right": "vim::WrappingRight",
+ "h": "vim::WrappingLeft",
+ "l": "vim::WrappingRight",
"shift-d": "vim::DeleteToEndOfLine",
"shift-j": "vim::JoinLines",
"y": "editor::Copy",
@@ -380,6 +389,10 @@
"shift-p": ["vim::Paste", { "before": true }],
"u": "vim::Undo",
"ctrl-r": "vim::Redo",
+ "f": ["vim::PushFindForward", { "before": false, "multiline": true }],
+ "t": ["vim::PushFindForward", { "before": true, "multiline": true }],
+ "shift-f": ["vim::PushFindBackward", { "after": false, "multiline": true }],
+ "shift-t": ["vim::PushFindBackward", { "after": true, "multiline": true }],
"r": "vim::PushReplace",
"s": "vim::Substitute",
"shift-s": "vim::SubstituteLine",
diff --git a/assets/prompts/assistant_system_prompt.hbs b/assets/prompts/assistant_system_prompt.hbs
index a155dea19d..b4545f5a74 100644
--- a/assets/prompts/assistant_system_prompt.hbs
+++ b/assets/prompts/assistant_system_prompt.hbs
@@ -27,11 +27,11 @@ If you are unsure how to fulfill the user's request, gather more information wit
If appropriate, use tool calls to explore the current project, which contains the following root directories:
{{#each worktrees}}
-- `{{root_name}}`
+- `{{abs_path}}`
{{/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 begin with a path that starts with a project root directory listed above.
+- When providing paths to tools, the path should always start with the name of 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.
diff --git a/assets/settings/default.json b/assets/settings/default.json
index 939f79e281..1b9a19615d 100644
--- a/assets/settings/default.json
+++ b/assets/settings/default.json
@@ -80,6 +80,7 @@
"inactive_opacity": 1.0
},
// Layout mode of the bottom dock. Defaults to "contained"
+ // choices: contained, full, left_aligned, right_aligned
"bottom_dock_layout": "contained",
// The direction that you want to split panes horizontally. Defaults to "up"
"pane_split_direction_horizontal": "up",
@@ -94,11 +95,9 @@
// workspace when the centered layout is used.
"right_padding": 0.2
},
- // All settings related to the image viewer.
+ // Image viewer settings
"image_viewer": {
- // The unit for image file sizes.
- // By default we're setting it to binary.
- // The second option is decimal.
+ // The unit for image file sizes: "binary" (KiB, MiB) or decimal (KB, MB)
"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.
@@ -110,6 +109,8 @@
"multi_cursor_modifier": "alt",
// Whether to enable vim modes and key bindings.
"vim_mode": false,
+ // Whether to enable helix mode and key bindings.
+ "helix_mode": false,
// Whether to show the informational hover box when moving the mouse
// over symbols in the editor.
"hover_popover_enabled": true,
@@ -117,7 +118,14 @@
"hover_popover_delay": 300,
// Whether to confirm before quitting Zed.
"confirm_quit": false,
- // Whether to restore last closed project when fresh Zed instance is opened.
+ // Whether to restore last closed project when fresh Zed instance is opened
+ // May take 3 values:
+ // 1. All workspaces open during last session
+ // "restore_on_startup": "last_session"
+ // 2. The workspace opened
+ // "restore_on_startup": "last_workspace",
+ // 3. Do not restore previous workspaces
+ // "restore_on_startup": "none",
"restore_on_startup": "last_session",
// Whether to attempt to restore previous file's state when opening it again.
// The state is stored per pane.
@@ -130,7 +138,9 @@
"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.
+ // Relative size of the drop target in the editor that will open dropped file as a split pane (0-0.5)
+ // E.g. 0.25 == If you drop onto the top/bottom quarter of the pane a new vertical split will be used
+ // If you drop onto the left/right quarter of the pane a new horizontal split will be used
"drop_target_size": 0.2,
// Whether the window should be closed when using 'close active item' on a window with no tabs.
// May take 3 values:
@@ -307,6 +317,8 @@
// "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)
@@ -398,6 +410,13 @@
// 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:
@@ -424,7 +443,9 @@
// 1. `null` to inherit the editor `current_line_highlight` setting (default)
// 2. "line" or "all" to highlight the current line in the minimap.
// 3. "gutter" or "none" to not highlight the current line in the minimap.
- "current_line_highlight": null
+ "current_line_highlight": null,
+ // Maximum number of columns to display in the minimap.
+ "max_width_columns": 80
},
// Enable middle-click paste on Linux.
"middle_click_paste": true,
@@ -445,7 +466,9 @@
// Whether to show breakpoints in the gutter.
"breakpoints": true,
// Whether to show fold buttons in the gutter.
- "folds": true
+ "folds": true,
+ // Minimum number of characters to reserve space for in the gutter.
+ "min_line_number_digits": 4
},
"indent_guides": {
// Whether to show indent guides in the editor.
@@ -470,7 +493,7 @@
},
// Whether the editor will scroll beyond the last line.
"scroll_beyond_last_line": "one_page",
- // The number of lines to keep above/below the cursor when scrolling.
+ // The number of lines to keep above/below the cursor when scrolling with the keyboard
"vertical_scroll_margin": 3,
// Whether to scroll when clicking near the edge of the visible text area.
"autoscroll_on_clicks": false,
@@ -686,23 +709,27 @@
"default_width": 360,
// Style of the git status indicator in the panel.
//
+ // Choices: label_color, icon
// Default: icon
"status_style": "icon",
- // What branch name to use if init.defaultBranch
- // is not set
+ // What branch name to use if `init.defaultBranch` is not set
//
// Default: main
"fallback_branch_name": "main",
- // Whether to sort entries in the panel by path
- // or by status (the default).
+ // Whether to sort entries in the panel by path or by status (the default).
//
// Default: false
"sort_by_path": false,
+ // Whether to collapse untracked files in the diff panel.
+ //
+ // Default: false
+ "collapse_untracked_diff": false,
"scrollbar": {
// When to show the scrollbar in the git panel.
//
+ // Choices: always, auto, never, system
// Default: inherits editor scrollbar settings
- "show": null
+ // "show": null
}
},
"message_editor": {
@@ -980,8 +1007,7 @@
// Removes any lines containing only whitespace at the end of the file and
// ensures just one newline at the end.
"ensure_final_newline_on_save": true,
- // Whether or not to perform a buffer format before saving
- //
+ // Whether or not to perform a buffer format before saving: [on, off, prettier, language_server]
// Keep in mind, if the autosave with delay is enabled, format_on_save will be ignored
"format_on_save": "on",
// How to perform a buffer format. This setting can take 4 values:
@@ -1034,6 +1060,19 @@
// 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.
@@ -1149,6 +1188,12 @@
// 2. Display predictions inline only when holding a modifier key (alt by default).
// "mode": "subtle"
"mode": "eager",
+ // Copilot-specific settings
+ // "copilot": {
+ // "enterprise_uri": "",
+ // "proxy": "",
+ // "proxy_no_verify": false
+ // },
// Whether edit predictions are enabled when editing text threads.
// This setting has no effect if globally disabled.
"enabled_in_text_threads": true
@@ -1314,6 +1359,8 @@
// the terminal will default to matching the buffer's font fallbacks.
// This will be merged with the platform's default font fallbacks
// "font_fallbacks": ["FiraCode Nerd Fonts"],
+ // The weight of the editor font in standard CSS units from 100 to 900.
+ // "font_weight": 400
// Sets the maximum number of lines in the terminal's scrollback buffer.
// Default: 10_000, maximum: 100_000 (all bigger values set will be treated as 100_000), 0 disables the scrolling.
// Existing terminals will not pick up this change until they are recreated.
@@ -1673,6 +1720,11 @@
// }
// }
},
+ // Common language server settings.
+ "global_lsp_settings": {
+ // Whether to show the LSP servers button in the status bar.
+ "button": true
+ },
// Jupyter settings
"jupyter": {
"enabled": true
@@ -1687,7 +1739,6 @@
"default_mode": "normal",
"toggle_relative_line_numbers": false,
"use_system_clipboard": "always",
- "use_multiline_find": false,
"use_smartcase_find": false,
"highlight_on_yank_duration": 200,
"custom_digraphs": {},
@@ -1770,6 +1821,7 @@
"debugger": {
"stepping_granularity": "line",
"save_breakpoints": true,
+ "dock": "bottom",
"button": true
}
}
diff --git a/assets/themes/one/one.json b/assets/themes/one/one.json
index bf38d9dccb..384ad28272 100644
--- a/assets/themes/one/one.json
+++ b/assets/themes/one/one.json
@@ -601,7 +601,7 @@
"font_weight": null
},
"constant": {
- "color": "#669f59ff",
+ "color": "#c18401ff",
"font_style": null,
"font_weight": null
},
diff --git a/crates/activity_indicator/Cargo.toml b/crates/activity_indicator/Cargo.toml
index 778cf472df..3a80f012f9 100644
--- a/crates/activity_indicator/Cargo.toml
+++ b/crates/activity_indicator/Cargo.toml
@@ -21,6 +21,7 @@ futures.workspace = true
gpui.workspace = true
language.workspace = true
project.workspace = true
+proto.workspace = true
smallvec.workspace = true
ui.workspace = true
util.workspace = true
diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs
index 86d60d2640..b3287e8222 100644
--- a/crates/activity_indicator/src/activity_indicator.rs
+++ b/crates/activity_indicator/src/activity_indicator.rs
@@ -7,7 +7,10 @@ use gpui::{
InteractiveElement as _, ParentElement as _, Render, SharedString, StatefulInteractiveElement,
Styled, Transformation, Window, actions, percentage,
};
-use language::{BinaryStatus, LanguageRegistry, LanguageServerId};
+use language::{
+ BinaryStatus, LanguageRegistry, LanguageServerId, LanguageServerName,
+ LanguageServerStatusUpdate, ServerHealth,
+};
use project::{
EnvironmentErrorMessage, LanguageServerProgress, LspStoreEvent, Project,
ProjectEnvironmentEvent,
@@ -16,6 +19,7 @@ use project::{
use smallvec::SmallVec;
use std::{
cmp::Reverse,
+ collections::HashSet,
fmt::Write,
path::Path,
sync::Arc,
@@ -30,9 +34,9 @@ const GIT_OPERATION_DELAY: Duration = Duration::from_millis(0);
actions!(activity_indicator, [ShowErrorMessage]);
pub enum Event {
- ShowError {
- server_name: SharedString,
- error: String,
+ ShowStatus {
+ server_name: LanguageServerName,
+ status: SharedString,
},
}
@@ -45,8 +49,8 @@ pub struct ActivityIndicator {
#[derive(Debug)]
struct ServerStatus {
- name: SharedString,
- status: BinaryStatus,
+ name: LanguageServerName,
+ status: LanguageServerStatusUpdate,
}
struct PendingWork<'a> {
@@ -76,10 +80,13 @@ impl ActivityIndicator {
let this = cx.new(|cx| {
let mut status_events = languages.language_server_binary_statuses();
cx.spawn(async move |this, cx| {
- while let Some((name, status)) = status_events.next().await {
+ while let Some((name, binary_status)) = status_events.next().await {
this.update(cx, |this: &mut ActivityIndicator, cx| {
this.statuses.retain(|s| s.name != name);
- this.statuses.push(ServerStatus { name, status });
+ this.statuses.push(ServerStatus {
+ name,
+ status: LanguageServerStatusUpdate::Binary(binary_status),
+ });
cx.notify();
})?;
}
@@ -108,8 +115,76 @@ impl ActivityIndicator {
cx.subscribe(
&project.read(cx).lsp_store(),
- |_, _, event, cx| match event {
- LspStoreEvent::LanguageServerUpdate { .. } => cx.notify(),
+ |activity_indicator, _, event, cx| match event {
+ LspStoreEvent::LanguageServerUpdate { name, message, .. } => {
+ if let proto::update_language_server::Variant::StatusUpdate(status_update) =
+ message
+ {
+ let Some(name) = name.clone() else {
+ return;
+ };
+ let status = match &status_update.status {
+ Some(proto::status_update::Status::Binary(binary_status)) => {
+ if let Some(binary_status) =
+ proto::ServerBinaryStatus::from_i32(*binary_status)
+ {
+ let binary_status = match binary_status {
+ proto::ServerBinaryStatus::None => BinaryStatus::None,
+ proto::ServerBinaryStatus::CheckingForUpdate => {
+ BinaryStatus::CheckingForUpdate
+ }
+ proto::ServerBinaryStatus::Downloading => {
+ BinaryStatus::Downloading
+ }
+ proto::ServerBinaryStatus::Starting => {
+ BinaryStatus::Starting
+ }
+ proto::ServerBinaryStatus::Stopping => {
+ BinaryStatus::Stopping
+ }
+ proto::ServerBinaryStatus::Stopped => {
+ BinaryStatus::Stopped
+ }
+ proto::ServerBinaryStatus::Failed => {
+ let Some(error) = status_update.message.clone()
+ else {
+ return;
+ };
+ BinaryStatus::Failed { error }
+ }
+ };
+ LanguageServerStatusUpdate::Binary(binary_status)
+ } else {
+ return;
+ }
+ }
+ Some(proto::status_update::Status::Health(health_status)) => {
+ if let Some(health) =
+ proto::ServerHealth::from_i32(*health_status)
+ {
+ let health = match health {
+ proto::ServerHealth::Ok => ServerHealth::Ok,
+ proto::ServerHealth::Warning => ServerHealth::Warning,
+ proto::ServerHealth::Error => ServerHealth::Error,
+ };
+ LanguageServerStatusUpdate::Health(
+ health,
+ status_update.message.clone().map(SharedString::from),
+ )
+ } else {
+ return;
+ }
+ }
+ None => return,
+ };
+
+ activity_indicator.statuses.retain(|s| s.name != name);
+ activity_indicator
+ .statuses
+ .push(ServerStatus { name, status });
+ }
+ cx.notify()
+ }
_ => {}
},
)
@@ -145,19 +220,19 @@ impl ActivityIndicator {
});
cx.subscribe_in(&this, window, move |_, _, event, window, cx| match event {
- Event::ShowError { server_name, error } => {
+ Event::ShowStatus {
+ server_name,
+ status,
+ } => {
let create_buffer = project.update(cx, |project, cx| project.create_buffer(cx));
let project = project.clone();
- let error = error.clone();
+ let status = status.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 error: {}\n\n{}", server_name, error),
- )],
+ [(0..0, format!("Language server {server_name}:\n\n{status}"))],
None,
cx,
);
@@ -166,7 +241,10 @@ impl ActivityIndicator {
workspace.update_in(cx, |workspace, window, cx| {
workspace.add_item_to_active_pane(
Box::new(cx.new(|cx| {
- Editor::for_buffer(buffer, Some(project.clone()), window, cx)
+ let mut editor =
+ Editor::for_buffer(buffer, Some(project.clone()), window, cx);
+ editor.set_read_only(true);
+ editor
})),
None,
true,
@@ -185,19 +263,34 @@ impl ActivityIndicator {
}
fn show_error_message(&mut self, _: &ShowErrorMessage, _: &mut Window, cx: &mut Context) {
- self.statuses.retain(|status| {
- if let BinaryStatus::Failed { error } = &status.status {
- cx.emit(Event::ShowError {
+ 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 {
server_name: status.name.clone(),
- error: error.clone(),
+ status: SharedString::from(error),
});
+ 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(
@@ -206,9 +299,23 @@ impl ActivityIndicator {
_: &mut Window,
cx: &mut Context,
) {
- if let Some(updater) = &self.auto_updater {
- updater.update(cx, |updater, cx| updater.dismiss_error(cx));
+ let error_dismissed = if let Some(updater) = &self.auto_updater {
+ updater.update(cx, |updater, cx| updater.dismiss_error(cx))
+ } else {
+ false
+ };
+ if error_dismissed {
+ return;
}
+
+ self.project.update(cx, |project, cx| {
+ if project.last_formatting_failure(cx).is_some() {
+ project.reset_last_formatting_failure(cx);
+ true
+ } else {
+ false
+ }
+ });
}
fn pending_language_server_work<'a>(
@@ -267,48 +374,52 @@ 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 message = progress
- .title
- .as_deref()
- .unwrap_or(progress_token)
- .to_string();
+ 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();
- if let Some(percentage) = progress.percentage {
- write!(&mut message, " ({}%)", percentage).unwrap();
+ 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(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
@@ -369,14 +480,44 @@ 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::::default();
for status in &self.statuses {
- 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 => {}
+ match &status.status {
+ LanguageServerStatusUpdate::Binary(
+ BinaryStatus::Starting | BinaryStatus::Stopping,
+ ) => {}
+ LanguageServerStatusUpdate::Binary(BinaryStatus::Stopped) => {
+ servers_to_clear_statuses.insert(status.name.clone());
+ }
+ 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());
+ }
+ },
}
}
+ 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 {
@@ -457,7 +598,7 @@ impl ActivityIndicator {
}),
),
on_click: Some(Arc::new(|this, window, cx| {
- this.show_error_message(&Default::default(), window, cx)
+ this.show_error_message(&ShowErrorMessage, window, cx)
})),
tooltip_message: None,
});
@@ -471,7 +612,7 @@ impl ActivityIndicator {
.size(IconSize::Small)
.into_any_element(),
),
- message: format!("Formatting failed: {}. Click to see logs.", failure),
+ message: format!("Formatting failed: {failure}. Click to see logs."),
on_click: Some(Arc::new(|indicator, window, cx| {
indicator.project.update(cx, |project, cx| {
project.reset_last_formatting_failure(cx);
@@ -482,6 +623,56 @@ 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::>()
+ .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() {
diff --git a/crates/agent/Cargo.toml b/crates/agent/Cargo.toml
index 66e4a5c78f..f320e58d00 100644
--- a/crates/agent/Cargo.toml
+++ b/crates/agent/Cargo.toml
@@ -21,94 +21,58 @@ test-support = [
[dependencies]
agent_settings.workspace = true
anyhow.workspace = true
-assistant_context_editor.workspace = true
-assistant_slash_command.workspace = true
-assistant_slash_commands.workspace = true
+assistant_context.workspace = true
assistant_tool.workspace = true
-audio.workspace = true
-buffer_diff.workspace = true
chrono.workspace = true
client.workspace = true
collections.workspace = true
component.workspace = true
context_server.workspace = true
convert_case.workspace = true
-db.workspace = true
-editor.workspace = true
-extension.workspace = true
feature_flags.workspace = true
-file_icons.workspace = true
fs.workspace = true
futures.workspace = true
-fuzzy.workspace = true
git.workspace = true
gpui.workspace = true
heed.workspace = true
-html_to_markdown.workspace = true
+icons.workspace = true
indoc.workspace = true
http_client.workspace = true
-indexed_docs.workspace = true
-inventory.workspace = true
itertools.workspace = true
-jsonschema.workspace = true
language.workspace = true
language_model.workspace = true
log.workspace = true
-lsp.workspace = true
-markdown.workspace = true
-menu.workspace = true
-multi_buffer.workspace = true
-notifications.workspace = true
-ordered-float.workspace = true
-parking_lot.workspace = true
paths.workspace = true
-picker.workspace = true
postage.workspace = true
project.workspace = true
prompt_store.workspace = true
proto.workspace = true
ref-cast.workspace = true
-release_channel.workspace = true
rope.workspace = true
-rules_library.workspace = true
schemars.workspace = true
-search.workspace = true
serde.workspace = true
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
-terminal.workspace = true
-terminal_view.workspace = true
text.workspace = true
theme.workspace = true
thiserror.workspace = true
time.workspace = true
-time_format.workspace = true
-ui.workspace = true
-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"] }
+workspace = { workspace = true, features = ["test-support"] }
rand.workspace = true
diff --git a/crates/agent/src/agent.rs b/crates/agent/src/agent.rs
index 0ac7869920..7e3590f05d 100644
--- a/crates/agent/src/agent.rs
+++ b/crates/agent/src/agent.rs
@@ -1,294 +1,20 @@
-mod active_thread;
-mod agent_configuration;
-mod agent_diff;
-mod agent_model_selector;
-mod agent_panel;
-mod agent_profile;
-mod buffer_codegen;
-mod context;
-mod context_picker;
-mod context_server_configuration;
-mod context_server_tool;
-mod context_store;
-mod context_strip;
-mod debug;
-mod history_store;
-mod inline_assistant;
-mod inline_prompt_editor;
-mod message_editor;
-mod profile_selector;
-mod slash_command_settings;
-mod terminal_codegen;
-mod terminal_inline_assistant;
-mod thread;
-mod thread_history;
-mod thread_store;
-mod tool_compatibility;
-mod tool_use;
-mod ui;
+pub mod agent_profile;
+pub mod context;
+pub mod context_server_tool;
+pub mod context_store;
+pub mod history_store;
+pub mod thread;
+pub mod thread_store;
+pub mod tool_use;
-use std::sync::Arc;
-
-use agent_settings::{AgentProfileId, AgentSettings, LanguageModelSelection};
-use assistant_slash_command::SlashCommandRegistry;
-use client::Client;
-use feature_flags::FeatureFlagAppExt as _;
-use fs::Fs;
-use gpui::{App, Entity, actions, impl_actions};
-use language::LanguageRegistry;
-use language_model::{
- ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry,
-};
-use prompt_store::PromptBuilder;
-use schemars::JsonSchema;
-use serde::Deserialize;
-use settings::{Settings as _, SettingsStore};
-use thread::ThreadId;
-
-pub use crate::active_thread::ActiveThread;
-use crate::agent_configuration::{AddContextServerModal, ManageProfilesModal};
-pub use crate::agent_panel::{AgentPanel, ConcreteAssistantPanelDelegate};
-pub use crate::context::{ContextLoadResult, LoadedContext};
-pub use crate::inline_assistant::InlineAssistant;
-use crate::slash_command_settings::SlashCommandSettings;
-pub use crate::thread::{Message, MessageSegment, Thread, ThreadEvent};
-pub use crate::thread_store::{SerializedThread, TextThreadStore, ThreadStore};
-pub use agent_diff::{AgentDiffPane, AgentDiffToolbar};
+pub use context::{AgentContext, ContextId, ContextLoadResult};
pub use context_store::ContextStore;
-pub use ui::preview::{all_agent_previews, get_agent_preview};
+pub use thread::{
+ LastRestoreCheckpoint, Message, MessageCrease, MessageId, MessageSegment, Thread, ThreadError,
+ ThreadEvent, ThreadFeedback, ThreadId, ThreadSummary, TokenUsageRatio,
+};
+pub use thread_store::{SerializedThread, TextThreadStore, ThreadStore};
-actions!(
- agent,
- [
- NewTextThread,
- ToggleContextPicker,
- ToggleNavigationMenu,
- ToggleOptionsMenu,
- DeleteRecentlyOpenThread,
- ToggleProfileSelector,
- RemoveAllContext,
- ExpandMessageEditor,
- OpenHistory,
- AddContextServer,
- RemoveSelectedThread,
- Chat,
- ChatWithFollow,
- CycleNextInlineAssist,
- CyclePreviousInlineAssist,
- FocusUp,
- FocusDown,
- FocusLeft,
- FocusRight,
- RemoveFocusedContext,
- AcceptSuggestedContext,
- OpenActiveThreadAsMarkdown,
- OpenAgentDiff,
- Keep,
- Reject,
- RejectAll,
- KeepAll,
- Follow,
- ResetTrialUpsell,
- ResetTrialEndUpsell,
- ContinueThread,
- ContinueWithBurnMode,
- ToggleBurnMode,
- ]
-);
-
-#[derive(Default, Clone, PartialEq, Deserialize, JsonSchema)]
-pub struct NewThread {
- #[serde(default)]
- from_thread_id: Option,
-}
-
-#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema)]
-pub struct ManageProfiles {
- #[serde(default)]
- pub customize_tools: Option,
-}
-
-impl ManageProfiles {
- pub fn customize_tools(profile_id: AgentProfileId) -> Self {
- Self {
- customize_tools: Some(profile_id),
- }
- }
-}
-
-impl_actions!(agent, [NewThread, ManageProfiles]);
-
-#[derive(Clone)]
-pub(crate) enum ModelUsageContext {
- Thread(Entity),
- InlineAssistant,
-}
-
-impl ModelUsageContext {
- pub fn configured_model(&self, cx: &App) -> Option {
- 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> {
- self.configured_model(cx)
- .map(|configured_model| configured_model.model)
- }
-}
-
-/// Initializes the `agent` crate.
-pub fn init(
- fs: Arc,
- client: Arc,
- prompt_builder: Arc,
- language_registry: Arc,
- is_eval: bool,
- cx: &mut App,
-) {
- AgentSettings::register(cx);
- SlashCommandSettings::register(cx);
-
- assistant_context_editor::init(client.clone(), cx);
- rules_library::init(cx);
- if !is_eval {
- // Initializing the language model from the user settings messes with the eval, so we only initialize them when
- // we're not running inside of the eval.
- init_language_model_settings(cx);
- }
- assistant_slash_command::init(cx);
+pub fn init(cx: &mut gpui::App) {
thread_store::init(cx);
- agent_panel::init(cx);
- context_server_configuration::init(language_registry, cx);
-
- register_slash_commands(cx);
- inline_assistant::init(
- fs.clone(),
- prompt_builder.clone(),
- client.telemetry().clone(),
- cx,
- );
- terminal_inline_assistant::init(
- fs.clone(),
- prompt_builder.clone(),
- client.telemetry().clone(),
- cx,
- );
- indexed_docs::init(cx);
- cx.observe_new(AddContextServerModal::register).detach();
- cx.observe_new(ManageProfilesModal::register).detach();
-}
-
-fn init_language_model_settings(cx: &mut App) {
- update_active_language_model_from_settings(cx);
-
- cx.observe_global::(update_active_language_model_from_settings)
- .detach();
- cx.subscribe(
- &LanguageModelRegistry::global(cx),
- |_, event: &language_model::Event, cx| match event {
- language_model::Event::ProviderStateChanged
- | language_model::Event::AddedProvider(_)
- | language_model::Event::RemovedProvider(_) => {
- update_active_language_model_from_settings(cx);
- }
- _ => {}
- },
- )
- .detach();
-}
-
-fn update_active_language_model_from_settings(cx: &mut App) {
- let settings = AgentSettings::get_global(cx);
-
- fn to_selected_model(selection: &LanguageModelSelection) -> language_model::SelectedModel {
- language_model::SelectedModel {
- provider: LanguageModelProviderId::from(selection.provider.0.clone()),
- model: LanguageModelId::from(selection.model.clone()),
- }
- }
-
- let default = to_selected_model(&settings.default_model);
- let inline_assistant = settings
- .inline_assistant_model
- .as_ref()
- .map(to_selected_model);
- let commit_message = settings
- .commit_message_model
- .as_ref()
- .map(to_selected_model);
- let thread_summary = settings
- .thread_summary_model
- .as_ref()
- .map(to_selected_model);
- let inline_alternatives = settings
- .inline_alternatives
- .iter()
- .map(to_selected_model)
- .collect::>();
-
- LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
- registry.select_default_model(Some(&default), cx);
- registry.select_inline_assistant_model(inline_assistant.as_ref(), cx);
- registry.select_commit_message_model(commit_message.as_ref(), cx);
- registry.select_thread_summary_model(thread_summary.as_ref(), cx);
- registry.select_inline_alternative_models(inline_alternatives, cx);
- });
-}
-
-fn register_slash_commands(cx: &mut App) {
- let slash_command_registry = SlashCommandRegistry::global(cx);
-
- slash_command_registry.register_command(assistant_slash_commands::FileSlashCommand, true);
- slash_command_registry.register_command(assistant_slash_commands::DeltaSlashCommand, true);
- slash_command_registry.register_command(assistant_slash_commands::OutlineSlashCommand, true);
- slash_command_registry.register_command(assistant_slash_commands::TabSlashCommand, true);
- slash_command_registry
- .register_command(assistant_slash_commands::CargoWorkspaceSlashCommand, true);
- slash_command_registry.register_command(assistant_slash_commands::PromptSlashCommand, true);
- slash_command_registry.register_command(assistant_slash_commands::SelectionCommand, true);
- slash_command_registry.register_command(assistant_slash_commands::DefaultSlashCommand, false);
- slash_command_registry.register_command(assistant_slash_commands::NowSlashCommand, false);
- slash_command_registry
- .register_command(assistant_slash_commands::DiagnosticsSlashCommand, true);
- slash_command_registry.register_command(assistant_slash_commands::FetchSlashCommand, true);
-
- cx.observe_flag::({
- let slash_command_registry = slash_command_registry.clone();
- move |is_enabled, _cx| {
- if is_enabled {
- slash_command_registry.register_command(
- assistant_slash_commands::StreamingExampleSlashCommand,
- false,
- );
- }
- }
- })
- .detach();
-
- update_slash_commands_from_settings(cx);
- cx.observe_global::(update_slash_commands_from_settings)
- .detach();
-}
-
-fn update_slash_commands_from_settings(cx: &mut App) {
- let slash_command_registry = SlashCommandRegistry::global(cx);
- let settings = SlashCommandSettings::get_global(cx);
-
- if settings.docs.enabled {
- slash_command_registry.register_command(assistant_slash_commands::DocsSlashCommand, true);
- } else {
- slash_command_registry.unregister_command(assistant_slash_commands::DocsSlashCommand);
- }
-
- if settings.cargo_workspace.enabled {
- slash_command_registry
- .register_command(assistant_slash_commands::CargoWorkspaceSlashCommand, true);
- } else {
- slash_command_registry
- .unregister_command(assistant_slash_commands::CargoWorkspaceSlashCommand);
- }
}
diff --git a/crates/agent/src/agent_configuration/add_context_server_modal.rs b/crates/agent/src/agent_configuration/add_context_server_modal.rs
deleted file mode 100644
index 47ba57b035..0000000000
--- a/crates/agent/src/agent_configuration/add_context_server_modal.rs
+++ /dev/null
@@ -1,197 +0,0 @@
-use context_server::ContextServerCommand;
-use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity, prelude::*};
-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;
-use workspace::{ModalView, Workspace};
-
-use crate::AddContextServer;
-
-pub struct AddContextServerModal {
- workspace: WeakEntity,
- name_editor: Entity,
- command_editor: Entity,
-}
-
-impl AddContextServerModal {
- pub fn register(
- workspace: &mut Workspace,
- _window: Option<&mut Window>,
- _cx: &mut Context,
- ) {
- workspace.register_action(|workspace, _: &AddContextServer, window, cx| {
- let workspace_handle = cx.entity().downgrade();
- workspace.toggle_modal(window, cx, |window, cx| {
- Self::new(workspace_handle, window, cx)
- })
- });
- }
-
- pub fn new(
- workspace: WeakEntity,
- window: &mut Window,
- cx: &mut Context,
- ) -> Self {
- let name_editor =
- cx.new(|cx| SingleLineInput::new(window, cx, "my-custom-server").label("Name"));
- let command_editor = cx.new(|cx| {
- SingleLineInput::new(window, cx, "Command").label("Command to run the MCP server")
- });
-
- Self {
- name_editor,
- command_editor,
- workspace,
- }
- }
-
- fn confirm(&mut self, _: &menu::Confirm, cx: &mut Context) {
- let name = self
- .name_editor
- .read(cx)
- .editor()
- .read(cx)
- .text(cx)
- .trim()
- .to_string();
- let command = self
- .command_editor
- .read(cx)
- .editor()
- .read(cx)
- .text(cx)
- .trim()
- .to_string();
-
- if name.is_empty() || command.is_empty() {
- return;
- }
-
- let mut command_parts = command.split(' ').map(|part| part.trim().to_string());
- let Some(path) = command_parts.next() else {
- return;
- };
- let args = command_parts.collect::>();
-
- if let Some(workspace) = self.workspace.upgrade() {
- workspace.update(cx, |workspace, cx| {
- let fs = workspace.app_state().fs.clone();
- update_settings_file::(fs.clone(), cx, |settings, _| {
- settings.context_servers.insert(
- name.into(),
- ContextServerConfiguration {
- command: Some(ContextServerCommand {
- path,
- args,
- env: None,
- }),
- settings: Some(json!({})),
- },
- );
- });
- });
- }
-
- cx.emit(DismissEvent);
- }
-
- fn cancel(&mut self, _: &menu::Cancel, cx: &mut Context) {
- cx.emit(DismissEvent);
- }
-}
-
-impl ModalView for AddContextServerModal {}
-
-impl Focusable for AddContextServerModal {
- fn focus_handle(&self, cx: &App) -> FocusHandle {
- self.name_editor.focus_handle(cx).clone()
- }
-}
-
-impl EventEmitter for AddContextServerModal {}
-
-impl Render for AddContextServerModal {
- fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement {
- let is_name_empty = self.name_editor.read(cx).is_empty(cx);
- let is_command_empty = self.command_editor.read(cx).is_empty(cx);
-
- let focus_handle = self.focus_handle(cx);
-
- div()
- .elevation_3(cx)
- .w(rems(34.))
- .key_context("AddContextServerModal")
- .on_action(
- cx.listener(|this, _: &menu::Cancel, _window, cx| this.cancel(&menu::Cancel, cx)),
- )
- .on_action(
- cx.listener(|this, _: &menu::Confirm, _window, cx| {
- this.confirm(&menu::Confirm, cx)
- }),
- )
- .capture_any_mouse_down(cx.listener(|this, _, window, cx| {
- this.focus_handle(cx).focus(window);
- }))
- .on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent)))
- .child(
- Modal::new("add-context-server", None)
- .header(ModalHeader::new().headline("Add MCP Server"))
- .section(
- Section::new().child(
- v_flex()
- .gap_2()
- .child(self.name_editor.clone())
- .child(self.command_editor.clone()),
- ),
- )
- .footer(
- ModalFooter::new().end_slot(
- h_flex()
- .gap_2()
- .child(
- Button::new("cancel", "Cancel")
- .key_binding(
- KeyBinding::for_action_in(
- &menu::Cancel,
- &focus_handle,
- window,
- cx,
- )
- .map(|kb| kb.size(rems_from_px(12.))),
- )
- .on_click(cx.listener(|this, _event, _window, cx| {
- this.cancel(&menu::Cancel, cx)
- })),
- )
- .child(
- Button::new("add-server", "Add Server")
- .disabled(is_name_empty || is_command_empty)
- .key_binding(
- KeyBinding::for_action_in(
- &menu::Confirm,
- &focus_handle,
- window,
- cx,
- )
- .map(|kb| kb.size(rems_from_px(12.))),
- )
- .map(|button| {
- if is_name_empty {
- button.tooltip(Tooltip::text("Name is required"))
- } else if is_command_empty {
- button.tooltip(Tooltip::text("Command is required"))
- } else {
- button
- }
- })
- .on_click(cx.listener(|this, _event, _window, cx| {
- this.confirm(&menu::Confirm, cx)
- })),
- ),
- ),
- ),
- )
- }
-}
diff --git a/crates/agent/src/agent_configuration/configure_context_server_modal.rs b/crates/agent/src/agent_configuration/configure_context_server_modal.rs
deleted file mode 100644
index c916e7dc32..0000000000
--- a/crates/agent/src/agent_configuration/configure_context_server_modal.rs
+++ /dev/null
@@ -1,553 +0,0 @@
-use std::{
- sync::{Arc, Mutex},
- time::Duration,
-};
-
-use anyhow::Context as _;
-use context_server::ContextServerId;
-use editor::{Editor, EditorElement, EditorStyle};
-use gpui::{
- Animation, AnimationExt, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task,
- TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, percentage,
-};
-use language::{Language, LanguageRegistry};
-use markdown::{Markdown, MarkdownElement, MarkdownStyle};
-use notifications::status_toast::{StatusToast, ToastIcon};
-use project::{
- context_server_store::{ContextServerStatus, ContextServerStore},
- project_settings::{ContextServerConfiguration, ProjectSettings},
-};
-use settings::{Settings as _, update_settings_file};
-use theme::ThemeSettings;
-use ui::{KeyBinding, Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*};
-use util::ResultExt;
-use workspace::{ModalView, Workspace};
-
-pub(crate) struct ConfigureContextServerModal {
- workspace: WeakEntity,
- focus_handle: FocusHandle,
- context_servers_to_setup: Vec,
- context_server_store: Entity,
-}
-
-enum Configuration {
- NotAvailable,
- Required(ConfigurationRequiredState),
-}
-
-struct ConfigurationRequiredState {
- installation_instructions: Entity,
- settings_validator: Option,
- settings_editor: Entity,
- last_error: Option,
- waiting_for_context_server: bool,
-}
-
-struct ContextServerSetup {
- id: ContextServerId,
- repository_url: Option,
- configuration: Configuration,
-}
-
-impl ConfigureContextServerModal {
- pub fn new(
- configurations: impl Iterator- ,
- context_server_store: Entity,
- jsonc_language: Option>,
- language_registry: Arc,
- workspace: WeakEntity,
- window: &mut Window,
- cx: &mut Context,
- ) -> Self {
- let context_servers_to_setup = configurations
- .map(|config| match config {
- crate::context_server_configuration::Configuration::NotAvailable(
- context_server_id,
- repository_url,
- ) => ContextServerSetup {
- id: context_server_id,
- repository_url,
- configuration: Configuration::NotAvailable,
- },
- crate::context_server_configuration::Configuration::Required(
- context_server_id,
- repository_url,
- config,
- ) => {
- let jsonc_language = jsonc_language.clone();
- let settings_validator = jsonschema::validator_for(&config.settings_schema)
- .context("Failed to load JSON schema for context server settings")
- .log_err();
- let state = ConfigurationRequiredState {
- installation_instructions: cx.new(|cx| {
- Markdown::new(
- config.installation_instructions.clone().into(),
- Some(language_registry.clone()),
- None,
- cx,
- )
- }),
- settings_validator,
- settings_editor: cx.new(|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(
- language::language_settings::SoftWrap::None,
- cx,
- );
- if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
- buffer.update(cx, |buffer, cx| {
- buffer.set_language(jsonc_language, cx)
- })
- }
- editor
- }),
- waiting_for_context_server: false,
- last_error: None,
- };
- ContextServerSetup {
- id: context_server_id,
- repository_url,
- configuration: Configuration::Required(state),
- }
- }
- })
- .collect::>();
-
- Self {
- workspace,
- focus_handle: cx.focus_handle(),
- context_servers_to_setup,
- context_server_store,
- }
- }
-}
-
-impl ConfigureContextServerModal {
- pub fn confirm(&mut self, cx: &mut Context) {
- if self.context_servers_to_setup.is_empty() {
- self.dismiss(cx);
- return;
- }
-
- let Some(workspace) = self.workspace.upgrade() else {
- return;
- };
-
- let id = self.context_servers_to_setup[0].id.clone();
- let configuration = match &mut self.context_servers_to_setup[0].configuration {
- Configuration::NotAvailable => {
- self.context_servers_to_setup.remove(0);
- if self.context_servers_to_setup.is_empty() {
- self.dismiss(cx);
- }
- return;
- }
- Configuration::Required(state) => state,
- };
-
- configuration.last_error.take();
- if configuration.waiting_for_context_server {
- return;
- }
-
- let settings_value = match serde_json_lenient::from_str::(
- &configuration.settings_editor.read(cx).text(cx),
- ) {
- Ok(value) => value,
- Err(error) => {
- configuration.last_error = Some(error.to_string().into());
- cx.notify();
- return;
- }
- };
-
- if let Some(validator) = configuration.settings_validator.as_ref() {
- if let Err(error) = validator.validate(&settings_value) {
- configuration.last_error = Some(error.to_string().into());
- cx.notify();
- return;
- }
- }
- let id = id.clone();
-
- let settings_changed = ProjectSettings::get_global(cx)
- .context_servers
- .get(&id.0)
- .map_or(true, |config| {
- config.settings.as_ref() != Some(&settings_value)
- });
-
- let is_running = self.context_server_store.read(cx).status_for_server(&id)
- == Some(ContextServerStatus::Running);
-
- if !settings_changed && is_running {
- self.complete_setup(id, cx);
- return;
- }
-
- configuration.waiting_for_context_server = true;
-
- let task = wait_for_context_server(&self.context_server_store, id.clone(), cx);
- cx.spawn({
- let id = id.clone();
- async move |this, cx| {
- let result = task.await;
- this.update(cx, |this, cx| match result {
- Ok(_) => {
- this.complete_setup(id, cx);
- }
- Err(err) => {
- if let Some(setup) = this.context_servers_to_setup.get_mut(0) {
- match &mut setup.configuration {
- Configuration::NotAvailable => {}
- Configuration::Required(state) => {
- state.last_error = Some(err.into());
- state.waiting_for_context_server = false;
- }
- }
- } else {
- this.dismiss(cx);
- }
- cx.notify();
- }
- })
- }
- })
- .detach();
-
- // When we write the settings to the file, the context server will be restarted.
- update_settings_file::(workspace.read(cx).app_state().fs.clone(), cx, {
- let id = id.clone();
- |settings, _| {
- 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()
- },
- );
- }
- }
- });
- }
-
- fn complete_setup(&mut self, id: ContextServerId, cx: &mut Context) {
- self.context_servers_to_setup.remove(0);
- cx.notify();
-
- if !self.context_servers_to_setup.is_empty() {
- return;
- }
-
- self.workspace
- .update(cx, {
- |workspace, cx| {
- let status_toast = StatusToast::new(
- format!("{} configured successfully.", id),
- cx,
- |this, _cx| {
- this.icon(ToastIcon::new(IconName::Hammer).color(Color::Muted))
- .action("Dismiss", |_, _| {})
- },
- );
-
- workspace.toggle_status_toast(status_toast, cx);
- }
- })
- .log_err();
-
- self.dismiss(cx);
- }
-
- fn dismiss(&self, cx: &mut Context) {
- cx.emit(DismissEvent);
- }
-}
-
-fn wait_for_context_server(
- context_server_store: &Entity,
- context_server_id: ContextServerId,
- cx: &mut App,
-) -> Task>> {
- let (tx, rx) = futures::channel::oneshot::channel();
- let tx = Arc::new(Mutex::new(Some(tx)));
-
- let subscription = cx.subscribe(context_server_store, move |_, event, _cx| match event {
- project::context_server_store::Event::ServerStatusChanged { server_id, status } => {
- match status {
- ContextServerStatus::Running => {
- if server_id == &context_server_id {
- if let Some(tx) = tx.lock().unwrap().take() {
- let _ = tx.send(Ok(()));
- }
- }
- }
- ContextServerStatus::Stopped => {
- if server_id == &context_server_id {
- if let Some(tx) = tx.lock().unwrap().take() {
- let _ = tx.send(Err("Context server stopped running".into()));
- }
- }
- }
- ContextServerStatus::Error(error) => {
- if server_id == &context_server_id {
- if let Some(tx) = tx.lock().unwrap().take() {
- let _ = tx.send(Err(error.clone()));
- }
- }
- }
- _ => {}
- }
- }
- });
-
- cx.spawn(async move |_cx| {
- let result = rx.await.unwrap();
- drop(subscription);
- result
- })
-}
-
-impl Render for ConfigureContextServerModal {
- fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement {
- let Some(setup) = self.context_servers_to_setup.first() else {
- return div().into_any_element();
- };
-
- let focus_handle = self.focus_handle(cx);
-
- div()
- .elevation_3(cx)
- .w(rems(42.))
- .key_context("ConfigureContextServerModal")
- .track_focus(&focus_handle)
- .on_action(cx.listener(|this, _: &menu::Confirm, _window, cx| this.confirm(cx)))
- .on_action(cx.listener(|this, _: &menu::Cancel, _window, cx| this.dismiss(cx)))
- .capture_any_mouse_down(cx.listener(|this, _, window, cx| {
- this.focus_handle(cx).focus(window);
- }))
- .child(
- Modal::new("configure-context-server", None)
- .header(ModalHeader::new().headline(format!("Configure {}", setup.id)))
- .section(match &setup.configuration {
- Configuration::NotAvailable => Section::new().child(
- Label::new(
- "No configuration options available for this context server. Visit the Repository for any further instructions.",
- )
- .color(Color::Muted),
- ),
- Configuration::Required(configuration) => Section::new()
- .child(div().pb_2().text_sm().child(MarkdownElement::new(
- configuration.installation_instructions.clone(),
- default_markdown_style(window, cx),
- )))
- .child(
- div()
- .p_2()
- .rounded_md()
- .border_1()
- .border_color(cx.theme().colors().border_variant)
- .bg(cx.theme().colors().editor_background)
- .gap_1()
- .child({
- let settings = ThemeSettings::get_global(cx);
- let text_style = TextStyle {
- color: cx.theme().colors().text,
- font_family: settings.buffer_font.family.clone(),
- font_fallbacks: settings.buffer_font.fallbacks.clone(),
- font_size: settings.buffer_font_size(cx).into(),
- font_weight: settings.buffer_font.weight,
- line_height: relative(
- settings.buffer_line_height.value(),
- ),
- ..Default::default()
- };
- EditorElement::new(
- &configuration.settings_editor,
- EditorStyle {
- background: cx.theme().colors().editor_background,
- local_player: cx.theme().players().local(),
- text: text_style,
- syntax: cx.theme().syntax().clone(),
- ..Default::default()
- },
- )
- })
- .when_some(configuration.last_error.clone(), |this, error| {
- this.child(
- h_flex()
- .gap_2()
- .px_2()
- .py_1()
- .child(
- Icon::new(IconName::Warning)
- .size(IconSize::XSmall)
- .color(Color::Warning),
- )
- .child(
- div().w_full().child(
- Label::new(error)
- .size(LabelSize::Small)
- .color(Color::Muted),
- ),
- ),
- )
- }),
- )
- .when(configuration.waiting_for_context_server, |this| {
- this.child(
- h_flex()
- .gap_1p5()
- .child(
- Icon::new(IconName::ArrowCircle)
- .size(IconSize::XSmall)
- .color(Color::Info)
- .with_animation(
- "arrow-circle",
- Animation::new(Duration::from_secs(2)).repeat(),
- |icon, delta| {
- icon.transform(Transformation::rotate(
- percentage(delta),
- ))
- },
- )
- .into_any_element(),
- )
- .child(
- Label::new("Waiting for Context Server")
- .size(LabelSize::Small)
- .color(Color::Muted),
- ),
- )
- }),
- })
- .footer(
- ModalFooter::new()
- .when_some(setup.repository_url.clone(), |this, repository_url| {
- this.start_slot(
- h_flex().w_full().child(
- Button::new("open-repository", "Open Repository")
- .icon(IconName::ArrowUpRight)
- .icon_color(Color::Muted)
- .icon_size(IconSize::XSmall)
- .tooltip({
- let repository_url = repository_url.clone();
- move |window, cx| {
- Tooltip::with_meta(
- "Open Repository",
- None,
- repository_url.clone(),
- window,
- cx,
- )
- }
- })
- .on_click(move |_, _, cx| cx.open_url(&repository_url)),
- ),
- )
- })
- .end_slot(match &setup.configuration {
- Configuration::NotAvailable => Button::new("dismiss", "Dismiss")
- .key_binding(
- KeyBinding::for_action_in(
- &menu::Cancel,
- &focus_handle,
- window,
- cx,
- )
- .map(|kb| kb.size(rems_from_px(12.))),
- )
- .on_click(
- cx.listener(|this, _event, _window, cx| this.dismiss(cx)),
- )
- .into_any_element(),
- Configuration::Required(state) => h_flex()
- .gap_2()
- .child(
- Button::new("cancel", "Cancel")
- .key_binding(
- KeyBinding::for_action_in(
- &menu::Cancel,
- &focus_handle,
- window,
- cx,
- )
- .map(|kb| kb.size(rems_from_px(12.))),
- )
- .on_click(cx.listener(|this, _event, _window, cx| {
- this.dismiss(cx)
- })),
- )
- .child(
- Button::new("configure-server", "Configure MCP")
- .disabled(state.waiting_for_context_server)
- .key_binding(
- KeyBinding::for_action_in(
- &menu::Confirm,
- &focus_handle,
- window,
- cx,
- )
- .map(|kb| kb.size(rems_from_px(12.))),
- )
- .on_click(cx.listener(|this, _event, _window, cx| {
- this.confirm(cx)
- })),
- )
- .into_any_element(),
- }),
- ),
- ).into_any_element()
- }
-}
-
-pub(crate) fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
- let theme_settings = ThemeSettings::get_global(cx);
- let colors = cx.theme().colors();
- let mut text_style = window.text_style();
- text_style.refine(&TextStyleRefinement {
- font_family: Some(theme_settings.ui_font.family.clone()),
- font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
- font_features: Some(theme_settings.ui_font.features.clone()),
- font_size: Some(TextSize::XSmall.rems(cx).into()),
- color: Some(colors.text_muted),
- ..Default::default()
- });
-
- MarkdownStyle {
- base_text_style: text_style.clone(),
- selection_background_color: cx.theme().players().local().selection,
- link: TextStyleRefinement {
- background_color: Some(colors.editor_foreground.opacity(0.025)),
- underline: Some(UnderlineStyle {
- color: Some(colors.text_accent.opacity(0.5)),
- thickness: px(1.),
- ..Default::default()
- }),
- ..Default::default()
- },
- ..Default::default()
- }
-}
-
-impl ModalView for ConfigureContextServerModal {}
-impl EventEmitter for ConfigureContextServerModal {}
-impl Focusable for ConfigureContextServerModal {
- fn focus_handle(&self, cx: &App) -> FocusHandle {
- if let Some(current) = self.context_servers_to_setup.first() {
- match ¤t.configuration {
- Configuration::NotAvailable => self.focus_handle.clone(),
- Configuration::Required(configuration) => {
- configuration.settings_editor.read(cx).focus_handle(cx)
- }
- }
- } else {
- self.focus_handle.clone()
- }
- }
-}
diff --git a/crates/agent/src/agent_profile.rs b/crates/agent/src/agent_profile.rs
index 5cd69bd324..07030c744f 100644
--- a/crates/agent/src/agent_profile.rs
+++ b/crates/agent/src/agent_profile.rs
@@ -5,9 +5,8 @@ use assistant_tool::{Tool, ToolSource, ToolWorkingSet};
use collections::IndexMap;
use convert_case::{Case, Casing};
use fs::Fs;
-use gpui::{App, Entity};
+use gpui::{App, Entity, SharedString};
use settings::{Settings, update_settings_file};
-use ui::SharedString;
use util::ResultExt;
#[derive(Clone, Debug, Eq, PartialEq)]
@@ -86,6 +85,14 @@ impl AgentProfile {
.collect()
}
+ pub fn is_tool_enabled(&self, source: ToolSource, tool_name: String, cx: &App) -> bool {
+ let Some(settings) = AgentSettings::get_global(cx).profiles.get(&self.id) else {
+ return false;
+ };
+
+ return Self::is_enabled(settings, source, tool_name);
+ }
+
fn is_enabled(settings: &AgentProfileSettings, source: ToolSource, name: String) -> bool {
match source {
ToolSource::Native => *settings.tools.get(name.as_str()).unwrap_or(&false),
@@ -108,11 +115,11 @@ mod tests {
use agent_settings::ContextServerPreset;
use assistant_tool::ToolRegistry;
use collections::IndexMap;
+ use gpui::SharedString;
use gpui::{AppContext, TestAppContext};
use http_client::FakeHttpClient;
use project::Project;
use settings::{Settings, SettingsStore};
- use ui::SharedString;
use super::*;
@@ -302,7 +309,7 @@ mod tests {
unimplemented!()
}
- fn icon(&self) -> ui::IconName {
+ fn icon(&self) -> icons::IconName {
unimplemented!()
}
diff --git a/crates/agent/src/context.rs b/crates/agent/src/context.rs
index aaf613ea5f..ddd13de491 100644
--- a/crates/agent/src/context.rs
+++ b/crates/agent/src/context.rs
@@ -1,30 +1,25 @@
-use std::fmt::{self, Display, Formatter, Write as _};
-use std::hash::{Hash, Hasher};
-use std::path::PathBuf;
-use std::{ops::Range, path::Path, sync::Arc};
-
-use assistant_context_editor::AssistantContext;
+use crate::thread::Thread;
+use assistant_context::AssistantContext;
use assistant_tool::outline;
-use collections::{HashMap, HashSet};
-use editor::display_map::CreaseId;
-use editor::{Addon, Editor};
+use collections::HashSet;
use futures::future;
use futures::{FutureExt, future::Shared};
-use gpui::{App, AppContext as _, Entity, SharedString, Subscription, Task};
+use gpui::{App, AppContext as _, ElementId, Entity, SharedString, Task};
+use icons::IconName;
use language::{Buffer, ParseStatus};
use language_model::{LanguageModelImage, LanguageModelRequestMessage, MessageContent};
use project::{Project, ProjectEntryId, ProjectPath, Worktree};
use prompt_store::{PromptStore, UserPromptId};
use ref_cast::RefCast;
use rope::Point;
+use std::fmt::{self, Display, Formatter, Write as _};
+use std::hash::{Hash, Hasher};
+use std::path::PathBuf;
+use std::{ops::Range, path::Path, sync::Arc};
use text::{Anchor, OffsetRangeExt as _};
-use ui::{Context, ElementId, IconName};
use util::markdown::MarkdownCodeBlock;
use util::{ResultExt as _, post_inc};
-use crate::context_store::{ContextStore, ContextStoreEvent};
-use crate::thread::Thread;
-
pub const RULES_ICON: IconName = IconName::Context;
pub enum ContextKind {
@@ -1117,69 +1112,6 @@ impl Hash for AgentContextKey {
}
}
-#[derive(Default)]
-pub struct ContextCreasesAddon {
- creases: HashMap>,
- _subscription: Option,
-}
-
-impl Addon for ContextCreasesAddon {
- fn to_any(&self) -> &dyn std::any::Any {
- self
- }
-
- fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
- Some(self)
- }
-}
-
-impl ContextCreasesAddon {
- pub fn new() -> Self {
- Self {
- creases: HashMap::default(),
- _subscription: None,
- }
- }
-
- pub fn add_creases(
- &mut self,
- context_store: &Entity,
- key: AgentContextKey,
- creases: impl IntoIterator
- ,
- cx: &mut Context,
- ) {
- self.creases.entry(key).or_default().extend(creases);
- self._subscription = Some(cx.subscribe(
- &context_store,
- |editor, _, event, cx| match event {
- ContextStoreEvent::ContextRemoved(key) => {
- let Some(this) = editor.addon_mut::() else {
- return;
- };
- let (crease_ids, replacement_texts): (Vec<_>, Vec<_>) = this
- .creases
- .remove(key)
- .unwrap_or_default()
- .into_iter()
- .unzip();
- let ranges = editor
- .remove_creases(crease_ids, cx)
- .into_iter()
- .map(|(_, range)| range)
- .collect::>();
- editor.unfold_ranges(&ranges, false, false, cx);
- editor.edit(ranges.into_iter().zip(replacement_texts), cx);
- cx.notify();
- }
- },
- ))
- }
-
- pub fn into_inner(self) -> HashMap> {
- self.creases
- }
-}
-
#[cfg(test)]
mod tests {
use super::*;
diff --git a/crates/agent/src/context_server_configuration.rs b/crates/agent/src/context_server_configuration.rs
deleted file mode 100644
index 49effbdc0f..0000000000
--- a/crates/agent/src/context_server_configuration.rs
+++ /dev/null
@@ -1,144 +0,0 @@
-use std::sync::Arc;
-
-use anyhow::Context as _;
-use context_server::ContextServerId;
-use extension::{ContextServerConfiguration, ExtensionManifest};
-use gpui::Task;
-use language::LanguageRegistry;
-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, cx: &mut App) {
- cx.observe_new(move |_: &mut Workspace, window, cx| {
- let Some(window) = window else {
- return;
- };
-
- if let Some(extension_events) = extension::ExtensionEvents::try_global(cx).as_ref() {
- cx.subscribe_in(extension_events, window, {
- let language_registry = language_registry.clone();
- move |workspace, _, event, window, cx| match event {
- extension::Event::ExtensionInstalled(manifest) => {
- show_configure_mcp_modal(
- language_registry.clone(),
- manifest,
- workspace,
- window,
- cx,
- );
- }
- extension::Event::ConfigureExtensionRequested(manifest) => {
- if !manifest.context_servers.is_empty() {
- show_configure_mcp_modal(
- language_registry.clone(),
- manifest,
- workspace,
- window,
- cx,
- );
- }
- }
- _ => {}
- }
- })
- .detach();
- } else {
- log::info!(
- "No extension events global found. Skipping context server configuration wizard"
- );
- }
- })
- .detach();
-}
-
-pub enum Configuration {
- NotAvailable(ContextServerId, Option),
- Required(
- ContextServerId,
- Option,
- ContextServerConfiguration,
- ),
-}
-
-fn show_configure_mcp_modal(
- language_registry: Arc,
- manifest: &Arc,
- workspace: &mut Workspace,
- 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 = manifest.repository.as_ref().map(|s| s.clone().into());
-
- let registry = ContextServerDescriptorRegistry::default_global(cx).read(cx);
- let worktree_store = workspace.project().read(cx).worktree_store();
- let configuration_tasks = manifest
- .context_servers
- .keys()
- .cloned()
- .map({
- |key| {
- let Some(descriptor) = registry.context_server_descriptor(&key) else {
- return Task::ready(Configuration::NotAvailable(
- ContextServerId(key),
- repository.clone(),
- ));
- };
- cx.spawn({
- let repository_url = repository.clone();
- let worktree_store = worktree_store.clone();
- async move |_, cx| {
- let configuration = descriptor
- .configuration(worktree_store.clone(), &cx)
- .await
- .context("Failed to resolve context server configuration")
- .log_err()
- .flatten();
-
- match configuration {
- Some(config) => Configuration::Required(
- ContextServerId(key),
- repository_url,
- config,
- ),
- None => {
- Configuration::NotAvailable(ContextServerId(key), repository_url)
- }
- }
- }
- })
- }
- })
- .collect::>();
-
- let jsonc_language = language_registry.language_for_name("jsonc");
-
- cx.spawn_in(window, async move |this, cx| {
- let configurations = futures::future::join_all(configuration_tasks).await;
- let jsonc_language = jsonc_language.await.ok();
-
- this.update_in(cx, |this, window, cx| {
- let workspace = cx.entity().downgrade();
- this.toggle_modal(window, cx, |window, cx| {
- ConfigureContextServerModal::new(
- configurations.into_iter(),
- context_server_store,
- jsonc_language,
- language_registry,
- workspace,
- window,
- cx,
- )
- });
- })
- })
- .detach();
-}
diff --git a/crates/agent/src/context_server_tool.rs b/crates/agent/src/context_server_tool.rs
index 17571fca04..da7de1e312 100644
--- a/crates/agent/src/context_server_tool.rs
+++ b/crates/agent/src/context_server_tool.rs
@@ -4,9 +4,9 @@ use anyhow::{Result, anyhow, bail};
use assistant_tool::{ActionLog, Tool, ToolResult, ToolSource};
use context_server::{ContextServerId, types};
use gpui::{AnyWindowHandle, App, Entity, Task};
+use icons::IconName;
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use project::{Project, context_server_store::ContextServerStore};
-use ui::IconName;
pub struct ContextServerTool {
store: Entity,
diff --git a/crates/agent/src/context_store.rs b/crates/agent/src/context_store.rs
index f4697d9eb4..60ba5527dc 100644
--- a/crates/agent/src/context_store.rs
+++ b/crates/agent/src/context_store.rs
@@ -1,28 +1,28 @@
-use std::ops::Range;
-use std::path::{Path, PathBuf};
-use std::sync::Arc;
-
+use crate::{
+ context::{
+ AgentContextHandle, AgentContextKey, ContextId, ContextKind, DirectoryContextHandle,
+ FetchedUrlContext, FileContextHandle, ImageContext, RulesContextHandle,
+ SelectionContextHandle, SymbolContextHandle, TextThreadContextHandle, ThreadContextHandle,
+ },
+ thread::{MessageId, Thread, ThreadId},
+ thread_store::ThreadStore,
+};
use anyhow::{Context as _, Result, anyhow};
-use assistant_context_editor::AssistantContext;
+use assistant_context::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_model::LanguageModelImage;
-use project::image_store::is_image_file;
-use project::{Project, ProjectItem, ProjectPath, Symbol};
+use project::{Project, ProjectItem, ProjectPath, Symbol, image_store::is_image_file};
use prompt_store::UserPromptId;
use ref_cast::RefCast as _;
-use text::{Anchor, OffsetRangeExt};
-
-use crate::ThreadStore;
-use crate::context::{
- AgentContextHandle, AgentContextKey, ContextId, DirectoryContextHandle, FetchedUrlContext,
- FileContextHandle, ImageContext, RulesContextHandle, SelectionContextHandle,
- SymbolContextHandle, TextThreadContextHandle, ThreadContextHandle,
+use std::{
+ ops::Range,
+ path::{Path, PathBuf},
+ sync::Arc,
};
-use crate::context_strip::SuggestedContext;
-use crate::thread::{MessageId, Thread, ThreadId};
+use text::{Anchor, OffsetRangeExt};
pub struct ContextStore {
project: WeakEntity,
@@ -561,6 +561,49 @@ impl ContextStore {
}
}
+#[derive(Clone)]
+pub enum SuggestedContext {
+ File {
+ name: SharedString,
+ icon_path: Option,
+ buffer: WeakEntity,
+ },
+ Thread {
+ name: SharedString,
+ thread: WeakEntity,
+ },
+ TextThread {
+ name: SharedString,
+ context: WeakEntity,
+ },
+}
+
+impl SuggestedContext {
+ pub fn name(&self) -> &SharedString {
+ match self {
+ Self::File { name, .. } => name,
+ Self::Thread { name, .. } => name,
+ Self::TextThread { name, .. } => name,
+ }
+ }
+
+ pub fn icon_path(&self) -> Option {
+ match self {
+ Self::File { icon_path, .. } => icon_path.clone(),
+ Self::Thread { .. } => None,
+ Self::TextThread { .. } => None,
+ }
+ }
+
+ pub fn kind(&self) -> ContextKind {
+ match self {
+ Self::File { .. } => ContextKind::File,
+ Self::Thread { .. } => ContextKind::Thread,
+ Self::TextThread { .. } => ContextKind::TextThread,
+ }
+ }
+}
+
pub enum FileInclusion {
Direct,
InDirectory { full_path: PathBuf },
diff --git a/crates/agent/src/history_store.rs b/crates/agent/src/history_store.rs
index 61fc430ddb..89f75a72bd 100644
--- a/crates/agent/src/history_store.rs
+++ b/crates/agent/src/history_store.rs
@@ -1,21 +1,17 @@
-use std::{collections::VecDeque, path::Path, sync::Arc};
-
+use crate::{
+ ThreadId,
+ thread_store::{SerializedThreadMetadata, ThreadStore},
+};
use anyhow::{Context as _, Result};
-use assistant_context_editor::SavedContextMetadata;
+use assistant_context::SavedContextMetadata;
use chrono::{DateTime, Utc};
-use gpui::{AsyncApp, Entity, SharedString, Task, prelude::*};
+use gpui::{App, AsyncApp, Entity, SharedString, Task, prelude::*};
use itertools::Itertools;
use paths::contexts_dir;
use serde::{Deserialize, Serialize};
-use std::time::Duration;
-use ui::App;
+use std::{collections::VecDeque, path::Path, sync::Arc, time::Duration};
use util::ResultExt as _;
-use crate::{
- thread::ThreadId,
- thread_store::{SerializedThreadMetadata, ThreadStore},
-};
-
const MAX_RECENTLY_OPENED_ENTRIES: usize = 6;
const NAVIGATION_HISTORY_PATH: &str = "agent-navigation-history.json";
const SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE: Duration = Duration::from_millis(50);
@@ -66,7 +62,7 @@ enum SerializedRecentOpen {
pub struct HistoryStore {
thread_store: Entity,
- context_store: Entity,
+ context_store: Entity,
recently_opened_entries: VecDeque,
_subscriptions: Vec,
_save_recently_opened_entries_task: Task<()>,
@@ -75,7 +71,7 @@ pub struct HistoryStore {
impl HistoryStore {
pub fn new(
thread_store: Entity,
- context_store: Entity,
+ context_store: Entity,
initial_recent_entries: impl IntoIterator
- ,
cx: &mut Context,
) -> Self {
diff --git a/crates/agent/src/prompts/stale_files_prompt_header.txt b/crates/agent/src/prompts/stale_files_prompt_header.txt
deleted file mode 100644
index 6686aba1e2..0000000000
--- a/crates/agent/src/prompts/stale_files_prompt_header.txt
+++ /dev/null
@@ -1 +0,0 @@
-These files changed since last read:
diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs
index eac99eefbe..33b9209f0c 100644
--- a/crates/agent/src/thread.rs
+++ b/crates/agent/src/thread.rs
@@ -1,53 +1,49 @@
-use std::fmt::Write as _;
-use std::io::Write;
-use std::ops::Range;
-use std::sync::Arc;
-use std::time::Instant;
-
+use crate::{
+ agent_profile::AgentProfile,
+ context::{AgentContext, AgentContextHandle, ContextLoadResult, LoadedContext},
+ thread_store::{
+ SerializedCrease, SerializedLanguageModel, SerializedMessage, SerializedMessageSegment,
+ SerializedThread, SerializedToolResult, SerializedToolUse, SharedProjectContext,
+ ThreadStore,
+ },
+ tool_use::{PendingToolUse, ToolUse, ToolUseMetadata, ToolUseState},
+};
use agent_settings::{AgentProfileId, AgentSettings, CompletionMode};
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet};
use chrono::{DateTime, Utc};
-use collections::HashMap;
-use editor::display_map::CreaseMetadata;
+use client::{ModelRequestUsage, RequestUsage};
+use collections::{HashMap, HashSet};
use feature_flags::{self, FeatureFlagAppExt};
-use futures::future::Shared;
-use futures::{FutureExt, StreamExt as _};
+use futures::{FutureExt, StreamExt as _, future::Shared};
use git::repository::DiffType;
use gpui::{
AnyWindowHandle, App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task,
- WeakEntity,
+ WeakEntity, Window,
};
use language_model::{
ConfiguredModel, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
LanguageModelId, LanguageModelKnownError, LanguageModelRegistry, LanguageModelRequest,
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
LanguageModelToolResultContent, LanguageModelToolUseId, MessageContent,
- ModelRequestLimitReachedError, PaymentRequiredError, RequestUsage, Role, SelectedModel,
- StopReason, TokenUsage,
+ ModelRequestLimitReachedError, PaymentRequiredError, Role, SelectedModel, StopReason,
+ TokenUsage,
};
use postage::stream::Stream as _;
-use project::Project;
-use project::git_store::{GitStore, GitStoreCheckpoint, RepositoryState};
+use project::{
+ Project,
+ git_store::{GitStore, GitStoreCheckpoint, RepositoryState},
+};
use prompt_store::{ModelContext, PromptBuilder};
use proto::Plan;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
+use std::{io::Write, ops::Range, sync::Arc, time::Instant};
use thiserror::Error;
-use ui::Window;
use util::{ResultExt as _, post_inc};
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,
- SerializedThread, SerializedToolResult, SerializedToolUse, SharedProjectContext,
-};
-use crate::tool_use::{PendingToolUse, ToolUse, ToolUseMetadata, ToolUseState};
+use zed_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit};
#[derive(
Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize, JsonSchema,
@@ -97,13 +93,18 @@ impl MessageId {
fn post_inc(&mut self) -> Self {
Self(post_inc(&mut self.0))
}
+
+ pub fn as_usize(&self) -> usize {
+ self.0
+ }
}
/// Stored information that can be used to resurrect a context crease when creating an editor for a past message.
#[derive(Clone, Debug)]
pub struct MessageCrease {
pub range: Range,
- pub metadata: CreaseMetadata,
+ pub icon_path: SharedString,
+ pub label: SharedString,
/// None for a deserialized message, Some otherwise.
pub context: Option,
}
@@ -144,6 +145,10 @@ impl Message {
}
}
+ pub fn push_redacted_thinking(&mut self, data: String) {
+ self.segments.push(MessageSegment::RedactedThinking(data));
+ }
+
pub fn push_text(&mut self, text: &str) {
if let Some(MessageSegment::Text(segment)) = self.segments.last_mut() {
segment.push_str(text);
@@ -182,7 +187,7 @@ pub enum MessageSegment {
text: String,
signature: Option,
},
- RedactedThinking(Vec),
+ RedactedThinking(String),
}
impl MessageSegment {
@@ -193,6 +198,13 @@ impl MessageSegment {
Self::RedactedThinking(_) => false,
}
}
+
+ pub fn text(&self) -> Option<&str> {
+ match self {
+ MessageSegment::Text(text) => Some(text),
+ _ => None,
+ }
+ }
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
@@ -272,8 +284,8 @@ impl DetailedSummaryState {
#[derive(Default, Debug)]
pub struct TotalTokenUsage {
- pub total: usize,
- pub max: usize,
+ pub total: u64,
+ pub max: u64,
}
impl TotalTokenUsage {
@@ -299,7 +311,7 @@ impl TotalTokenUsage {
}
}
- pub fn add(&self, tokens: usize) -> TotalTokenUsage {
+ pub fn add(&self, tokens: u64) -> TotalTokenUsage {
TotalTokenUsage {
total: self.total + tokens,
max: self.max,
@@ -350,7 +362,6 @@ pub struct Thread {
request_token_usage: Vec,
cumulative_token_usage: TokenUsage,
exceeded_window_error: Option,
- last_usage: Option,
tool_use_limit_reached: bool,
feedback: Option,
message_feedback: HashMap,
@@ -396,7 +407,7 @@ pub struct ExceededWindowError {
/// Model used when last message exceeded context window
model_id: LanguageModelId,
/// Token count including last message
- token_count: usize,
+ token_count: u64,
}
impl Thread {
@@ -443,7 +454,6 @@ impl Thread {
request_token_usage: Vec::new(),
cumulative_token_usage: TokenUsage::default(),
exceeded_window_error: None,
- last_usage: None,
tool_use_limit_reached: false,
feedback: None,
message_feedback: HashMap::default(),
@@ -541,10 +551,8 @@ impl Thread {
.into_iter()
.map(|crease| MessageCrease {
range: crease.start..crease.end,
- metadata: CreaseMetadata {
- icon_path: crease.icon_path,
- label: crease.label,
- },
+ icon_path: crease.icon_path,
+ label: crease.label,
context: None,
})
.collect(),
@@ -568,7 +576,6 @@ impl Thread {
request_token_usage: serialized.request_token_usage,
cumulative_token_usage: serialized.cumulative_token_usage,
exceeded_window_error: None,
- last_usage: None,
tool_use_limit_reached: serialized.tool_use_limit_reached,
feedback: None,
message_feedback: HashMap::default(),
@@ -875,10 +882,6 @@ impl Thread {
.unwrap_or(false)
}
- pub fn last_usage(&self) -> Option {
- self.last_usage
- }
-
pub fn tool_use_limit_reached(&self) -> bool {
self.tool_use_limit_reached
}
@@ -938,14 +941,13 @@ impl Thread {
model: Arc,
) -> Vec {
if model.supports_tools() {
- self.profile
- .enabled_tools(cx)
+ resolve_tool_name_conflicts(self.profile.enabled_tools(cx).as_slice())
.into_iter()
- .filter_map(|tool| {
+ .filter_map(|(name, tool)| {
// Skip tools that cannot be supported
let input_schema = tool.input_schema(model.tool_input_format()).ok()?;
Some(LanguageModelRequestTool {
- name: tool.name(),
+ name,
description: tool.description(),
input_schema,
})
@@ -1177,8 +1179,8 @@ impl Thread {
.map(|crease| SerializedCrease {
start: crease.range.start,
end: crease.range.end,
- icon_path: crease.metadata.icon_path.clone(),
- label: crease.metadata.label.clone(),
+ icon_path: crease.icon_path.clone(),
+ label: crease.label.clone(),
})
.collect(),
is_hidden: message.is_hidden,
@@ -1389,8 +1391,6 @@ impl Thread {
request.messages[message_ix_to_cache].cache = true;
}
- self.attached_tracked_files_state(&mut request.messages, cx);
-
request.tools = available_tools;
request.mode = if model.supports_max_mode() {
Some(self.completion_mode.into())
@@ -1453,46 +1453,6 @@ impl Thread {
request
}
- fn attached_tracked_files_state(
- &self,
- messages: &mut Vec,
- cx: &App,
- ) {
- 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) {
- 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();
- }
-
- let mut content = Vec::with_capacity(2);
-
- if !stale_message.is_empty() {
- content.push(stale_message.into());
- }
-
- if !content.is_empty() {
- let context_message = LanguageModelRequestMessage {
- role: Role::User,
- content,
- cache: false,
- };
-
- messages.push(context_message);
- }
- }
-
pub fn stream_completion(
&mut self,
request: LanguageModelRequest,
@@ -1544,27 +1504,76 @@ impl Thread {
thread.update(cx, |thread, cx| {
let event = match event {
Ok(event) => event,
- Err(LanguageModelCompletionError::BadInputJson {
- id,
- tool_name,
- raw_input: invalid_input_json,
- json_parse_error,
- }) => {
- thread.receive_invalid_tool_json(
- id,
- tool_name,
- invalid_input_json,
- json_parse_error,
- window,
- cx,
- );
- return Ok(());
- }
- Err(LanguageModelCompletionError::Other(error)) => {
- return Err(error);
- }
- Err(err @ LanguageModelCompletionError::RateLimit(..)) => {
- return Err(err.into());
+ Err(error) => {
+ match error {
+ LanguageModelCompletionError::RateLimitExceeded { retry_after } => {
+ anyhow::bail!(LanguageModelKnownError::RateLimitExceeded { retry_after });
+ }
+ LanguageModelCompletionError::Overloaded => {
+ anyhow::bail!(LanguageModelKnownError::Overloaded);
+ }
+ LanguageModelCompletionError::ApiInternalServerError =>{
+ anyhow::bail!(LanguageModelKnownError::ApiInternalServerError);
+ }
+ LanguageModelCompletionError::PromptTooLarge { tokens } => {
+ let tokens = tokens.unwrap_or_else(|| {
+ // We didn't get an exact token count from the API, so fall back on our estimate.
+ thread.total_token_usage()
+ .map(|usage| usage.total)
+ .unwrap_or(0)
+ // We know the context window was exceeded in practice, so if our estimate was
+ // lower than max tokens, the estimate was wrong; return that we exceeded by 1.
+ .max(model.max_token_count().saturating_add(1))
+ });
+
+ anyhow::bail!(LanguageModelKnownError::ContextWindowLimitExceeded { tokens })
+ }
+ LanguageModelCompletionError::ApiReadResponseError(io_error) => {
+ anyhow::bail!(LanguageModelKnownError::ReadResponseError(io_error));
+ }
+ LanguageModelCompletionError::UnknownResponseFormat(error) => {
+ anyhow::bail!(LanguageModelKnownError::UnknownResponseFormat(error));
+ }
+ LanguageModelCompletionError::HttpResponseError { status, ref body } => {
+ if let Some(known_error) = LanguageModelKnownError::from_http_response(status, body) {
+ anyhow::bail!(known_error);
+ } else {
+ return Err(error.into());
+ }
+ }
+ LanguageModelCompletionError::DeserializeResponse(error) => {
+ anyhow::bail!(LanguageModelKnownError::DeserializeResponse(error));
+ }
+ LanguageModelCompletionError::BadInputJson {
+ id,
+ tool_name,
+ raw_input: invalid_input_json,
+ json_parse_error,
+ } => {
+ thread.receive_invalid_tool_json(
+ id,
+ tool_name,
+ invalid_input_json,
+ json_parse_error,
+ window,
+ cx,
+ );
+ return Ok(());
+ }
+ // These are all errors we can't automatically attempt to recover from (e.g. by retrying)
+ err @ LanguageModelCompletionError::BadRequestFormat |
+ err @ LanguageModelCompletionError::AuthenticationError |
+ err @ LanguageModelCompletionError::PermissionError |
+ err @ LanguageModelCompletionError::ApiEndpointNotFound |
+ err @ LanguageModelCompletionError::SerializeRequest(_) |
+ err @ LanguageModelCompletionError::BuildRequestBody(_) |
+ err @ LanguageModelCompletionError::HttpSend(_) => {
+ anyhow::bail!(err);
+ }
+ LanguageModelCompletionError::Other(error) => {
+ return Err(error);
+ }
+ }
}
};
@@ -1645,6 +1654,25 @@ impl Thread {
};
}
}
+ LanguageModelCompletionEvent::RedactedThinking {
+ data
+ } => {
+ thread.received_chunk();
+
+ if let Some(last_message) = thread.messages.last_mut() {
+ if last_message.role == Role::Assistant
+ && !thread.tool_use.has_tool_results(last_message.id)
+ {
+ last_message.push_redacted_thinking(data);
+ } else {
+ request_assistant_message_id =
+ Some(thread.insert_assistant_message(
+ vec![MessageSegment::RedactedThinking(data)],
+ cx,
+ ));
+ };
+ }
+ }
LanguageModelCompletionEvent::ToolUse(tool_use) => {
let last_assistant_message_id = request_assistant_message_id
.unwrap_or_else(|| {
@@ -1700,9 +1728,7 @@ impl Thread {
CompletionRequestStatus::UsageUpdated {
amount, limit
} => {
- let usage = RequestUsage { limit, amount: amount as i32 };
-
- thread.last_usage = Some(usage);
+ thread.update_model_request_usage(amount as u32, limit, cx);
}
CompletionRequestStatus::ToolUseLimitReached => {
thread.tool_use_limit_reached = true;
@@ -1751,7 +1777,7 @@ impl Thread {
match result.as_ref() {
Ok(stop_reason) => match stop_reason {
StopReason::ToolUse => {
- let tool_uses = thread.use_pending_tools(window, cx, model.clone());
+ let tool_uses = thread.use_pending_tools(window, model.clone(), cx);
cx.emit(ThreadEvent::UsePendingTools { tool_uses });
}
StopReason::EndTurn | StopReason::MaxTokens => {
@@ -1802,6 +1828,18 @@ impl Thread {
project.set_agent_location(None, cx);
});
+ fn emit_generic_error(error: &anyhow::Error, cx: &mut Context) {
+ let error_message = error
+ .chain()
+ .map(|err| err.to_string())
+ .collect::>()
+ .join("\n");
+ cx.emit(ThreadEvent::ShowError(ThreadError::Message {
+ header: "Error interacting with language model".into(),
+ message: SharedString::from(error_message.clone()),
+ }));
+ }
+
if error.is::() {
cx.emit(ThreadEvent::ShowError(ThreadError::PaymentRequired));
} else if let Some(error) =
@@ -1814,26 +1852,34 @@ impl Thread {
error.downcast_ref::()
{
match known_error {
- LanguageModelKnownError::ContextWindowLimitExceeded {
- tokens,
- } => {
+ LanguageModelKnownError::ContextWindowLimitExceeded { tokens } => {
thread.exceeded_window_error = Some(ExceededWindowError {
model_id: model.id(),
token_count: *tokens,
});
cx.notify();
}
+ LanguageModelKnownError::RateLimitExceeded { .. } => {
+ // In the future we will report the error to the user, wait retry_after, and then retry.
+ emit_generic_error(error, cx);
+ }
+ LanguageModelKnownError::Overloaded => {
+ // In the future we will wait and then retry, up to N times.
+ emit_generic_error(error, cx);
+ }
+ LanguageModelKnownError::ApiInternalServerError => {
+ // In the future we will retry the request, but only once.
+ emit_generic_error(error, cx);
+ }
+ LanguageModelKnownError::ReadResponseError(_) |
+ LanguageModelKnownError::DeserializeResponse(_) |
+ LanguageModelKnownError::UnknownResponseFormat(_) => {
+ // In the future we will attempt to re-roll response, but only once
+ emit_generic_error(error, cx);
+ }
}
} else {
- let error_message = error
- .chain()
- .map(|err| err.to_string())
- .collect::>()
- .join("\n");
- cx.emit(ThreadEvent::ShowError(ThreadError::Message {
- header: "Error interacting with language model".into(),
- message: SharedString::from(error_message.clone()),
- }));
+ emit_generic_error(error, cx);
}
thread.cancel_last_completion(window, cx);
@@ -1913,11 +1959,8 @@ impl Thread {
LanguageModelCompletionEvent::StatusUpdate(
CompletionRequestStatus::UsageUpdated { amount, limit },
) => {
- this.update(cx, |thread, _cx| {
- thread.last_usage = Some(RequestUsage {
- limit,
- amount: amount as i32,
- });
+ this.update(cx, |thread, cx| {
+ thread.update_model_request_usage(amount as u32, limit, cx);
})?;
continue;
}
@@ -2084,8 +2127,8 @@ impl Thread {
pub fn use_pending_tools(
&mut self,
window: Option,
- cx: &mut Context,
model: Arc,
+ cx: &mut Context,
) -> Vec {
self.auto_capture_telemetry(cx);
let request =
@@ -2099,43 +2142,53 @@ impl Thread {
.collect::>();
for tool_use in pending_tool_uses.iter() {
- if let Some(tool) = self.tools.read(cx).tool(&tool_use.name, cx) {
- if tool.needs_confirmation(&tool_use.input, cx)
- && !AgentSettings::get_global(cx).always_allow_tool_actions
- {
- self.tool_use.confirm_tool_use(
- tool_use.id.clone(),
- tool_use.ui_text.clone(),
- tool_use.input.clone(),
- request.clone(),
- tool,
- );
- cx.emit(ThreadEvent::ToolConfirmationNeeded);
- } else {
- self.run_tool(
- tool_use.id.clone(),
- tool_use.ui_text.clone(),
- tool_use.input.clone(),
- request.clone(),
- tool,
- model.clone(),
- window,
- cx,
- );
- }
- } else {
- self.handle_hallucinated_tool_use(
- tool_use.id.clone(),
- tool_use.name.clone(),
- window,
- cx,
- );
- }
+ self.use_pending_tool(tool_use.clone(), request.clone(), model.clone(), window, cx);
}
pending_tool_uses
}
+ fn use_pending_tool(
+ &mut self,
+ tool_use: PendingToolUse,
+ request: Arc,
+ model: Arc,
+ window: Option,
+ cx: &mut Context,
+ ) {
+ let Some(tool) = self.tools.read(cx).tool(&tool_use.name, cx) else {
+ return self.handle_hallucinated_tool_use(tool_use.id, tool_use.name, window, cx);
+ };
+
+ if !self.profile.is_tool_enabled(tool.source(), tool.name(), cx) {
+ return self.handle_hallucinated_tool_use(tool_use.id, tool_use.name, window, cx);
+ }
+
+ if tool.needs_confirmation(&tool_use.input, cx)
+ && !AgentSettings::get_global(cx).always_allow_tool_actions
+ {
+ self.tool_use.confirm_tool_use(
+ tool_use.id,
+ tool_use.ui_text,
+ tool_use.input,
+ request,
+ tool,
+ );
+ cx.emit(ThreadEvent::ToolConfirmationNeeded);
+ } else {
+ self.run_tool(
+ tool_use.id,
+ tool_use.ui_text,
+ tool_use.input,
+ request,
+ tool,
+ model,
+ window,
+ cx,
+ );
+ }
+ }
+
pub fn handle_hallucinated_tool_use(
&mut self,
tool_use_id: LanguageModelToolUseId,
@@ -2755,7 +2808,7 @@ impl Thread {
.unwrap_or_default();
TotalTokenUsage {
- total: token_usage.total_tokens() as usize,
+ total: token_usage.total_tokens(),
max,
}
}
@@ -2777,7 +2830,7 @@ impl Thread {
let total = self
.token_usage_at_last_message()
.unwrap_or_default()
- .total_tokens() as usize;
+ .total_tokens();
Some(TotalTokenUsage { total, max })
}
@@ -2799,6 +2852,20 @@ impl Thread {
}
}
+ fn update_model_request_usage(&self, amount: u32, limit: UsageLimit, cx: &mut Context) {
+ self.project.update(cx, |project, cx| {
+ project.user_store().update(cx, |user_store, cx| {
+ user_store.update_model_request_usage(
+ ModelRequestUsage(RequestUsage {
+ amount: amount as i32,
+ limit,
+ }),
+ cx,
+ )
+ })
+ });
+ }
+
pub fn deny_tool_use(
&mut self,
tool_use_id: LanguageModelToolUseId,
@@ -2886,14 +2953,95 @@ struct PendingCompletion {
_task: Task<()>,
}
+/// Resolves tool name conflicts by ensuring all tool names are unique.
+///
+/// When multiple tools have the same name, this function applies the following rules:
+/// 1. Native tools always keep their original name
+/// 2. Context server tools get prefixed with their server ID and an underscore
+/// 3. All tool names are truncated to MAX_TOOL_NAME_LENGTH (64 characters)
+/// 4. If conflicts still exist after prefixing, the conflicting tools are filtered out
+///
+/// Note: This function assumes that built-in tools occur before MCP tools in the tools list.
+fn resolve_tool_name_conflicts(tools: &[Arc]) -> Vec<(String, Arc)> {
+ fn resolve_tool_name(tool: &Arc) -> String {
+ let mut tool_name = tool.name();
+ tool_name.truncate(MAX_TOOL_NAME_LENGTH);
+ tool_name
+ }
+
+ const MAX_TOOL_NAME_LENGTH: usize = 64;
+
+ let mut duplicated_tool_names = HashSet::default();
+ let mut seen_tool_names = HashSet::default();
+ for tool in tools {
+ let tool_name = resolve_tool_name(tool);
+ if seen_tool_names.contains(&tool_name) {
+ debug_assert!(
+ tool.source() != assistant_tool::ToolSource::Native,
+ "There are two built-in tools with the same name: {}",
+ tool_name
+ );
+ duplicated_tool_names.insert(tool_name);
+ } else {
+ seen_tool_names.insert(tool_name);
+ }
+ }
+
+ if duplicated_tool_names.is_empty() {
+ return tools
+ .into_iter()
+ .map(|tool| (resolve_tool_name(tool), tool.clone()))
+ .collect();
+ }
+
+ tools
+ .into_iter()
+ .filter_map(|tool| {
+ let mut tool_name = resolve_tool_name(tool);
+ if !duplicated_tool_names.contains(&tool_name) {
+ return Some((tool_name, tool.clone()));
+ }
+ match tool.source() {
+ assistant_tool::ToolSource::Native => {
+ // Built-in tools always keep their original name
+ Some((tool_name, tool.clone()))
+ }
+ assistant_tool::ToolSource::ContextServer { id } => {
+ // Context server tools are prefixed with the context server ID, and truncated if necessary
+ tool_name.insert(0, '_');
+ if tool_name.len() + id.len() > MAX_TOOL_NAME_LENGTH {
+ let len = MAX_TOOL_NAME_LENGTH - tool_name.len();
+ let mut id = id.to_string();
+ id.truncate(len);
+ tool_name.insert_str(0, &id);
+ } else {
+ tool_name.insert_str(0, &id);
+ }
+
+ tool_name.truncate(MAX_TOOL_NAME_LENGTH);
+
+ if seen_tool_names.contains(&tool_name) {
+ log::error!("Cannot resolve tool name conflict for tool {}", tool.name());
+ None
+ } else {
+ Some((tool_name, tool.clone()))
+ }
+ }
+ }
+ })
+ .collect()
+}
+
#[cfg(test)]
mod tests {
use super::*;
- use crate::{ThreadStore, context::load_context, context_store::ContextStore, thread_store};
+ use crate::{
+ context::load_context, context_store::ContextStore, thread_store, thread_store::ThreadStore,
+ };
use agent_settings::{AgentProfileId, AgentSettings, LanguageModelParameters};
use assistant_tool::ToolRegistry;
- use editor::EditorSettings;
use gpui::TestAppContext;
+ use icons::IconName;
use language_model::fake_provider::{FakeLanguageModel, FakeLanguageModelProvider};
use project::{FakeFs, Project};
use prompt_store::PromptBuilder;
@@ -3215,94 +3363,6 @@ fn main() {{
);
}
- #[gpui::test]
- async fn test_stale_buffer_notification(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;
-
- // Open buffer and add it to context
- let buffer = add_file_to_context(&project, &context_store, "test/code.rs", cx)
- .await
- .unwrap();
-
- let context =
- context_store.read_with(cx, |store, _| store.context().next().cloned().unwrap());
- let loaded_context = cx
- .update(|cx| load_context(vec![context], &project, &None, cx))
- .await;
-
- // Insert user message with the buffer as context
- thread.update(cx, |thread, cx| {
- thread.insert_user_message("Explain this code", loaded_context, None, Vec::new(), cx)
- });
-
- // Create a request and check that it doesn't have a stale buffer warning yet
- let initial_request = thread.update(cx, |thread, cx| {
- thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
- });
-
- // Make sure we don't have a stale file warning yet
- let has_stale_warning = initial_request.messages.iter().any(|msg| {
- msg.string_contents()
- .contains("These files changed since last read:")
- });
- assert!(
- !has_stale_warning,
- "Should not have stale buffer warning before buffer is modified"
- );
-
- // Modify the buffer
- buffer.update(cx, |buffer, cx| {
- // Find a position at the end of line 1
- buffer.edit(
- [(1..1, "\n println!(\"Added a new line\");\n")],
- None,
- cx,
- );
- });
-
- // Insert another user message without context
- thread.update(cx, |thread, cx| {
- thread.insert_user_message(
- "What does the code do now?",
- ContextLoadResult::default(),
- None,
- Vec::new(),
- cx,
- )
- });
-
- // Create a new request and check for the stale buffer warning
- let new_request = thread.update(cx, |thread, cx| {
- thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
- });
-
- // We should have a stale file warning as the last message
- let last_message = new_request
- .messages
- .last()
- .expect("Request should have messages");
-
- // The last message should be the stale buffer notification
- assert_eq!(last_message.role, Role::User);
-
- // Check the exact content of the message
- 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"
- );
- }
-
#[gpui::test]
async fn test_storing_profile_setting_per_thread(cx: &mut TestAppContext) {
init_test_settings(cx);
@@ -3620,6 +3680,148 @@ fn main() {{
});
}
+ #[gpui::test]
+ fn test_resolve_tool_name_conflicts() {
+ use assistant_tool::{Tool, ToolSource};
+
+ assert_resolve_tool_name_conflicts(
+ vec![
+ TestTool::new("tool1", ToolSource::Native),
+ TestTool::new("tool2", ToolSource::Native),
+ TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-1".into() }),
+ ],
+ vec!["tool1", "tool2", "tool3"],
+ );
+
+ assert_resolve_tool_name_conflicts(
+ vec![
+ TestTool::new("tool1", ToolSource::Native),
+ TestTool::new("tool2", ToolSource::Native),
+ TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-1".into() }),
+ TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-2".into() }),
+ ],
+ vec!["tool1", "tool2", "mcp-1_tool3", "mcp-2_tool3"],
+ );
+
+ assert_resolve_tool_name_conflicts(
+ vec![
+ TestTool::new("tool1", ToolSource::Native),
+ TestTool::new("tool2", ToolSource::Native),
+ TestTool::new("tool3", ToolSource::Native),
+ TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-1".into() }),
+ TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-2".into() }),
+ ],
+ vec!["tool1", "tool2", "tool3", "mcp-1_tool3", "mcp-2_tool3"],
+ );
+
+ // Test that tool with very long name is always truncated
+ assert_resolve_tool_name_conflicts(
+ vec![TestTool::new(
+ "tool-with-more-then-64-characters-blah-blah-blah-blah-blah-blah-blah-blah",
+ ToolSource::Native,
+ )],
+ vec!["tool-with-more-then-64-characters-blah-blah-blah-blah-blah-blah-"],
+ );
+
+ // Test deduplication of tools with very long names, in this case the mcp server name should be truncated
+ assert_resolve_tool_name_conflicts(
+ vec![
+ TestTool::new("tool-with-very-very-very-long-name", ToolSource::Native),
+ TestTool::new(
+ "tool-with-very-very-very-long-name",
+ ToolSource::ContextServer {
+ id: "mcp-with-very-very-very-long-name".into(),
+ },
+ ),
+ ],
+ vec![
+ "tool-with-very-very-very-long-name",
+ "mcp-with-very-very-very-long-_tool-with-very-very-very-long-name",
+ ],
+ );
+
+ fn assert_resolve_tool_name_conflicts(
+ tools: Vec,
+ expected: Vec>,
+ ) {
+ let tools: Vec> = tools
+ .into_iter()
+ .map(|t| Arc::new(t) as Arc)
+ .collect();
+ let tools = resolve_tool_name_conflicts(&tools);
+ assert_eq!(tools.len(), expected.len());
+ for (i, expected_name) in expected.into_iter().enumerate() {
+ let expected_name = expected_name.into();
+ let actual_name = &tools[i].0;
+ assert_eq!(
+ actual_name, &expected_name,
+ "Expected '{}' got '{}' at index {}",
+ expected_name, actual_name, i
+ );
+ }
+ }
+
+ struct TestTool {
+ name: String,
+ source: ToolSource,
+ }
+
+ impl TestTool {
+ fn new(name: impl Into, source: ToolSource) -> Self {
+ Self {
+ name: name.into(),
+ source,
+ }
+ }
+ }
+
+ impl Tool for TestTool {
+ fn name(&self) -> String {
+ self.name.clone()
+ }
+
+ fn icon(&self) -> IconName {
+ IconName::Ai
+ }
+
+ fn may_perform_edits(&self) -> bool {
+ false
+ }
+
+ fn needs_confirmation(&self, _input: &serde_json::Value, _cx: &App) -> bool {
+ true
+ }
+
+ fn source(&self) -> ToolSource {
+ self.source.clone()
+ }
+
+ fn description(&self) -> String {
+ "Test tool".to_string()
+ }
+
+ fn ui_text(&self, _input: &serde_json::Value) -> String {
+ "Test tool".to_string()
+ }
+
+ fn run(
+ self: Arc,
+ _input: serde_json::Value,
+ _request: Arc,
+ _project: Entity,
+ _action_log: Entity,
+ _model: Arc,
+ _window: Option,
+ _cx: &mut App,
+ ) -> assistant_tool::ToolResult {
+ assistant_tool::ToolResult {
+ output: Task::ready(Err(anyhow::anyhow!("No content"))),
+ card: None,
+ }
+ }
+ }
+ }
+
fn test_summarize_error(
model: &Arc,
thread: &Entity,
@@ -3674,7 +3876,6 @@ fn main() {{
workspace::init_settings(cx);
language_model::init_settings(cx);
ThemeSettings::register(cx);
- EditorSettings::register(cx);
ToolRegistry::default_global(cx);
});
}
diff --git a/crates/agent/src/thread_store.rs b/crates/agent/src/thread_store.rs
index db87bdd3a5..516151e9ff 100644
--- a/crates/agent/src/thread_store.rs
+++ b/crates/agent/src/thread_store.rs
@@ -1,22 +1,25 @@
-use std::cell::{Ref, RefCell};
-use std::path::{Path, PathBuf};
-use std::rc::Rc;
-use std::sync::{Arc, Mutex};
-
+use crate::{
+ context_server_tool::ContextServerTool,
+ thread::{
+ DetailedSummaryState, ExceededWindowError, MessageId, ProjectSnapshot, Thread, ThreadId,
+ },
+};
use agent_settings::{AgentProfileId, CompletionMode};
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ToolId, ToolWorkingSet};
use chrono::{DateTime, Utc};
use collections::HashMap;
use context_server::ContextServerId;
-use futures::channel::{mpsc, oneshot};
-use futures::future::{self, BoxFuture, Shared};
-use futures::{FutureExt as _, StreamExt as _};
+use futures::{
+ FutureExt as _, StreamExt as _,
+ channel::{mpsc, oneshot},
+ future::{self, BoxFuture, Shared},
+};
use gpui::{
App, BackgroundExecutor, Context, Entity, EventEmitter, Global, ReadGlobal, SharedString,
- Subscription, Task, prelude::*,
+ Subscription, Task, Window, prelude::*,
};
-
+use indoc::indoc;
use language_model::{LanguageModelToolResultContent, LanguageModelToolUseId, Role, TokenUsage};
use project::context_server_store::{ContextServerStatus, ContextServerStore};
use project::{Project, ProjectItem, ProjectPath, Worktree};
@@ -25,19 +28,18 @@ use prompt_store::{
UserRulesContext, WorktreeContext,
};
use serde::{Deserialize, Serialize};
-use ui::Window;
-use util::ResultExt as _;
-
-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,
};
+use std::{
+ cell::{Ref, RefCell},
+ path::{Path, PathBuf},
+ rc::Rc,
+ sync::{Arc, Mutex},
+};
+use util::ResultExt as _;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DataType {
@@ -69,7 +71,7 @@ impl Column for DataType {
}
}
-const RULES_FILE_NAMES: [&'static str; 8] = [
+const RULES_FILE_NAMES: [&'static str; 9] = [
".rules",
".cursorrules",
".windsurfrules",
@@ -78,6 +80,7 @@ const RULES_FILE_NAMES: [&'static str; 8] = [
"CLAUDE.md",
"AGENT.md",
"AGENTS.md",
+ "GEMINI.md",
];
pub fn init(cx: &mut App) {
@@ -94,7 +97,7 @@ impl SharedProjectContext {
}
}
-pub type TextThreadStore = assistant_context_editor::ContextStore;
+pub type TextThreadStore = assistant_context::ContextStore;
pub struct ThreadStore {
project: Entity,
@@ -305,17 +308,19 @@ impl ThreadStore {
project: Entity,
cx: &mut App,
) -> Task<(WorktreeContext, Option)> {
- let root_name = worktree.read(cx).root_name().into();
+ 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 rules_task = Self::load_worktree_rules_file(worktree, project, cx);
let Some(rules_task) = rules_task else {
- return Task::ready((
- WorktreeContext {
- root_name,
- rules_file: None,
- },
- None,
- ));
+ return Task::ready((context, None));
};
cx.spawn(async move |_| {
@@ -328,11 +333,8 @@ impl ThreadStore {
}),
),
};
- let worktree_info = WorktreeContext {
- root_name,
- rules_file,
- };
- (worktree_info, rules_file_error)
+ context.rules_file = rules_file;
+ (context, rules_file_error)
})
}
@@ -341,12 +343,12 @@ impl ThreadStore {
project: Entity,
cx: &mut App,
) -> Option>> {
- let worktree_ref = worktree.read(cx);
- let worktree_id = worktree_ref.id();
+ let worktree = worktree.read(cx);
+ let worktree_id = worktree.id();
let selected_rules_file = RULES_FILE_NAMES
.into_iter()
.filter_map(|name| {
- worktree_ref
+ worktree
.entry_for_path(name)
.filter(|entry| entry.is_file())
.map(|entry| entry.path.clone())
@@ -730,7 +732,7 @@ pub enum SerializedMessageSegment {
signature: Option,
},
RedactedThinking {
- data: Vec,
+ data: String,
},
}
diff --git a/crates/agent/src/tool_use.rs b/crates/agent/src/tool_use.rs
index da6adc07f0..76de3d2022 100644
--- a/crates/agent/src/tool_use.rs
+++ b/crates/agent/src/tool_use.rs
@@ -1,24 +1,23 @@
-use std::sync::Arc;
-
+use crate::{
+ thread::{MessageId, PromptId, ThreadId},
+ thread_store::SerializedMessage,
+};
use anyhow::Result;
use assistant_tool::{
AnyToolCard, Tool, ToolResultContent, ToolResultOutput, ToolUseStatus, ToolWorkingSet,
};
use collections::HashMap;
-use futures::FutureExt as _;
-use futures::future::Shared;
-use gpui::{App, Entity, SharedString, Task};
+use futures::{FutureExt as _, future::Shared};
+use gpui::{App, Entity, SharedString, Task, Window};
+use icons::IconName;
use language_model::{
ConfiguredModel, LanguageModel, LanguageModelRequest, LanguageModelToolResult,
LanguageModelToolResultContent, LanguageModelToolUse, LanguageModelToolUseId, Role,
};
use project::Project;
-use ui::{IconName, Window};
+use std::sync::Arc;
use util::truncate_lines_to_byte_limit;
-use crate::thread::{MessageId, PromptId, ThreadId};
-use crate::thread_store::SerializedMessage;
-
#[derive(Debug)]
pub struct ToolUse {
pub id: LanguageModelToolUseId,
@@ -26,7 +25,7 @@ pub struct ToolUse {
pub ui_text: SharedString,
pub status: ToolUseStatus,
pub input: serde_json::Value,
- pub icon: ui::IconName,
+ pub icon: icons::IconName,
pub needs_confirmation: bool,
}
@@ -427,7 +426,7 @@ impl ToolUseState {
// Protect from overly large output
let tool_output_limit = configured_model
- .map(|model| model.model.max_token_count() * BYTES_PER_TOKEN_ESTIMATE)
+ .map(|model| model.model.max_token_count() as usize * BYTES_PER_TOKEN_ESTIMATE)
.unwrap_or(usize::MAX);
let content = match tool_result {
diff --git a/crates/agent_settings/Cargo.toml b/crates/agent_settings/Cargo.toml
index c6a4bedbb5..3afe5ae547 100644
--- a/crates/agent_settings/Cargo.toml
+++ b/crates/agent_settings/Cargo.toml
@@ -12,17 +12,10 @@ workspace = true
path = "src/agent_settings.rs"
[dependencies]
-anthropic = { workspace = true, features = ["schemars"] }
anyhow.workspace = true
collections.workspace = true
gpui.workspace = true
language_model.workspace = true
-lmstudio = { workspace = true, features = ["schemars"] }
-log.workspace = true
-ollama = { workspace = true, features = ["schemars"] }
-open_ai = { workspace = true, features = ["schemars"] }
-deepseek = { workspace = true, features = ["schemars"] }
-mistral = { workspace = true, features = ["schemars"] }
schemars.workspace = true
serde.workspace = true
settings.workspace = true
diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs
index 9e8fd0c699..294d793e79 100644
--- a/crates/agent_settings/src/agent_settings.rs
+++ b/crates/agent_settings/src/agent_settings.rs
@@ -2,16 +2,10 @@ mod agent_profile;
use std::sync::Arc;
-use ::open_ai::Model as OpenAiModel;
-use anthropic::Model as AnthropicModel;
use anyhow::{Result, bail};
use collections::IndexMap;
-use deepseek::Model as DeepseekModel;
use gpui::{App, Pixels, SharedString};
use language_model::LanguageModel;
-use lmstudio::Model as LmStudioModel;
-use mistral::Model as MistralModel;
-use ollama::Model as OllamaModel;
use schemars::{JsonSchema, schema::Schema};
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
@@ -48,45 +42,6 @@ pub enum NotifyWhenAgentWaiting {
Never,
}
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
-#[serde(tag = "name", rename_all = "snake_case")]
-#[schemars(deny_unknown_fields)]
-pub enum AgentProviderContentV1 {
- #[serde(rename = "zed.dev")]
- ZedDotDev { default_model: Option },
- #[serde(rename = "openai")]
- OpenAi {
- default_model: Option,
- api_url: Option,
- available_models: Option>,
- },
- #[serde(rename = "anthropic")]
- Anthropic {
- default_model: Option,
- api_url: Option,
- },
- #[serde(rename = "ollama")]
- Ollama {
- default_model: Option,
- api_url: Option,
- },
- #[serde(rename = "lmstudio")]
- LmStudio {
- default_model: Option,
- api_url: Option,
- },
- #[serde(rename = "deepseek")]
- DeepSeek {
- default_model: Option,
- api_url: Option,
- },
- #[serde(rename = "mistral")]
- Mistral {
- default_model: Option,
- api_url: Option,
- },
-}
-
#[derive(Default, Clone, Debug)]
pub struct AgentSettings {
pub enabled: bool,
@@ -168,364 +123,56 @@ impl LanguageModelParameters {
}
}
-/// Agent panel settings
-#[derive(Clone, Serialize, Deserialize, Debug, Default)]
-pub struct AgentSettingsContent {
- #[serde(flatten)]
- pub inner: Option,
-}
-
-#[derive(Clone, Serialize, Deserialize, Debug)]
-#[serde(untagged)]
-pub enum AgentSettingsContentInner {
- Versioned(Box),
- Legacy(LegacyAgentSettingsContent),
-}
-
-impl AgentSettingsContentInner {
- fn for_v2(content: AgentSettingsContentV2) -> Self {
- AgentSettingsContentInner::Versioned(Box::new(VersionedAgentSettingsContent::V2(content)))
- }
-}
-
-impl JsonSchema for AgentSettingsContent {
- fn schema_name() -> String {
- VersionedAgentSettingsContent::schema_name()
- }
-
- fn json_schema(r#gen: &mut schemars::r#gen::SchemaGenerator) -> Schema {
- VersionedAgentSettingsContent::json_schema(r#gen)
- }
-
- fn is_referenceable() -> bool {
- VersionedAgentSettingsContent::is_referenceable()
- }
-}
-
impl AgentSettingsContent {
- pub fn is_version_outdated(&self) -> bool {
- match &self.inner {
- Some(AgentSettingsContentInner::Versioned(settings)) => match **settings {
- VersionedAgentSettingsContent::V1(_) => true,
- VersionedAgentSettingsContent::V2(_) => false,
- },
- Some(AgentSettingsContentInner::Legacy(_)) => true,
- None => false,
- }
- }
-
- fn upgrade(&self) -> AgentSettingsContentV2 {
- match &self.inner {
- Some(AgentSettingsContentInner::Versioned(settings)) => match **settings {
- VersionedAgentSettingsContent::V1(ref settings) => AgentSettingsContentV2 {
- enabled: settings.enabled,
- button: settings.button,
- dock: settings.dock,
- default_width: settings.default_width,
- default_height: settings.default_width,
- default_model: settings
- .provider
- .clone()
- .and_then(|provider| match provider {
- AgentProviderContentV1::ZedDotDev { default_model } => default_model
- .map(|model| LanguageModelSelection {
- provider: "zed.dev".into(),
- model,
- }),
- AgentProviderContentV1::OpenAi { default_model, .. } => default_model
- .map(|model| LanguageModelSelection {
- provider: "openai".into(),
- model: model.id().to_string(),
- }),
- AgentProviderContentV1::Anthropic { default_model, .. } => {
- default_model.map(|model| LanguageModelSelection {
- provider: "anthropic".into(),
- model: model.id().to_string(),
- })
- }
- AgentProviderContentV1::Ollama { default_model, .. } => default_model
- .map(|model| LanguageModelSelection {
- provider: "ollama".into(),
- model: model.id().to_string(),
- }),
- AgentProviderContentV1::LmStudio { default_model, .. } => default_model
- .map(|model| LanguageModelSelection {
- provider: "lmstudio".into(),
- model: model.id().to_string(),
- }),
- AgentProviderContentV1::DeepSeek { default_model, .. } => default_model
- .map(|model| LanguageModelSelection {
- provider: "deepseek".into(),
- model: model.id().to_string(),
- }),
- AgentProviderContentV1::Mistral { default_model, .. } => default_model
- .map(|model| LanguageModelSelection {
- provider: "mistral".into(),
- model: model.id().to_string(),
- }),
- }),
- inline_assistant_model: None,
- commit_message_model: None,
- thread_summary_model: None,
- inline_alternatives: None,
- default_profile: None,
- default_view: None,
- profiles: None,
- always_allow_tool_actions: None,
- notify_when_agent_waiting: None,
- stream_edits: None,
- single_file_review: None,
- model_parameters: Vec::new(),
- preferred_completion_mode: None,
- enable_feedback: None,
- play_sound_when_agent_done: None,
- },
- VersionedAgentSettingsContent::V2(ref settings) => settings.clone(),
- },
- Some(AgentSettingsContentInner::Legacy(settings)) => AgentSettingsContentV2 {
- enabled: None,
- button: settings.button,
- dock: settings.dock,
- default_width: settings.default_width,
- default_height: settings.default_height,
- default_model: Some(LanguageModelSelection {
- provider: "openai".into(),
- model: settings
- .default_open_ai_model
- .clone()
- .unwrap_or_default()
- .id()
- .to_string(),
- }),
- inline_assistant_model: None,
- commit_message_model: None,
- thread_summary_model: None,
- inline_alternatives: None,
- default_profile: None,
- default_view: None,
- profiles: None,
- always_allow_tool_actions: None,
- notify_when_agent_waiting: None,
- stream_edits: None,
- single_file_review: None,
- model_parameters: Vec::new(),
- preferred_completion_mode: None,
- enable_feedback: None,
- play_sound_when_agent_done: None,
- },
- None => AgentSettingsContentV2::default(),
- }
- }
-
pub fn set_dock(&mut self, dock: AgentDockPosition) {
- match &mut self.inner {
- Some(AgentSettingsContentInner::Versioned(settings)) => match **settings {
- VersionedAgentSettingsContent::V1(ref mut settings) => {
- settings.dock = Some(dock);
- }
- VersionedAgentSettingsContent::V2(ref mut settings) => {
- settings.dock = Some(dock);
- }
- },
- Some(AgentSettingsContentInner::Legacy(settings)) => {
- settings.dock = Some(dock);
- }
- None => {
- self.inner = Some(AgentSettingsContentInner::for_v2(AgentSettingsContentV2 {
- dock: Some(dock),
- ..Default::default()
- }))
- }
- }
+ self.dock = Some(dock);
}
pub fn set_model(&mut self, language_model: Arc) {
let model = language_model.id().0.to_string();
let provider = language_model.provider_id().0.to_string();
- match &mut self.inner {
- Some(AgentSettingsContentInner::Versioned(settings)) => match **settings {
- VersionedAgentSettingsContent::V1(ref mut settings) => match provider.as_ref() {
- "zed.dev" => {
- log::warn!("attempted to set zed.dev model on outdated settings");
- }
- "anthropic" => {
- let api_url = match &settings.provider {
- Some(AgentProviderContentV1::Anthropic { api_url, .. }) => {
- api_url.clone()
- }
- _ => None,
- };
- settings.provider = Some(AgentProviderContentV1::Anthropic {
- default_model: AnthropicModel::from_id(&model).ok(),
- api_url,
- });
- }
- "ollama" => {
- let api_url = match &settings.provider {
- Some(AgentProviderContentV1::Ollama { api_url, .. }) => api_url.clone(),
- _ => None,
- };
- settings.provider = Some(AgentProviderContentV1::Ollama {
- default_model: Some(ollama::Model::new(
- &model,
- None,
- None,
- Some(language_model.supports_tools()),
- Some(language_model.supports_images()),
- None,
- )),
- api_url,
- });
- }
- "lmstudio" => {
- let api_url = match &settings.provider {
- Some(AgentProviderContentV1::LmStudio { api_url, .. }) => {
- api_url.clone()
- }
- _ => None,
- };
- settings.provider = Some(AgentProviderContentV1::LmStudio {
- default_model: Some(lmstudio::Model::new(&model, None, None, false)),
- api_url,
- });
- }
- "openai" => {
- let (api_url, available_models) = match &settings.provider {
- Some(AgentProviderContentV1::OpenAi {
- api_url,
- available_models,
- ..
- }) => (api_url.clone(), available_models.clone()),
- _ => (None, None),
- };
- settings.provider = Some(AgentProviderContentV1::OpenAi {
- default_model: OpenAiModel::from_id(&model).ok(),
- api_url,
- available_models,
- });
- }
- "deepseek" => {
- let api_url = match &settings.provider {
- Some(AgentProviderContentV1::DeepSeek { api_url, .. }) => {
- api_url.clone()
- }
- _ => None,
- };
- settings.provider = Some(AgentProviderContentV1::DeepSeek {
- default_model: DeepseekModel::from_id(&model).ok(),
- api_url,
- });
- }
- _ => {}
- },
- VersionedAgentSettingsContent::V2(ref mut settings) => {
- settings.default_model = Some(LanguageModelSelection {
- provider: provider.into(),
- model,
- });
- }
- },
- Some(AgentSettingsContentInner::Legacy(settings)) => {
- if let Ok(model) = OpenAiModel::from_id(&language_model.id().0) {
- settings.default_open_ai_model = Some(model);
- }
- }
- None => {
- self.inner = Some(AgentSettingsContentInner::for_v2(AgentSettingsContentV2 {
- default_model: Some(LanguageModelSelection {
- provider: provider.into(),
- model,
- }),
- ..Default::default()
- }));
- }
- }
+ self.default_model = Some(LanguageModelSelection {
+ provider: provider.into(),
+ model,
+ });
}
pub fn set_inline_assistant_model(&mut self, provider: String, model: String) {
- self.v2_setting(|setting| {
- setting.inline_assistant_model = Some(LanguageModelSelection {
- provider: provider.into(),
- model,
- });
- Ok(())
- })
- .ok();
+ self.inline_assistant_model = Some(LanguageModelSelection {
+ provider: provider.into(),
+ model,
+ });
}
pub fn set_commit_message_model(&mut self, provider: String, model: String) {
- self.v2_setting(|setting| {
- setting.commit_message_model = Some(LanguageModelSelection {
- provider: provider.into(),
- model,
- });
- Ok(())
- })
- .ok();
- }
-
- pub fn v2_setting(
- &mut self,
- f: impl FnOnce(&mut AgentSettingsContentV2) -> anyhow::Result<()>,
- ) -> anyhow::Result<()> {
- match self.inner.get_or_insert_with(|| {
- AgentSettingsContentInner::for_v2(AgentSettingsContentV2 {
- ..Default::default()
- })
- }) {
- AgentSettingsContentInner::Versioned(boxed) => {
- if let VersionedAgentSettingsContent::V2(ref mut settings) = **boxed {
- f(settings)
- } else {
- Ok(())
- }
- }
- _ => Ok(()),
- }
+ self.commit_message_model = Some(LanguageModelSelection {
+ provider: provider.into(),
+ model,
+ });
}
pub fn set_thread_summary_model(&mut self, provider: String, model: String) {
- self.v2_setting(|setting| {
- setting.thread_summary_model = Some(LanguageModelSelection {
- provider: provider.into(),
- model,
- });
- Ok(())
- })
- .ok();
+ self.thread_summary_model = Some(LanguageModelSelection {
+ provider: provider.into(),
+ model,
+ });
}
pub fn set_always_allow_tool_actions(&mut self, allow: bool) {
- self.v2_setting(|setting| {
- setting.always_allow_tool_actions = Some(allow);
- Ok(())
- })
- .ok();
+ self.always_allow_tool_actions = Some(allow);
}
pub fn set_play_sound_when_agent_done(&mut self, allow: bool) {
- self.v2_setting(|setting| {
- setting.play_sound_when_agent_done = Some(allow);
- Ok(())
- })
- .ok();
+ self.play_sound_when_agent_done = Some(allow);
}
pub fn set_single_file_review(&mut self, allow: bool) {
- self.v2_setting(|setting| {
- setting.single_file_review = Some(allow);
- Ok(())
- })
- .ok();
+ self.single_file_review = Some(allow);
}
pub fn set_profile(&mut self, profile_id: AgentProfileId) {
- self.v2_setting(|setting| {
- setting.default_profile = Some(profile_id);
- Ok(())
- })
- .ok();
+ self.default_profile = Some(profile_id);
}
pub fn create_profile(
@@ -533,79 +180,39 @@ impl AgentSettingsContent {
profile_id: AgentProfileId,
profile_settings: AgentProfileSettings,
) -> Result<()> {
- self.v2_setting(|settings| {
- let profiles = settings.profiles.get_or_insert_default();
- if profiles.contains_key(&profile_id) {
- bail!("profile with ID '{profile_id}' already exists");
- }
+ let profiles = self.profiles.get_or_insert_default();
+ if profiles.contains_key(&profile_id) {
+ bail!("profile with ID '{profile_id}' already exists");
+ }
- 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
- .context_servers
- .into_iter()
- .map(|(server_id, preset)| {
- (
- server_id,
- ContextServerPresetContent {
- tools: preset.tools,
- },
- )
- })
- .collect(),
- },
- );
+ 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
+ .context_servers
+ .into_iter()
+ .map(|(server_id, preset)| {
+ (
+ server_id,
+ ContextServerPresetContent {
+ tools: preset.tools,
+ },
+ )
+ })
+ .collect(),
+ },
+ );
- Ok(())
- })
- }
-}
-
-#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
-#[serde(tag = "version")]
-#[schemars(deny_unknown_fields)]
-pub enum VersionedAgentSettingsContent {
- #[serde(rename = "1")]
- V1(AgentSettingsContentV1),
- #[serde(rename = "2")]
- V2(AgentSettingsContentV2),
-}
-
-impl Default for VersionedAgentSettingsContent {
- fn default() -> Self {
- Self::V2(AgentSettingsContentV2 {
- enabled: None,
- button: None,
- dock: None,
- default_width: None,
- default_height: None,
- default_model: None,
- inline_assistant_model: None,
- commit_message_model: None,
- thread_summary_model: None,
- inline_alternatives: None,
- default_profile: None,
- default_view: None,
- profiles: None,
- always_allow_tool_actions: None,
- notify_when_agent_waiting: None,
- stream_edits: None,
- single_file_review: None,
- model_parameters: Vec::new(),
- preferred_completion_mode: None,
- enable_feedback: None,
- play_sound_when_agent_done: None,
- })
+ Ok(())
}
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default)]
#[schemars(deny_unknown_fields)]
-pub struct AgentSettingsContentV2 {
+pub struct AgentSettingsContent {
/// Whether the Agent is enabled.
///
/// Default: true
@@ -732,6 +339,7 @@ impl JsonSchema for LanguageModelProviderSetting {
"deepseek".into(),
"openrouter".into(),
"mistral".into(),
+ "vercel".into(),
]),
..Default::default()
}
@@ -776,65 +384,6 @@ pub struct ContextServerPresetContent {
pub tools: IndexMap, bool>,
}
-#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
-#[schemars(deny_unknown_fields)]
-pub struct AgentSettingsContentV1 {
- /// Whether the Agent is enabled.
- ///
- /// Default: true
- enabled: Option,
- /// Whether to show the Agent panel button in the status bar.
- ///
- /// Default: true
- button: Option,
- /// Where to dock the Agent.
- ///
- /// Default: right
- dock: Option,
- /// Default width in pixels when the Agent is docked to the left or right.
- ///
- /// Default: 640
- default_width: Option,
- /// Default height in pixels when the Agent is docked to the bottom.
- ///
- /// Default: 320
- default_height: Option,
- /// The provider of the Agent service.
- ///
- /// This can be "openai", "anthropic", "ollama", "lmstudio", "deepseek", "zed.dev"
- /// each with their respective default models and configurations.
- provider: Option,
-}
-
-#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
-#[schemars(deny_unknown_fields)]
-pub struct LegacyAgentSettingsContent {
- /// Whether to show the Agent panel button in the status bar.
- ///
- /// Default: true
- pub button: Option,
- /// Where to dock the Agent.
- ///
- /// Default: right
- pub dock: Option,
- /// Default width in pixels when the Agent is docked to the left or right.
- ///
- /// Default: 640
- pub default_width: Option,
- /// Default height in pixels when the Agent is docked to the bottom.
- ///
- /// Default: 320
- pub default_height: Option,
- /// The default OpenAI model to use when creating new chats.
- ///
- /// Default: gpt-4-1106-preview
- pub default_open_ai_model: Option,
- /// OpenAI API base URL to use when creating new chats.
- ///
- /// Default:
- pub openai_api_url: Option,
-}
-
impl Settings for AgentSettings {
const KEY: Option<&'static str> = Some("agent");
@@ -851,11 +400,6 @@ impl Settings for AgentSettings {
let mut settings = AgentSettings::default();
for value in sources.defaults_and_customizations() {
- if value.is_version_outdated() {
- settings.using_outdated_settings_version = true;
- }
-
- let value = value.upgrade();
merge(&mut settings.enabled, value.enabled);
merge(&mut settings.button, value.button);
merge(&mut settings.dock, value.dock);
@@ -867,17 +411,23 @@ impl Settings for AgentSettings {
&mut settings.default_height,
value.default_height.map(Into::into),
);
- merge(&mut settings.default_model, value.default_model);
+ merge(&mut settings.default_model, value.default_model.clone());
settings.inline_assistant_model = value
.inline_assistant_model
+ .clone()
.or(settings.inline_assistant_model.take());
settings.commit_message_model = value
+ .clone()
.commit_message_model
.or(settings.commit_message_model.take());
settings.thread_summary_model = value
+ .clone()
.thread_summary_model
.or(settings.thread_summary_model.take());
- merge(&mut settings.inline_alternatives, value.inline_alternatives);
+ merge(
+ &mut settings.inline_alternatives,
+ value.inline_alternatives.clone(),
+ );
merge(
&mut settings.always_allow_tool_actions,
value.always_allow_tool_actions,
@@ -892,7 +442,7 @@ impl Settings for AgentSettings {
);
merge(&mut settings.stream_edits, value.stream_edits);
merge(&mut settings.single_file_review, value.single_file_review);
- merge(&mut settings.default_profile, value.default_profile);
+ merge(&mut settings.default_profile, value.default_profile.clone());
merge(&mut settings.default_view, value.default_view);
merge(
&mut settings.preferred_completion_mode,
@@ -904,24 +454,24 @@ impl Settings for AgentSettings {
.model_parameters
.extend_from_slice(&value.model_parameters);
- if let Some(profiles) = value.profiles {
+ if let Some(profiles) = value.profiles.as_ref() {
settings
.profiles
.extend(profiles.into_iter().map(|(id, profile)| {
(
- id,
+ id.clone(),
AgentProfileSettings {
- name: profile.name.into(),
- tools: profile.tools,
+ name: profile.name.clone().into(),
+ tools: profile.tools.clone(),
enable_all_context_servers: profile
.enable_all_context_servers
.unwrap_or_default(),
context_servers: profile
.context_servers
- .into_iter()
+ .iter()
.map(|(context_server_id, preset)| {
(
- context_server_id,
+ context_server_id.clone(),
ContextServerPreset {
tools: preset.tools.clone(),
},
@@ -942,28 +492,8 @@ impl Settings for AgentSettings {
.read_value("chat.agent.enabled")
.and_then(|b| b.as_bool())
{
- match &mut current.inner {
- Some(AgentSettingsContentInner::Versioned(versioned)) => match versioned.as_mut() {
- VersionedAgentSettingsContent::V1(setting) => {
- setting.enabled = Some(b);
- setting.button = Some(b);
- }
-
- VersionedAgentSettingsContent::V2(setting) => {
- setting.enabled = Some(b);
- setting.button = Some(b);
- }
- },
- Some(AgentSettingsContentInner::Legacy(setting)) => setting.button = Some(b),
- None => {
- current.inner =
- Some(AgentSettingsContentInner::for_v2(AgentSettingsContentV2 {
- enabled: Some(b),
- button: Some(b),
- ..Default::default()
- }));
- }
- }
+ current.enabled = Some(b);
+ current.button = Some(b);
}
}
}
@@ -973,149 +503,3 @@ fn merge(target: &mut T, value: Option) {
*target = value;
}
}
-
-#[cfg(test)]
-mod tests {
- use fs::Fs;
- use gpui::{ReadGlobal, TestAppContext};
- use settings::SettingsStore;
-
- use super::*;
-
- #[gpui::test]
- async fn test_deserialize_agent_settings_with_version(cx: &mut TestAppContext) {
- let fs = fs::FakeFs::new(cx.executor().clone());
- fs.create_dir(paths::settings_file().parent().unwrap())
- .await
- .unwrap();
-
- cx.update(|cx| {
- let test_settings = settings::SettingsStore::test(cx);
- cx.set_global(test_settings);
- AgentSettings::register(cx);
- });
-
- cx.update(|cx| {
- assert!(!AgentSettings::get_global(cx).using_outdated_settings_version);
- assert_eq!(
- AgentSettings::get_global(cx).default_model,
- LanguageModelSelection {
- provider: "zed.dev".into(),
- model: "claude-sonnet-4".into(),
- }
- );
- });
-
- cx.update(|cx| {
- settings::SettingsStore::global(cx).update_settings_file::(
- fs.clone(),
- |settings, _| {
- *settings = AgentSettingsContent {
- inner: Some(AgentSettingsContentInner::for_v2(AgentSettingsContentV2 {
- default_model: Some(LanguageModelSelection {
- provider: "test-provider".into(),
- model: "gpt-99".into(),
- }),
- inline_assistant_model: None,
- commit_message_model: None,
- thread_summary_model: None,
- inline_alternatives: None,
- enabled: None,
- button: None,
- dock: None,
- default_width: None,
- default_height: None,
- default_profile: None,
- default_view: None,
- profiles: None,
- always_allow_tool_actions: None,
- play_sound_when_agent_done: None,
- notify_when_agent_waiting: None,
- stream_edits: None,
- single_file_review: None,
- enable_feedback: None,
- model_parameters: Vec::new(),
- preferred_completion_mode: None,
- })),
- }
- },
- );
- });
-
- cx.run_until_parked();
-
- let raw_settings_value = fs.load(paths::settings_file()).await.unwrap();
- assert!(raw_settings_value.contains(r#""version": "2""#));
-
- #[derive(Debug, Deserialize)]
- struct AgentSettingsTest {
- agent: AgentSettingsContent,
- }
-
- let agent_settings: AgentSettingsTest =
- serde_json_lenient::from_str(&raw_settings_value).unwrap();
-
- assert!(!agent_settings.agent.is_version_outdated());
- }
-
- #[gpui::test]
- async fn test_load_settings_from_old_key(cx: &mut TestAppContext) {
- let fs = fs::FakeFs::new(cx.executor().clone());
- fs.create_dir(paths::settings_file().parent().unwrap())
- .await
- .unwrap();
-
- cx.update(|cx| {
- let mut test_settings = settings::SettingsStore::test(cx);
- let user_settings_content = r#"{
- "assistant": {
- "enabled": true,
- "version": "2",
- "default_model": {
- "provider": "zed.dev",
- "model": "gpt-99"
- },
- }}"#;
- test_settings
- .set_user_settings(user_settings_content, cx)
- .unwrap();
- cx.set_global(test_settings);
- AgentSettings::register(cx);
- });
-
- cx.run_until_parked();
-
- let agent_settings = cx.update(|cx| AgentSettings::get_global(cx).clone());
- assert!(agent_settings.enabled);
- assert!(!agent_settings.using_outdated_settings_version);
- assert_eq!(agent_settings.default_model.model, "gpt-99");
-
- cx.update_global::(|settings_store, cx| {
- settings_store.update_user_settings::(cx, |settings| {
- *settings = AgentSettingsContent {
- inner: Some(AgentSettingsContentInner::for_v2(AgentSettingsContentV2 {
- enabled: Some(false),
- default_model: Some(LanguageModelSelection {
- provider: "xai".to_owned().into(),
- model: "grok".to_owned(),
- }),
- ..Default::default()
- })),
- };
- });
- });
-
- cx.run_until_parked();
-
- let settings = cx.update(|cx| SettingsStore::global(cx).raw_user_settings().clone());
-
- #[derive(Debug, Deserialize)]
- struct AgentSettingsTest {
- assistant: AgentSettingsContent,
- agent: Option,
- }
-
- let agent_settings: AgentSettingsTest = serde_json::from_value(settings).unwrap();
- assert!(agent_settings.agent.is_none());
- }
-}
diff --git a/crates/agent_ui/Cargo.toml b/crates/agent_ui/Cargo.toml
new file mode 100644
index 0000000000..070e8eb585
--- /dev/null
+++ b/crates/agent_ui/Cargo.toml
@@ -0,0 +1,110 @@
+[package]
+name = "agent_ui"
+version = "0.1.0"
+edition.workspace = true
+publish.workspace = true
+license = "GPL-3.0-or-later"
+
+[lints]
+workspace = true
+
+[lib]
+path = "src/agent_ui.rs"
+doctest = false
+
+[features]
+test-support = [
+ "gpui/test-support",
+ "language/test-support",
+]
+
+[dependencies]
+agent.workspace = true
+agent_settings.workspace = true
+anyhow.workspace = true
+assistant_context.workspace = true
+assistant_slash_command.workspace = true
+assistant_slash_commands.workspace = true
+assistant_tool.workspace = true
+audio.workspace = true
+buffer_diff.workspace = true
+chrono.workspace = true
+client.workspace = true
+collections.workspace = true
+component.workspace = true
+context_server.workspace = true
+db.workspace = true
+editor.workspace = true
+extension.workspace = true
+extension_host.workspace = true
+feature_flags.workspace = true
+file_icons.workspace = true
+fs.workspace = true
+futures.workspace = true
+fuzzy.workspace = true
+gpui.workspace = true
+html_to_markdown.workspace = true
+indoc.workspace = true
+http_client.workspace = true
+indexed_docs.workspace = true
+inventory.workspace = true
+itertools.workspace = true
+jsonschema.workspace = true
+language.workspace = true
+language_model.workspace = true
+log.workspace = true
+lsp.workspace = true
+markdown.workspace = true
+menu.workspace = true
+multi_buffer.workspace = true
+notifications.workspace = true
+ordered-float.workspace = true
+parking_lot.workspace = true
+paths.workspace = true
+picker.workspace = true
+project.workspace = true
+prompt_store.workspace = true
+proto.workspace = true
+release_channel.workspace = true
+rope.workspace = true
+rules_library.workspace = true
+schemars.workspace = true
+search.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+serde_json_lenient.workspace = true
+settings.workspace = true
+smol.workspace = true
+streaming_diff.workspace = true
+telemetry.workspace = true
+telemetry_events.workspace = true
+terminal.workspace = true
+terminal_view.workspace = true
+text.workspace = true
+theme.workspace = true
+time.workspace = true
+time_format.workspace = true
+ui.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
+
+[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"] }
+languages = { 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
+tree-sitter-md.workspace = true
+unindent.workspace = true
diff --git a/crates/assistant_context_editor/LICENSE-GPL b/crates/agent_ui/LICENSE-GPL
similarity index 100%
rename from crates/assistant_context_editor/LICENSE-GPL
rename to crates/agent_ui/LICENSE-GPL
diff --git a/crates/agent/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs
similarity index 93%
rename from crates/agent/src/active_thread.rs
rename to crates/agent_ui/src/active_thread.rs
index eff74f1786..0e7ca9aa89 100644
--- a/crates/agent/src/active_thread.rs
+++ b/crates/agent_ui/src/active_thread.rs
@@ -1,18 +1,17 @@
-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::thread::{
- LastRestoreCheckpoint, MessageCrease, MessageId, MessageSegment, Thread, ThreadError,
- ThreadEvent, ThreadFeedback, ThreadSummary,
-};
-use crate::thread_store::{RulesLoadingError, TextThreadStore, ThreadStore};
-use crate::tool_use::{PendingToolUseStatus, ToolUse};
use crate::ui::{
AddedContext, AgentNotification, AgentNotificationEvent, AnimatedLabel, ContextPill,
};
use crate::{AgentPanel, ModelUsageContext};
+use agent::{
+ ContextStore, LastRestoreCheckpoint, MessageCrease, MessageId, MessageSegment, TextThreadStore,
+ Thread, ThreadError, ThreadEvent, ThreadFeedback, ThreadStore, ThreadSummary,
+ context::{self, AgentContextHandle, RULES_ICON},
+ thread_store::RulesLoadingError,
+ tool_use::{PendingToolUseStatus, ToolUse},
+};
use agent_settings::{AgentSettings, NotifyWhenAgentWaiting};
use anyhow::Context as _;
use assistant_tool::ToolUseStatus;
@@ -24,7 +23,7 @@ use editor::{Editor, EditorElement, EditorEvent, EditorStyle, MultiBuffer};
use gpui::{
AbsoluteLength, Animation, AnimationExt, AnyElement, App, ClickEvent, ClipboardEntry,
ClipboardItem, DefiniteLength, EdgesRefinement, Empty, Entity, EventEmitter, Focusable, Hsla,
- ListAlignment, ListState, MouseButton, PlatformDisplay, ScrollHandle, Stateful,
+ ListAlignment, ListOffset, ListState, MouseButton, PlatformDisplay, ScrollHandle, Stateful,
StyleRefinement, Subscription, Task, TextStyle, TextStyleRefinement, Transformation,
UnderlineStyle, WeakEntity, WindowHandle, linear_color_stop, linear_gradient, list, percentage,
pulsating_between,
@@ -48,8 +47,8 @@ use std::time::Duration;
use text::ToPoint;
use theme::ThemeSettings;
use ui::{
- Disclosure, IconButton, KeyBinding, PopoverMenuHandle, Scrollbar, ScrollbarState, TextSize,
- Tooltip, prelude::*,
+ Disclosure, KeyBinding, PopoverMenuHandle, Scrollbar, ScrollbarState, TextSize, Tooltip,
+ prelude::*,
};
use util::ResultExt as _;
use util::markdown::MarkdownCodeBlock;
@@ -57,6 +56,9 @@ use workspace::{CollaboratorId, Workspace};
use zed_actions::assistant::OpenRulesLibrary;
use zed_llm_client::CompletionIntent;
+const CODEBLOCK_CONTAINER_GROUP: &str = "codeblock_container";
+const EDIT_PREVIOUS_MESSAGE_MIN_LINES: usize = 1;
+
pub struct ActiveThread {
context_store: Entity,
language_registry: Arc,
@@ -300,7 +302,7 @@ fn tool_use_markdown_style(window: &Window, cx: &mut App) -> MarkdownStyle {
base_text_style: text_style,
syntax: cx.theme().syntax().clone(),
selection_background_color: cx.theme().players().local().selection,
- code_block_overflow_x_scroll: true,
+ code_block_overflow_x_scroll: false,
code_block: StyleRefinement {
margin: EdgesRefinement::default(),
padding: EdgesRefinement::default(),
@@ -334,8 +336,6 @@ fn tool_use_markdown_style(window: &Window, cx: &mut App) -> MarkdownStyle {
}
}
-const CODEBLOCK_CONTAINER_GROUP: &str = "codeblock_container";
-
fn render_markdown_code_block(
message_id: MessageId,
ix: usize,
@@ -750,7 +750,7 @@ struct EditingMessageState {
editor: Entity,
context_strip: Entity,
context_picker_menu_handle: PopoverMenuHandle,
- last_estimated_token_count: Option,
+ last_estimated_token_count: Option,
_subscriptions: [Subscription; 2],
_update_token_count_task: Option>,
}
@@ -809,7 +809,12 @@ impl ActiveThread {
};
for message in thread.read(cx).messages().cloned().collect::>() {
- this.push_message(&message.id, &message.segments, window, cx);
+ let rendered_message = RenderedMessage::from_segments(
+ &message.segments,
+ this.language_registry.clone(),
+ cx,
+ );
+ this.push_rendered_message(message.id, rendered_message);
for tool_use in thread.read(cx).tool_uses_for_message(message.id, cx) {
this.render_tool_use_markdown(
@@ -857,7 +862,7 @@ impl ActiveThread {
}
/// Returns the editing message id and the estimated token count in the content
- pub fn editing_message_id(&self) -> Option<(MessageId, usize)> {
+ pub fn editing_message_id(&self) -> Option<(MessageId, u64)> {
self.editing_message
.as_ref()
.map(|(id, state)| (*id, state.last_estimated_token_count.unwrap_or(0)))
@@ -875,36 +880,11 @@ impl ActiveThread {
&self.text_thread_store
}
- fn push_message(
- &mut self,
- id: &MessageId,
- segments: &[MessageSegment],
- _window: &mut Window,
- cx: &mut Context,
- ) {
+ fn push_rendered_message(&mut self, id: MessageId, rendered_message: RenderedMessage) {
let old_len = self.messages.len();
- self.messages.push(*id);
+ self.messages.push(id);
self.list_state.splice(old_len..old_len, 1);
-
- let rendered_message =
- RenderedMessage::from_segments(segments, self.language_registry.clone(), cx);
- self.rendered_messages_by_id.insert(*id, rendered_message);
- }
-
- fn edited_message(
- &mut self,
- id: &MessageId,
- segments: &[MessageSegment],
- _window: &mut Window,
- cx: &mut Context,
- ) {
- let Some(index) = self.messages.iter().position(|message_id| message_id == id) else {
- return;
- };
- self.list_state.splice(index..index + 1, 1);
- let rendered_message =
- RenderedMessage::from_segments(segments, self.language_registry.clone(), cx);
- self.rendered_messages_by_id.insert(*id, rendered_message);
+ self.rendered_messages_by_id.insert(id, rendered_message);
}
fn deleted_message(&mut self, id: &MessageId) {
@@ -1037,31 +1017,43 @@ impl ActiveThread {
}
}
ThreadEvent::MessageAdded(message_id) => {
- if let Some(message_segments) = self
- .thread
- .read(cx)
- .message(*message_id)
- .map(|message| message.segments.clone())
- {
- self.push_message(message_id, &message_segments, window, cx);
+ if let Some(rendered_message) = self.thread.update(cx, |thread, cx| {
+ thread.message(*message_id).map(|message| {
+ RenderedMessage::from_segments(
+ &message.segments,
+ self.language_registry.clone(),
+ cx,
+ )
+ })
+ }) {
+ self.push_rendered_message(*message_id, rendered_message);
}
self.save_thread(cx);
cx.notify();
}
ThreadEvent::MessageEdited(message_id) => {
- if let Some(message_segments) = self
- .thread
- .read(cx)
- .message(*message_id)
- .map(|message| message.segments.clone())
- {
- self.edited_message(message_id, &message_segments, window, cx);
+ if let Some(index) = self.messages.iter().position(|id| id == message_id) {
+ if let Some(rendered_message) = self.thread.update(cx, |thread, cx| {
+ thread.message(*message_id).map(|message| {
+ let mut rendered_message = RenderedMessage {
+ language_registry: self.language_registry.clone(),
+ segments: Vec::with_capacity(message.segments.len()),
+ };
+ for segment in &message.segments {
+ rendered_message.push_segment(segment, cx);
+ }
+ rendered_message
+ })
+ }) {
+ self.list_state.splice(index..index + 1, 1);
+ self.rendered_messages_by_id
+ .insert(*message_id, rendered_message);
+ self.scroll_to_bottom(cx);
+ self.save_thread(cx);
+ cx.notify();
+ }
}
-
- self.scroll_to_bottom(cx);
- self.save_thread(cx);
- cx.notify();
}
ThreadEvent::MessageDeleted(message_id) => {
self.deleted_message(message_id);
@@ -1311,27 +1303,23 @@ impl ActiveThread {
fn start_editing_message(
&mut self,
message_id: MessageId,
- message_segments: &[MessageSegment],
+ message_text: impl Into>,
message_creases: &[MessageCrease],
window: &mut Window,
cx: &mut Context,
) {
- // User message should always consist of a single text segment,
- // therefore we can skip returning early if it's not a text segment.
- let Some(MessageSegment::Text(message_text)) = message_segments.first() else {
- return;
- };
-
let editor = crate::message_editor::create_editor(
self.workspace.clone(),
self.context_store.downgrade(),
self.thread_store.downgrade(),
self.text_thread_store.downgrade(),
+ EDIT_PREVIOUS_MESSAGE_MIN_LINES,
+ None,
window,
cx,
);
editor.update(cx, |editor, cx| {
- editor.set_text(message_text.clone(), window, cx);
+ editor.set_text(message_text, window, cx);
insert_message_creases(editor, message_creases, &self.context_store, window, cx);
editor.focus_handle(cx).focus(window);
editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
@@ -1580,8 +1568,7 @@ impl ActiveThread {
let git_store = project.read(cx).git_store().clone();
let checkpoint = git_store.update(cx, |git_store, cx| git_store.checkpoint(cx));
- let load_context_task =
- crate::context::load_context(new_context, &project, &prompt_store, cx);
+ let load_context_task = context::load_context(new_context, &project, &prompt_store, cx);
self._load_edited_message_context_task =
Some(cx.spawn_in(window, async move |this, cx| {
let (context, checkpoint) =
@@ -1605,6 +1592,7 @@ 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,
@@ -1617,6 +1605,14 @@ impl ActiveThread {
})
.log_err();
}));
+
+ if let Some(workspace) = self.workspace.upgrade() {
+ workspace.update(cx, |workspace, cx| {
+ if let Some(panel) = workspace.panel::(cx) {
+ panel.focus_handle(cx).focus(window);
+ }
+ });
+ }
}
fn messages_after(&self, message_id: MessageId) -> &[MessageId] {
@@ -1680,7 +1676,10 @@ impl ActiveThread {
let editor = cx.new(|cx| {
let mut editor = Editor::new(
- editor::EditorMode::AutoHeight { max_lines: 4 },
+ editor::EditorMode::AutoHeight {
+ min_lines: 1,
+ max_lines: Some(4),
+ },
buffer,
None,
window,
@@ -1722,7 +1721,7 @@ impl ActiveThread {
telemetry::event!(
"Assistant Thread Feedback Comments",
thread_id,
- message_id = message_id.0,
+ message_id = message_id.as_usize(),
message_content,
comments = comments_value
);
@@ -1815,8 +1814,6 @@ impl ActiveThread {
return div().children(loading_dots).into_any();
}
- let message_creases = message.creases.clone();
-
let Some(rendered_message) = self.rendered_messages_by_id.get(&message_id) else {
return Empty.into_any();
};
@@ -1857,6 +1854,14 @@ impl ActiveThread {
}
});
+ let scroll_to_top = IconButton::new(("scroll_to_top", ix), IconName::ArrowUpAlt)
+ .icon_size(IconSize::XSmall)
+ .icon_color(Color::Ignored)
+ .tooltip(Tooltip::text("Scroll To Top"))
+ .on_click(cx.listener(move |this, _, _, cx| {
+ this.scroll_to_top(cx);
+ }));
+
// For all items that should be aligned with the LLM's response.
const RESPONSE_PADDING_X: Pixels = px(19.);
@@ -1966,11 +1971,14 @@ impl ActiveThread {
);
})),
)
- .child(open_as_markdown),
+ .child(open_as_markdown)
+ .child(scroll_to_top),
)
.into_any_element(),
None => feedback_container
- .child(h_flex().child(open_as_markdown))
+ .child(h_flex()
+ .child(open_as_markdown))
+ .child(scroll_to_top)
.into_any_element(),
};
@@ -2120,15 +2128,30 @@ impl ActiveThread {
}),
)
.on_click(cx.listener({
- let message_segments = message.segments.clone();
+ let message_creases = message.creases.clone();
move |this, _, window, cx| {
- this.start_editing_message(
- message_id,
- &message_segments,
- &message_creases,
- window,
- cx,
- );
+ if let Some(message_text) =
+ this.thread.read(cx).message(message_id).and_then(|message| {
+ message.segments.first().and_then(|segment| {
+ match segment {
+ MessageSegment::Text(message_text) => {
+ Some(Into::>::into(message_text.as_str()))
+ }
+ _ => {
+ None
+ }
+ }
+ })
+ })
+ {
+ this.start_editing_message(
+ message_id,
+ message_text,
+ &message_creases,
+ window,
+ cx,
+ );
+ }
}
})),
),
@@ -3084,6 +3107,7 @@ impl ActiveThread {
.pr_1()
.gap_1()
.justify_between()
+ .flex_wrap()
.bg(cx.theme().colors().editor_background)
.border_t_1()
.border_color(self.tool_card_border_color(cx))
@@ -3459,6 +3483,11 @@ impl ActiveThread {
*is_expanded = !*is_expanded;
}
+ pub fn scroll_to_top(&mut self, cx: &mut Context) {
+ self.list_state.scroll_to(ListOffset::default());
+ cx.notify();
+ }
+
pub fn scroll_to_bottom(&mut self, cx: &mut Context) {
self.list_state.reset(self.messages.len());
cx.notify();
@@ -3691,8 +3720,10 @@ fn open_editor_at_position(
#[cfg(test)]
mod tests {
+ use super::*;
+ use agent::{MessageSegment, context::ContextLoadResult, thread_store};
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::{
@@ -3706,10 +3737,6 @@ mod tests {
use util::path;
use workspace::CollaboratorId;
- use crate::{ContextLoadResult, thread_store};
-
- use super::*;
-
#[gpui::test]
async fn test_agent_is_unfollowed_after_cancelling_completion(cx: &mut TestAppContext) {
init_test_settings(cx);
@@ -3781,10 +3808,8 @@ mod tests {
let creases = vec![MessageCrease {
range: 14..22,
- metadata: CreaseMetadata {
- icon_path: "icon".into(),
- label: "foo.txt".into(),
- },
+ icon_path: "icon".into(),
+ label: "foo.txt".into(),
context: None,
}];
@@ -3800,13 +3825,15 @@ mod tests {
});
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,
- );
+ if let Some(message_text) = message.segments.first().and_then(MessageSegment::text) {
+ active_thread.start_editing_message(
+ message.id,
+ message_text,
+ message.creases.as_slice(),
+ window,
+ cx,
+ );
+ }
let editor = active_thread
.editing_message
.as_ref()
@@ -3821,13 +3848,15 @@ mod tests {
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,
- );
+ if let Some(message_text) = message.segments.first().and_then(MessageSegment::text) {
+ active_thread.start_editing_message(
+ message.id,
+ message_text,
+ message.creases.as_slice(),
+ window,
+ cx,
+ );
+ }
let editor = active_thread
.editing_message
.as_ref()
@@ -3840,6 +3869,116 @@ mod tests {
});
}
+ #[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| {
+ if let Some(message_text) = message.segments.first().and_then(MessageSegment::text) {
+ active_thread.start_editing_message(
+ message.id,
+ message_text,
+ 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);
diff --git a/crates/agent/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs
similarity index 56%
rename from crates/agent/src/agent_configuration.rs
rename to crates/agent_ui/src/agent_configuration.rs
index be76804bbf..e91a0f7ebe 100644
--- a/crates/agent/src/agent_configuration.rs
+++ b/crates/agent_ui/src/agent_configuration.rs
@@ -1,4 +1,3 @@
-mod add_context_server_modal;
mod configure_context_server_modal;
mod manage_profiles_modal;
mod tool_picker;
@@ -9,22 +8,29 @@ use agent_settings::AgentSettings;
use assistant_tool::{ToolSource, ToolWorkingSet};
use collections::HashMap;
use context_server::ContextServerId;
+use extension::ExtensionManifest;
+use extension_host::ExtensionStore;
use fs::Fs;
use gpui::{
- Action, Animation, AnimationExt as _, AnyView, App, Entity, EventEmitter, FocusHandle,
- Focusable, ScrollHandle, Subscription, Transformation, percentage,
+ Action, Animation, AnimationExt as _, AnyView, App, Corner, Entity, EventEmitter, FocusHandle,
+ Focusable, ScrollHandle, Subscription, Task, Transformation, WeakEntity, percentage,
};
+use language::LanguageRegistry;
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
-use project::context_server_store::{ContextServerStatus, ContextServerStore};
+use notifications::status_toast::{StatusToast, ToastIcon};
+use project::{
+ context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
+ project_settings::{ContextServerSettings, ProjectSettings},
+};
use settings::{Settings, update_settings_file};
use ui::{
- Disclosure, ElevationIndex, Indicator, Scrollbar, ScrollbarState, Switch, SwitchColor, Tooltip,
- prelude::*,
+ ContextMenu, Disclosure, ElevationIndex, Indicator, PopoverMenu, Scrollbar, ScrollbarState,
+ Switch, SwitchColor, Tooltip, prelude::*,
};
use util::ResultExt as _;
+use workspace::Workspace;
use zed_actions::ExtensionCategoryFilter;
-pub(crate) use add_context_server_modal::AddContextServerModal;
pub(crate) use configure_context_server_modal::ConfigureContextServerModal;
pub(crate) use manage_profiles_modal::ManageProfilesModal;
@@ -32,6 +38,8 @@ use crate::AddContextServer;
pub struct AgentConfiguration {
fs: Arc,
+ language_registry: Arc,
+ workspace: WeakEntity,
focus_handle: FocusHandle,
configuration_views_by_provider: HashMap,
context_server_store: Entity,
@@ -48,6 +56,8 @@ impl AgentConfiguration {
fs: Arc,
context_server_store: Entity,
tools: Entity,
+ language_registry: Arc,
+ workspace: WeakEntity,
window: &mut Window,
cx: &mut Context,
) -> Self {
@@ -70,11 +80,16 @@ impl AgentConfiguration {
},
);
+ cx.subscribe(&context_server_store, |_, _, _, cx| cx.notify())
+ .detach();
+
let scroll_handle = ScrollHandle::new();
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
let mut this = Self {
fs,
+ language_registry,
+ workspace,
focus_handle,
configuration_views_by_provider: HashMap::default(),
context_server_store,
@@ -133,6 +148,8 @@ impl AgentConfiguration {
) -> impl IntoElement + use<> {
let provider_id = provider.id().0.clone();
let provider_name = provider.name().0.clone();
+ let provider_id_string = SharedString::from(format!("provider-disclosure-{provider_id}"));
+
let configuration_view = self
.configuration_views_by_provider
.get(&provider.id())
@@ -145,72 +162,80 @@ impl AgentConfiguration {
.unwrap_or(false);
v_flex()
- .pt_3()
+ .py_2()
.gap_1p5()
.border_t_1()
.border_color(cx.theme().colors().border.opacity(0.6))
.child(
h_flex()
+ .w_full()
+ .gap_1()
.justify_between()
.child(
h_flex()
- .gap_2()
+ .id(provider_id_string.clone())
+ .cursor_pointer()
+ .py_0p5()
+ .w_full()
+ .justify_between()
+ .rounded_sm()
+ .hover(|hover| hover.bg(cx.theme().colors().element_hover))
.child(
- Icon::new(provider.icon())
- .size(IconSize::Small)
- .color(Color::Muted),
- )
- .child(Label::new(provider_name.clone()).size(LabelSize::Large))
- .when(provider.is_authenticated(cx) && !is_expanded, |parent| {
- parent.child(Icon::new(IconName::Check).color(Color::Success))
- }),
- )
- .child(
- h_flex()
- .gap_1()
- .when(provider.is_authenticated(cx), |parent| {
- parent.child(
- Button::new(
- SharedString::from(format!("new-thread-{provider_id}")),
- "Start New Thread",
+ h_flex()
+ .gap_2()
+ .child(
+ Icon::new(provider.icon())
+ .size(IconSize::Small)
+ .color(Color::Muted),
)
- .icon_position(IconPosition::Start)
- .icon(IconName::Plus)
- .icon_size(IconSize::Small)
- .layer(ElevationIndex::ModalSurface)
- .label_size(LabelSize::Small)
- .on_click(cx.listener({
- let provider = provider.clone();
- move |_this, _event, _window, cx| {
- cx.emit(AssistantConfigurationEvent::NewThread(
- provider.clone(),
- ))
- }
- })),
- )
- })
+ .child(Label::new(provider_name.clone()).size(LabelSize::Large))
+ .when(
+ provider.is_authenticated(cx) && !is_expanded,
+ |parent| {
+ parent.child(
+ Icon::new(IconName::Check).color(Color::Success),
+ )
+ },
+ ),
+ )
.child(
- Disclosure::new(
- SharedString::from(format!(
- "provider-disclosure-{provider_id}"
- )),
- is_expanded,
- )
- .opened_icon(IconName::ChevronUp)
- .closed_icon(IconName::ChevronDown)
- .on_click(cx.listener({
- let provider_id = provider.id().clone();
- move |this, _event, _window, _cx| {
- let is_expanded = this
- .expanded_provider_configurations
- .entry(provider_id.clone())
- .or_insert(false);
+ Disclosure::new(provider_id_string, is_expanded)
+ .opened_icon(IconName::ChevronUp)
+ .closed_icon(IconName::ChevronDown),
+ )
+ .on_click(cx.listener({
+ let provider_id = provider.id().clone();
+ move |this, _event, _window, _cx| {
+ let is_expanded = this
+ .expanded_provider_configurations
+ .entry(provider_id.clone())
+ .or_insert(false);
- *is_expanded = !*is_expanded;
- }
- })),
- ),
- ),
+ *is_expanded = !*is_expanded;
+ }
+ })),
+ )
+ .when(provider.is_authenticated(cx), |parent| {
+ parent.child(
+ Button::new(
+ SharedString::from(format!("new-thread-{provider_id}")),
+ "Start New Thread",
+ )
+ .icon_position(IconPosition::Start)
+ .icon(IconName::Plus)
+ .icon_size(IconSize::Small)
+ .icon_color(Color::Muted)
+ .label_size(LabelSize::Small)
+ .on_click(cx.listener({
+ let provider = provider.clone();
+ move |_this, _event, _window, cx| {
+ cx.emit(AssistantConfigurationEvent::NewThread(
+ provider.clone(),
+ ))
+ }
+ })),
+ )
+ }),
)
.when(is_expanded, |parent| match configuration_view {
Some(configuration_view) => parent.child(configuration_view),
@@ -229,11 +254,11 @@ impl AgentConfiguration {
v_flex()
.p(DynamicSpacing::Base16.rems(cx))
.pr(DynamicSpacing::Base20.rems(cx))
- .gap_4()
.border_b_1()
.border_color(cx.theme().colors().border)
.child(
v_flex()
+ .mb_2p5()
.gap_0p5()
.child(Headline::new("LLM Providers"))
.child(
@@ -460,9 +485,22 @@ impl AgentConfiguration {
.read(cx)
.status_for_server(&context_server_id)
.unwrap_or(ContextServerStatus::Stopped);
+ let server_configuration = self
+ .context_server_store
+ .read(cx)
+ .configuration_for_server(&context_server_id);
let is_running = matches!(server_status, ContextServerStatus::Running);
let item_id = SharedString::from(context_server_id.0.clone());
+ let is_from_extension = server_configuration
+ .as_ref()
+ .map(|config| {
+ matches!(
+ config.as_ref(),
+ ContextServerConfiguration::Extension { .. }
+ )
+ })
+ .unwrap_or(false);
let error = if let ContextServerStatus::Error(error) = server_status.clone() {
Some(error)
@@ -484,6 +522,18 @@ impl AgentConfiguration {
let border_color = cx.theme().colors().border.opacity(0.6);
+ let (source_icon, source_tooltip) = if is_from_extension {
+ (
+ IconName::ZedMcpExtension,
+ "This MCP server was installed from an extension.",
+ )
+ } else {
+ (
+ IconName::ZedMcpCustom,
+ "This custom MCP server was installed directly.",
+ )
+ };
+
let (status_indicator, tooltip_text) = match server_status {
ContextServerStatus::Starting => (
Icon::new(IconName::LoadCircle)
@@ -511,6 +561,105 @@ impl AgentConfiguration {
),
};
+ let context_server_configuration_menu = PopoverMenu::new("context-server-config-menu")
+ .trigger_with_tooltip(
+ IconButton::new("context-server-config-menu", IconName::Settings)
+ .icon_color(Color::Muted)
+ .icon_size(IconSize::Small),
+ Tooltip::text("Open MCP server options"),
+ )
+ .anchor(Corner::TopRight)
+ .menu({
+ let fs = self.fs.clone();
+ let context_server_id = context_server_id.clone();
+ let language_registry = self.language_registry.clone();
+ let context_server_store = self.context_server_store.clone();
+ let workspace = self.workspace.clone();
+ move |window, cx| {
+ Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
+ menu.entry("Configure Server", None, {
+ let context_server_id = context_server_id.clone();
+ let language_registry = language_registry.clone();
+ let workspace = workspace.clone();
+ move |window, cx| {
+ ConfigureContextServerModal::show_modal_for_existing_server(
+ context_server_id.clone(),
+ language_registry.clone(),
+ workspace.clone(),
+ window,
+ cx,
+ )
+ .detach_and_log_err(cx);
+ }
+ })
+ .separator()
+ .entry("Uninstall", None, {
+ let fs = fs.clone();
+ let context_server_id = context_server_id.clone();
+ let context_server_store = context_server_store.clone();
+ let workspace = workspace.clone();
+ move |_, cx| {
+ let is_provided_by_extension = context_server_store
+ .read(cx)
+ .configuration_for_server(&context_server_id)
+ .as_ref()
+ .map(|config| {
+ matches!(
+ config.as_ref(),
+ ContextServerConfiguration::Extension { .. }
+ )
+ })
+ .unwrap_or(false);
+
+ let uninstall_extension_task = match (
+ is_provided_by_extension,
+ resolve_extension_for_context_server(&context_server_id, cx),
+ ) {
+ (true, Some((id, manifest))) => {
+ if extension_only_provides_context_server(manifest.as_ref())
+ {
+ ExtensionStore::global(cx).update(cx, |store, cx| {
+ store.uninstall_extension(id, cx)
+ })
+ } else {
+ workspace.update(cx, |workspace, cx| {
+ show_unable_to_uninstall_extension_with_context_server(workspace, context_server_id.clone(), cx);
+ }).log_err();
+ Task::ready(Ok(()))
+ }
+ }
+ _ => Task::ready(Ok(())),
+ };
+
+ cx.spawn({
+ let fs = fs.clone();
+ let context_server_id = context_server_id.clone();
+ async move |cx| {
+ uninstall_extension_task.await?;
+ cx.update(|cx| {
+ update_settings_file::(
+ fs.clone(),
+ cx,
+ {
+ let context_server_id =
+ context_server_id.clone();
+ move |settings, _| {
+ settings
+ .context_servers
+ .remove(&context_server_id.0);
+ }
+ },
+ )
+ })
+ }
+ })
+ .detach_and_log_err(cx);
+ }
+ })
+ }))
+ }
+ });
+
v_flex()
.id(item_id.clone())
.border_1()
@@ -556,7 +705,19 @@ impl AgentConfiguration {
.tooltip(Tooltip::text(tooltip_text))
.child(status_indicator),
)
- .child(Label::new(item_id).ml_0p5().mr_1p5())
+ .child(Label::new(item_id).ml_0p5())
+ .child(
+ div()
+ .id("extension-source")
+ .mt_0p5()
+ .mx_1()
+ .tooltip(Tooltip::text(source_tooltip))
+ .child(
+ Icon::new(source_icon)
+ .size(IconSize::Small)
+ .color(Color::Muted),
+ ),
+ )
.when(is_running, |this| {
this.child(
Label::new(if tool_count == 1 {
@@ -570,28 +731,72 @@ impl AgentConfiguration {
}),
)
.child(
- Switch::new("context-server-switch", is_running.into())
- .color(SwitchColor::Accent)
- .on_click({
- let context_server_manager = self.context_server_store.clone();
- let context_server_id = context_server_id.clone();
- move |state, _window, cx| match state {
- ToggleState::Unselected | ToggleState::Indeterminate => {
- context_server_manager.update(cx, |this, cx| {
- this.stop_server(&context_server_id, cx).log_err();
- });
- }
- ToggleState::Selected => {
- context_server_manager.update(cx, |this, cx| {
- if let Some(server) =
- this.get_server(&context_server_id)
- {
- this.start_server(server, cx).log_err();
- }
- })
- }
- }
- }),
+ h_flex()
+ .gap_1()
+ .child(context_server_configuration_menu)
+ .child(
+ Switch::new("context-server-switch", is_running.into())
+ .color(SwitchColor::Accent)
+ .on_click({
+ let context_server_manager =
+ self.context_server_store.clone();
+ let context_server_id = context_server_id.clone();
+ let fs = self.fs.clone();
+
+ move |state, _window, cx| {
+ let is_enabled = match state {
+ ToggleState::Unselected
+ | ToggleState::Indeterminate => {
+ context_server_manager.update(
+ cx,
+ |this, cx| {
+ this.stop_server(
+ &context_server_id,
+ cx,
+ )
+ .log_err();
+ },
+ );
+ false
+ }
+ ToggleState::Selected => {
+ context_server_manager.update(
+ cx,
+ |this, cx| {
+ if let Some(server) =
+ this.get_server(&context_server_id)
+ {
+ this.start_server(server, cx);
+ }
+ },
+ );
+ true
+ }
+ };
+ update_settings_file::(
+ fs.clone(),
+ cx,
+ {
+ let context_server_id =
+ context_server_id.clone();
+
+ move |settings, _| {
+ settings
+ .context_servers
+ .entry(context_server_id.0)
+ .or_insert_with(|| {
+ ContextServerSettings::Extension {
+ enabled: is_enabled,
+ settings: serde_json::json!({}),
+ }
+ })
+ .set_enabled(is_enabled);
+ }
+ },
+ );
+ }
+ }),
+ ),
),
)
.map(|parent| {
@@ -701,3 +906,92 @@ impl Render for AgentConfiguration {
)
}
}
+
+fn extension_only_provides_context_server(manifest: &ExtensionManifest) -> bool {
+ manifest.context_servers.len() == 1
+ && manifest.themes.is_empty()
+ && manifest.icon_themes.is_empty()
+ && manifest.languages.is_empty()
+ && manifest.grammars.is_empty()
+ && manifest.language_servers.is_empty()
+ && manifest.slash_commands.is_empty()
+ && manifest.indexed_docs_providers.is_empty()
+ && manifest.snippets.is_none()
+ && manifest.debug_locators.is_empty()
+}
+
+pub(crate) fn resolve_extension_for_context_server(
+ id: &ContextServerId,
+ cx: &App,
+) -> Option<(Arc, Arc)> {
+ ExtensionStore::global(cx)
+ .read(cx)
+ .installed_extensions()
+ .iter()
+ .find(|(_, entry)| entry.manifest.context_servers.contains_key(&id.0))
+ .map(|(id, entry)| (id.clone(), entry.manifest.clone()))
+}
+
+// This notification appears when trying to delete
+// an MCP server extension that not only provides
+// the server, but other things, too, like language servers and more.
+fn show_unable_to_uninstall_extension_with_context_server(
+ workspace: &mut Workspace,
+ id: ContextServerId,
+ cx: &mut App,
+) {
+ let workspace_handle = workspace.weak_handle();
+ let context_server_id = id.clone();
+
+ let status_toast = StatusToast::new(
+ format!(
+ "The {} extension provides more than just the MCP server. Proceed to uninstall anyway?",
+ id.0
+ ),
+ cx,
+ move |this, _cx| {
+ let workspace_handle = workspace_handle.clone();
+ let context_server_id = context_server_id.clone();
+
+ this.icon(ToastIcon::new(IconName::Warning).color(Color::Warning))
+ .dismiss_button(true)
+ .action("Uninstall", move |_, _cx| {
+ if let Some((extension_id, _)) =
+ resolve_extension_for_context_server(&context_server_id, _cx)
+ {
+ ExtensionStore::global(_cx).update(_cx, |store, cx| {
+ store
+ .uninstall_extension(extension_id, cx)
+ .detach_and_log_err(cx);
+ });
+
+ workspace_handle
+ .update(_cx, |workspace, cx| {
+ let fs = workspace.app_state().fs.clone();
+ cx.spawn({
+ let context_server_id = context_server_id.clone();
+ async move |_workspace_handle, cx| {
+ cx.update(|cx| {
+ update_settings_file::(
+ fs,
+ cx,
+ move |settings, _| {
+ settings
+ .context_servers
+ .remove(&context_server_id.0);
+ },
+ );
+ })?;
+ anyhow::Ok(())
+ }
+ })
+ .detach_and_log_err(cx);
+ })
+ .log_err();
+ }
+ })
+ },
+ );
+
+ workspace.toggle_status_toast(status_toast, cx);
+}
diff --git a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs b/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs
new file mode 100644
index 0000000000..30fad51cfc
--- /dev/null
+++ b/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs
@@ -0,0 +1,763 @@
+use std::{
+ sync::{Arc, Mutex},
+ time::Duration,
+};
+
+use anyhow::{Context as _, Result};
+use context_server::{ContextServerCommand, ContextServerId};
+use editor::{Editor, EditorElement, EditorStyle};
+use gpui::{
+ Animation, AnimationExt as _, AsyncWindowContext, DismissEvent, Entity, EventEmitter,
+ FocusHandle, Focusable, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle,
+ WeakEntity, percentage, prelude::*,
+};
+use language::{Language, LanguageRegistry};
+use markdown::{Markdown, MarkdownElement, MarkdownStyle};
+use notifications::status_toast::{StatusToast, ToastIcon};
+use project::{
+ context_server_store::{
+ ContextServerStatus, ContextServerStore, registry::ContextServerDescriptorRegistry,
+ },
+ project_settings::{ContextServerSettings, ProjectSettings},
+ worktree_store::WorktreeStore,
+};
+use settings::{Settings as _, update_settings_file};
+use theme::ThemeSettings;
+use ui::{KeyBinding, Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*};
+use util::ResultExt as _;
+use workspace::{ModalView, Workspace};
+
+use crate::AddContextServer;
+
+enum ConfigurationTarget {
+ New,
+ Existing {
+ id: ContextServerId,
+ command: ContextServerCommand,
+ },
+ Extension {
+ id: ContextServerId,
+ repository_url: Option,
+ installation: Option,
+ },
+}
+
+enum ConfigurationSource {
+ New {
+ editor: Entity,
+ },
+ Existing {
+ editor: Entity,
+ },
+ Extension {
+ id: ContextServerId,
+ editor: Option>,
+ repository_url: Option,
+ installation_instructions: Option>,
+ settings_validator: Option,
+ },
+}
+
+impl ConfigurationSource {
+ fn has_configuration_options(&self) -> bool {
+ !matches!(self, ConfigurationSource::Extension { editor: None, .. })
+ }
+
+ fn is_new(&self) -> bool {
+ matches!(self, ConfigurationSource::New { .. })
+ }
+
+ fn from_target(
+ target: ConfigurationTarget,
+ language_registry: Arc,
+ jsonc_language: Option>,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> Self {
+ fn create_editor(
+ json: String,
+ jsonc_language: Option>,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> Entity {
+ cx.new(|cx| {
+ let mut editor = Editor::auto_height(4, 16, window, cx);
+ editor.set_text(json, window, cx);
+ editor.set_show_gutter(false, cx);
+ editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx);
+ if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
+ buffer.update(cx, |buffer, cx| buffer.set_language(jsonc_language, cx))
+ }
+ editor
+ })
+ }
+
+ match target {
+ ConfigurationTarget::New => ConfigurationSource::New {
+ editor: create_editor(context_server_input(None), jsonc_language, window, cx),
+ },
+ ConfigurationTarget::Existing { id, command } => ConfigurationSource::Existing {
+ editor: create_editor(
+ context_server_input(Some((id, command))),
+ jsonc_language,
+ window,
+ cx,
+ ),
+ },
+ ConfigurationTarget::Extension {
+ id,
+ repository_url,
+ installation,
+ } => {
+ let settings_validator = installation.as_ref().and_then(|installation| {
+ jsonschema::validator_for(&installation.settings_schema)
+ .context("Failed to load JSON schema for context server settings")
+ .log_err()
+ });
+ let installation_instructions = installation.as_ref().map(|installation| {
+ cx.new(|cx| {
+ Markdown::new(
+ installation.installation_instructions.clone().into(),
+ Some(language_registry.clone()),
+ None,
+ cx,
+ )
+ })
+ });
+ ConfigurationSource::Extension {
+ id,
+ repository_url,
+ installation_instructions,
+ settings_validator,
+ editor: installation.map(|installation| {
+ create_editor(installation.default_settings, jsonc_language, window, cx)
+ }),
+ }
+ }
+ }
+ }
+
+ fn output(&self, cx: &mut App) -> Result<(ContextServerId, ContextServerSettings)> {
+ match self {
+ ConfigurationSource::New { editor } | ConfigurationSource::Existing { editor } => {
+ parse_input(&editor.read(cx).text(cx)).map(|(id, command)| {
+ (
+ id,
+ ContextServerSettings::Custom {
+ enabled: true,
+ command,
+ },
+ )
+ })
+ }
+ ConfigurationSource::Extension {
+ id,
+ editor,
+ settings_validator,
+ ..
+ } => {
+ let text = editor
+ .as_ref()
+ .context("No output available")?
+ .read(cx)
+ .text(cx);
+ let settings = serde_json_lenient::from_str::(&text)?;
+ if let Some(settings_validator) = settings_validator {
+ if let Err(error) = settings_validator.validate(&settings) {
+ return Err(anyhow::anyhow!(error.to_string()));
+ }
+ }
+ Ok((
+ id.clone(),
+ ContextServerSettings::Extension {
+ enabled: true,
+ settings,
+ },
+ ))
+ }
+ }
+ }
+}
+
+fn context_server_input(existing: Option<(ContextServerId, ContextServerCommand)>) -> String {
+ let (name, path, args, env) = match existing {
+ Some((id, cmd)) => {
+ let args = serde_json::to_string(&cmd.args).unwrap();
+ let env = serde_json::to_string(&cmd.env.unwrap_or_default()).unwrap();
+ (id.0.to_string(), cmd.path, args, env)
+ }
+ None => (
+ "some-mcp-server".to_string(),
+ "".to_string(),
+ "[]".to_string(),
+ "{}".to_string(),
+ ),
+ };
+
+ format!(
+ r#"{{
+ /// The name of your MCP server
+ "{name}": {{
+ "command": {{
+ /// The path to the executable
+ "path": "{path}",
+ /// The arguments to pass to the executable
+ "args": {args},
+ /// The environment variables to set for the executable
+ "env": {env}
+ }}
+ }}
+}}"#
+ )
+}
+
+fn resolve_context_server_extension(
+ id: ContextServerId,
+ worktree_store: Entity,
+ cx: &mut App,
+) -> Task