Compare commits

..

3 Commits

Author SHA1 Message Date
Kate
5dec098818 use the windows keymap 2025-08-25 16:15:11 +02:00
Kate
96460c8ba2 finish up the keymap for windows 2025-08-25 16:12:57 +02:00
Kate
fde98e3a7b Initial keymap 2025-08-19 11:36:31 +02:00
955 changed files with 32601 additions and 52691 deletions

3
.gitattributes vendored
View File

@@ -1,5 +1,2 @@
# Prevent GitHub from displaying comments within JSON files as errors.
*.json linguist-language=JSON-with-Comments
# Ensure the WSL script always has LF line endings, even on Windows
crates/zed/resources/windows/zed-wsl text eol=lf

View File

@@ -14,7 +14,7 @@ body:
### Description
<!-- Describe with sufficient detail to reproduce from a clean Zed install.
- Any code must be sufficient to reproduce (include context!)
- Include code as text, not just as a screenshot.
- Code must as text, not just as a screenshot.
- Issues with insufficient detail may be summarily closed.
-->

View File

@@ -19,27 +19,14 @@ self-hosted-runner:
- namespace-profile-16x32-ubuntu-2004-arm
- namespace-profile-32x64-ubuntu-2004-arm
# Namespace Ubuntu 22.04 (Everything else)
- namespace-profile-2x4-ubuntu-2204
- namespace-profile-4x8-ubuntu-2204
- namespace-profile-8x16-ubuntu-2204
- namespace-profile-16x32-ubuntu-2204
- namespace-profile-32x64-ubuntu-2204
# Namespace Ubuntu 24.04 (like ubuntu-latest)
- namespace-profile-2x4-ubuntu-2404
# Namespace Limited Preview
- namespace-profile-8x16-ubuntu-2004-arm-m4
- namespace-profile-8x32-ubuntu-2004-arm-m4
# Self Hosted Runners
- self-mini-macos
- self-32vcpu-windows-2022
# Disable shellcheck because it doesn't like powershell
# This should have been triggered with initial rollout of actionlint
# but https://github.com/zed-industries/zed/pull/36693
# somehow caused actionlint to actually check those windows jobs
# where previously they were being skipped. Likely caused by an
# unknown bug in actionlint where parsing of `runs-on: [ ]`
# breaks something else. (yuck)
paths:
.github/workflows/{ci,release_nightly}.yml:
ignore:
- "shellcheck"

View File

@@ -56,6 +56,7 @@ runs:
$env:COMPlus_CreateDumpDiagnostics = "1"
cargo nextest run --workspace --no-fail-fast
continue-on-error: true
- name: Analyze crash dumps
if: always()

View File

@@ -8,7 +8,7 @@ on:
jobs:
update-collab-staging-tag:
if: github.repository_owner == 'zed-industries'
runs-on: namespace-profile-2x4-ubuntu-2404
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4

View File

@@ -37,7 +37,7 @@ jobs:
run_nix: ${{ steps.filter.outputs.run_nix }}
run_actionlint: ${{ steps.filter.outputs.run_actionlint }}
runs-on:
- namespace-profile-2x4-ubuntu-2404
- ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@@ -237,7 +237,7 @@ jobs:
uses: ./.github/actions/build_docs
actionlint:
runs-on: namespace-profile-2x4-ubuntu-2404
runs-on: ubuntu-latest
if: github.repository_owner == 'zed-industries' && needs.job_spec.outputs.run_actionlint == 'true'
needs: [job_spec]
steps:
@@ -418,7 +418,7 @@ jobs:
if: |
github.repository_owner == 'zed-industries' &&
needs.job_spec.outputs.run_tests == 'true'
runs-on: [self-32vcpu-windows-2022]
runs-on: [self-hosted, Windows, X64]
steps:
- name: Environment Setup
run: |
@@ -458,7 +458,7 @@ jobs:
tests_pass:
name: Tests Pass
runs-on: namespace-profile-2x4-ubuntu-2404
runs-on: ubuntu-latest
needs:
- job_spec
- style
@@ -784,7 +784,7 @@ jobs:
bundle-windows-x64:
timeout-minutes: 120
name: Create a Windows installer
runs-on: [self-32vcpu-windows-2022]
runs-on: [self-hosted, Windows, X64]
if: contains(github.event.pull_request.labels.*.name, 'run-bundling')
# if: (startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling'))
needs: [windows_tests]

View File

@@ -12,7 +12,7 @@ on:
jobs:
danger:
if: github.repository_owner == 'zed-industries'
runs-on: namespace-profile-2x4-ubuntu-2404
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4

View File

@@ -59,7 +59,7 @@ jobs:
timeout-minutes: 60
name: Run tests on Windows
if: github.repository_owner == 'zed-industries'
runs-on: [self-32vcpu-windows-2022]
runs-on: [self-hosted, Windows, X64]
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@@ -206,6 +206,9 @@ jobs:
runs-on: github-8vcpu-ubuntu-2404
needs: tests
name: Build Zed on FreeBSD
# env:
# MYTOKEN : ${{ secrets.MYTOKEN }}
# MYTOKEN2: "value2"
steps:
- uses: actions/checkout@v4
- name: Build FreeBSD remote-server
@@ -240,6 +243,7 @@ jobs:
bundle-nix:
name: Build and cache Nix package
if: false
needs: tests
secrets: inherit
uses: ./.github/workflows/nix.yml
@@ -248,7 +252,7 @@ jobs:
timeout-minutes: 60
name: Create a Windows installer
if: github.repository_owner == 'zed-industries'
runs-on: [self-32vcpu-windows-2022]
runs-on: [self-hosted, Windows, X64]
needs: windows-tests
env:
AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }}
@@ -290,7 +294,7 @@ jobs:
update-nightly-tag:
name: Update nightly tag
if: github.repository_owner == 'zed-industries'
runs-on: namespace-profile-2x4-ubuntu-2404
runs-on: ubuntu-latest
needs:
- bundle-mac
- bundle-linux-x86

View File

@@ -12,7 +12,7 @@ jobs:
shellcheck:
name: "ShellCheck Scripts"
if: github.repository_owner == 'zed-industries'
runs-on: namespace-profile-2x4-ubuntu-2404
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4

View File

@@ -27,22 +27,6 @@ By effectively engaging with the Zed team and community early in your process, w
We plan to set aside time each week to pair program with contributors on promising pull requests in Zed. This will be an experiment. We tend to prefer pairing over async code review on our team, and we'd like to see how well it works in an open source setting. If we're finding it difficult to get on the same page with async review, we may ask you to pair with us if you're open to it. The closer a contribution is to the goals outlined in our roadmap, the more likely we'll be to spend time pairing on it.
## Mandatory PR contents
Please ensure the PR contains
- Before & after screenshots, if there are visual adjustments introduced.
Examples of visual adjustments: tree-sitter query updates, UI changes, etc.
- A disclosure of the AI assistance usage, if any was used.
Any kind of AI assistance must be disclosed in the PR, along with the extent to which AI assistance was used (e.g. docs only vs. code generation).
If the PR responses are being generated by an AI, disclose that as well.
As a small exception, trivial tab-completion doesn't need to be disclosed, as long as it's limited to single keywords or short phrases.
## Tips to improve the chances of your PR getting reviewed and merged
- Discuss your plans ahead of time with the team
@@ -65,8 +49,6 @@ If you would like to add a new icon to the Zed icon theme, [open a Discussion](h
## Bird's-eye view of Zed
We suggest you keep the [zed glossary](docs/src/development/GLOSSARY.md) at your side when starting out. It lists and explains some of the structures and terms you will see throughout the codebase.
Zed is made up of several smaller crates - let's go over those you're most likely to interact with:
- [`gpui`](/crates/gpui) is a GPU-accelerated UI framework which provides all of the building blocks for Zed. **We recommend familiarizing yourself with the root level GPUI documentation.**

360
Cargo.lock generated
View File

@@ -7,8 +7,8 @@ name = "acp_thread"
version = "0.1.0"
dependencies = [
"action_log",
"agent",
"agent-client-protocol",
"agent_settings",
"anyhow",
"buffer_diff",
"collections",
@@ -23,7 +23,6 @@ dependencies = [
"language_model",
"markdown",
"parking_lot",
"portable-pty",
"project",
"prompt_store",
"rand 0.8.5",
@@ -31,7 +30,6 @@ dependencies = [
"serde_json",
"settings",
"smol",
"task",
"tempfile",
"terminal",
"ui",
@@ -39,27 +37,6 @@ dependencies = [
"util",
"uuid",
"watch",
"which 6.0.3",
"workspace-hack",
]
[[package]]
name = "acp_tools"
version = "0.1.0"
dependencies = [
"agent-client-protocol",
"collections",
"gpui",
"language",
"markdown",
"project",
"serde",
"serde_json",
"settings",
"theme",
"ui",
"util",
"workspace",
"workspace-hack",
]
@@ -154,6 +131,7 @@ dependencies = [
"component",
"context_server",
"convert_case 0.8.0",
"feature_flags",
"fs",
"futures 0.3.31",
"git",
@@ -195,12 +173,11 @@ dependencies = [
[[package]]
name = "agent-client-protocol"
version = "0.2.0-alpha.4"
version = "0.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "603941db1d130ee275840c465b73a2312727d4acef97449550ccf033de71301f"
checksum = "160971bb53ca0b2e70ebc857c21e24eb448745f1396371015f4c59e9a9e51ed0"
dependencies = [
"anyhow",
"async-broadcast",
"futures 0.3.31",
"log",
"parking_lot",
@@ -215,12 +192,10 @@ version = "0.1.0"
dependencies = [
"acp_thread",
"action_log",
"agent",
"agent-client-protocol",
"agent_servers",
"agent_settings",
"anyhow",
"assistant_context",
"assistant_tool",
"assistant_tools",
"chrono",
@@ -230,12 +205,10 @@ dependencies = [
"collections",
"context_server",
"ctor",
"db",
"editor",
"env_logger 0.11.8",
"fs",
"futures 0.3.31",
"git",
"gpui",
"gpui_tokio",
"handlebars 4.5.0",
@@ -249,8 +222,8 @@ dependencies = [
"log",
"lsp",
"open",
"parking_lot",
"paths",
"portable-pty",
"pretty_assertions",
"project",
"prompt_store",
@@ -261,14 +234,11 @@ dependencies = [
"serde_json",
"settings",
"smol",
"sqlez",
"task",
"telemetry",
"tempfile",
"terminal",
"text",
"theme",
"thiserror 2.0.12",
"tree-sitter-rust",
"ui",
"unindent",
@@ -276,10 +246,10 @@ dependencies = [
"uuid",
"watch",
"web_search",
"which 6.0.3",
"workspace-hack",
"worktree",
"zlog",
"zstd",
]
[[package]]
@@ -287,39 +257,37 @@ name = "agent_servers"
version = "0.1.0"
dependencies = [
"acp_thread",
"acp_tools",
"action_log",
"agent-client-protocol",
"agent_settings",
"agentic-coding-protocol",
"anyhow",
"client",
"collections",
"context_server",
"env_logger 0.11.8",
"fs",
"futures 0.3.31",
"gpui",
"gpui_tokio",
"indoc",
"itertools 0.14.0",
"language",
"language_model",
"language_models",
"libc",
"log",
"nix 0.29.0",
"node_runtime",
"paths",
"project",
"reqwest_client",
"rand 0.8.5",
"schemars",
"semver",
"serde",
"serde_json",
"settings",
"smol",
"strum 0.27.1",
"tempfile",
"thiserror 2.0.12",
"ui",
"util",
"uuid",
"watch",
"which 6.0.3",
"workspace-hack",
@@ -401,7 +369,6 @@ dependencies = [
"parking_lot",
"paths",
"picker",
"postage",
"pretty_assertions",
"project",
"prompt_store",
@@ -416,7 +383,6 @@ dependencies = [
"serde_json",
"serde_json_lenient",
"settings",
"shlex",
"smol",
"streaming_diff",
"task",
@@ -442,6 +408,24 @@ dependencies = [
"zed_actions",
]
[[package]]
name = "agentic-coding-protocol"
version = "0.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e6ae951b36fa2f8d9dd6e1af6da2fcaba13d7c866cf6a9e65deda9dc6c5fe4"
dependencies = [
"anyhow",
"chrono",
"derive_more 2.0.1",
"futures 0.3.31",
"log",
"parking_lot",
"schemars",
"semver",
"serde",
"serde_json",
]
[[package]]
name = "ahash"
version = "0.7.8"
@@ -508,7 +492,7 @@ dependencies = [
"parking_lot",
"piper",
"polling",
"regex-automata",
"regex-automata 0.4.9",
"rustix-openpty",
"serde",
"signal-hook",
@@ -857,7 +841,7 @@ dependencies = [
"anyhow",
"async-trait",
"collections",
"derive_more",
"derive_more 0.99.19",
"extension",
"futures 0.3.31",
"gpui",
@@ -920,7 +904,7 @@ dependencies = [
"clock",
"collections",
"ctor",
"derive_more",
"derive_more 0.99.19",
"gpui",
"icons",
"indoc",
@@ -957,7 +941,7 @@ dependencies = [
"cloud_llm_client",
"collections",
"component",
"derive_more",
"derive_more 0.99.19",
"diffy",
"editor",
"feature_flags",
@@ -1383,11 +1367,10 @@ version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"derive_more 0.99.19",
"gpui",
"parking_lot",
"rodio",
"schemars",
"serde",
"settings",
"util",
"workspace-hack",
]
@@ -2458,7 +2441,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
dependencies = [
"memchr",
"regex-automata",
"regex-automata 0.4.9",
"serde",
]
@@ -3070,7 +3053,7 @@ dependencies = [
"cocoa 0.26.0",
"collections",
"credentials_provider",
"derive_more",
"derive_more 0.99.19",
"feature_flags",
"fs",
"futures 0.3.31",
@@ -3502,7 +3485,7 @@ name = "command_palette_hooks"
version = "0.1.0"
dependencies = [
"collections",
"derive_more",
"derive_more 0.99.19",
"gpui",
"workspace-hack",
]
@@ -3883,7 +3866,7 @@ dependencies = [
"jni",
"js-sys",
"libc",
"mach2 0.4.2",
"mach2",
"ndk",
"ndk-context",
"num-derive",
@@ -4033,7 +4016,7 @@ checksum = "031ed29858d90cfdf27fe49fae28028a1f20466db97962fa2f4ea34809aeebf3"
dependencies = [
"cfg-if",
"libc",
"mach2 0.4.2",
"mach2",
]
[[package]]
@@ -4045,7 +4028,7 @@ dependencies = [
"cfg-if",
"crash-context",
"libc",
"mach2 0.4.2",
"mach2",
"parking_lot",
]
@@ -4053,17 +4036,14 @@ dependencies = [
name = "crashes"
version = "0.1.0"
dependencies = [
"bincode",
"crash-handler",
"log",
"mach2 0.5.0",
"minidumper",
"paths",
"release_channel",
"serde",
"serde_json",
"smol",
"system_specs",
"workspace-hack",
]
@@ -4665,6 +4645,27 @@ dependencies = [
"syn 2.0.101",
]
[[package]]
name = "derive_more"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
dependencies = [
"derive_more-impl",
]
[[package]]
name = "derive_more-impl"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"unicode-xid",
]
[[package]]
name = "derive_refineable"
version = "0.1.0"
@@ -4685,6 +4686,7 @@ dependencies = [
"component",
"ctor",
"editor",
"futures 0.3.31",
"gpui",
"indoc",
"language",
@@ -4733,7 +4735,7 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b545b8c50194bdd008283985ab0b31dba153cfd5b3066a92770634fbc0d7d291"
dependencies = [
"nu-ansi-term",
"nu-ansi-term 0.50.1",
]
[[package]]
@@ -5632,8 +5634,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2"
dependencies = [
"bit-set 0.5.3",
"regex-automata",
"regex-syntax",
"regex-automata 0.4.9",
"regex-syntax 0.8.5",
]
[[package]]
@@ -5643,8 +5645,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298"
dependencies = [
"bit-set 0.8.0",
"regex-automata",
"regex-syntax",
"regex-automata 0.4.9",
"regex-syntax 0.8.5",
]
[[package]]
@@ -5721,10 +5723,14 @@ dependencies = [
name = "feedback"
version = "0.1.0"
dependencies = [
"client",
"editor",
"gpui",
"human_bytes",
"menu",
"system_specs",
"release_channel",
"serde",
"sysinfo",
"ui",
"urlencoding",
"util",
@@ -6400,7 +6406,7 @@ dependencies = [
"askpass",
"async-trait",
"collections",
"derive_more",
"derive_more 0.99.19",
"futures 0.3.31",
"git2",
"gpui",
@@ -7294,8 +7300,8 @@ dependencies = [
"aho-corasick",
"bstr",
"log",
"regex-automata",
"regex-syntax",
"regex-automata 0.4.9",
"regex-syntax 0.8.5",
]
[[package]]
@@ -7430,7 +7436,7 @@ dependencies = [
"core-video",
"cosmic-text",
"ctor",
"derive_more",
"derive_more 0.99.19",
"embed-resource",
"env_logger 0.11.8",
"etagere",
@@ -7518,7 +7524,6 @@ dependencies = [
name = "gpui_tokio"
version = "0.1.0"
dependencies = [
"anyhow",
"gpui",
"tokio",
"util",
@@ -7955,7 +7960,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"bytes 1.10.1",
"derive_more",
"derive_more 0.99.19",
"futures 0.3.31",
"http 1.3.1",
"http-body 1.0.1",
@@ -8300,7 +8305,7 @@ dependencies = [
"globset",
"log",
"memchr",
"regex-automata",
"regex-automata 0.4.9",
"same-file",
"walkdir",
"winapi-util",
@@ -8467,7 +8472,6 @@ dependencies = [
"theme",
"ui",
"util",
"util_macros",
"workspace",
"workspace-hack",
"zed_actions",
@@ -8899,7 +8903,7 @@ dependencies = [
"percent-encoding",
"referencing",
"regex",
"regex-syntax",
"regex-syntax 0.8.5",
"reqwest 0.12.15 (registry+https://github.com/rust-lang/crates.io-index)",
"serde",
"serde_json",
@@ -8952,44 +8956,6 @@ dependencies = [
"uuid",
]
[[package]]
name = "keymap_editor"
version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"command_palette",
"component",
"db",
"editor",
"fs",
"fuzzy",
"gpui",
"itertools 0.14.0",
"language",
"log",
"menu",
"notifications",
"paths",
"project",
"search",
"serde",
"serde_json",
"settings",
"telemetry",
"tempfile",
"theme",
"tree-sitter-json",
"tree-sitter-rust",
"ui",
"ui_input",
"util",
"vim",
"workspace",
"workspace-hack",
"zed_actions",
]
[[package]]
name = "khronos-egl"
version = "6.0.0"
@@ -9250,7 +9216,6 @@ dependencies = [
"language",
"lsp",
"project",
"proto",
"release_channel",
"serde_json",
"settings",
@@ -9643,7 +9608,6 @@ version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"audio",
"collections",
"core-foundation 0.10.0",
"core-video",
@@ -9666,7 +9630,6 @@ dependencies = [
"scap",
"serde",
"serde_json",
"settings",
"sha2",
"simplelog",
"smallvec",
@@ -9739,7 +9702,7 @@ dependencies = [
"lazy_static",
"proc-macro2",
"quote",
"regex-syntax",
"regex-syntax 0.8.5",
"rustc_version",
"syn 2.0.101",
]
@@ -9811,7 +9774,7 @@ dependencies = [
[[package]]
name = "lsp-types"
version = "0.95.1"
source = "git+https://github.com/zed-industries/lsp-types?rev=0874f8742fe55b4dc94308c1e3c0069710d8eeaf#0874f8742fe55b4dc94308c1e3c0069710d8eeaf"
source = "git+https://github.com/zed-industries/lsp-types?rev=39f629bdd03d59abd786ed9fc27e8bca02c0c0ec#39f629bdd03d59abd786ed9fc27e8bca02c0c0ec"
dependencies = [
"bitflags 1.3.2",
"serde",
@@ -9897,15 +9860,6 @@ dependencies = [
"libc",
]
[[package]]
name = "mach2"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea"
dependencies = [
"libc",
]
[[package]]
name = "malloc_buf"
version = "0.0.6"
@@ -9954,11 +9908,9 @@ dependencies = [
"editor",
"fs",
"gpui",
"html5ever 0.27.0",
"language",
"linkify",
"log",
"markup5ever_rcdom",
"pretty_assertions",
"pulldown-cmark 0.12.2",
"settings",
@@ -10019,11 +9971,11 @@ dependencies = [
[[package]]
name = "matchers"
version = "0.2.0"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
dependencies = [
"regex-automata",
"regex-automata 0.1.10",
]
[[package]]
@@ -10244,7 +10196,7 @@ dependencies = [
"goblin",
"libc",
"log",
"mach2 0.4.2",
"mach2",
"memmap2",
"memoffset",
"minidump-common",
@@ -10724,6 +10676,16 @@ dependencies = [
"winapi",
]
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
dependencies = [
"overload",
"winapi",
]
[[package]]
name = "nu-ansi-term"
version = "0.50.1"
@@ -11417,6 +11379,12 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e"
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "p256"
version = "0.11.1"
@@ -11639,12 +11607,6 @@ dependencies = [
"hmac",
]
[[package]]
name = "pciid-parser"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0008e816fcdaf229cdd540e9b6ca2dc4a10d65c31624abb546c6420a02846e61"
[[package]]
name = "pem"
version = "3.0.5"
@@ -13407,8 +13369,17 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
"regex-automata 0.4.9",
"regex-syntax 0.8.5",
]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
dependencies = [
"regex-syntax 0.6.29",
]
[[package]]
@@ -13419,7 +13390,7 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
"regex-syntax 0.8.5",
]
[[package]]
@@ -13428,6 +13399,12 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a"
[[package]]
name = "regex-syntax"
version = "0.6.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
name = "regex-syntax"
version = "0.8.5"
@@ -13530,7 +13507,6 @@ dependencies = [
"smol",
"sysinfo",
"telemetry_events",
"thiserror 2.0.12",
"toml 0.8.20",
"unindent",
"util",
@@ -14370,10 +14346,12 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe8c9d1c68d67dd9f97ecbc6f932b60eb289c5dbddd8aa1405484a8fd2fcd984"
dependencies = [
"chrono",
"dyn-clone",
"indexmap",
"ref-cast",
"schemars_derive",
"semver",
"serde",
"serde_json",
]
@@ -14866,8 +14844,6 @@ dependencies = [
"serde_derive",
"serde_json",
"serde_json_lenient",
"serde_path_to_error",
"settings_ui_macros",
"smallvec",
"tree-sitter",
"tree-sitter-json",
@@ -14903,29 +14879,39 @@ name = "settings_ui"
version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"command_palette",
"command_palette_hooks",
"component",
"db",
"editor",
"feature_flags",
"fs",
"fuzzy",
"gpui",
"itertools 0.14.0",
"language",
"log",
"menu",
"notifications",
"paths",
"project",
"search",
"serde",
"serde_json",
"settings",
"smallvec",
"telemetry",
"tempfile",
"theme",
"tree-sitter-json",
"tree-sitter-rust",
"ui",
"ui_input",
"util",
"vim",
"workspace",
"workspace-hack",
]
[[package]]
name = "settings_ui_macros"
version = "0.1.0"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.101",
"workspace-hack",
"zed_actions",
]
[[package]]
@@ -16141,21 +16127,6 @@ dependencies = [
"winx",
]
[[package]]
name = "system_specs"
version = "0.1.0"
dependencies = [
"anyhow",
"client",
"gpui",
"human_bytes",
"pciid-parser",
"release_channel",
"serde",
"sysinfo",
"workspace-hack",
]
[[package]]
name = "tab_switcher"
version = "0.1.0"
@@ -16449,7 +16420,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"derive_more",
"derive_more 0.99.19",
"fs",
"futures 0.3.31",
"gpui",
@@ -16741,7 +16712,6 @@ dependencies = [
"db",
"gpui",
"http_client",
"keymap_editor",
"notifications",
"pretty_assertions",
"project",
@@ -16750,6 +16720,7 @@ dependencies = [
"schemars",
"serde",
"settings",
"settings_ui",
"smallvec",
"story",
"telemetry",
@@ -17118,14 +17089,14 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
version = "0.3.20"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
dependencies = [
"matchers",
"nu-ansi-term",
"nu-ansi-term 0.46.0",
"once_cell",
"regex-automata",
"regex",
"serde",
"serde_json",
"sharded-slab",
@@ -17156,7 +17127,7 @@ checksum = "a7cf18d43cbf0bfca51f657132cc616a5097edc4424d538bae6fa60142eaf9f0"
dependencies = [
"cc",
"regex",
"regex-syntax",
"regex-syntax 0.8.5",
"serde_json",
"streaming-iterator",
"tree-sitter-language",
@@ -17186,7 +17157,8 @@ dependencies = [
[[package]]
name = "tree-sitter-cpp"
version = "0.23.4"
source = "git+https://github.com/tree-sitter/tree-sitter-cpp?rev=5cb9b693cfd7bfacab1d9ff4acac1a4150700609#5cb9b693cfd7bfacab1d9ff4acac1a4150700609"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df2196ea9d47b4ab4a31b9297eaa5a5d19a0b121dceb9f118f6790ad0ab94743"
dependencies = [
"cc",
"tree-sitter-language",
@@ -18314,7 +18286,7 @@ dependencies = [
"indexmap",
"libc",
"log",
"mach2 0.4.2",
"mach2",
"memfd",
"object",
"once_cell",
@@ -19789,6 +19761,7 @@ dependencies = [
"any_vec",
"anyhow",
"async-recursion",
"bincode",
"call",
"client",
"clock",
@@ -19807,7 +19780,6 @@ dependencies = [
"node_runtime",
"parking_lot",
"postage",
"pretty_assertions",
"project",
"remote",
"schemars",
@@ -19954,8 +19926,8 @@ dependencies = [
"rand_core 0.6.4",
"regalloc2",
"regex",
"regex-automata",
"regex-syntax",
"regex-automata 0.4.9",
"regex-syntax 0.8.5",
"ring",
"rust_decimal",
"rustc-hash 1.1.0",
@@ -19963,6 +19935,7 @@ dependencies = [
"rustix 1.0.7",
"rustls 0.23.26",
"rustls-webpki 0.103.1",
"schemars",
"scopeguard",
"sea-orm",
"sea-query-binder",
@@ -20137,9 +20110,9 @@ dependencies = [
[[package]]
name = "xcb"
version = "1.6.0"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f07c123b796139bfe0603e654eaf08e132e52387ba95b252c78bad3640ba37ea"
checksum = "f1e2f212bb1a92cd8caac8051b829a6582ede155ccb60b5d5908b81b100952be"
dependencies = [
"bitflags 1.3.2",
"libc",
@@ -20396,9 +20369,8 @@ dependencies = [
[[package]]
name = "zed"
version = "0.203.0"
version = "0.201.0"
dependencies = [
"acp_tools",
"activity_indicator",
"agent",
"agent_servers",
@@ -20414,7 +20386,6 @@ dependencies = [
"auto_update",
"auto_update_ui",
"backtrace",
"bincode",
"breadcrumbs",
"call",
"channel",
@@ -20460,7 +20431,6 @@ dependencies = [
"itertools 0.14.0",
"jj_ui",
"journal",
"keymap_editor",
"language",
"language_extension",
"language_model",
@@ -20514,7 +20484,6 @@ dependencies = [
"supermaven",
"svg_preview",
"sysinfo",
"system_specs",
"tab_switcher",
"task",
"tasks_ui",
@@ -20591,7 +20560,7 @@ dependencies = [
[[package]]
name = "zed_html"
version = "0.2.2"
version = "0.2.1"
dependencies = [
"zed_extension_api 0.1.0",
]
@@ -20790,7 +20759,6 @@ dependencies = [
"gpui",
"http_client",
"indoc",
"itertools 0.14.0",
"language",
"language_model",
"log",
@@ -20805,7 +20773,6 @@ dependencies = [
"serde",
"serde_json",
"settings",
"strum 0.27.1",
"telemetry",
"telemetry_events",
"theme",
@@ -20813,6 +20780,7 @@ dependencies = [
"tree-sitter-go",
"tree-sitter-rust",
"ui",
"unindent",
"util",
"uuid",
"workspace",

View File

@@ -1,7 +1,6 @@
[workspace]
resolver = "2"
members = [
"crates/acp_tools",
"crates/acp_thread",
"crates/action_log",
"crates/activity_indicator",
@@ -54,8 +53,6 @@ members = [
"crates/deepseek",
"crates/diagnostics",
"crates/docs_preprocessor",
"crates/edit_prediction",
"crates/edit_prediction_button",
"crates/editor",
"crates/eval",
"crates/explorer_command_injector",
@@ -84,12 +81,13 @@ members = [
"crates/http_client_tls",
"crates/icons",
"crates/image_viewer",
"crates/edit_prediction",
"crates/edit_prediction_button",
"crates/inspector_ui",
"crates/install_cli",
"crates/jj",
"crates/jj_ui",
"crates/journal",
"crates/keymap_editor",
"crates/language",
"crates/language_extension",
"crates/language_model",
@@ -147,7 +145,6 @@ members = [
"crates/settings",
"crates/settings_profile_selector",
"crates/settings_ui",
"crates/settings_ui_macros",
"crates/snippet",
"crates/snippet_provider",
"crates/snippets_ui",
@@ -160,7 +157,6 @@ members = [
"crates/supermaven",
"crates/supermaven_api",
"crates/svg_preview",
"crates/system_specs",
"crates/tab_switcher",
"crates/task",
"crates/tasks_ui",
@@ -230,7 +226,6 @@ edition = "2024"
# Workspace member crates
#
acp_tools = { path = "crates/acp_tools" }
acp_thread = { path = "crates/acp_thread" }
action_log = { path = "crates/action_log" }
agent = { path = "crates/agent" }
@@ -316,7 +311,6 @@ install_cli = { path = "crates/install_cli" }
jj = { path = "crates/jj" }
jj_ui = { path = "crates/jj_ui" }
journal = { path = "crates/journal" }
keymap_editor = { path = "crates/keymap_editor" }
language = { path = "crates/language" }
language_extension = { path = "crates/language_extension" }
language_model = { path = "crates/language_model" }
@@ -376,7 +370,6 @@ semantic_version = { path = "crates/semantic_version" }
session = { path = "crates/session" }
settings = { path = "crates/settings" }
settings_ui = { path = "crates/settings_ui" }
settings_ui_macros = { path = "crates/settings_ui_macros" }
snippet = { path = "crates/snippet" }
snippet_provider = { path = "crates/snippet_provider" }
snippets_ui = { path = "crates/snippets_ui" }
@@ -388,7 +381,6 @@ streaming_diff = { path = "crates/streaming_diff" }
sum_tree = { path = "crates/sum_tree" }
supermaven = { path = "crates/supermaven" }
supermaven_api = { path = "crates/supermaven_api" }
system_specs = { path = "crates/system_specs" }
tab_switcher = { path = "crates/tab_switcher" }
task = { path = "crates/task" }
tasks_ui = { path = "crates/tasks_ui" }
@@ -430,7 +422,8 @@ zlog_settings = { path = "crates/zlog_settings" }
# External crates
#
agent-client-protocol = { version = "0.2.0-alpha.4", features = ["unstable"]}
agentic-coding-protocol = "0.0.10"
agent-client-protocol = "0.0.26"
aho-corasick = "1.1"
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
any_vec = "0.14"
@@ -457,7 +450,6 @@ aws-sdk-bedrockruntime = { version = "1.80.0", features = [
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"
bincode = "1.2.1"
bitflags = "2.6.0"
blade-graphics = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
@@ -501,7 +493,6 @@ handlebars = "4.3"
heck = "0.5"
heed = { version = "0.21.0", features = ["read-txn-no-tls"] }
hex = "0.4.3"
human_bytes = "0.4.1"
html5ever = "0.27.0"
http = "1.1"
http-body = "1.0"
@@ -523,8 +514,7 @@ libc = "0.2"
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
linkify = "0.10.0"
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "0874f8742fe55b4dc94308c1e3c0069710d8eeaf" }
mach2 = "0.5"
lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "39f629bdd03d59abd786ed9fc27e8bca02c0c0ec" }
markup5ever_rcdom = "0.3.0"
metal = "0.29"
minidumper = "0.8"
@@ -541,7 +531,6 @@ palette = { version = "0.7.5", default-features = false, features = ["std"] }
parking_lot = "0.12.1"
partial-json-fixer = "0.5.3"
parse_int = "0.9"
pciid-parser = "0.8.0"
pathdiff = "0.2"
pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
@@ -592,7 +581,6 @@ serde_json_lenient = { version = "0.2", features = [
"preserve_order",
"raw_value",
] }
serde_path_to_error = "0.1.17"
serde_repr = "0.1"
serde_urlencoded = "0.7"
sha2 = "0.10"
@@ -629,7 +617,7 @@ tower-http = "0.4.4"
tree-sitter = { version = "0.25.6", features = ["wasm"] }
tree-sitter-bash = "0.25.0"
tree-sitter-c = "0.23"
tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "5cb9b693cfd7bfacab1d9ff4acac1a4150700609" }
tree-sitter-cpp = "0.23"
tree-sitter-css = "0.23"
tree-sitter-diff = "0.1.0"
tree-sitter-elixir = "0.3"
@@ -696,7 +684,6 @@ features = [
"Win32_Graphics_Dxgi_Common",
"Win32_Graphics_Gdi",
"Win32_Graphics_Imaging",
"Win32_Graphics_Hlsl",
"Win32_Networking_WinSock",
"Win32_Security",
"Win32_Security_Credentials",
@@ -814,33 +801,47 @@ unexpected_cfgs = { level = "allow" }
dbg_macro = "deny"
todo = "deny"
# This is not a style lint, see https://github.com/rust-lang/rust-clippy/pull/15454
# Remove when the lint gets promoted to `suspicious`.
declare_interior_mutable_const = "deny"
redundant_clone = "deny"
# We currently do not restrict any style rules
# as it slows down shipping code to Zed.
#
# Running ./script/clippy can take several minutes, and so it's
# common to skip that step and let CI do it. Any unexpected failures
# (which also take minutes to discover) thus require switching back
# to an old branch, manual fixing, and re-pushing.
#
# In the future we could improve this by either making sure
# Zed can surface clippy errors in diagnostics (in addition to the
# rust-analyzer errors), or by having CI fix style nits automatically.
style = { level = "allow", priority = -1 }
# Individual rules that have violations in the codebase:
type_complexity = "allow"
let_underscore_future = "allow"
# Motivation: We use `vec![a..b]` a lot when dealing with ranges in text, so
# warning on this rule produces a lot of noise.
single_range_in_vec_init = "allow"
# These are all of the rules that currently have violations in the Zed
# codebase.
#
# We'll want to drive this list down by either:
# 1. fixing violations of the rule and begin enforcing it
# 2. deciding we want to allow the rule permanently, at which point
# we should codify that separately above.
#
# This list shouldn't be added to; it should only get shorter.
# =============================================================================
# There are a bunch of rules currently failing in the `style` group, so
# allow all of those, for now.
style = { level = "allow", priority = -1 }
# Temporary list of style lints that we've fixed so far.
iter_cloned_collect = "warn"
iter_next_slice = "warn"
iter_nth = "warn"
iter_nth_zero = "warn"
iter_skip_next = "warn"
module_inception = { level = "deny" }
question_mark = { level = "deny" }
redundant_closure = { level = "deny" }
declare_interior_mutable_const = { level = "deny" }
needless_borrow = { level = "warn"}
# Individual rules that have violations in the codebase:
type_complexity = "allow"
# We often return trait objects from `new` functions.
new_ret_no_self = { level = "allow" }
# We have a few `next` functions that differ in lifetimes
# compared to Iterator::next. Yet, clippy complains about those.
should_implement_trait = { level = "allow" }
let_underscore_future = "allow"
# It doesn't make sense to implement `Default` unilaterally.
new_without_default = "allow"
# in Rust it can be very tedious to reduce argument count without
# running afoul of the borrow checker.
too_many_arguments = "allow"

View File

@@ -1,2 +0,0 @@
postgrest_llm: postgrest crates/collab/postgrest_llm.conf
website: cd ../zed.dev; npm run dev -- --port=3000

View File

@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.37288 4.48506L7.43539 10.6638C7.43539 10.9365 7.54373 11.1981 7.73655 11.3909C7.92938 11.5837 8.19092 11.6921 8.46362 11.6921C8.73632 11.6921 8.99785 11.5837 9.19068 11.3909C9.38351 11.1981 9.49184 10.9366 9.49184 10.6638L9.42933 4.48506C9.42933 3.93975 9.2127 3.41678 8.82711 3.03119C8.44152 2.6456 7.91855 2.42898 7.37324 2.42898C6.82794 2.42898 6.30496 2.6456 5.91937 3.03119C5.53378 3.41678 5.31716 3.93975 5.31716 4.48506L5.37968 10.6384C5.37636 11.0455 5.45368 11.4492 5.60718 11.8263C5.76067 12.2034 5.98731 12.5463 6.27401 12.8354C6.56071 13.1244 6.9018 13.3538 7.27761 13.5104C7.65341 13.667 8.0565 13.7476 8.46362 13.7476C8.87073 13.7476 9.27382 13.667 9.64963 13.5104C10.0254 13.3538 10.3665 13.1244 10.6532 12.8354C10.9399 12.5463 11.1666 12.2034 11.3201 11.8263C11.4736 11.4492 11.5509 11.0455 11.5476 10.6384L11.485 4.48506" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1,4 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.486 6.2H7.24795C6.66895 6.2 6.19995 6.669 6.19995 7.248V12.486C6.19995 13.064 6.66895 13.533 7.24795 13.533H12.486C13.064 13.533 13.533 13.064 13.533 12.486V7.248C13.533 6.669 13.064 6.2 12.486 6.2Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.91712 10.203C3.63951 10.2022 3.37351 10.0915 3.1773 9.89511C2.98109 9.69872 2.87064 9.43261 2.87012 9.155V3.917C2.87091 3.63956 2.98147 3.37371 3.17765 3.17753C3.37383 2.98135 3.63968 2.87079 3.91712 2.87H9.15512C9.43273 2.87053 9.69883 2.98097 9.89523 3.17718C10.0916 3.37339 10.2023 3.63939 10.2031 3.917" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M12.286 6H7.048C6.469 6 6 6.469 6 7.048v5.238c0 .578.469 1.047 1.048 1.047h5.238c.578 0 1.047-.469 1.047-1.047V7.048c0-.579-.469-1.048-1.047-1.048Z"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M3.714 10a1.05 1.05 0 0 1-1.047-1.048V3.714a1.05 1.05 0 0 1 1.047-1.047h5.238A1.05 1.05 0 0 1 10 3.714"/></svg>

Before

Width:  |  Height:  |  Size: 802 B

After

Width:  |  Height:  |  Size: 515 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-list-filter-icon lucide-list-filter"><path d="M3 6h18"/><path d="M7 12h10"/><path d="M10 18h4"/></svg>

Before

Width:  |  Height:  |  Size: 305 B

View File

@@ -1,3 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.66699 8H10.667M2.66699 4H13.333M2.66699 12H7.99999" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M2.667 8h8M2.667 4h10.666M2.667 12H8"/></svg>

Before

Width:  |  Height:  |  Size: 251 B

After

Width:  |  Height:  |  Size: 210 B

View File

@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.333 10H8M13.333 6H2.66701" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 227 B

View File

@@ -1,6 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.5 5.50621L10.5941 3.41227C10.8585 3.14798 11.217 2.99953 11.5908 2.99957C11.9646 2.99962 12.3231 3.14816 12.5874 3.41252C12.8517 3.67688 13.0001 4.03541 13.0001 4.40922C13.0001 4.78304 12.8515 5.14152 12.5872 5.40582L10.493 7.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.50789 8.5L3.92098 10.0869C3.80488 10.2027 3.71903 10.3452 3.67097 10.5019L3.01047 12.678C2.99754 12.7212 2.99657 12.7672 3.00764 12.8109C3.01872 12.8547 3.04143 12.8946 3.07337 12.9265C3.1053 12.9584 3.14528 12.981 3.18905 12.992C3.23282 13.003 3.27875 13.002 3.32197 12.989L5.49849 12.329C5.65508 12.2813 5.79758 12.196 5.91349 12.0805L7.49184 10.5019" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 5L11 7" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 3L13 13" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,4 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 12.375H13" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 11.125L6.75003 7.375L3 3.62497" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 336 B

View File

@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.9526 10.2625C10.0833 9.62316 9.9526 9.60566 10.9315 8.47946C11.5008 7.82461 11.9105 7.07306 11.9105 6.242C11.9105 5.22472 11.4979 4.2491 10.7637 3.52978C10.0294 2.81046 9.03338 2.40634 7.99491 2.40634C6.95644 2.40634 5.96051 2.81046 5.22619 3.52978C4.49189 4.2491 4.07935 5.22472 4.07935 6.242C4.07935 6.88128 4.20987 7.64842 5.05825 8.47946C6.03714 9.62442 5.90666 9.62316 6.03718 10.2625M9.9526 10.2625V12.3893C9.9526 13.0544 9.40223 13.5937 8.72319 13.5937H7.26665C6.58761 13.5937 6.03714 13.0544 6.03714 12.3893L6.03718 10.2625M9.9526 10.2625H7.99491H6.03718" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.95231 10.2159C10.0803 9.58974 9.95231 9.57261 10.9111 8.46959C11.4686 7.82822 11.8699 7.09214 11.8699 6.27818C11.8699 5.28184 11.4658 4.32631 10.7467 3.62179C10.0275 2.91728 9.05201 2.52148 8.03492 2.52148C7.01782 2.52148 6.04239 2.91728 5.32319 3.62179C4.604 4.32631 4.19995 5.28184 4.19995 6.27818C4.19995 6.9043 4.32779 7.65565 5.1587 8.46959C6.11744 9.59098 5.98965 9.58974 6.11748 10.2159M9.95231 10.2159V12.2989C9.95231 12.9504 9.41327 13.4786 8.7482 13.4786H7.32165C6.65658 13.4786 6.11744 12.9504 6.11744 12.2989L6.11748 10.2159M9.95231 10.2159H8.03492H6.11748" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 762 B

After

Width:  |  Height:  |  Size: 768 B

View File

@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 2C11.3137 2 14 4.68629 14 8C14 11.3137 11.3137 14 8 14C4.68629 14 2 11.3137 2 8C2 4.68629 4.68629 2 8 2ZM10.4238 5.57617C10.1895 5.34187 9.81049 5.3419 9.57617 5.57617L8 7.15234L6.42383 5.57617C6.18953 5.34187 5.81049 5.3419 5.57617 5.57617C5.34186 5.81049 5.34186 6.18951 5.57617 6.42383L7.15234 8L5.57617 9.57617C5.34186 9.81049 5.34186 10.1895 5.57617 10.4238C5.81049 10.6581 6.18954 10.6581 6.42383 10.4238L8 8.84766L9.57617 10.4238C9.81049 10.6581 10.1895 10.6581 10.4238 10.4238C10.6581 10.1895 10.658 9.81048 10.4238 9.57617L8.84766 8L10.4238 6.42383C10.6581 6.18954 10.658 5.81048 10.4238 5.57617Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 737 B

View File

@@ -1,27 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 8.75V10.5C8.93097 10.5 8.06903 10.5 6 10.5V10L11 6V5.5H6V7.25" stroke="black" stroke-width="1.5"/>
<path d="M2 8.5C2.27614 8.5 2.5 8.27614 2.5 8C2.5 7.72386 2.27614 7.5 2 7.5C1.72386 7.5 1.5 7.72386 1.5 8C1.5 8.27614 1.72386 8.5 2 8.5Z" fill="black"/>
<path opacity="0.6" d="M2.99976 6.33002C3.2759 6.33002 3.49976 6.10616 3.49976 5.83002C3.49976 5.55387 3.2759 5.33002 2.99976 5.33002C2.72361 5.33002 2.49976 5.55387 2.49976 5.83002C2.49976 6.10616 2.72361 6.33002 2.99976 6.33002Z" fill="black"/>
<path opacity="0.6" d="M2.99976 10.66C3.2759 10.66 3.49976 10.4361 3.49976 10.16C3.49976 9.88383 3.2759 9.65997 2.99976 9.65997C2.72361 9.65997 2.49976 9.88383 2.49976 10.16C2.49976 10.4361 2.72361 10.66 2.99976 10.66Z" fill="black"/>
<path d="M15 8.5C15.2761 8.5 15.5 8.27614 15.5 8C15.5 7.72386 15.2761 7.5 15 7.5C14.7239 7.5 14.5 7.72386 14.5 8C14.5 8.27614 14.7239 8.5 15 8.5Z" fill="black"/>
<path opacity="0.6" d="M14 6.33002C14.2761 6.33002 14.5 6.10616 14.5 5.83002C14.5 5.55387 14.2761 5.33002 14 5.33002C13.7239 5.33002 13.5 5.55387 13.5 5.83002C13.5 6.10616 13.7239 6.33002 14 6.33002Z" fill="black"/>
<path opacity="0.6" d="M14 10.66C14.2761 10.66 14.5 10.4361 14.5 10.16C14.5 9.88383 14.2761 9.65997 14 9.65997C13.7239 9.65997 13.5 9.88383 13.5 10.16C13.5 10.4361 13.7239 10.66 14 10.66Z" fill="black"/>
<path d="M8.49219 2C8.76833 2 8.99219 1.77614 8.99219 1.5C8.99219 1.22386 8.76833 1 8.49219 1C8.21605 1 7.99219 1.22386 7.99219 1.5C7.99219 1.77614 8.21605 2 8.49219 2Z" fill="black"/>
<path opacity="0.6" d="M6 3C6.27614 3 6.5 2.77614 6.5 2.5C6.5 2.22386 6.27614 2 6 2C5.72386 2 5.5 2.22386 5.5 2.5C5.5 2.77614 5.72386 3 6 3Z" fill="black"/>
<path d="M4 4C4.27614 4 4.5 3.77614 4.5 3.5C4.5 3.22386 4.27614 3 4 3C3.72386 3 3.5 3.22386 3.5 3.5C3.5 3.77614 3.72386 4 4 4Z" fill="black"/>
<path d="M3.99976 13C4.2759 13 4.49976 12.7761 4.49976 12.5C4.49976 12.2239 4.2759 12 3.99976 12C3.72361 12 3.49976 12.2239 3.49976 12.5C3.49976 12.7761 3.72361 13 3.99976 13Z" fill="black"/>
<path opacity="0.2" d="M2 12.5C2.27614 12.5 2.5 12.2761 2.5 12C2.5 11.7239 2.27614 11.5 2 11.5C1.72386 11.5 1.5 11.7239 1.5 12C1.5 12.2761 1.72386 12.5 2 12.5Z" fill="black"/>
<path opacity="0.2" d="M2 4.5C2.27614 4.5 2.5 4.27614 2.5 4C2.5 3.72386 2.27614 3.5 2 3.5C1.72386 3.5 1.5 3.72386 1.5 4C1.5 4.27614 1.72386 4.5 2 4.5Z" fill="black"/>
<path opacity="0.2" d="M15 12.5C15.2761 12.5 15.5 12.2761 15.5 12C15.5 11.7239 15.2761 11.5 15 11.5C14.7239 11.5 14.5 11.7239 14.5 12C14.5 12.2761 14.7239 12.5 15 12.5Z" fill="black"/>
<path opacity="0.2" d="M15 4.5C15.2761 4.5 15.5 4.27614 15.5 4C15.5 3.72386 15.2761 3.5 15 3.5C14.7239 3.5 14.5 3.72386 14.5 4C14.5 4.27614 14.7239 4.5 15 4.5Z" fill="black"/>
<path opacity="0.5" d="M3.99976 15C4.2759 15 4.49976 14.7761 4.49976 14.5C4.49976 14.2239 4.2759 14 3.99976 14C3.72361 14 3.49976 14.2239 3.49976 14.5C3.49976 14.7761 3.72361 15 3.99976 15Z" fill="black"/>
<path opacity="0.5" d="M4 2C4.27614 2 4.5 1.77614 4.5 1.5C4.5 1.22386 4.27614 1 4 1C3.72386 1 3.5 1.22386 3.5 1.5C3.5 1.77614 3.72386 2 4 2Z" fill="black"/>
<path opacity="0.5" d="M13 15C13.2761 15 13.5 14.7761 13.5 14.5C13.5 14.2239 13.2761 14 13 14C12.7239 14 12.5 14.2239 12.5 14.5C12.5 14.7761 12.7239 15 13 15Z" fill="black"/>
<path opacity="0.5" d="M13 2C13.2761 2 13.5 1.77614 13.5 1.5C13.5 1.22386 13.2761 1 13 1C12.7239 1 12.5 1.22386 12.5 1.5C12.5 1.77614 12.7239 2 13 2Z" fill="black"/>
<path d="M13 4C13.2761 4 13.5 3.77614 13.5 3.5C13.5 3.22386 13.2761 3 13 3C12.7239 3 12.5 3.22386 12.5 3.5C12.5 3.77614 12.7239 4 13 4Z" fill="black"/>
<path d="M13 13C13.2761 13 13.5 12.7761 13.5 12.5C13.5 12.2239 13.2761 12 13 12C12.7239 12 12.5 12.2239 12.5 12.5C12.5 12.7761 12.7239 13 13 13Z" fill="black"/>
<path opacity="0.6" d="M11 3C11.2761 3 11.5 2.77614 11.5 2.5C11.5 2.22386 11.2761 2 11 2C10.7239 2 10.5 2.22386 10.5 2.5C10.5 2.77614 10.7239 3 11 3Z" fill="black"/>
<path d="M8.5 15C8.77614 15 9 14.7761 9 14.5C9 14.2239 8.77614 14 8.5 14C8.22386 14 8 14.2239 8 14.5C8 14.7761 8.22386 15 8.5 15Z" fill="black"/>
<path opacity="0.6" d="M6 14C6.27614 14 6.5 13.7761 6.5 13.5C6.5 13.2239 6.27614 13 6 13C5.72386 13 5.5 13.2239 5.5 13.5C5.5 13.7761 5.72386 14 6 14Z" fill="black"/>
<path opacity="0.6" d="M11 14C11.2761 14 11.5 13.7761 11.5 13.5C11.5 13.2239 11.2761 13 11 13C10.7239 13 10.5 13.2239 10.5 13.5C10.5 13.7761 10.7239 14 11 14Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 2.93652L6.9243 6.20697C6.86924 6.37435 6.77565 6.52646 6.65105 6.65105C6.52646 6.77565 6.37435 6.86924 6.20697 6.9243L2.93652 8L6.20697 9.0757C6.37435 9.13076 6.52646 9.22435 6.65105 9.34895C6.77565 9.47354 6.86924 9.62565 6.9243 9.79306L8 13.0635L9.0757 9.79306C9.13076 9.62565 9.22435 9.47354 9.34895 9.34895C9.47354 9.22435 9.62565 9.13076 9.79306 9.0757L13.0635 8L9.79306 6.9243C9.62565 6.86924 9.47354 6.77565 9.34895 6.65105C9.22435 6.52646 9.13076 6.37435 9.0757 6.20697L8 2.93652Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.33334 2V4.66666M2 3.33334H4.66666" stroke="black" stroke-opacity="0.75" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.6665 11.3333V14M11.3333 12.6666H13.9999" stroke="black" stroke-opacity="0.75" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.33334 2V4.66666M2 3.33334H4.66666" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.6665 11.3333V14M11.3333 12.6666H13.9999" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 176 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="160" height="61" fill="none"><g clip-path="url(#a)"><path fill="#000" d="M130.75.385c5.428 0 10.297 2.81 13.011 7.511l14.214 24.618-.013-.005c2.599 4.504 2.707 9.932.28 14.513-2.618 4.944-7.862 8.015-13.679 8.015h-31.811c-.452 0-.873-.242-1.103-.637a1.268 1.268 0 0 1 0-1.274l3.919-6.78c.223-.394.65-.636 1.102-.636h28.288a5.622 5.622 0 0 0 4.925-2.849 5.615 5.615 0 0 0 0-5.69l-14.214-24.617a5.621 5.621 0 0 0-4.925-2.848 5.621 5.621 0 0 0-4.925 2.848l-14.214 24.618a6.267 6.267 0 0 0-.319.643.998.998 0 0 1-.069.14L101.724 54.4l-.823 1.313-2.529 4.39a1.27 1.27 0 0 1-1.103.636h-7.83c-.452 0-.873-.242-1.102-.637-.23-.394-.23-.879 0-1.274l2.188-3.791H66.803c-3.32 0-6.454-1.122-8.818-3.167a17.141 17.141 0 0 1-3.394-3.96 1.261 1.261 0 0 1-.091-.137L34.2 12.573a5.622 5.622 0 0 0-4.925-2.849 5.621 5.621 0 0 0-4.924 2.85L10.137 37.19a5.615 5.615 0 0 0 0 5.69 5.63 5.63 0 0 0 4.925 2.841h29.862a1.276 1.276 0 0 1 1.102 1.912l-3.912 6.778a1.27 1.27 0 0 1-1.102.638H14.495c-3.32 0-6.454-1.128-8.817-3.173-5.906-5.104-7.36-12.883-3.62-19.363L16.267 7.89C18.872 3.385 23.517.583 28.697.39c.184-.006.356-.006.534-.006 5.378 0 10.45 3.007 13.246 7.85l12.986 22.372L68.58 7.891C71.186 3.385 75.83.582 81.01.39c.185-.006.358-.006.536-.006 4.453 0 8.71 2.039 11.672 5.588.337.407.388.98.127 1.446l-3.765 6.6a1.268 1.268 0 0 1-2.205.006l-.847-1.465a5.623 5.623 0 0 0-4.926-2.848 5.622 5.622 0 0 0-4.924 2.848L62.464 37.18a5.614 5.614 0 0 0 0 5.689 5.628 5.628 0 0 0 4.925 2.842H95.91L117.76 7.87c2.714-4.683 7.575-7.486 12.99-7.486Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 .385h160v60.36H0z"/></clipPath></defs></svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -16,6 +16,7 @@
"up": "menu::SelectPrevious",
"enter": "menu::Confirm",
"ctrl-enter": "menu::SecondaryConfirm",
"ctrl-escape": "menu::Cancel",
"ctrl-c": "menu::Cancel",
"escape": "menu::Cancel",
"alt-shift-enter": "menu::Restart",
@@ -40,7 +41,7 @@
"shift-f11": "debugger::StepOut",
"f11": "zed::ToggleFullScreen",
"ctrl-alt-z": "edit_prediction::RateCompletions",
"ctrl-alt-shift-i": "edit_prediction::ToggleMenu",
"ctrl-shift-i": "edit_prediction::ToggleMenu",
"ctrl-alt-l": "lsp_tool::ToggleMenu"
}
},
@@ -120,7 +121,7 @@
"alt-g m": "git::OpenModifiedFiles",
"menu": "editor::OpenContextMenu",
"shift-f10": "editor::OpenContextMenu",
"ctrl-alt-shift-e": "editor::ToggleEditPrediction",
"ctrl-shift-e": "editor::ToggleEditPrediction",
"f9": "editor::ToggleBreakpoint",
"shift-f9": "editor::EditLogBreakpoint"
}
@@ -130,14 +131,14 @@
"bindings": {
"shift-enter": "editor::Newline",
"enter": "editor::Newline",
"ctrl-enter": "editor::NewlineBelow",
"ctrl-shift-enter": "editor::NewlineAbove",
"ctrl-enter": "editor::NewlineAbove",
"ctrl-shift-enter": "editor::NewlineBelow",
"ctrl-k ctrl-z": "editor::ToggleSoftWrap",
"ctrl-k z": "editor::ToggleSoftWrap",
"find": "buffer_search::Deploy",
"ctrl-f": "buffer_search::Deploy",
"ctrl-h": "buffer_search::DeployReplace",
"ctrl->": "agent::QuoteSelection",
"ctrl->": "assistant::QuoteSelection",
"ctrl-<": "assistant::InsertIntoEditor",
"ctrl-alt-e": "editor::SelectEnclosingSymbol",
"ctrl-shift-backspace": "editor::GoToPreviousChange",
@@ -170,7 +171,6 @@
"context": "Markdown",
"bindings": {
"copy": "markdown::Copy",
"ctrl-insert": "markdown::Copy",
"ctrl-c": "markdown::Copy"
}
},
@@ -241,7 +241,7 @@
"ctrl-shift-i": "agent::ToggleOptionsMenu",
"ctrl-alt-shift-n": "agent::ToggleNewThreadMenu",
"shift-alt-escape": "agent::ExpandMessageEditor",
"ctrl->": "agent::QuoteSelection",
"ctrl->": "assistant::QuoteSelection",
"ctrl-alt-e": "agent::RemoveAllContext",
"ctrl-shift-e": "project_panel::ToggleFocus",
"ctrl-shift-enter": "agent::ContinueThread",
@@ -259,7 +259,6 @@
"context": "AgentPanel > Markdown",
"bindings": {
"copy": "markdown::CopyAsMarkdown",
"ctrl-insert": "markdown::CopyAsMarkdown",
"ctrl-c": "markdown::CopyAsMarkdown"
}
},
@@ -328,7 +327,7 @@
}
},
{
"context": "AcpThread > Editor && !use_modifier_to_send",
"context": "AcpThread > Editor",
"use_key_equivalents": true,
"bindings": {
"enter": "agent::Chat",
@@ -337,16 +336,6 @@
"ctrl-shift-n": "agent::RejectAll"
}
},
{
"context": "AcpThread > Editor && use_modifier_to_send",
"use_key_equivalents": true,
"bindings": {
"ctrl-enter": "agent::Chat",
"shift-ctrl-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll"
}
},
{
"context": "ThreadHistory",
"bindings": {
@@ -857,7 +846,7 @@
"ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }],
"ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }],
"alt-ctrl-r": "project_panel::RevealInFileManager",
"ctrl-shift-enter": "workspace::OpenWithSystem",
"ctrl-shift-enter": "project_panel::OpenWithSystem",
"alt-d": "project_panel::CompareMarkedFiles",
"shift-find": "project_panel::NewSearchInDirectory",
"ctrl-alt-shift-f": "project_panel::NewSearchInDirectory",
@@ -1196,16 +1185,9 @@
"ctrl-1": "onboarding::ActivateBasicsPage",
"ctrl-2": "onboarding::ActivateEditingPage",
"ctrl-3": "onboarding::ActivateAISetupPage",
"ctrl-enter": "onboarding::Finish",
"alt-shift-l": "onboarding::SignIn",
"ctrl-escape": "onboarding::Finish",
"alt-tab": "onboarding::SignIn",
"alt-shift-a": "onboarding::OpenAccount"
}
},
{
"context": "InvalidBuffer",
"use_key_equivalents": true,
"bindings": {
"ctrl-shift-enter": "workspace::OpenWithSystem"
}
}
]

View File

@@ -162,7 +162,7 @@
"cmd-alt-f": "buffer_search::DeployReplace",
"cmd-alt-l": ["buffer_search::Deploy", { "selection_search_enabled": true }],
"cmd-e": ["buffer_search::Deploy", { "focus": false }],
"cmd->": "agent::QuoteSelection",
"cmd->": "assistant::QuoteSelection",
"cmd-<": "assistant::InsertIntoEditor",
"cmd-alt-e": "editor::SelectEnclosingSymbol",
"alt-enter": "editor::OpenSelectionsInMultibuffer"
@@ -281,7 +281,7 @@
"cmd-shift-i": "agent::ToggleOptionsMenu",
"cmd-alt-shift-n": "agent::ToggleNewThreadMenu",
"shift-alt-escape": "agent::ExpandMessageEditor",
"cmd->": "agent::QuoteSelection",
"cmd->": "assistant::QuoteSelection",
"cmd-alt-e": "agent::RemoveAllContext",
"cmd-shift-e": "project_panel::ToggleFocus",
"cmd-ctrl-b": "agent::ToggleBurnMode",
@@ -379,7 +379,7 @@
}
},
{
"context": "AcpThread > Editor && !use_modifier_to_send",
"context": "AcpThread > Editor",
"use_key_equivalents": true,
"bindings": {
"enter": "agent::Chat",
@@ -388,16 +388,6 @@
"cmd-shift-n": "agent::RejectAll"
}
},
{
"context": "AcpThread > Editor && use_modifier_to_send",
"use_key_equivalents": true,
"bindings": {
"cmd-enter": "agent::Chat",
"shift-ctrl-r": "agent::OpenAgentDiff",
"cmd-shift-y": "agent::KeepAll",
"cmd-shift-n": "agent::RejectAll"
}
},
{
"context": "ThreadHistory",
"bindings": {
@@ -915,7 +905,7 @@
"cmd-backspace": ["project_panel::Trash", { "skip_prompt": true }],
"cmd-delete": ["project_panel::Delete", { "skip_prompt": false }],
"alt-cmd-r": "project_panel::RevealInFileManager",
"ctrl-shift-enter": "workspace::OpenWithSystem",
"ctrl-shift-enter": "project_panel::OpenWithSystem",
"alt-d": "project_panel::CompareMarkedFiles",
"cmd-alt-backspace": ["project_panel::Delete", { "skip_prompt": false }],
"cmd-alt-shift-f": "project_panel::NewSearchInDirectory",
@@ -1301,12 +1291,5 @@
"alt-tab": "onboarding::SignIn",
"alt-shift-a": "onboarding::OpenAccount"
}
},
{
"context": "InvalidBuffer",
"use_key_equivalents": true,
"bindings": {
"ctrl-shift-enter": "workspace::OpenWithSystem"
}
}
]

View File

@@ -1,7 +1,6 @@
[
// Standard Windows bindings
{
"use_key_equivalents": true,
"bindings": {
"home": "menu::SelectFirst",
"shift-pageup": "menu::SelectFirst",
@@ -20,7 +19,7 @@
"ctrl-escape": "menu::Cancel",
"ctrl-c": "menu::Cancel",
"escape": "menu::Cancel",
"shift-alt-enter": "menu::Restart",
"alt-shift-enter": "menu::Restart",
"alt-enter": ["picker::ConfirmInput", { "secondary": false }],
"ctrl-alt-enter": ["picker::ConfirmInput", { "secondary": true }],
"ctrl-shift-w": "workspace::CloseWindow",
@@ -28,7 +27,7 @@
"open": "workspace::Open",
"ctrl-o": "workspace::Open",
"ctrl-=": ["zed::IncreaseBufferFontSize", { "persist": false }],
"ctrl-shift-=": ["zed::IncreaseBufferFontSize", { "persist": false }],
"ctrl-+": ["zed::IncreaseBufferFontSize", { "persist": false }],
"ctrl--": ["zed::DecreaseBufferFontSize", { "persist": false }],
"ctrl-0": ["zed::ResetBufferFontSize", { "persist": false }],
"ctrl-,": "zed::OpenSettings",
@@ -41,13 +40,13 @@
"ctrl-f11": "debugger::StepInto",
"shift-f11": "debugger::StepOut",
"f11": "zed::ToggleFullScreen",
"ctrl-alt-z": "edit_prediction::RateCompletions",
"ctrl-shift-i": "edit_prediction::ToggleMenu",
"shift-alt-l": "lsp_tool::ToggleMenu"
"alt-shift-l": "lsp_tool::ToggleMenu"
}
},
{
"context": "Picker || menu",
"use_key_equivalents": true,
"bindings": {
"up": "menu::SelectPrevious",
"down": "menu::SelectNext"
@@ -55,7 +54,6 @@
},
{
"context": "Editor",
"use_key_equivalents": true,
"bindings": {
"escape": "editor::Cancel",
"shift-backspace": "editor::Backspace",
@@ -110,8 +108,8 @@
"ctrl-shift-end": "editor::SelectToEnd",
"ctrl-a": "editor::SelectAll",
"ctrl-l": "editor::SelectLine",
"shift-alt-f": "editor::Format",
"shift-alt-o": "editor::OrganizeImports",
"ctrl-shift-i": "editor::Format",
"alt-shift-o": "editor::OrganizeImports",
"shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
"shift-end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
"ctrl-alt-space": "editor::ShowCharacterPalette",
@@ -130,20 +128,19 @@
},
{
"context": "Editor && mode == full",
"use_key_equivalents": true,
"bindings": {
"shift-enter": "editor::Newline",
"enter": "editor::Newline",
"ctrl-enter": "editor::NewlineBelow",
"ctrl-shift-enter": "editor::NewlineAbove",
"ctrl-enter": "editor::NewlineAbove",
"ctrl-shift-enter": "editor::NewlineBelow",
"ctrl-k ctrl-z": "editor::ToggleSoftWrap",
"ctrl-k z": "editor::ToggleSoftWrap",
"find": "buffer_search::Deploy",
"ctrl-f": "buffer_search::Deploy",
"ctrl-h": "buffer_search::DeployReplace",
"ctrl-shift-.": "assistant::QuoteSelection",
"ctrl-shift-,": "assistant::InsertIntoEditor",
"shift-alt-e": "editor::SelectEnclosingSymbol",
"ctrl->": "assistant::QuoteSelection",
"ctrl-<": "assistant::InsertIntoEditor",
"alt-shift-e": "editor::SelectEnclosingSymbol",
"ctrl-shift-backspace": "editor::GoToPreviousChange",
"ctrl-shift-alt-backspace": "editor::GoToNextChange",
"alt-enter": "editor::OpenSelectionsInMultibuffer"
@@ -151,7 +148,6 @@
},
{
"context": "Editor && mode == full && edit_prediction",
"use_key_equivalents": true,
"bindings": {
"alt-]": "editor::NextEditPrediction",
"alt-[": "editor::PreviousEditPrediction"
@@ -159,14 +155,12 @@
},
{
"context": "Editor && !edit_prediction",
"use_key_equivalents": true,
"bindings": {
"alt-\\": "editor::ShowEditPrediction"
}
},
{
"context": "Editor && mode == auto_height",
"use_key_equivalents": true,
"bindings": {
"ctrl-enter": "editor::Newline",
"shift-enter": "editor::Newline",
@@ -175,7 +169,6 @@
},
{
"context": "Markdown",
"use_key_equivalents": true,
"bindings": {
"copy": "markdown::Copy",
"ctrl-c": "markdown::Copy"
@@ -183,7 +176,6 @@
},
{
"context": "Editor && jupyter && !ContextEditor",
"use_key_equivalents": true,
"bindings": {
"ctrl-shift-enter": "repl::Run",
"ctrl-alt-enter": "repl::RunInPlace"
@@ -191,27 +183,25 @@
},
{
"context": "Editor && !agent_diff",
"use_key_equivalents": true,
"bindings": {
"ctrl-k ctrl-r": "git::Restore",
"alt-shift-y": "git::ToggleStaged",
"alt-y": "git::StageAndNext",
"shift-alt-y": "git::UnstageAndNext"
"alt-shift-n": "git::UnstageAndNext"
}
},
{
"context": "Editor && editor_agent_diff",
"use_key_equivalents": true,
"bindings": {
"ctrl-y": "agent::Keep",
"ctrl-n": "agent::Reject",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll",
"ctrl-shift-r": "agent::OpenAgentDiff"
"shift-ctrl-r": "agent::OpenAgentDiff"
}
},
{
"context": "AgentDiff",
"use_key_equivalents": true,
"bindings": {
"ctrl-y": "agent::Keep",
"ctrl-n": "agent::Reject",
@@ -221,12 +211,11 @@
},
{
"context": "ContextEditor > Editor",
"use_key_equivalents": true,
"bindings": {
"ctrl-enter": "assistant::Assist",
"ctrl-s": "workspace::Save",
"save": "workspace::Save",
"ctrl-shift-,": "assistant::InsertIntoEditor",
"ctrl-<": "assistant::InsertIntoEditor",
"shift-enter": "assistant::Split",
"ctrl-r": "assistant::CycleMessageRole",
"enter": "assistant::ConfirmCommand",
@@ -239,22 +228,21 @@
},
{
"context": "AgentPanel",
"use_key_equivalents": true,
"bindings": {
"ctrl-n": "agent::NewThread",
"shift-alt-n": "agent::NewTextThread",
"alt-shift-n": "agent::NewTextThread",
"ctrl-shift-h": "agent::OpenHistory",
"shift-alt-c": "agent::OpenSettings",
"shift-alt-p": "agent::OpenRulesLibrary",
"alt-shift-c": "agent::OpenSettings",
"alt-shift-p": "agent::OpenRulesLibrary",
"ctrl-i": "agent::ToggleProfileSelector",
"shift-alt-/": "agent::ToggleModelSelector",
"alt-?": "agent::ToggleModelSelector",
"ctrl-shift-a": "agent::ToggleContextPicker",
"ctrl-shift-j": "agent::ToggleNavigationMenu",
"ctrl-shift-i": "agent::ToggleOptionsMenu",
// "ctrl-shift-alt-n": "agent::ToggleNewThreadMenu",
"shift-alt-escape": "agent::ExpandMessageEditor",
"ctrl-shift-.": "assistant::QuoteSelection",
"shift-alt-e": "agent::RemoveAllContext",
"ctrl-alt-shift-n": "agent::ToggleNewThreadMenu",
"alt-shift-escape": "agent::ExpandMessageEditor",
"ctrl->": "assistant::QuoteSelection",
"alt-shift-e": "agent::RemoveAllContext",
"ctrl-shift-e": "project_panel::ToggleFocus",
"ctrl-shift-enter": "agent::ContinueThread",
"super-ctrl-b": "agent::ToggleBurnMode",
@@ -263,14 +251,12 @@
},
{
"context": "AgentPanel > NavigationMenu",
"use_key_equivalents": true,
"bindings": {
"shift-backspace": "agent::DeleteRecentlyOpenThread"
}
},
{
"context": "AgentPanel > Markdown",
"use_key_equivalents": true,
"bindings": {
"copy": "markdown::CopyAsMarkdown",
"ctrl-c": "markdown::CopyAsMarkdown"
@@ -278,7 +264,6 @@
},
{
"context": "AgentPanel && prompt_editor",
"use_key_equivalents": true,
"bindings": {
"ctrl-n": "agent::NewTextThread",
"ctrl-alt-t": "agent::NewThread"
@@ -294,31 +279,28 @@
},
{
"context": "MessageEditor && !Picker > Editor && !use_modifier_to_send",
"use_key_equivalents": true,
"bindings": {
"enter": "agent::Chat",
"ctrl-enter": "agent::ChatWithFollow",
"ctrl-i": "agent::ToggleProfileSelector",
"ctrl-shift-r": "agent::OpenAgentDiff",
"shift-ctrl-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll"
}
},
{
"context": "MessageEditor && !Picker > Editor && use_modifier_to_send",
"use_key_equivalents": true,
"bindings": {
"ctrl-enter": "agent::Chat",
"enter": "editor::Newline",
"ctrl-i": "agent::ToggleProfileSelector",
"ctrl-shift-r": "agent::OpenAgentDiff",
"shift-ctrl-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll"
}
},
{
"context": "EditMessageEditor > Editor",
"use_key_equivalents": true,
"bindings": {
"escape": "menu::Cancel",
"enter": "menu::Confirm",
@@ -327,7 +309,6 @@
},
{
"context": "AgentFeedbackMessageEditor > Editor",
"use_key_equivalents": true,
"bindings": {
"escape": "menu::Cancel",
"enter": "menu::Confirm",
@@ -336,7 +317,6 @@
},
{
"context": "ContextStrip",
"use_key_equivalents": true,
"bindings": {
"up": "agent::FocusUp",
"right": "agent::FocusRight",
@@ -351,21 +331,19 @@
"use_key_equivalents": true,
"bindings": {
"enter": "agent::Chat",
"ctrl-shift-r": "agent::OpenAgentDiff",
"shift-ctrl-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll"
}
},
{
"context": "ThreadHistory",
"use_key_equivalents": true,
"bindings": {
"backspace": "agent::RemoveSelectedThread"
}
},
{
"context": "PromptLibrary",
"use_key_equivalents": true,
"bindings": {
"new": "rules_library::NewRule",
"ctrl-n": "rules_library::NewRule",
@@ -374,7 +352,6 @@
},
{
"context": "BufferSearchBar",
"use_key_equivalents": true,
"bindings": {
"escape": "buffer_search::Dismiss",
"tab": "buffer_search::FocusEditor",
@@ -389,7 +366,6 @@
},
{
"context": "BufferSearchBar && in_replace > Editor",
"use_key_equivalents": true,
"bindings": {
"enter": "search::ReplaceNext",
"ctrl-enter": "search::ReplaceAll"
@@ -397,7 +373,6 @@
},
{
"context": "BufferSearchBar && !in_replace > Editor",
"use_key_equivalents": true,
"bindings": {
"up": "search::PreviousHistoryQuery",
"down": "search::NextHistoryQuery"
@@ -405,18 +380,17 @@
},
{
"context": "ProjectSearchBar",
"use_key_equivalents": true,
"bindings": {
"escape": "project_search::ToggleFocus",
"shift-find": "search::FocusSearch",
"ctrl-shift-f": "search::FocusSearch",
"ctrl-shift-h": "search::ToggleReplace",
"alt-r": "search::ToggleRegex" // vscode
"alt-shift-g": "search::ToggleRegex",
"alt-shift-x": "search::ToggleRegex"
}
},
{
"context": "ProjectSearchBar > Editor",
"use_key_equivalents": true,
"bindings": {
"up": "search::PreviousHistoryQuery",
"down": "search::NextHistoryQuery"
@@ -424,7 +398,6 @@
},
{
"context": "ProjectSearchBar && in_replace > Editor",
"use_key_equivalents": true,
"bindings": {
"enter": "search::ReplaceNext",
"ctrl-alt-enter": "search::ReplaceAll"
@@ -432,16 +405,15 @@
},
{
"context": "ProjectSearchView",
"use_key_equivalents": true,
"bindings": {
"escape": "project_search::ToggleFocus",
"ctrl-shift-h": "search::ToggleReplace",
"alt-r": "search::ToggleRegex" // vscode
"alt-shift-g": "search::ToggleRegex",
"alt-shift-x": "search::ToggleRegex"
}
},
{
"context": "Pane",
"use_key_equivalents": true,
"bindings": {
"alt-1": ["pane::ActivateItem", 0],
"alt-2": ["pane::ActivateItem", 1],
@@ -459,8 +431,8 @@
"ctrl-shift-pagedown": "pane::SwapItemRight",
"ctrl-f4": ["pane::CloseActiveItem", { "close_pinned": false }],
"ctrl-w": ["pane::CloseActiveItem", { "close_pinned": false }],
"ctrl-shift-alt-t": ["pane::CloseOtherItems", { "close_pinned": false }],
"ctrl-shift-alt-w": "workspace::CloseInactiveTabsAndPanes",
"ctrl-alt-shift-t": ["pane::CloseOtherItems", { "close_pinned": false }],
"ctrl-alt-shift-w": "workspace::CloseInactiveTabsAndPanes",
"ctrl-k e": ["pane::CloseItemsToTheLeft", { "close_pinned": false }],
"ctrl-k t": ["pane::CloseItemsToTheRight", { "close_pinned": false }],
"ctrl-k u": ["pane::CloseCleanItems", { "close_pinned": false }],
@@ -471,37 +443,38 @@
"alt-=": "pane::GoForward",
"forward": "pane::GoForward",
"f3": "search::SelectNextMatch",
"ctrl-alt-shift-g": "search::SelectPreviousMatch",
"shift-f3": "search::SelectPreviousMatch",
"shift-find": "project_search::ToggleFocus",
"ctrl-shift-f": "project_search::ToggleFocus",
"shift-alt-h": "search::ToggleReplace",
"alt-l": "search::ToggleSelection",
"ctrl-alt-shift-h": "search::ToggleReplace",
"ctrl-alt-shift-l": "search::ToggleSelection",
"alt-enter": "search::SelectAllMatches",
"alt-c": "search::ToggleCaseSensitive",
"alt-w": "search::ToggleWholeWord",
"alt-find": "project_search::ToggleFilters",
"alt-f": "project_search::ToggleFilters",
"ctrl-alt-shift-r": "search::ToggleRegex",
"ctrl-alt-shift-x": "search::ToggleRegex",
"alt-r": "search::ToggleRegex",
// "ctrl-shift-alt-x": "search::ToggleRegex",
"ctrl-k shift-enter": "pane::TogglePinTab"
}
},
// Bindings from VS Code
{
"context": "Editor",
"use_key_equivalents": true,
"bindings": {
"ctrl-[": "editor::Outdent",
"ctrl-]": "editor::Indent",
"ctrl-shift-alt-up": "editor::AddSelectionAbove", // Insert Cursor Above
"ctrl-shift-alt-down": "editor::AddSelectionBelow", // Insert Cursor Below
"alt-shift-up": "editor::AddSelectionAbove", // Insert Cursor Above
"alt-shift-down": "editor::AddSelectionBelow", // Insert Cursor Below
"ctrl-shift-k": "editor::DeleteLine",
"alt-up": "editor::MoveLineUp",
"alt-down": "editor::MoveLineDown",
"shift-alt-up": "editor::DuplicateLineUp",
"shift-alt-down": "editor::DuplicateLineDown",
"shift-alt-right": "editor::SelectLargerSyntaxNode", // Expand Selection
"shift-alt-left": "editor::SelectSmallerSyntaxNode", // Shrink Selection
"ctrl-alt-shift-up": "editor::DuplicateLineUp",
"ctrl-alt-shift-down": "editor::DuplicateLineDown",
"alt-shift-right": "editor::SelectLargerSyntaxNode", // Expand Selection
"alt-shift-left": "editor::SelectSmallerSyntaxNode", // Shrink Selection
"ctrl-shift-l": "editor::SelectAllMatches", // Select all occurrences of current selection
"ctrl-f2": "editor::SelectAllMatches", // Select all occurrences of current word
"ctrl-d": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch / find_under_expand
@@ -518,14 +491,14 @@
"f12": "editor::GoToDefinition",
"alt-f12": "editor::GoToDefinitionSplit",
"ctrl-shift-f10": "editor::GoToDefinitionSplit",
"ctrl-f12": "editor::GoToImplementation",
"shift-f12": "editor::GoToTypeDefinition",
"ctrl-f12": "editor::GoToTypeDefinition",
"shift-f12": "editor::GoToImplementation",
"ctrl-alt-f12": "editor::GoToTypeDefinitionSplit",
"shift-alt-f12": "editor::FindAllReferences",
"alt-shift-f12": "editor::FindAllReferences",
"ctrl-m": "editor::MoveToEnclosingBracket", // from jetbrains
"ctrl-shift-\\": "editor::MoveToEnclosingBracket",
"ctrl-shift-[": "editor::Fold",
"ctrl-shift-]": "editor::UnfoldLines",
"ctrl-|": "editor::MoveToEnclosingBracket",
"ctrl-{": "editor::Fold",
// "ctrl-}": "editor::UnfoldLines",
"ctrl-k ctrl-l": "editor::ToggleFold",
"ctrl-k ctrl-[": "editor::FoldRecursive",
"ctrl-k ctrl-]": "editor::UnfoldRecursive",
@@ -546,7 +519,7 @@
"ctrl-k r": "editor::RevealInFileManager",
"ctrl-k p": "editor::CopyPath",
"ctrl-\\": "pane::SplitRight",
"ctrl-shift-alt-c": "editor::DisplayCursorNames",
"ctrl-alt-shift-c": "editor::DisplayCursorNames",
"alt-.": "editor::GoToHunk",
"alt-,": "editor::GoToPreviousHunk"
}
@@ -569,7 +542,6 @@
},
{
"context": "Editor && mode == full",
"use_key_equivalents": true,
"bindings": {
"ctrl-shift-o": "outline::Toggle",
"ctrl-g": "go_to_line::Toggle"
@@ -577,22 +549,21 @@
},
{
"context": "Workspace",
"use_key_equivalents": true,
"bindings": {
"alt-open": ["projects::OpenRecent", { "create_new_window": false }],
// Change the default action on `menu::Confirm` by setting the parameter
// "ctrl-alt-o": ["projects::OpenRecent", { "create_new_window": true }],
"ctrl-r": ["projects::OpenRecent", { "create_new_window": false }],
"shift-alt-open": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }],
"alt-shift-p": ["projects::OpenRecent", { "create_new_window": false }],
"alt-shift-open": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }],
// Change to open path modal for existing remote connection by setting the parameter
// "ctrl-shift-alt-o": "["projects::OpenRemote", { "from_existing_connection": true }]",
"ctrl-shift-alt-o": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }],
"shift-alt-b": "branches::OpenRecent",
"shift-alt-enter": "toast::RunAction",
"ctrl-shift-`": "workspace::NewTerminal",
// "ctrl-alt-shift-o": "["projects::OpenRemote", { "from_existing_connection": true }]",
"ctrl-alt-shift-o": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }],
"ctrl-alt-shift-b": "branches::OpenRecent",
"alt-shift-enter": "toast::RunAction",
"ctrl-~": "workspace::NewTerminal",
"save": "workspace::Save",
"ctrl-s": "workspace::Save",
"ctrl-k ctrl-shift-s": "workspace::SaveWithoutFormat",
"ctrl-k s": "workspace::SaveWithoutFormat",
"shift-save": "workspace::SaveAs",
"ctrl-shift-s": "workspace::SaveAs",
"new": "workspace::NewFile",
@@ -613,14 +584,14 @@
"ctrl-alt-b": "workspace::ToggleRightDock",
"ctrl-b": "workspace::ToggleLeftDock",
"ctrl-j": "workspace::ToggleBottomDock",
"ctrl-shift-y": "workspace::CloseAllDocks",
"ctrl-alt-shift-y": "workspace::CloseAllDocks",
"alt-r": "workspace::ResetActiveDockSize",
// For 0px parameter, uses UI font size value.
"shift-alt--": ["workspace::DecreaseActiveDockSize", { "px": 0 }],
"shift-alt-=": ["workspace::IncreaseActiveDockSize", { "px": 0 }],
"shift-alt-0": "workspace::ResetOpenDocksSize",
"ctrl-shift-alt--": ["workspace::DecreaseOpenDocksSize", { "px": 0 }],
"ctrl-shift-alt-=": ["workspace::IncreaseOpenDocksSize", { "px": 0 }],
"alt--": ["workspace::DecreaseActiveDockSize", { "px": 0 }],
"alt-=": ["workspace::IncreaseActiveDockSize", { "px": 0 }],
"alt-)": "workspace::ResetOpenDocksSize",
"alt-_": ["workspace::DecreaseOpenDocksSize", { "px": 0 }],
"alt-+": ["workspace::IncreaseOpenDocksSize", { "px": 0 }],
"shift-find": "pane::DeploySearch",
"ctrl-shift-f": "pane::DeploySearch",
"ctrl-shift-h": ["pane::DeploySearch", { "replace_enabled": true }],
@@ -640,9 +611,9 @@
"ctrl-shift-b": "outline_panel::ToggleFocus",
"ctrl-shift-g": "git_panel::ToggleFocus",
"ctrl-shift-d": "debug_panel::ToggleFocus",
"ctrl-shift-/": "agent::ToggleFocus",
"ctrl-?": "agent::ToggleFocus",
"alt-save": "workspace::SaveAll",
"ctrl-k s": "workspace::SaveAll",
"alt-shift-s": "workspace::SaveAll",
"ctrl-k m": "language_selector::Toggle",
"escape": "workspace::Unfollow",
"ctrl-k ctrl-left": "workspace::ActivatePaneLeft",
@@ -655,9 +626,10 @@
"ctrl-k shift-down": "workspace::SwapPaneDown",
"ctrl-shift-x": "zed::Extensions",
"ctrl-shift-r": "task::Rerun",
"ctrl-alt-r": "task::Rerun",
"alt-t": "task::Rerun",
"shift-alt-t": "task::Spawn",
"shift-alt-r": ["task::Spawn", { "reveal_target": "center" }],
"alt-shift-t": "task::Spawn",
"alt-shift-r": ["task::Spawn", { "reveal_target": "center" }],
// also possible to spawn tasks by name:
// "foo-bar": ["task::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }]
// or by tag:
@@ -669,21 +641,18 @@
},
{
"context": "Workspace && debugger_running",
"use_key_equivalents": true,
"bindings": {
"f5": "zed::NoAction"
}
},
{
"context": "Workspace && debugger_stopped",
"use_key_equivalents": true,
"bindings": {
"f5": "debugger::Continue"
}
},
{
"context": "ApplicationMenu",
"use_key_equivalents": true,
"bindings": {
"f10": "menu::Cancel",
"left": "app_menu::ActivateMenuLeft",
@@ -693,25 +662,25 @@
// Bindings from Sublime Text
{
"context": "Editor",
"use_key_equivalents": true,
"bindings": {
"ctrl-u": "editor::UndoSelection",
"ctrl-shift-u": "editor::RedoSelection",
"ctrl-shift-j": "editor::JoinLines",
"ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",
"shift-alt-h": "editor::DeleteToPreviousSubwordStart",
"ctrl-alt-delete": "editor::DeleteToNextSubwordEnd",
"shift-alt-d": "editor::DeleteToNextSubwordEnd",
"alt-shift-backspace": "editor::DeleteToPreviousSubwordStart",
"alt-shift-h": "editor::DeleteToPreviousSubwordStart",
"alt-shift-delete": "editor::DeleteToNextSubwordEnd",
"alt-shift-d": "editor::DeleteToNextSubwordEnd",
"ctrl-alt-left": "editor::MoveToPreviousSubwordStart",
"ctrl-alt-right": "editor::MoveToNextSubwordEnd",
"ctrl-shift-alt-left": "editor::SelectToPreviousSubwordStart",
"ctrl-shift-alt-right": "editor::SelectToNextSubwordEnd"
"ctrl-alt-shift-left": "editor::SelectToPreviousSubwordStart",
"ctrl-alt-shift-b": "editor::SelectToPreviousSubwordStart",
"ctrl-alt-shift-right": "editor::SelectToNextSubwordEnd",
"ctrl-alt-shift-f": "editor::SelectToNextSubwordEnd"
}
},
// Bindings from Atom
{
"context": "Pane",
"use_key_equivalents": true,
"bindings": {
"ctrl-k up": "pane::SplitUp",
"ctrl-k down": "pane::SplitDown",
@@ -722,14 +691,12 @@
// Bindings that should be unified with bindings for more general actions
{
"context": "Editor && renaming",
"use_key_equivalents": true,
"bindings": {
"enter": "editor::ConfirmRename"
}
},
{
"context": "Editor && showing_completions",
"use_key_equivalents": true,
"bindings": {
"enter": "editor::ConfirmCompletion",
"shift-enter": "editor::ConfirmCompletionReplace",
@@ -742,7 +709,6 @@
// because alt-tab may not be available, as it is often used for window switching.
{
"context": "Editor && edit_prediction",
"use_key_equivalents": true,
"bindings": {
"alt-tab": "editor::AcceptEditPrediction",
"alt-l": "editor::AcceptEditPrediction",
@@ -752,7 +718,6 @@
},
{
"context": "Editor && edit_prediction_conflict",
"use_key_equivalents": true,
"bindings": {
"alt-tab": "editor::AcceptEditPrediction",
"alt-l": "editor::AcceptEditPrediction",
@@ -761,14 +726,12 @@
},
{
"context": "Editor && showing_code_actions",
"use_key_equivalents": true,
"bindings": {
"enter": "editor::ConfirmCodeAction"
}
},
{
"context": "Editor && (showing_code_actions || showing_completions)",
"use_key_equivalents": true,
"bindings": {
"ctrl-p": "editor::ContextMenuPrevious",
"up": "editor::ContextMenuPrevious",
@@ -780,7 +743,6 @@
},
{
"context": "Editor && showing_signature_help && !showing_completions",
"use_key_equivalents": true,
"bindings": {
"up": "editor::SignatureHelpPrevious",
"down": "editor::SignatureHelpNext"
@@ -788,46 +750,41 @@
},
// Custom bindings
{
"use_key_equivalents": true,
"bindings": {
"ctrl-shift-alt-f": "workspace::FollowNextCollaborator",
"ctrl-alt-shift-f": "workspace::FollowNextCollaborator",
// Only available in debug builds: opens an element inspector for development.
"shift-alt-i": "dev::ToggleInspector"
"ctrl-shift-i": "dev::ToggleInspector"
}
},
{
"context": "!Terminal",
"use_key_equivalents": true,
"bindings": {
"ctrl-shift-c": "collab_panel::ToggleFocus"
}
},
{
"context": "!ContextEditor > Editor && mode == full",
"use_key_equivalents": true,
"bindings": {
"alt-enter": "editor::OpenExcerpts",
"shift-enter": "editor::ExpandExcerpts",
"ctrl-alt-enter": "editor::OpenExcerptsSplit",
"alt-shift-enter": "editor::OpenExcerptsSplit",
"ctrl-shift-e": "pane::RevealInProjectPanel",
"ctrl-f8": "editor::GoToHunk",
"ctrl-shift-f8": "editor::GoToPreviousHunk",
"ctrl-enter": "assistant::InlineAssist",
"ctrl-shift-;": "editor::ToggleInlayHints"
"ctrl-:": "editor::ToggleInlayHints"
}
},
{
"context": "PromptEditor",
"use_key_equivalents": true,
"bindings": {
"ctrl-[": "agent::CyclePreviousInlineAssist",
"ctrl-]": "agent::CycleNextInlineAssist",
"shift-alt-e": "agent::RemoveAllContext"
"alt-shift-e": "agent::RemoveAllContext"
}
},
{
"context": "Prompt",
"use_key_equivalents": true,
"bindings": {
"left": "menu::SelectPrevious",
"right": "menu::SelectNext",
@@ -837,39 +794,37 @@
},
{
"context": "ProjectSearchBar && !in_replace",
"use_key_equivalents": true,
"bindings": {
"ctrl-enter": "project_search::SearchInNew"
}
},
{
"context": "OutlinePanel && not_editing",
"use_key_equivalents": true,
"bindings": {
"escape": "menu::Cancel",
"left": "outline_panel::CollapseSelectedEntry",
"right": "outline_panel::ExpandSelectedEntry",
"alt-copy": "outline_panel::CopyPath",
"shift-alt-c": "outline_panel::CopyPath",
"shift-alt-copy": "workspace::CopyRelativePath",
"ctrl-shift-alt-c": "workspace::CopyRelativePath",
"ctrl-alt-r": "outline_panel::RevealInFileManager",
"ctrl-shift-c": "outline_panel::CopyPath",
"alt-shift-copy": "workspace::CopyRelativePath",
"ctrl-alt-shift-c": "workspace::CopyRelativePath",
"shift-alt-r": "outline_panel::RevealInFileManager",
"space": "outline_panel::OpenSelectedEntry",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrevious",
"alt-enter": "editor::OpenExcerpts",
"ctrl-alt-enter": "editor::OpenExcerptsSplit"
"alt-shift-enter": "editor::OpenExcerptsSplit"
}
},
{
"context": "ProjectPanel",
"use_key_equivalents": true,
"bindings": {
"left": "project_panel::CollapseSelectedEntry",
"right": "project_panel::ExpandSelectedEntry",
"new": "project_panel::NewFile",
"ctrl-n": "project_panel::NewFile",
"alt-new": "project_panel::NewDirectory",
"alt-n": "project_panel::NewDirectory",
"ctrl-shift-n": "project_panel::NewDirectory",
"cut": "project_panel::Cut",
"ctrl-x": "project_panel::Cut",
"copy": "project_panel::Copy",
@@ -880,8 +835,8 @@
"ctrl-v": "project_panel::Paste",
"alt-copy": "project_panel::CopyPath",
"shift-alt-c": "project_panel::CopyPath",
"shift-alt-copy": "workspace::CopyRelativePath",
"ctrl-k ctrl-shift-c": "workspace::CopyRelativePath",
"alt-shift-copy": "workspace::CopyRelativePath",
"ctrl-alt-shift-c": "workspace::CopyRelativePath",
"enter": "project_panel::Rename",
"f2": "project_panel::Rename",
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
@@ -889,11 +844,11 @@
"shift-delete": ["project_panel::Delete", { "skip_prompt": false }],
"ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }],
"ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }],
"ctrl-alt-r": "project_panel::RevealInFileManager",
"alt-shift-r": "project_panel::RevealInFileManager",
"ctrl-shift-enter": "project_panel::OpenWithSystem",
"alt-d": "project_panel::CompareMarkedFiles",
"shift-find": "project_panel::NewSearchInDirectory",
"ctrl-k ctrl-shift-f": "project_panel::NewSearchInDirectory",
"ctrl-alt-shift-f": "project_panel::NewSearchInDirectory",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrevious",
"escape": "menu::Cancel"
@@ -901,20 +856,19 @@
},
{
"context": "ProjectPanel && not_editing",
"use_key_equivalents": true,
"bindings": {
"space": "project_panel::Open"
}
},
{
"context": "GitPanel && ChangesList",
"use_key_equivalents": true,
"bindings": {
"up": "menu::SelectPrevious",
"down": "menu::SelectNext",
"enter": "menu::Confirm",
"alt-y": "git::StageFile",
"shift-alt-y": "git::UnstageFile",
"alt-shift-y": "git::UnstageFile",
"ctrl-shift-y": "git::ToggleStaged",
"space": "git::ToggleStaged",
"shift-space": "git::StageRange",
"tab": "git_panel::FocusEditor",
@@ -937,7 +891,6 @@
},
{
"context": "GitCommit > Editor",
"use_key_equivalents": true,
"bindings": {
"escape": "menu::Cancel",
"enter": "editor::Newline",
@@ -948,7 +901,6 @@
},
{
"context": "GitPanel",
"use_key_equivalents": true,
"bindings": {
"ctrl-g ctrl-g": "git::Fetch",
"ctrl-g up": "git::Push",
@@ -965,7 +917,6 @@
},
{
"context": "GitDiff > Editor",
"use_key_equivalents": true,
"bindings": {
"ctrl-enter": "git::Commit",
"ctrl-shift-enter": "git::Amend",
@@ -975,14 +926,12 @@
},
{
"context": "AskPass > Editor",
"use_key_equivalents": true,
"bindings": {
"enter": "menu::Confirm"
}
},
{
"context": "CommitEditor > Editor",
"use_key_equivalents": true,
"bindings": {
"escape": "git_panel::FocusChanges",
"tab": "git_panel::FocusChanges",
@@ -996,16 +945,14 @@
},
{
"context": "DebugPanel",
"use_key_equivalents": true,
"bindings": {
"ctrl-t": "debugger::ToggleThreadPicker",
"ctrl-i": "debugger::ToggleSessionPicker",
"shift-alt-escape": "debugger::ToggleExpandItem"
"alt-shift-escape": "debugger::ToggleExpandItem"
}
},
{
"context": "VariableList",
"use_key_equivalents": true,
"bindings": {
"left": "variable_list::CollapseSelectedEntry",
"right": "variable_list::ExpandSelectedEntry",
@@ -1019,7 +966,6 @@
},
{
"context": "BreakpointList",
"use_key_equivalents": true,
"bindings": {
"space": "debugger::ToggleEnableBreakpoint",
"backspace": "debugger::UnsetBreakpoint",
@@ -1029,7 +975,6 @@
},
{
"context": "CollabPanel && not_editing",
"use_key_equivalents": true,
"bindings": {
"ctrl-backspace": "collab_panel::Remove",
"space": "menu::Confirm"
@@ -1037,7 +982,6 @@
},
{
"context": "CollabPanel",
"use_key_equivalents": true,
"bindings": {
"alt-up": "collab_panel::MoveChannelUp",
"alt-down": "collab_panel::MoveChannelDown"
@@ -1045,21 +989,18 @@
},
{
"context": "(CollabPanel && editing) > Editor",
"use_key_equivalents": true,
"bindings": {
"space": "collab_panel::InsertSpace"
}
},
{
"context": "ChannelModal",
"use_key_equivalents": true,
"bindings": {
"tab": "channel_modal::ToggleMode"
}
},
{
"context": "Picker > Editor",
"use_key_equivalents": true,
"bindings": {
"escape": "menu::Cancel",
"up": "menu::SelectPrevious",
@@ -1070,14 +1011,12 @@
},
{
"context": "ChannelModal > Picker > Editor",
"use_key_equivalents": true,
"bindings": {
"tab": "channel_modal::ToggleMode"
}
},
{
"context": "FileFinder || (FileFinder > Picker > Editor)",
"use_key_equivalents": true,
"bindings": {
"ctrl-p": "file_finder::Toggle",
"ctrl-shift-a": "file_finder::ToggleSplitMenu",
@@ -1086,7 +1025,6 @@
},
{
"context": "FileFinder || (FileFinder > Picker > Editor) || (FileFinder > Picker > menu)",
"use_key_equivalents": true,
"bindings": {
"ctrl-shift-p": "file_finder::SelectPrevious",
"ctrl-j": "pane::SplitDown",
@@ -1097,7 +1035,6 @@
},
{
"context": "TabSwitcher",
"use_key_equivalents": true,
"bindings": {
"ctrl-shift-tab": "menu::SelectPrevious",
"ctrl-up": "menu::SelectPrevious",
@@ -1107,7 +1044,6 @@
},
{
"context": "Terminal",
"use_key_equivalents": true,
"bindings": {
"ctrl-alt-space": "terminal::ShowCharacterPalette",
"copy": "terminal::Copy",
@@ -1153,14 +1089,12 @@
},
{
"context": "ZedPredictModal",
"use_key_equivalents": true,
"bindings": {
"escape": "menu::Cancel"
}
},
{
"context": "ConfigureContextServerModal > Editor",
"use_key_equivalents": true,
"bindings": {
"escape": "menu::Cancel",
"enter": "editor::Newline",
@@ -1191,7 +1125,6 @@
},
{
"context": "RunModal",
"use_key_equivalents": true,
"bindings": {
"ctrl-tab": "pane::ActivateNextItem",
"ctrl-shift-tab": "pane::ActivatePreviousItem"
@@ -1199,7 +1132,6 @@
},
{
"context": "MarkdownPreview",
"use_key_equivalents": true,
"bindings": {
"pageup": "markdown::MovePageUp",
"pagedown": "markdown::MovePageDown"
@@ -1254,7 +1186,7 @@
"ctrl-3": "onboarding::ActivateAISetupPage",
"ctrl-escape": "onboarding::Finish",
"alt-tab": "onboarding::SignIn",
"shift-alt-a": "onboarding::OpenAccount"
"alt-shift-a": "onboarding::OpenAccount"
}
}
]

View File

@@ -17,8 +17,8 @@
"bindings": {
"ctrl-i": "agent::ToggleFocus",
"ctrl-shift-i": "agent::ToggleFocus",
"ctrl-shift-l": "agent::QuoteSelection", // In cursor uses "Ask" mode
"ctrl-l": "agent::QuoteSelection", // In cursor uses "Agent" mode
"ctrl-shift-l": "assistant::QuoteSelection", // In cursor uses "Ask" mode
"ctrl-l": "assistant::QuoteSelection", // In cursor uses "Agent" mode
"ctrl-k": "assistant::InlineAssist",
"ctrl-shift-k": "assistant::InsertIntoEditor"
}

View File

@@ -38,7 +38,6 @@
"alt-;": ["editor::ToggleComments", { "advance_downwards": false }],
"ctrl-x ctrl-;": "editor::ToggleComments",
"alt-.": "editor::GoToDefinition", // xref-find-definitions
"alt-?": "editor::FindAllReferences", // xref-find-references
"alt-,": "pane::GoBack", // xref-pop-marker-stack
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer
"ctrl-d": "editor::Delete", // delete-char

View File

@@ -17,8 +17,8 @@
"bindings": {
"cmd-i": "agent::ToggleFocus",
"cmd-shift-i": "agent::ToggleFocus",
"cmd-shift-l": "agent::QuoteSelection", // In cursor uses "Ask" mode
"cmd-l": "agent::QuoteSelection", // In cursor uses "Agent" mode
"cmd-shift-l": "assistant::QuoteSelection", // In cursor uses "Ask" mode
"cmd-l": "assistant::QuoteSelection", // In cursor uses "Agent" mode
"cmd-k": "assistant::InlineAssist",
"cmd-shift-k": "assistant::InsertIntoEditor"
}

View File

@@ -38,7 +38,6 @@
"alt-;": ["editor::ToggleComments", { "advance_downwards": false }],
"ctrl-x ctrl-;": "editor::ToggleComments",
"alt-.": "editor::GoToDefinition", // xref-find-definitions
"alt-?": "editor::FindAllReferences", // xref-find-references
"alt-,": "pane::GoBack", // xref-pop-marker-stack
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer
"ctrl-d": "editor::Delete", // delete-char

View File

@@ -354,15 +354,6 @@
"ctrl-s": "editor::ShowSignatureHelp"
}
},
{
"context": "showing_completions",
"bindings": {
"ctrl-d": "vim::ScrollDown",
"ctrl-u": "vim::ScrollUp",
"ctrl-e": "vim::LineDown",
"ctrl-y": "vim::LineUp"
}
},
{
"context": "(vim_mode == normal || vim_mode == helix_normal) && !menu",
"bindings": {
@@ -437,14 +428,12 @@
"g h": "vim::StartOfLine",
"g s": "vim::FirstNonWhitespace", // "g s" default behavior is "space s"
"g e": "vim::EndOfDocument",
"g .": "vim::HelixGotoLastModification", // go to last modification
"g r": "editor::FindAllReferences", // zed specific
"g t": "vim::WindowTop",
"g c": "vim::WindowMiddle",
"g b": "vim::WindowBottom",
"shift-r": "editor::Paste",
"x": "vim::HelixSelectLine",
"x": "editor::SelectLine",
"shift-x": "editor::SelectLine",
"%": "editor::SelectAll",
// Window mode
@@ -830,7 +819,7 @@
"v": "project_panel::OpenPermanent",
"p": "project_panel::Open",
"x": "project_panel::RevealInFileManager",
"s": "workspace::OpenWithSystem",
"s": "project_panel::OpenWithSystem",
"z d": "project_panel::CompareMarkedFiles",
"] c": "project_panel::SelectNextGitEntry",
"[ c": "project_panel::SelectPrevGitEntry",

View File

@@ -172,7 +172,7 @@ The user has specified the following rules that should be applied:
Rules title: {{title}}
{{/if}}
``````
{{contents}}
{{contents}}}
``````
{{/each}}
{{/if}}

View File

@@ -162,12 +162,6 @@
// 2. Always quit the application
// "on_last_window_closed": "quit_app",
"on_last_window_closed": "platform_default",
// Whether to show padding for zoomed panels.
// When enabled, zoomed center panels (e.g. code editor) will have padding all around,
// while zoomed bottom/left/right panels will have padding to the top/right/left (respectively).
//
// Default: true
"zoomed_padding": true,
// Whether to use the system provided dialogs for Open and Save As.
// When set to false, Zed will use the built-in keyboard-first pickers.
"use_system_path_prompts": true,
@@ -188,8 +182,8 @@
// 4. A box drawn around the following character
// "hollow"
//
// Default: "bar"
"cursor_shape": "bar",
// Default: not set, defaults to "bar"
"cursor_shape": null,
// Determines when the mouse cursor should be hidden in an editor or input box.
//
// 1. Never hide the mouse cursor:
@@ -223,25 +217,9 @@
"current_line_highlight": "all",
// Whether to highlight all occurrences of the selected text in an editor.
"selection_highlight": true,
// Whether the text selection should have rounded corners.
"rounded_selection": true,
// The debounce delay before querying highlights from the language
// server based on the current cursor location.
"lsp_highlight_debounce": 75,
// The minimum APCA perceptual contrast between foreground and background colors.
// APCA (Accessible Perceptual Contrast Algorithm) is more accurate than WCAG 2.x,
// especially for dark mode. Values range from 0 to 106.
//
// Based on APCA Readability Criterion (ARC) Bronze Simple Mode:
// https://readtech.org/ARC/tests/bronze-simple-mode/
// - 0: No contrast adjustment
// - 45: Minimum for large fluent text (36px+)
// - 60: Minimum for other content text
// - 75: Minimum for body text
// - 90: Preferred for body text
//
// This only affects text drawn over highlight backgrounds in the editor.
"minimum_contrast_for_highlights": 45,
// Whether to pop the completions menu while typing in an editor without
// explicitly requesting it.
"show_completions_on_input": true,
@@ -282,8 +260,8 @@
// - "warning"
// - "info"
// - "hint"
// - "all" — allow all diagnostics (default)
"diagnostics_max_severity": "all",
// - null — allow all diagnostics (default)
"diagnostics_max_severity": null,
// Whether to show wrap guides (vertical rulers) in the editor.
// Setting this to true will show a guide at the 'preferred_line_length' value
// if 'soft_wrap' is set to 'preferred_line_length', and will show any
@@ -295,8 +273,6 @@
"redact_private_values": false,
// The default number of lines to expand excerpts in the multibuffer by.
"expand_excerpt_lines": 5,
// The default number of context lines shown in multibuffer excerpts.
"excerpt_context_lines": 2,
// Globs to match against file paths to determine if a file is private.
"private_files": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/secrets.yml"],
// Whether to use additional LSP queries to format (and amend) the code after
@@ -381,8 +357,6 @@
// Whether to show code action buttons in the editor toolbar.
"code_actions": false
},
// Whether to allow windows to tab together based on the users tabbing preference (macOS only).
"use_system_window_tabs": false,
// Titlebar related settings
"title_bar": {
// Whether to show the branch icon beside branch switcher in the titlebar.
@@ -673,8 +647,6 @@
// "never"
"show": "always"
},
// Whether to enable drag-and-drop operations in the project panel.
"drag_and_drop": true,
// Whether to hide the root entry when only one folder is open in the window.
"hide_root": false
},
@@ -745,7 +717,7 @@
// Can be 'never', 'always', or 'when_in_call',
// or a boolean (interpreted as 'never'/'always').
"button": "when_in_call",
// Where to dock the chat panel. Can be 'left' or 'right'.
// Where to the chat panel. Can be 'left' or 'right'.
"dock": "right",
// Default width of the chat panel.
"default_width": 240
@@ -753,7 +725,7 @@
"git_panel": {
// Whether to show the git panel button in the status bar.
"button": true,
// Where to dock the git panel. Can be 'left' or 'right'.
// Where to show the git panel. Can be 'left' or 'right'.
"dock": "left",
// Default width of the git panel.
"default_width": 360,
@@ -1161,6 +1133,11 @@
// The minimum severity of the diagnostics to show inline.
// Inherits editor's diagnostics' max severity settings when `null`.
"max_severity": null
},
"cargo": {
// When enabled, Zed disables rust-analyzer's check on save and starts to query
// Cargo diagnostics separately.
"fetch_cargo_diagnostics": false
}
},
// Files or globs of files that will be excluded by Zed entirely. They will be skipped during file
@@ -1526,11 +1503,6 @@
//
// Default: fallback
"words": "fallback",
// Minimum number of characters required to automatically trigger word-based completions.
// Before that value, it's still possible to trigger the words-based completion manually with the corresponding editor command.
//
// Default: 3
"words_min_length": 3,
// Whether to fetch LSP completions or not.
//
// Default: true
@@ -1603,7 +1575,7 @@
"ensure_final_newline_on_save": false
},
"Elixir": {
"language_servers": ["elixir-ls", "!expert", "!next-ls", "!lexical", "..."]
"language_servers": ["elixir-ls", "!next-ls", "!lexical", "..."]
},
"Elm": {
"tab_size": 4
@@ -1628,7 +1600,7 @@
}
},
"HEEX": {
"language_servers": ["elixir-ls", "!expert", "!next-ls", "!lexical", "..."]
"language_servers": ["elixir-ls", "!next-ls", "!lexical", "..."]
},
"HTML": {
"prettier": {
@@ -1657,9 +1629,6 @@
"allowed": true
}
},
"Kotlin": {
"language_servers": ["kotlin-language-server", "!kotlin-lsp", "..."]
},
"LaTeX": {
"formatter": "language_server",
"language_servers": ["texlab", "..."],
@@ -1673,6 +1642,9 @@
"use_on_type_format": false,
"allow_rewrap": "anywhere",
"soft_wrap": "editor_width",
"completions": {
"words": "disabled"
},
"prettier": {
"allowed": true
}
@@ -1686,6 +1658,9 @@
}
},
"Plain Text": {
"completions": {
"words": "disabled"
},
"allow_rewrap": "anywhere"
},
"Python": {
@@ -1776,7 +1751,7 @@
"api_url": "http://localhost:1234/api/v0"
},
"deepseek": {
"api_url": "https://api.deepseek.com/v1"
"api_url": "https://api.deepseek.com"
},
"mistral": {
"api_url": "https://api.mistral.ai/v1"
@@ -1924,10 +1899,7 @@
"debugger": {
"stepping_granularity": "line",
"save_breakpoints": true,
"timeout": 2000,
"dock": "bottom",
"log_dap_communications": true,
"format_dap_log_messages": true,
"button": true
},
// Configures any number of settings profiles that are temporarily applied on

View File

@@ -43,8 +43,8 @@
// "args": ["--login"]
// }
// }
"shell": "system"
"shell": "system",
// Represents the tags for inline runnable indicators, or spawning multiple tasks at once.
// "tags": []
"tags": []
}
]

View File

@@ -93,7 +93,7 @@
"terminal.ansi.bright_cyan": "#4c806fff",
"terminal.ansi.dim_cyan": "#cbf2e4ff",
"terminal.ansi.white": "#bfbdb6ff",
"terminal.ansi.bright_white": "#fafafaff",
"terminal.ansi.bright_white": "#bfbdb6ff",
"terminal.ansi.dim_white": "#787876ff",
"link_text.hover": "#5ac1feff",
"conflict": "#feb454ff",
@@ -479,7 +479,7 @@
"terminal.ansi.bright_cyan": "#ace0cbff",
"terminal.ansi.dim_cyan": "#2a5f4aff",
"terminal.ansi.white": "#fcfcfcff",
"terminal.ansi.bright_white": "#ffffffff",
"terminal.ansi.bright_white": "#fcfcfcff",
"terminal.ansi.dim_white": "#bcbec0ff",
"link_text.hover": "#3b9ee5ff",
"conflict": "#f1ad49ff",
@@ -865,7 +865,7 @@
"terminal.ansi.bright_cyan": "#4c806fff",
"terminal.ansi.dim_cyan": "#cbf2e4ff",
"terminal.ansi.white": "#cccac2ff",
"terminal.ansi.bright_white": "#fafafaff",
"terminal.ansi.bright_white": "#cccac2ff",
"terminal.ansi.dim_white": "#898a8aff",
"link_text.hover": "#72cffeff",
"conflict": "#fecf72ff",

View File

@@ -94,7 +94,7 @@
"terminal.ansi.bright_cyan": "#45603eff",
"terminal.ansi.dim_cyan": "#c7dfbdff",
"terminal.ansi.white": "#fbf1c7ff",
"terminal.ansi.bright_white": "#ffffffff",
"terminal.ansi.bright_white": "#fbf1c7ff",
"terminal.ansi.dim_white": "#b0a189ff",
"link_text.hover": "#83a598ff",
"version_control.added": "#b7bb26ff",
@@ -494,7 +494,7 @@
"terminal.ansi.bright_cyan": "#45603eff",
"terminal.ansi.dim_cyan": "#c7dfbdff",
"terminal.ansi.white": "#fbf1c7ff",
"terminal.ansi.bright_white": "#ffffffff",
"terminal.ansi.bright_white": "#fbf1c7ff",
"terminal.ansi.dim_white": "#b0a189ff",
"link_text.hover": "#83a598ff",
"version_control.added": "#b7bb26ff",
@@ -894,7 +894,7 @@
"terminal.ansi.bright_cyan": "#45603eff",
"terminal.ansi.dim_cyan": "#c7dfbdff",
"terminal.ansi.white": "#fbf1c7ff",
"terminal.ansi.bright_white": "#ffffffff",
"terminal.ansi.bright_white": "#fbf1c7ff",
"terminal.ansi.dim_white": "#b0a189ff",
"link_text.hover": "#83a598ff",
"version_control.added": "#b7bb26ff",
@@ -1294,7 +1294,7 @@
"terminal.ansi.bright_cyan": "#9fbca8ff",
"terminal.ansi.dim_cyan": "#253e2eff",
"terminal.ansi.white": "#fbf1c7ff",
"terminal.ansi.bright_white": "#ffffffff",
"terminal.ansi.bright_white": "#fbf1c7ff",
"terminal.ansi.dim_white": "#b0a189ff",
"link_text.hover": "#0b6678ff",
"version_control.added": "#797410ff",
@@ -1694,7 +1694,7 @@
"terminal.ansi.bright_cyan": "#9fbca8ff",
"terminal.ansi.dim_cyan": "#253e2eff",
"terminal.ansi.white": "#f9f5d7ff",
"terminal.ansi.bright_white": "#ffffffff",
"terminal.ansi.bright_white": "#f9f5d7ff",
"terminal.ansi.dim_white": "#b0a189ff",
"link_text.hover": "#0b6678ff",
"version_control.added": "#797410ff",
@@ -2094,7 +2094,7 @@
"terminal.ansi.bright_cyan": "#9fbca8ff",
"terminal.ansi.dim_cyan": "#253e2eff",
"terminal.ansi.white": "#f2e5bcff",
"terminal.ansi.bright_white": "#ffffffff",
"terminal.ansi.bright_white": "#f2e5bcff",
"terminal.ansi.dim_white": "#b0a189ff",
"link_text.hover": "#0b6678ff",
"version_control.added": "#797410ff",

View File

@@ -93,7 +93,7 @@
"terminal.ansi.bright_cyan": "#3a565bff",
"terminal.ansi.dim_cyan": "#b9d9dfff",
"terminal.ansi.white": "#dce0e5ff",
"terminal.ansi.bright_white": "#fafafaff",
"terminal.ansi.bright_white": "#dce0e5ff",
"terminal.ansi.dim_white": "#575d65ff",
"link_text.hover": "#74ade8ff",
"version_control.added": "#27a657ff",
@@ -468,7 +468,7 @@
"terminal.bright_foreground": "#242529ff",
"terminal.dim_foreground": "#fafafaff",
"terminal.ansi.black": "#242529ff",
"terminal.ansi.bright_black": "#747579ff",
"terminal.ansi.bright_black": "#242529ff",
"terminal.ansi.dim_black": "#97979aff",
"terminal.ansi.red": "#d36151ff",
"terminal.ansi.bright_red": "#f0b0a4ff",
@@ -489,7 +489,7 @@
"terminal.ansi.bright_cyan": "#a3bedaff",
"terminal.ansi.dim_cyan": "#254058ff",
"terminal.ansi.white": "#fafafaff",
"terminal.ansi.bright_white": "#ffffffff",
"terminal.ansi.bright_white": "#fafafaff",
"terminal.ansi.dim_white": "#aaaaaaff",
"link_text.hover": "#5c78e2ff",
"version_control.added": "#27a657ff",

View File

@@ -18,8 +18,8 @@ test-support = ["gpui/test-support", "project/test-support", "dep:parking_lot"]
[dependencies]
action_log.workspace = true
agent-client-protocol.workspace = true
agent.workspace = true
anyhow.workspace = true
agent_settings.workspace = true
buffer_diff.workspace = true
collections.workspace = true
editor.workspace = true
@@ -31,21 +31,18 @@ language.workspace = true
language_model.workspace = true
markdown.workspace = true
parking_lot = { workspace = true, optional = true }
portable-pty.workspace = true
project.workspace = true
prompt_store.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
smol.workspace = true
task.workspace = true
terminal.workspace = true
ui.workspace = true
url.workspace = true
util.workspace = true
uuid.workspace = true
watch.workspace = true
which.workspace = true
workspace-hack.workspace = true
[dev-dependencies]

File diff suppressed because it is too large Load Diff

View File

@@ -5,12 +5,11 @@ use collections::IndexMap;
use gpui::{Entity, SharedString, Task};
use language_model::LanguageModelProviderId;
use project::Project;
use serde::{Deserialize, Serialize};
use std::{any::Any, error::Error, fmt, path::Path, rc::Rc, sync::Arc};
use ui::{App, IconName};
use uuid::Uuid;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct UserMessageId(Arc<str>);
impl UserMessageId {
@@ -41,26 +40,18 @@ pub trait AgentConnection {
fn resume(
&self,
_session_id: &acp::SessionId,
_cx: &App,
_cx: &mut App,
) -> Option<Rc<dyn AgentSessionResume>> {
None
}
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App);
fn truncate(
fn session_editor(
&self,
_session_id: &acp::SessionId,
_cx: &App,
) -> Option<Rc<dyn AgentSessionTruncate>> {
None
}
fn set_title(
&self,
_session_id: &acp::SessionId,
_cx: &App,
) -> Option<Rc<dyn AgentSessionSetTitle>> {
_cx: &mut App,
) -> Option<Rc<dyn AgentSessionEditor>> {
None
}
@@ -72,9 +63,6 @@ pub trait AgentConnection {
None
}
fn telemetry(&self) -> Option<Rc<dyn AgentTelemetry>> {
None
}
fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
}
@@ -84,31 +72,14 @@ impl dyn AgentConnection {
}
}
pub trait AgentSessionTruncate {
fn run(&self, message_id: UserMessageId, cx: &mut App) -> Task<Result<()>>;
pub trait AgentSessionEditor {
fn truncate(&self, message_id: UserMessageId, cx: &mut App) -> Task<Result<()>>;
}
pub trait AgentSessionResume {
fn run(&self, cx: &mut App) -> Task<Result<acp::PromptResponse>>;
}
pub trait AgentSessionSetTitle {
fn run(&self, title: SharedString, cx: &mut App) -> Task<Result<()>>;
}
pub trait AgentTelemetry {
/// The name of the agent used for telemetry.
fn agent_name(&self) -> String;
/// A representation of the current thread state that can be serialized for
/// storage with telemetry events.
fn thread_data(
&self,
session_id: &acp::SessionId,
cx: &mut App,
) -> Task<Result<serde_json::Value>>;
}
#[derive(Debug)]
pub struct AuthRequired {
pub description: Option<String>,
@@ -237,7 +208,6 @@ impl AgentModelList {
mod test_support {
use std::sync::Arc;
use action_log::ActionLog;
use collections::HashMap;
use futures::{channel::oneshot, future::try_join_all};
use gpui::{AppContext as _, WeakEntity};
@@ -325,23 +295,8 @@ mod test_support {
cx: &mut gpui::App,
) -> Task<gpui::Result<Entity<AcpThread>>> {
let session_id = acp::SessionId(self.sessions.lock().len().to_string().into());
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let thread = cx.new(|cx| {
AcpThread::new(
"Test",
self.clone(),
project,
action_log,
session_id.clone(),
watch::Receiver::constant(acp::PromptCapabilities {
image: true,
audio: true,
embedded_context: true,
}),
vec![],
cx,
)
});
let thread =
cx.new(|cx| AcpThread::new("Test", self.clone(), project, session_id.clone(), cx));
self.sessions.lock().insert(
session_id,
Session {
@@ -393,15 +348,14 @@ mod test_support {
};
let task = cx.spawn(async move |cx| {
if let Some((tool_call, options)) = permission_request {
thread
.update(cx, |thread, cx| {
thread.request_tool_call_authorization(
tool_call.clone().into(),
options.clone(),
cx,
)
})??
.await;
let permission = thread.update(cx, |thread, cx| {
thread.request_tool_call_authorization(
tool_call.clone().into(),
options.clone(),
cx,
)
})?;
permission?.await?;
}
thread.update(cx, |thread, cx| {
thread.handle_session_update(update.clone(), cx).unwrap();
@@ -429,15 +383,15 @@ mod test_support {
.response_tx
.take()
{
end_turn_tx.send(acp::StopReason::Cancelled).unwrap();
end_turn_tx.send(acp::StopReason::Canceled).unwrap();
}
}
fn truncate(
fn session_editor(
&self,
_session_id: &agent_client_protocol::SessionId,
_cx: &App,
) -> Option<Rc<dyn AgentSessionTruncate>> {
_cx: &mut App,
) -> Option<Rc<dyn AgentSessionEditor>> {
Some(Rc::new(StubAgentSessionEditor))
}
@@ -448,8 +402,8 @@ mod test_support {
struct StubAgentSessionEditor;
impl AgentSessionTruncate for StubAgentSessionEditor {
fn run(&self, _: UserMessageId, _: &mut App) -> Task<Result<()>> {
impl AgentSessionEditor for StubAgentSessionEditor {
fn truncate(&self, _: UserMessageId, _: &mut App) -> Task<Result<()>> {
Task::ready(Ok(()))
}
}

View File

@@ -1,6 +1,7 @@
use agent_client_protocol as acp;
use anyhow::Result;
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
use editor::{MultiBuffer, PathKey, multibuffer_context_lines};
use editor::{MultiBuffer, PathKey};
use gpui::{App, AppContext, AsyncApp, Context, Entity, Subscription, Task};
use itertools::Itertools;
use language::{
@@ -20,54 +21,69 @@ pub enum Diff {
}
impl Diff {
pub fn finalized(
path: PathBuf,
old_text: Option<String>,
new_text: String,
pub fn from_acp(
diff: acp::Diff,
language_registry: Arc<LanguageRegistry>,
cx: &mut Context<Self>,
) -> Self {
let acp::Diff {
path,
old_text,
new_text,
} = diff;
let multibuffer = cx.new(|_cx| MultiBuffer::without_headers(Capability::ReadOnly));
let new_buffer = cx.new(|cx| Buffer::local(new_text, cx));
let base_text = old_text.clone().unwrap_or(String::new()).into();
let old_buffer = cx.new(|cx| Buffer::local(old_text.unwrap_or("".into()), cx));
let new_buffer_snapshot = new_buffer.read(cx).text_snapshot();
let buffer_diff = cx.new(|cx| BufferDiff::new(&new_buffer_snapshot, cx));
let task = cx.spawn({
let multibuffer = multibuffer.clone();
let path = path.clone();
let buffer = new_buffer.clone();
async move |_, cx| {
let language = language_registry
.language_for_file_path(&path)
.await
.log_err();
buffer.update(cx, |buffer, cx| buffer.set_language(language.clone(), cx))?;
new_buffer.update(cx, |buffer, cx| buffer.set_language(language.clone(), cx))?;
let diff = build_buffer_diff(
old_text.unwrap_or("".into()).into(),
&buffer,
Some(language_registry.clone()),
cx,
)
.await?;
let old_buffer_snapshot = old_buffer.update(cx, |buffer, cx| {
buffer.set_language(language, cx);
buffer.snapshot()
})?;
buffer_diff
.update(cx, |diff, cx| {
diff.set_base_text(
old_buffer_snapshot,
Some(language_registry),
new_buffer_snapshot,
cx,
)
})?
.await?;
multibuffer
.update(cx, |multibuffer, cx| {
let hunk_ranges = {
let buffer = buffer.read(cx);
let diff = diff.read(cx);
let buffer = new_buffer.read(cx);
let diff = buffer_diff.read(cx);
diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer, cx)
.map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer))
.collect::<Vec<_>>()
};
multibuffer.set_excerpts_for_path(
PathKey::for_buffer(&buffer, cx),
buffer.clone(),
PathKey::for_buffer(&new_buffer, cx),
new_buffer.clone(),
hunk_ranges,
multibuffer_context_lines(cx),
editor::DEFAULT_MULTIBUFFER_CONTEXT,
cx,
);
multibuffer.add_diff(diff, cx);
multibuffer.add_diff(buffer_diff, cx);
})
.log_err();
@@ -78,26 +94,23 @@ impl Diff {
Self::Finalized(FinalizedDiff {
multibuffer,
path,
base_text,
new_buffer,
_update_diff: task,
})
}
pub fn new(buffer: Entity<Buffer>, cx: &mut Context<Self>) -> Self {
let buffer_text_snapshot = buffer.read(cx).text_snapshot();
let base_text_snapshot = buffer.read(cx).snapshot();
let base_text = base_text_snapshot.text();
debug_assert_eq!(buffer_text_snapshot.text(), base_text);
let buffer_snapshot = buffer.read(cx).snapshot();
let base_text = buffer_snapshot.text();
let language_registry = buffer.read(cx).language_registry();
let text_snapshot = buffer.read(cx).text_snapshot();
let buffer_diff = cx.new(|cx| {
let mut diff = BufferDiff::new_unchanged(&buffer_text_snapshot, base_text_snapshot);
let snapshot = diff.snapshot(cx);
let secondary_diff = cx.new(|cx| {
let mut diff = BufferDiff::new(&buffer_text_snapshot, cx);
diff.set_snapshot(snapshot, &buffer_text_snapshot, cx);
diff
});
diff.set_secondary_diff(secondary_diff);
let mut diff = BufferDiff::new(&text_snapshot, cx);
let _ = diff.set_base_text(
buffer_snapshot.clone(),
language_registry,
text_snapshot,
cx,
);
diff
});
@@ -115,7 +128,7 @@ impl Diff {
diff.update(cx);
}
}),
new_buffer: buffer,
buffer,
diff: buffer_diff,
revealed_ranges: Vec::new(),
update_diff: Task::ready(Ok(())),
@@ -150,9 +163,9 @@ impl Diff {
.map(|buffer| buffer.read(cx).text())
.join("\n");
let path = match self {
Diff::Pending(PendingDiff {
new_buffer: buffer, ..
}) => buffer.read(cx).file().map(|file| file.path().as_ref()),
Diff::Pending(PendingDiff { buffer, .. }) => {
buffer.read(cx).file().map(|file| file.path().as_ref())
}
Diff::Finalized(FinalizedDiff { path, .. }) => Some(path.as_path()),
};
format!(
@@ -165,33 +178,12 @@ impl Diff {
pub fn has_revealed_range(&self, cx: &App) -> bool {
self.multibuffer().read(cx).excerpt_paths().next().is_some()
}
pub fn needs_update(&self, old_text: &str, new_text: &str, cx: &App) -> bool {
match self {
Diff::Pending(PendingDiff {
base_text,
new_buffer,
..
}) => {
base_text.as_str() != old_text
|| !new_buffer.read(cx).as_rope().chunks().equals_str(new_text)
}
Diff::Finalized(FinalizedDiff {
base_text,
new_buffer,
..
}) => {
base_text.as_str() != old_text
|| !new_buffer.read(cx).as_rope().chunks().equals_str(new_text)
}
}
}
}
pub struct PendingDiff {
multibuffer: Entity<MultiBuffer>,
base_text: Arc<String>,
new_buffer: Entity<Buffer>,
buffer: Entity<Buffer>,
diff: Entity<BufferDiff>,
revealed_ranges: Vec<Range<Anchor>>,
_subscription: Subscription,
@@ -200,7 +192,7 @@ pub struct PendingDiff {
impl PendingDiff {
pub fn update(&mut self, cx: &mut Context<Diff>) {
let buffer = self.new_buffer.clone();
let buffer = self.buffer.clone();
let buffer_diff = self.diff.clone();
let base_text = self.base_text.clone();
self.update_diff = cx.spawn(async move |diff, cx| {
@@ -217,10 +209,7 @@ impl PendingDiff {
)
.await?;
buffer_diff.update(cx, |diff, cx| {
diff.set_snapshot(diff_snapshot.clone(), &text_snapshot, cx);
diff.secondary_diff().unwrap().update(cx, |diff, cx| {
diff.set_snapshot(diff_snapshot.clone(), &text_snapshot, cx);
});
diff.set_snapshot(diff_snapshot, &text_snapshot, cx)
})?;
diff.update(cx, |diff, cx| {
if let Diff::Pending(diff) = diff {
@@ -238,10 +227,10 @@ impl PendingDiff {
fn finalize(&self, cx: &mut Context<Diff>) -> FinalizedDiff {
let ranges = self.excerpt_ranges(cx);
let base_text = self.base_text.clone();
let language_registry = self.new_buffer.read(cx).language_registry();
let language_registry = self.buffer.read(cx).language_registry().clone();
let path = self
.new_buffer
.buffer
.read(cx)
.file()
.map(|file| file.path().as_ref())
@@ -250,12 +239,12 @@ impl PendingDiff {
// Replace the buffer in the multibuffer with the snapshot
let buffer = cx.new(|cx| {
let language = self.new_buffer.read(cx).language().cloned();
let language = self.buffer.read(cx).language().cloned();
let buffer = TextBuffer::new_normalized(
0,
cx.entity_id().as_non_zero_u64().into(),
self.new_buffer.read(cx).line_ending(),
self.new_buffer.read(cx).as_rope().clone(),
self.buffer.read(cx).line_ending(),
self.buffer.read(cx).as_rope().clone(),
);
let mut buffer = Buffer::build(buffer, None, Capability::ReadWrite);
buffer.set_language(language, cx);
@@ -264,6 +253,7 @@ impl PendingDiff {
let buffer_diff = cx.spawn({
let buffer = buffer.clone();
let language_registry = language_registry.clone();
async move |_this, cx| {
build_buffer_diff(base_text, &buffer, language_registry, cx).await
}
@@ -279,7 +269,7 @@ impl PendingDiff {
path_key,
buffer,
ranges,
multibuffer_context_lines(cx),
editor::DEFAULT_MULTIBUFFER_CONTEXT,
cx,
);
multibuffer.add_diff(buffer_diff.clone(), cx);
@@ -291,9 +281,7 @@ impl PendingDiff {
FinalizedDiff {
path,
base_text: self.base_text.clone(),
multibuffer: self.multibuffer.clone(),
new_buffer: self.new_buffer.clone(),
_update_diff: update_diff,
}
}
@@ -302,10 +290,10 @@ impl PendingDiff {
let ranges = self.excerpt_ranges(cx);
self.multibuffer.update(cx, |multibuffer, cx| {
multibuffer.set_excerpts_for_path(
PathKey::for_buffer(&self.new_buffer, cx),
self.new_buffer.clone(),
PathKey::for_buffer(&self.buffer, cx),
self.buffer.clone(),
ranges,
multibuffer_context_lines(cx),
editor::DEFAULT_MULTIBUFFER_CONTEXT,
cx,
);
let end = multibuffer.len(cx);
@@ -315,7 +303,7 @@ impl PendingDiff {
}
fn excerpt_ranges(&self, cx: &App) -> Vec<Range<Point>> {
let buffer = self.new_buffer.read(cx);
let buffer = self.buffer.read(cx);
let diff = self.diff.read(cx);
let mut ranges = diff
.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer, cx)
@@ -349,8 +337,6 @@ impl PendingDiff {
pub struct FinalizedDiff {
path: PathBuf,
base_text: Arc<String>,
new_buffer: Entity<Buffer>,
multibuffer: Entity<MultiBuffer>,
_update_diff: Task<Result<()>>,
}
@@ -404,21 +390,3 @@ async fn build_buffer_diff(
diff
})
}
#[cfg(test)]
mod tests {
use gpui::{AppContext as _, TestAppContext};
use language::Buffer;
use crate::Diff;
#[gpui::test]
async fn test_pending_diff(cx: &mut TestAppContext) {
let buffer = cx.new(|cx| Buffer::local("hello!", cx));
let _diff = cx.new(|cx| Diff::new(buffer.clone(), cx));
buffer.update(cx, |buffer, cx| {
buffer.set_text("HELLO!", cx);
});
cx.run_until_parked();
}
}

View File

@@ -1,33 +1,31 @@
use agent_client_protocol as acp;
use agent::ThreadId;
use anyhow::{Context as _, Result, bail};
use file_icons::FileIcons;
use prompt_store::{PromptId, UserPromptId};
use serde::{Deserialize, Serialize};
use std::{
fmt,
ops::RangeInclusive,
ops::Range,
path::{Path, PathBuf},
str::FromStr,
};
use ui::{App, IconName, SharedString};
use url::Url;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Hash)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum MentionUri {
File {
abs_path: PathBuf,
},
PastedImage,
Directory {
abs_path: PathBuf,
},
Symbol {
abs_path: PathBuf,
path: PathBuf,
name: String,
line_range: RangeInclusive<u32>,
line_range: Range<u32>,
},
Thread {
id: acp::SessionId,
id: ThreadId,
name: String,
},
TextThread {
@@ -39,9 +37,8 @@ pub enum MentionUri {
name: String,
},
Selection {
#[serde(default, skip_serializing_if = "Option::is_none")]
abs_path: Option<PathBuf>,
line_range: RangeInclusive<u32>,
path: PathBuf,
line_range: Range<u32>,
},
Fetch {
url: Url,
@@ -50,56 +47,55 @@ pub enum MentionUri {
impl MentionUri {
pub fn parse(input: &str) -> Result<Self> {
fn parse_line_range(fragment: &str) -> Result<RangeInclusive<u32>> {
let range = fragment
.strip_prefix("L")
.context("Line range must start with \"L\"")?;
let (start, end) = range
.split_once(":")
.context("Line range must use colon as separator")?;
let range = start
.parse::<u32>()
.context("Parsing line range start")?
.checked_sub(1)
.context("Line numbers should be 1-based")?
..=end
.parse::<u32>()
.context("Parsing line range end")?
.checked_sub(1)
.context("Line numbers should be 1-based")?;
Ok(range)
}
let url = url::Url::parse(input)?;
let path = url.path();
match url.scheme() {
"file" => {
let path = url.to_file_path().ok().context("Extracting file path")?;
if let Some(fragment) = url.fragment() {
let line_range = parse_line_range(fragment)?;
let range = fragment
.strip_prefix("L")
.context("Line range must start with \"L\"")?;
let (start, end) = range
.split_once(":")
.context("Line range must use colon as separator")?;
let line_range = start
.parse::<u32>()
.context("Parsing line range start")?
.checked_sub(1)
.context("Line numbers should be 1-based")?
..end
.parse::<u32>()
.context("Parsing line range end")?
.checked_sub(1)
.context("Line numbers should be 1-based")?;
if let Some(name) = single_query_param(&url, "symbol")? {
Ok(Self::Symbol {
name,
abs_path: path,
path: path.into(),
line_range,
})
} else {
Ok(Self::Selection {
abs_path: Some(path),
path: path.into(),
line_range,
})
}
} else if input.ends_with("/") {
Ok(Self::Directory { abs_path: path })
} else {
Ok(Self::File { abs_path: path })
let abs_path =
PathBuf::from(format!("{}{}", url.host_str().unwrap_or(""), path));
if input.ends_with("/") {
Ok(Self::Directory { abs_path })
} else {
Ok(Self::File { abs_path })
}
}
}
"zed" => {
if let Some(thread_id) = path.strip_prefix("/agent/thread/") {
let name = single_query_param(&url, "name")?.context("Missing thread name")?;
Ok(Self::Thread {
id: acp::SessionId(thread_id.into()),
id: thread_id.into(),
name,
})
} else if let Some(path) = path.strip_prefix("/agent/text-thread/") {
@@ -115,17 +111,6 @@ impl MentionUri {
id: rule_id.into(),
name,
})
} else if path.starts_with("/agent/pasted-image") {
Ok(Self::PastedImage)
} else if path.starts_with("/agent/untitled-buffer") {
let fragment = url
.fragment()
.context("Missing fragment for untitled buffer selection")?;
let line_range = parse_line_range(fragment)?;
Ok(Self::Selection {
abs_path: None,
line_range,
})
} else {
bail!("invalid zed url: {:?}", input);
}
@@ -142,16 +127,13 @@ impl MentionUri {
.unwrap_or_default()
.to_string_lossy()
.into_owned(),
MentionUri::PastedImage => "Image".to_string(),
MentionUri::Symbol { name, .. } => name.clone(),
MentionUri::Thread { name, .. } => name.clone(),
MentionUri::TextThread { name, .. } => name.clone(),
MentionUri::Rule { name, .. } => name.clone(),
MentionUri::Selection {
abs_path: path,
line_range,
..
} => selection_name(path.as_deref(), line_range),
path, line_range, ..
} => selection_name(path, line_range),
MentionUri::Fetch { url } => url.to_string(),
}
}
@@ -161,7 +143,6 @@ impl MentionUri {
MentionUri::File { abs_path } => {
FileIcons::get_icon(abs_path, cx).unwrap_or_else(|| IconName::File.path().into())
}
MentionUri::PastedImage => IconName::Image.path().into(),
MentionUri::Directory { .. } => FileIcons::get_folder_icon(false, cx)
.unwrap_or_else(|| IconName::Folder.path().into()),
MentionUri::Symbol { .. } => IconName::Code.path().into(),
@@ -180,42 +161,42 @@ impl MentionUri {
pub fn to_uri(&self) -> Url {
match self {
MentionUri::File { abs_path } => {
Url::from_file_path(abs_path).expect("mention path should be absolute")
let mut url = Url::parse("file:///").unwrap();
let path = abs_path.to_string_lossy();
url.set_path(&path);
url
}
MentionUri::PastedImage => Url::parse("zed:///agent/pasted-image").unwrap(),
MentionUri::Directory { abs_path } => {
Url::from_directory_path(abs_path).expect("mention path should be absolute")
let mut url = Url::parse("file:///").unwrap();
let mut path = abs_path.to_string_lossy().to_string();
if !path.ends_with("/") {
path.push_str("/");
}
url.set_path(&path);
url
}
MentionUri::Symbol {
abs_path,
path,
name,
line_range,
} => {
let mut url =
Url::from_file_path(abs_path).expect("mention path should be absolute");
let mut url = Url::parse("file:///").unwrap();
url.set_path(&path.to_string_lossy());
url.query_pairs_mut().append_pair("symbol", name);
url.set_fragment(Some(&format!(
"L{}:{}",
line_range.start() + 1,
line_range.end() + 1
line_range.start + 1,
line_range.end + 1
)));
url
}
MentionUri::Selection {
abs_path: path,
line_range,
} => {
let mut url = if let Some(path) = path {
Url::from_file_path(path).expect("mention path should be absolute")
} else {
let mut url = Url::parse("zed:///").unwrap();
url.set_path("/agent/untitled-buffer");
url
};
MentionUri::Selection { path, line_range } => {
let mut url = Url::parse("file:///").unwrap();
url.set_path(&path.to_string_lossy());
url.set_fragment(Some(&format!(
"L{}:{}",
line_range.start() + 1,
line_range.end() + 1
line_range.start + 1,
line_range.end + 1
)));
url
}
@@ -227,10 +208,7 @@ impl MentionUri {
}
MentionUri::TextThread { path, name } => {
let mut url = Url::parse("zed:///").unwrap();
url.set_path(&format!(
"/agent/text-thread/{}",
path.to_string_lossy().trim_start_matches('/')
));
url.set_path(&format!("/agent/text-thread/{}", path.to_string_lossy()));
url.query_pairs_mut().append_pair("name", name);
url
}
@@ -276,30 +254,26 @@ fn single_query_param(url: &Url, name: &'static str) -> Result<Option<String>> {
}
}
pub fn selection_name(path: Option<&Path>, line_range: &RangeInclusive<u32>) -> String {
pub fn selection_name(path: &Path, line_range: &Range<u32>) -> String {
format!(
"{} ({}:{})",
path.and_then(|path| path.file_name())
.unwrap_or("Untitled".as_ref())
.display(),
*line_range.start() + 1,
*line_range.end() + 1
path.file_name().unwrap_or_default().display(),
line_range.start + 1,
line_range.end + 1
)
}
#[cfg(test)]
mod tests {
use util::{path, uri};
use super::*;
#[test]
fn test_parse_file_uri() {
let file_uri = uri!("file:///path/to/file.rs");
let file_uri = "file:///path/to/file.rs";
let parsed = MentionUri::parse(file_uri).unwrap();
match &parsed {
MentionUri::File { abs_path } => {
assert_eq!(abs_path.to_str().unwrap(), path!("/path/to/file.rs"));
assert_eq!(abs_path.to_str().unwrap(), "/path/to/file.rs");
}
_ => panic!("Expected File variant"),
}
@@ -308,11 +282,11 @@ mod tests {
#[test]
fn test_parse_directory_uri() {
let file_uri = uri!("file:///path/to/dir/");
let file_uri = "file:///path/to/dir/";
let parsed = MentionUri::parse(file_uri).unwrap();
match &parsed {
MentionUri::Directory { abs_path } => {
assert_eq!(abs_path.to_str().unwrap(), path!("/path/to/dir/"));
assert_eq!(abs_path.to_str().unwrap(), "/path/to/dir/");
}
_ => panic!("Expected Directory variant"),
}
@@ -322,35 +296,33 @@ mod tests {
#[test]
fn test_to_directory_uri_with_slash() {
let uri = MentionUri::Directory {
abs_path: PathBuf::from(path!("/path/to/dir/")),
abs_path: PathBuf::from("/path/to/dir/"),
};
let expected = uri!("file:///path/to/dir/");
assert_eq!(uri.to_uri().to_string(), expected);
assert_eq!(uri.to_uri().to_string(), "file:///path/to/dir/");
}
#[test]
fn test_to_directory_uri_without_slash() {
let uri = MentionUri::Directory {
abs_path: PathBuf::from(path!("/path/to/dir")),
abs_path: PathBuf::from("/path/to/dir"),
};
let expected = uri!("file:///path/to/dir/");
assert_eq!(uri.to_uri().to_string(), expected);
assert_eq!(uri.to_uri().to_string(), "file:///path/to/dir/");
}
#[test]
fn test_parse_symbol_uri() {
let symbol_uri = uri!("file:///path/to/file.rs?symbol=MySymbol#L10:20");
let symbol_uri = "file:///path/to/file.rs?symbol=MySymbol#L10:20";
let parsed = MentionUri::parse(symbol_uri).unwrap();
match &parsed {
MentionUri::Symbol {
abs_path: path,
path,
name,
line_range,
} => {
assert_eq!(path.to_str().unwrap(), path!("/path/to/file.rs"));
assert_eq!(path.to_str().unwrap(), "/path/to/file.rs");
assert_eq!(name, "MySymbol");
assert_eq!(line_range.start(), &9);
assert_eq!(line_range.end(), &19);
assert_eq!(line_range.start, 9);
assert_eq!(line_range.end, 19);
}
_ => panic!("Expected Symbol variant"),
}
@@ -359,42 +331,19 @@ mod tests {
#[test]
fn test_parse_selection_uri() {
let selection_uri = uri!("file:///path/to/file.rs#L5:15");
let selection_uri = "file:///path/to/file.rs#L5:15";
let parsed = MentionUri::parse(selection_uri).unwrap();
match &parsed {
MentionUri::Selection {
abs_path: path,
line_range,
} => {
assert_eq!(
path.as_ref().unwrap().to_str().unwrap(),
path!("/path/to/file.rs")
);
assert_eq!(line_range.start(), &4);
assert_eq!(line_range.end(), &14);
MentionUri::Selection { path, line_range } => {
assert_eq!(path.to_str().unwrap(), "/path/to/file.rs");
assert_eq!(line_range.start, 4);
assert_eq!(line_range.end, 14);
}
_ => panic!("Expected Selection variant"),
}
assert_eq!(parsed.to_uri().to_string(), selection_uri);
}
#[test]
fn test_parse_untitled_selection_uri() {
let selection_uri = uri!("zed:///agent/untitled-buffer#L1:10");
let parsed = MentionUri::parse(selection_uri).unwrap();
match &parsed {
MentionUri::Selection {
abs_path: None,
line_range,
} => {
assert_eq!(line_range.start(), &0);
assert_eq!(line_range.end(), &9);
}
_ => panic!("Expected Selection variant without path"),
}
assert_eq!(parsed.to_uri().to_string(), selection_uri);
}
#[test]
fn test_parse_thread_uri() {
let thread_uri = "zed:///agent/thread/session123?name=Thread+name";
@@ -468,35 +417,32 @@ mod tests {
#[test]
fn test_invalid_line_range_format() {
// Missing L prefix
assert!(MentionUri::parse(uri!("file:///path/to/file.rs#10:20")).is_err());
assert!(MentionUri::parse("file:///path/to/file.rs#10:20").is_err());
// Missing colon separator
assert!(MentionUri::parse(uri!("file:///path/to/file.rs#L1020")).is_err());
assert!(MentionUri::parse("file:///path/to/file.rs#L1020").is_err());
// Invalid numbers
assert!(MentionUri::parse(uri!("file:///path/to/file.rs#L10:abc")).is_err());
assert!(MentionUri::parse(uri!("file:///path/to/file.rs#Labc:20")).is_err());
assert!(MentionUri::parse("file:///path/to/file.rs#L10:abc").is_err());
assert!(MentionUri::parse("file:///path/to/file.rs#Labc:20").is_err());
}
#[test]
fn test_invalid_query_parameters() {
// Invalid query parameter name
assert!(MentionUri::parse(uri!("file:///path/to/file.rs#L10:20?invalid=test")).is_err());
assert!(MentionUri::parse("file:///path/to/file.rs#L10:20?invalid=test").is_err());
// Too many query parameters
assert!(
MentionUri::parse(uri!(
"file:///path/to/file.rs#L10:20?symbol=test&another=param"
))
.is_err()
MentionUri::parse("file:///path/to/file.rs#L10:20?symbol=test&another=param").is_err()
);
}
#[test]
fn test_zero_based_line_numbers() {
// Test that 0-based line numbers are rejected (should be 1-based)
assert!(MentionUri::parse(uri!("file:///path/to/file.rs#L0:10")).is_err());
assert!(MentionUri::parse(uri!("file:///path/to/file.rs#L1:0")).is_err());
assert!(MentionUri::parse(uri!("file:///path/to/file.rs#L0:0")).is_err());
assert!(MentionUri::parse("file:///path/to/file.rs#L0:10").is_err());
assert!(MentionUri::parse("file:///path/to/file.rs#L1:0").is_err());
assert!(MentionUri::parse("file:///path/to/file.rs#L0:0").is_err());
}
}

View File

@@ -1,43 +1,34 @@
use agent_client_protocol as acp;
use futures::{FutureExt as _, future::Shared};
use gpui::{App, AppContext, Context, Entity, Task};
use gpui::{App, AppContext, Context, Entity};
use language::LanguageRegistry;
use markdown::Markdown;
use std::{path::PathBuf, process::ExitStatus, sync::Arc, time::Instant};
pub struct Terminal {
id: acp::TerminalId,
command: Entity<Markdown>,
working_dir: Option<PathBuf>,
terminal: Entity<terminal::Terminal>,
started_at: Instant,
output: Option<TerminalOutput>,
output_byte_limit: Option<usize>,
_output_task: Shared<Task<acp::TerminalExitStatus>>,
}
pub struct TerminalOutput {
pub ended_at: Instant,
pub exit_status: Option<ExitStatus>,
pub content: String,
pub was_content_truncated: bool,
pub original_content_len: usize,
pub content_line_count: usize,
pub finished_with_empty_output: bool,
}
impl Terminal {
pub fn new(
id: acp::TerminalId,
command: String,
working_dir: Option<PathBuf>,
output_byte_limit: Option<usize>,
terminal: Entity<terminal::Terminal>,
language_registry: Arc<LanguageRegistry>,
cx: &mut Context<Self>,
) -> Self {
let command_task = terminal.read(cx).wait_for_completed_task(cx);
Self {
id,
command: cx.new(|cx| {
Markdown::new(
format!("```\n{}\n```", command).into(),
@@ -50,93 +41,27 @@ impl Terminal {
terminal,
started_at: Instant::now(),
output: None,
output_byte_limit,
_output_task: cx
.spawn(async move |this, cx| {
let exit_status = command_task.await;
this.update(cx, |this, cx| {
let (content, original_content_len) = this.truncated_output(cx);
let content_line_count = this.terminal.read(cx).total_lines();
this.output = Some(TerminalOutput {
ended_at: Instant::now(),
exit_status,
content,
original_content_len,
content_line_count,
});
cx.notify();
})
.ok();
let exit_status = exit_status.map(portable_pty::ExitStatus::from);
acp::TerminalExitStatus {
exit_code: exit_status.as_ref().map(|e| e.exit_code()),
signal: exit_status.and_then(|e| e.signal().map(Into::into)),
}
})
.shared(),
}
}
pub fn id(&self) -> &acp::TerminalId {
&self.id
}
pub fn wait_for_exit(&self) -> Shared<Task<acp::TerminalExitStatus>> {
self._output_task.clone()
}
pub fn kill(&mut self, cx: &mut App) {
self.terminal.update(cx, |terminal, _cx| {
terminal.kill_active_task();
pub fn finish(
&mut self,
exit_status: Option<ExitStatus>,
original_content_len: usize,
truncated_content_len: usize,
content_line_count: usize,
finished_with_empty_output: bool,
cx: &mut Context<Self>,
) {
self.output = Some(TerminalOutput {
ended_at: Instant::now(),
exit_status,
was_content_truncated: truncated_content_len < original_content_len,
original_content_len,
content_line_count,
finished_with_empty_output,
});
}
pub fn current_output(&self, cx: &App) -> acp::TerminalOutputResponse {
if let Some(output) = self.output.as_ref() {
let exit_status = output.exit_status.map(portable_pty::ExitStatus::from);
acp::TerminalOutputResponse {
output: output.content.clone(),
truncated: output.original_content_len > output.content.len(),
exit_status: Some(acp::TerminalExitStatus {
exit_code: exit_status.as_ref().map(|e| e.exit_code()),
signal: exit_status.and_then(|e| e.signal().map(Into::into)),
}),
}
} else {
let (current_content, original_len) = self.truncated_output(cx);
acp::TerminalOutputResponse {
truncated: current_content.len() < original_len,
output: current_content,
exit_status: None,
}
}
}
fn truncated_output(&self, cx: &App) -> (String, usize) {
let terminal = self.terminal.read(cx);
let mut content = terminal.get_content();
let original_content_len = content.len();
if let Some(limit) = self.output_byte_limit
&& content.len() > limit
{
let mut end_ix = limit.min(content.len());
while !content.is_char_boundary(end_ix) {
end_ix -= 1;
}
// Don't truncate mid-line, clear the remainder of the last line
end_ix = content[..end_ix].rfind('\n').unwrap_or(end_ix);
content.truncate(end_ix);
}
(content, original_content_len)
cx.notify();
}
pub fn command(&self) -> &Entity<Markdown> {

View File

@@ -1,30 +0,0 @@
[package]
name = "acp_tools"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/acp_tools.rs"
doctest = false
[dependencies]
agent-client-protocol.workspace = true
collections.workspace = true
gpui.workspace = true
language.workspace= true
markdown.workspace = true
project.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
theme.workspace = true
ui.workspace = true
util.workspace = true
workspace-hack.workspace = true
workspace.workspace = true

View File

@@ -1 +0,0 @@
../../LICENSE-GPL

View File

@@ -1,494 +0,0 @@
use std::{
cell::RefCell,
collections::HashSet,
fmt::Display,
rc::{Rc, Weak},
sync::Arc,
};
use agent_client_protocol as acp;
use collections::HashMap;
use gpui::{
App, Empty, Entity, EventEmitter, FocusHandle, Focusable, Global, ListAlignment, ListState,
StyleRefinement, Subscription, Task, TextStyleRefinement, Window, actions, list, prelude::*,
};
use language::LanguageRegistry;
use markdown::{CodeBlockRenderer, Markdown, MarkdownElement, MarkdownStyle};
use project::Project;
use settings::Settings;
use theme::ThemeSettings;
use ui::prelude::*;
use util::ResultExt as _;
use workspace::{Item, Workspace};
actions!(dev, [OpenAcpLogs]);
pub fn init(cx: &mut App) {
cx.observe_new(
|workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
workspace.register_action(|workspace, _: &OpenAcpLogs, window, cx| {
let acp_tools =
Box::new(cx.new(|cx| AcpTools::new(workspace.project().clone(), cx)));
workspace.add_item_to_active_pane(acp_tools, None, true, window, cx);
});
},
)
.detach();
}
struct GlobalAcpConnectionRegistry(Entity<AcpConnectionRegistry>);
impl Global for GlobalAcpConnectionRegistry {}
#[derive(Default)]
pub struct AcpConnectionRegistry {
active_connection: RefCell<Option<ActiveConnection>>,
}
struct ActiveConnection {
server_name: SharedString,
connection: Weak<acp::ClientSideConnection>,
}
impl AcpConnectionRegistry {
pub fn default_global(cx: &mut App) -> Entity<Self> {
if cx.has_global::<GlobalAcpConnectionRegistry>() {
cx.global::<GlobalAcpConnectionRegistry>().0.clone()
} else {
let registry = cx.new(|_cx| AcpConnectionRegistry::default());
cx.set_global(GlobalAcpConnectionRegistry(registry.clone()));
registry
}
}
pub fn set_active_connection(
&self,
server_name: impl Into<SharedString>,
connection: &Rc<acp::ClientSideConnection>,
cx: &mut Context<Self>,
) {
self.active_connection.replace(Some(ActiveConnection {
server_name: server_name.into(),
connection: Rc::downgrade(connection),
}));
cx.notify();
}
}
struct AcpTools {
project: Entity<Project>,
focus_handle: FocusHandle,
expanded: HashSet<usize>,
watched_connection: Option<WatchedConnection>,
connection_registry: Entity<AcpConnectionRegistry>,
_subscription: Subscription,
}
struct WatchedConnection {
server_name: SharedString,
messages: Vec<WatchedConnectionMessage>,
list_state: ListState,
connection: Weak<acp::ClientSideConnection>,
incoming_request_methods: HashMap<i32, Arc<str>>,
outgoing_request_methods: HashMap<i32, Arc<str>>,
_task: Task<()>,
}
impl AcpTools {
fn new(project: Entity<Project>, cx: &mut Context<Self>) -> Self {
let connection_registry = AcpConnectionRegistry::default_global(cx);
let subscription = cx.observe(&connection_registry, |this, _, cx| {
this.update_connection(cx);
cx.notify();
});
let mut this = Self {
project,
focus_handle: cx.focus_handle(),
expanded: HashSet::default(),
watched_connection: None,
connection_registry,
_subscription: subscription,
};
this.update_connection(cx);
this
}
fn update_connection(&mut self, cx: &mut Context<Self>) {
let active_connection = self.connection_registry.read(cx).active_connection.borrow();
let Some(active_connection) = active_connection.as_ref() else {
return;
};
if let Some(watched_connection) = self.watched_connection.as_ref() {
if Weak::ptr_eq(
&watched_connection.connection,
&active_connection.connection,
) {
return;
}
}
if let Some(connection) = active_connection.connection.upgrade() {
let mut receiver = connection.subscribe();
let task = cx.spawn(async move |this, cx| {
while let Ok(message) = receiver.recv().await {
this.update(cx, |this, cx| {
this.push_stream_message(message, cx);
})
.ok();
}
});
self.watched_connection = Some(WatchedConnection {
server_name: active_connection.server_name.clone(),
messages: vec![],
list_state: ListState::new(0, ListAlignment::Bottom, px(2048.)),
connection: active_connection.connection.clone(),
incoming_request_methods: HashMap::default(),
outgoing_request_methods: HashMap::default(),
_task: task,
});
}
}
fn push_stream_message(&mut self, stream_message: acp::StreamMessage, cx: &mut Context<Self>) {
let Some(connection) = self.watched_connection.as_mut() else {
return;
};
let language_registry = self.project.read(cx).languages().clone();
let index = connection.messages.len();
let (request_id, method, message_type, params) = match stream_message.message {
acp::StreamMessageContent::Request { id, method, params } => {
let method_map = match stream_message.direction {
acp::StreamMessageDirection::Incoming => {
&mut connection.incoming_request_methods
}
acp::StreamMessageDirection::Outgoing => {
&mut connection.outgoing_request_methods
}
};
method_map.insert(id, method.clone());
(Some(id), method.into(), MessageType::Request, Ok(params))
}
acp::StreamMessageContent::Response { id, result } => {
let method_map = match stream_message.direction {
acp::StreamMessageDirection::Incoming => {
&mut connection.outgoing_request_methods
}
acp::StreamMessageDirection::Outgoing => {
&mut connection.incoming_request_methods
}
};
if let Some(method) = method_map.remove(&id) {
(Some(id), method.into(), MessageType::Response, result)
} else {
(
Some(id),
"[unrecognized response]".into(),
MessageType::Response,
result,
)
}
}
acp::StreamMessageContent::Notification { method, params } => {
(None, method.into(), MessageType::Notification, Ok(params))
}
};
let message = WatchedConnectionMessage {
name: method,
message_type,
request_id,
direction: stream_message.direction,
collapsed_params_md: match params.as_ref() {
Ok(params) => params
.as_ref()
.map(|params| collapsed_params_md(params, &language_registry, cx)),
Err(err) => {
if let Ok(err) = &serde_json::to_value(err) {
Some(collapsed_params_md(&err, &language_registry, cx))
} else {
None
}
}
},
expanded_params_md: None,
params,
};
connection.messages.push(message);
connection.list_state.splice(index..index, 1);
cx.notify();
}
fn render_message(
&mut self,
index: usize,
window: &mut Window,
cx: &mut Context<Self>,
) -> AnyElement {
let Some(connection) = self.watched_connection.as_ref() else {
return Empty.into_any();
};
let Some(message) = connection.messages.get(index) else {
return Empty.into_any();
};
let base_size = TextSize::Editor.rems(cx);
let theme_settings = ThemeSettings::get_global(cx);
let text_style = window.text_style();
let colors = cx.theme().colors();
let expanded = self.expanded.contains(&index);
v_flex()
.w_full()
.px_4()
.py_3()
.border_color(colors.border)
.border_b_1()
.gap_2()
.items_start()
.font_buffer(cx)
.text_size(base_size)
.id(index)
.group("message")
.hover(|this| this.bg(colors.element_background.opacity(0.5)))
.on_click(cx.listener(move |this, _, _, cx| {
if this.expanded.contains(&index) {
this.expanded.remove(&index);
} else {
this.expanded.insert(index);
let Some(connection) = &mut this.watched_connection else {
return;
};
let Some(message) = connection.messages.get_mut(index) else {
return;
};
message.expanded(this.project.read(cx).languages().clone(), cx);
connection.list_state.scroll_to_reveal_item(index);
}
cx.notify()
}))
.child(
h_flex()
.w_full()
.gap_2()
.items_center()
.flex_shrink_0()
.child(match message.direction {
acp::StreamMessageDirection::Incoming => {
ui::Icon::new(ui::IconName::ArrowDown).color(Color::Error)
}
acp::StreamMessageDirection::Outgoing => {
ui::Icon::new(ui::IconName::ArrowUp).color(Color::Success)
}
})
.child(
Label::new(message.name.clone())
.buffer_font(cx)
.color(Color::Muted),
)
.child(div().flex_1())
.child(
div()
.child(ui::Chip::new(message.message_type.to_string()))
.visible_on_hover("message"),
)
.children(
message
.request_id
.map(|req_id| div().child(ui::Chip::new(req_id.to_string()))),
),
)
// I'm aware using markdown is a hack. Trying to get something working for the demo.
// Will clean up soon!
.when_some(
if expanded {
message.expanded_params_md.clone()
} else {
message.collapsed_params_md.clone()
},
|this, params| {
this.child(
div().pl_6().w_full().child(
MarkdownElement::new(
params,
MarkdownStyle {
base_text_style: text_style,
selection_background_color: colors.element_selection_background,
syntax: cx.theme().syntax().clone(),
code_block_overflow_x_scroll: true,
code_block: StyleRefinement {
text: Some(TextStyleRefinement {
font_family: Some(
theme_settings.buffer_font.family.clone(),
),
font_size: Some((base_size * 0.8).into()),
..Default::default()
}),
..Default::default()
},
..Default::default()
},
)
.code_block_renderer(
CodeBlockRenderer::Default {
copy_button: false,
copy_button_on_hover: expanded,
border: false,
},
),
),
)
},
)
.into_any()
}
}
struct WatchedConnectionMessage {
name: SharedString,
request_id: Option<i32>,
direction: acp::StreamMessageDirection,
message_type: MessageType,
params: Result<Option<serde_json::Value>, acp::Error>,
collapsed_params_md: Option<Entity<Markdown>>,
expanded_params_md: Option<Entity<Markdown>>,
}
impl WatchedConnectionMessage {
fn expanded(&mut self, language_registry: Arc<LanguageRegistry>, cx: &mut App) {
let params_md = match &self.params {
Ok(Some(params)) => Some(expanded_params_md(params, &language_registry, cx)),
Err(err) => {
if let Some(err) = &serde_json::to_value(err).log_err() {
Some(expanded_params_md(&err, &language_registry, cx))
} else {
None
}
}
_ => None,
};
self.expanded_params_md = params_md;
}
}
fn collapsed_params_md(
params: &serde_json::Value,
language_registry: &Arc<LanguageRegistry>,
cx: &mut App,
) -> Entity<Markdown> {
let params_json = serde_json::to_string(params).unwrap_or_default();
let mut spaced_out_json = String::with_capacity(params_json.len() + params_json.len() / 4);
for ch in params_json.chars() {
match ch {
'{' => spaced_out_json.push_str("{ "),
'}' => spaced_out_json.push_str(" }"),
':' => spaced_out_json.push_str(": "),
',' => spaced_out_json.push_str(", "),
c => spaced_out_json.push(c),
}
}
let params_md = format!("```json\n{}\n```", spaced_out_json);
cx.new(|cx| Markdown::new(params_md.into(), Some(language_registry.clone()), None, cx))
}
fn expanded_params_md(
params: &serde_json::Value,
language_registry: &Arc<LanguageRegistry>,
cx: &mut App,
) -> Entity<Markdown> {
let params_json = serde_json::to_string_pretty(params).unwrap_or_default();
let params_md = format!("```json\n{}\n```", params_json);
cx.new(|cx| Markdown::new(params_md.into(), Some(language_registry.clone()), None, cx))
}
enum MessageType {
Request,
Response,
Notification,
}
impl Display for MessageType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MessageType::Request => write!(f, "Request"),
MessageType::Response => write!(f, "Response"),
MessageType::Notification => write!(f, "Notification"),
}
}
}
enum AcpToolsEvent {}
impl EventEmitter<AcpToolsEvent> for AcpTools {}
impl Item for AcpTools {
type Event = AcpToolsEvent;
fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
format!(
"ACP: {}",
self.watched_connection
.as_ref()
.map_or("Disconnected", |connection| &connection.server_name)
)
.into()
}
fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
Some(ui::Icon::new(IconName::Thread))
}
}
impl Focusable for AcpTools {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Render for AcpTools {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.track_focus(&self.focus_handle)
.size_full()
.bg(cx.theme().colors().editor_background)
.child(match self.watched_connection.as_ref() {
Some(connection) => {
if connection.messages.is_empty() {
h_flex()
.size_full()
.justify_center()
.items_center()
.child("No messages recorded yet")
.into_any()
} else {
list(
connection.list_state.clone(),
cx.processor(Self::render_message),
)
.with_sizing_behavior(gpui::ListSizingBehavior::Auto)
.flex_grow()
.into_any()
}
}
None => h_flex()
.size_full()
.justify_center()
.items_center()
.child("No active connection")
.into_any(),
})
}
}

View File

@@ -116,7 +116,7 @@ impl ActionLog {
} else if buffer
.read(cx)
.file()
.is_some_and(|file| file.disk_state().exists())
.map_or(false, |file| file.disk_state().exists())
{
TrackedBufferStatus::Created {
existing_file_content: Some(buffer.read(cx).as_rope().clone()),
@@ -161,7 +161,7 @@ impl ActionLog {
diff_base,
last_seen_base,
unreviewed_edits,
snapshot: text_snapshot,
snapshot: text_snapshot.clone(),
status,
version: buffer.read(cx).version(),
diff,
@@ -190,7 +190,7 @@ impl ActionLog {
cx: &mut Context<Self>,
) {
match event {
BufferEvent::Edited => self.handle_buffer_edited(buffer, cx),
BufferEvent::Edited { .. } => self.handle_buffer_edited(buffer, cx),
BufferEvent::FileHandleChanged => {
self.handle_buffer_file_changed(buffer, cx);
}
@@ -215,7 +215,7 @@ impl ActionLog {
if buffer
.read(cx)
.file()
.is_some_and(|file| file.disk_state() == DiskState::Deleted)
.map_or(false, |file| file.disk_state() == DiskState::Deleted)
{
// If the buffer had been edited by a tool, but it got
// deleted externally, we want to stop tracking it.
@@ -227,7 +227,7 @@ impl ActionLog {
if buffer
.read(cx)
.file()
.is_some_and(|file| file.disk_state() != DiskState::Deleted)
.map_or(false, |file| file.disk_state() != DiskState::Deleted)
{
// If the buffer had been deleted by a tool, but it got
// resurrected externally, we want to clear the edits we
@@ -264,14 +264,15 @@ impl ActionLog {
if let Some((git_diff, (buffer_repo, _))) = git_diff.as_ref().zip(buffer_repo) {
cx.update(|cx| {
let mut old_head = buffer_repo.read(cx).head_commit.clone();
Some(cx.subscribe(git_diff, move |_, event, cx| {
if let buffer_diff::BufferDiffEvent::DiffChanged { .. } = event {
Some(cx.subscribe(git_diff, move |_, event, cx| match event {
buffer_diff::BufferDiffEvent::DiffChanged { .. } => {
let new_head = buffer_repo.read(cx).head_commit.clone();
if new_head != old_head {
old_head = new_head;
git_diff_updates_tx.send(()).ok();
}
}
_ => {}
}))
})?
} else {
@@ -461,7 +462,7 @@ impl ActionLog {
anyhow::Ok((
tracked_buffer.diff.clone(),
buffer.read(cx).language().cloned(),
buffer.read(cx).language_registry(),
buffer.read(cx).language_registry().clone(),
))
})??;
let diff_snapshot = BufferDiff::update_diff(
@@ -529,12 +530,12 @@ impl ActionLog {
/// Mark a buffer as created by agent, so we can refresh it in the context
pub fn buffer_created(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
self.track_buffer_internal(buffer, true, cx);
self.track_buffer_internal(buffer.clone(), true, cx);
}
/// Mark a buffer as edited by agent, so we can refresh it in the context
pub fn buffer_edited(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
let tracked_buffer = self.track_buffer_internal(buffer, false, cx);
let tracked_buffer = self.track_buffer_internal(buffer.clone(), false, cx);
if let TrackedBufferStatus::Deleted = tracked_buffer.status {
tracked_buffer.status = TrackedBufferStatus::Modified;
}
@@ -613,10 +614,10 @@ impl ActionLog {
false
}
});
if tracked_buffer.unreviewed_edits.is_empty()
&& let TrackedBufferStatus::Created { .. } = &mut tracked_buffer.status
{
tracked_buffer.status = TrackedBufferStatus::Modified;
if tracked_buffer.unreviewed_edits.is_empty() {
if let TrackedBufferStatus::Created { .. } = &mut tracked_buffer.status {
tracked_buffer.status = TrackedBufferStatus::Modified;
}
}
tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
}
@@ -810,7 +811,7 @@ impl ActionLog {
tracked.version != buffer.version
&& buffer
.file()
.is_some_and(|file| file.disk_state() != DiskState::Deleted)
.map_or(false, |file| file.disk_state() != DiskState::Deleted)
})
.map(|(buffer, _)| buffer)
}
@@ -846,7 +847,7 @@ fn apply_non_conflicting_edits(
conflict = true;
if new_edits
.peek()
.is_some_and(|next_edit| next_edit.old.overlaps(&old_edit.new))
.map_or(false, |next_edit| next_edit.old.overlaps(&old_edit.new))
{
new_edit = new_edits.next().unwrap();
} else {
@@ -2425,7 +2426,7 @@ mod tests {
assert_eq!(
unreviewed_hunks(&action_log, cx),
vec![(
buffer,
buffer.clone(),
vec![
HunkStatus {
range: Point::new(6, 0)..Point::new(7, 0),

View File

@@ -1,10 +1,11 @@
use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage, VersionCheckType};
use editor::Editor;
use extension_host::{ExtensionOperation, ExtensionStore};
use extension_host::ExtensionStore;
use futures::StreamExt;
use gpui::{
App, Context, CursorStyle, Entity, EventEmitter, InteractiveElement as _, ParentElement as _,
Render, SharedString, StatefulInteractiveElement, Styled, Window, actions,
Animation, AnimationExt as _, App, Context, CursorStyle, Entity, EventEmitter,
InteractiveElement as _, ParentElement as _, Render, SharedString, StatefulInteractiveElement,
Styled, Transformation, Window, actions, percentage,
};
use language::{
BinaryStatus, LanguageRegistry, LanguageServerId, LanguageServerName,
@@ -24,10 +25,7 @@ use std::{
sync::Arc,
time::{Duration, Instant},
};
use ui::{
ButtonLike, CommonAnimationExt, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip,
prelude::*,
};
use ui::{ButtonLike, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
use util::truncate_and_trailoff;
use workspace::{StatusItemView, Workspace, item::ItemHandle};
@@ -105,21 +103,26 @@ impl ActivityIndicator {
cx.subscribe_in(
&workspace_handle,
window,
|activity_indicator, _, event, window, cx| {
if let workspace::Event::ClearActivityIndicator = event
&& activity_indicator.statuses.pop().is_some()
{
activity_indicator.dismiss_error_message(&DismissErrorMessage, window, cx);
cx.notify();
|activity_indicator, _, event, window, cx| match event {
workspace::Event::ClearActivityIndicator { .. } => {
if activity_indicator.statuses.pop().is_some() {
activity_indicator.dismiss_error_message(
&DismissErrorMessage,
window,
cx,
);
cx.notify();
}
}
_ => {}
},
)
.detach();
cx.subscribe(
&project.read(cx).lsp_store(),
|activity_indicator, _, event, cx| {
if let LspStoreEvent::LanguageServerUpdate { name, message, .. } = event {
|activity_indicator, _, event, cx| match event {
LspStoreEvent::LanguageServerUpdate { name, message, .. } => {
if let proto::update_language_server::Variant::StatusUpdate(status_update) =
message
{
@@ -188,6 +191,7 @@ impl ActivityIndicator {
}
cx.notify()
}
_ => {}
},
)
.detach();
@@ -202,10 +206,9 @@ impl ActivityIndicator {
cx.subscribe(
&project.read(cx).git_store().clone(),
|_, _, event: &GitStoreEvent, cx| {
if let project::git_store::GitStoreEvent::JobsUpdated = event {
cx.notify()
}
|_, _, event: &GitStoreEvent, cx| match event {
project::git_store::GitStoreEvent::JobsUpdated => cx.notify(),
_ => {}
},
)
.detach();
@@ -407,7 +410,13 @@ impl ActivityIndicator {
icon: Some(
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.with_rotate_animation(2)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| {
icon.transform(Transformation::rotate(percentage(delta)))
},
)
.into_any_element(),
),
message,
@@ -429,7 +438,11 @@ impl ActivityIndicator {
icon: Some(
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.with_rotate_animation(2)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
)
.into_any_element(),
),
message: format!("Debug: {}", session.read(cx).adapter()),
@@ -445,20 +458,26 @@ impl ActivityIndicator {
.map(|r| r.read(cx))
.and_then(Repository::current_job);
// Show any long-running git command
if let Some(job_info) = current_job
&& Instant::now() - job_info.start >= GIT_OPERATION_DELAY
{
return Some(Content {
icon: Some(
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.with_rotate_animation(2)
.into_any_element(),
),
message: job_info.message.into(),
on_click: None,
tooltip_message: None,
});
if let Some(job_info) = current_job {
if Instant::now() - job_info.start >= GIT_OPERATION_DELAY {
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: job_info.message.into(),
on_click: None,
tooltip_message: None,
});
}
}
// Show any language server installation info.
@@ -659,9 +678,8 @@ impl ActivityIndicator {
}
// Show any application auto-update info.
self.auto_updater
.as_ref()
.and_then(|updater| match &updater.read(cx).status() {
if let Some(updater) = &self.auto_updater {
return match &updater.read(cx).status() {
AutoUpdateStatus::Checking => Some(Content {
icon: Some(
Icon::new(IconName::Download)
@@ -717,49 +735,29 @@ impl ActivityIndicator {
tooltip_message: None,
}),
AutoUpdateStatus::Idle => None,
})
.or_else(|| {
if let Some(extension_store) =
ExtensionStore::try_global(cx).map(|extension_store| extension_store.read(cx))
&& let Some((extension_id, operation)) =
extension_store.outstanding_operations().iter().next()
{
let (message, icon, rotate) = match operation {
ExtensionOperation::Install => (
format!("Installing {extension_id} extension…"),
IconName::LoadCircle,
true,
),
ExtensionOperation::Upgrade => (
format!("Updating {extension_id} extension…"),
IconName::Download,
false,
),
ExtensionOperation::Remove => (
format!("Removing {extension_id} extension…"),
IconName::LoadCircle,
true,
),
};
};
}
Some(Content {
icon: Some(Icon::new(icon).size(IconSize::Small).map(|this| {
if rotate {
this.with_rotate_animation(3).into_any_element()
} else {
this.into_any_element()
}
})),
message,
on_click: Some(Arc::new(|this, window, cx| {
this.dismiss_error_message(&Default::default(), window, cx)
})),
tooltip_message: None,
})
} else {
None
}
})
if let Some(extension_store) =
ExtensionStore::try_global(cx).map(|extension_store| extension_store.read(cx))
{
if let Some(extension_id) = extension_store.outstanding_operations().keys().next() {
return Some(Content {
icon: Some(
Icon::new(IconName::Download)
.size(IconSize::Small)
.into_any_element(),
),
message: format!("Updating {extension_id} extension…"),
on_click: Some(Arc::new(|this, window, cx| {
this.dismiss_error_message(&DismissErrorMessage, window, cx)
})),
tooltip_message: None,
});
}
}
None
}
fn version_tooltip_message(version: &VersionCheckType) -> String {

View File

@@ -31,6 +31,7 @@ collections.workspace = true
component.workspace = true
context_server.workspace = true
convert_case.workspace = true
feature_flags.workspace = true
fs.workspace = true
futures.workspace = true
git.workspace = true

View File

@@ -90,7 +90,7 @@ impl AgentProfile {
return false;
};
Self::is_enabled(settings, source, tool_name)
return Self::is_enabled(settings, source, tool_name);
}
fn is_enabled(settings: &AgentProfileSettings, source: ToolSource, name: String) -> bool {
@@ -132,7 +132,7 @@ mod tests {
});
let tool_set = default_tool_set(cx);
let profile = AgentProfile::new(id, tool_set);
let profile = AgentProfile::new(id.clone(), tool_set);
let mut enabled_tools = cx
.read(|cx| profile.enabled_tools(cx))
@@ -169,7 +169,7 @@ mod tests {
});
let tool_set = default_tool_set(cx);
let profile = AgentProfile::new(id, tool_set);
let profile = AgentProfile::new(id.clone(), tool_set);
let mut enabled_tools = cx
.read(|cx| profile.enabled_tools(cx))
@@ -202,7 +202,7 @@ mod tests {
});
let tool_set = default_tool_set(cx);
let profile = AgentProfile::new(id, tool_set);
let profile = AgentProfile::new(id.clone(), tool_set);
let mut enabled_tools = cx
.read(|cx| profile.enabled_tools(cx))

View File

@@ -201,24 +201,24 @@ impl FileContextHandle {
parse_status.changed().await.log_err();
}
if let Ok(snapshot) = buffer.read_with(cx, |buffer, _| buffer.snapshot())
&& let Some(outline) = snapshot.outline(None)
{
let items = outline
.items
.into_iter()
.map(|item| item.to_point(&snapshot));
if let Ok(snapshot) = buffer.read_with(cx, |buffer, _| buffer.snapshot()) {
if let Some(outline) = snapshot.outline(None) {
let items = outline
.items
.into_iter()
.map(|item| item.to_point(&snapshot));
if let Ok(outline_text) =
outline::render_outline(items, None, 0, usize::MAX).await
{
let context = AgentContext::File(FileContext {
handle: self,
full_path,
text: outline_text.into(),
is_outline: true,
});
return Some((context, vec![buffer]));
if let Ok(outline_text) =
outline::render_outline(items, None, 0, usize::MAX).await
{
let context = AgentContext::File(FileContext {
handle: self,
full_path,
text: outline_text.into(),
is_outline: true,
});
return Some((context, vec![buffer]));
}
}
}
}
@@ -362,7 +362,7 @@ impl Display for DirectoryContext {
let mut is_first = true;
for descendant in &self.descendants {
if !is_first {
writeln!(f)?;
write!(f, "\n")?;
} else {
is_first = false;
}
@@ -650,7 +650,7 @@ impl TextThreadContextHandle {
impl Display for TextThreadContext {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
// TODO: escape title?
writeln!(f, "<text_thread title=\"{}\">", self.title)?;
write!(f, "<text_thread title=\"{}\">\n", self.title)?;
write!(f, "{}", self.text.trim())?;
write!(f, "\n</text_thread>")
}
@@ -716,7 +716,7 @@ impl RulesContextHandle {
impl Display for RulesContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(title) = &self.title {
writeln!(f, "Rules title: {}", title)?;
write!(f, "Rules title: {}\n", title)?;
}
let code_block = MarkdownCodeBlock {
tag: "",

View File

@@ -86,13 +86,15 @@ impl Tool for ContextServerTool {
) -> ToolResult {
if let Some(server) = self.store.read(cx).get_running_server(&self.server_id) {
let tool_name = self.tool.name.clone();
let server_clone = server.clone();
let input_clone = input.clone();
cx.spawn(async move |_cx| {
let Some(protocol) = server.client() else {
let Some(protocol) = server_clone.client() else {
bail!("Context server not initialized");
};
let arguments = if let serde_json::Value::Object(map) = input {
let arguments = if let serde_json::Value::Object(map) = input_clone {
Some(map.into_iter().collect())
} else {
None

View File

@@ -338,9 +338,11 @@ impl ContextStore {
image_task,
context_id: self.next_context_id.post_inc(),
});
if self.has_context(&context) && remove_if_exists {
self.remove_context(&context, cx);
return None;
if self.has_context(&context) {
if remove_if_exists {
self.remove_context(&context, cx);
return None;
}
}
self.insert_context(context.clone(), cx);

View File

@@ -254,9 +254,10 @@ impl HistoryStore {
}
pub fn remove_recently_opened_thread(&mut self, id: ThreadId, cx: &mut Context<Self>) {
self.recently_opened_entries.retain(
|entry| !matches!(entry, HistoryEntryId::Thread(thread_id) if thread_id == &id),
);
self.recently_opened_entries.retain(|entry| match entry {
HistoryEntryId::Thread(thread_id) if thread_id == &id => false,
_ => true,
});
self.save_recently_opened_entries(cx);
}

View File

@@ -9,16 +9,14 @@ use crate::{
tool_use::{PendingToolUse, ToolUse, ToolUseMetadata, ToolUseState},
};
use action_log::ActionLog;
use agent_settings::{
AgentProfileId, AgentSettings, CompletionMode, SUMMARIZE_THREAD_DETAILED_PROMPT,
SUMMARIZE_THREAD_PROMPT,
};
use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, SUMMARIZE_THREAD_PROMPT};
use anyhow::{Result, anyhow};
use assistant_tool::{AnyToolCard, Tool, ToolWorkingSet};
use chrono::{DateTime, Utc};
use client::{ModelRequestUsage, RequestUsage};
use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, Plan, UsageLimit};
use collections::HashMap;
use feature_flags::{self, FeatureFlagAppExt};
use futures::{FutureExt, StreamExt as _, future::Shared};
use git::repository::DiffType;
use gpui::{
@@ -110,7 +108,7 @@ impl std::fmt::Display for PromptId {
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
pub struct MessageId(pub usize);
pub struct MessageId(pub(crate) usize);
impl MessageId {
fn post_inc(&mut self) -> Self {
@@ -181,7 +179,7 @@ impl Message {
}
}
pub fn to_message_content(&self) -> String {
pub fn to_string(&self) -> String {
let mut result = String::new();
if !self.loaded_context.text.is_empty() {
@@ -387,8 +385,10 @@ pub struct Thread {
cumulative_token_usage: TokenUsage,
exceeded_window_error: Option<ExceededWindowError>,
tool_use_limit_reached: bool,
feedback: Option<ThreadFeedback>,
retry_state: Option<RetryState>,
message_feedback: HashMap<MessageId, ThreadFeedback>,
last_auto_capture_at: Option<Instant>,
last_received_chunk_at: Option<Instant>,
request_callback: Option<
Box<dyn FnMut(&LanguageModelRequest, &[Result<LanguageModelCompletionEvent, String>])>,
@@ -486,13 +486,15 @@ impl Thread {
cumulative_token_usage: TokenUsage::default(),
exceeded_window_error: None,
tool_use_limit_reached: false,
feedback: None,
retry_state: None,
message_feedback: HashMap::default(),
last_auto_capture_at: None,
last_error_context: None,
last_received_chunk_at: None,
request_callback: None,
remaining_turns: u32::MAX,
configured_model,
configured_model: configured_model.clone(),
profile: AgentProfile::new(profile_id, tools),
}
}
@@ -530,7 +532,7 @@ impl Thread {
.and_then(|model| {
let model = SelectedModel {
provider: model.provider.clone().into(),
model: model.model.into(),
model: model.model.clone().into(),
};
registry.select_model(&model, cx)
})
@@ -610,7 +612,9 @@ impl Thread {
cumulative_token_usage: serialized.cumulative_token_usage,
exceeded_window_error: None,
tool_use_limit_reached: serialized.tool_use_limit_reached,
feedback: None,
message_feedback: HashMap::default(),
last_auto_capture_at: None,
last_error_context: None,
last_received_chunk_at: None,
request_callback: None,
@@ -1029,6 +1033,8 @@ impl Thread {
});
}
self.auto_capture_telemetry(cx);
message_id
}
@@ -1643,15 +1649,17 @@ impl Thread {
};
self.tool_use
.request_tool_use(tool_message_id, tool_use, tool_use_metadata, cx);
.request_tool_use(tool_message_id, tool_use, tool_use_metadata.clone(), cx);
self.tool_use.insert_tool_output(
tool_use_id,
let pending_tool_use = self.tool_use.insert_tool_output(
tool_use_id.clone(),
tool_name,
tool_output,
self.configured_model.as_ref(),
self.completion_mode,
)
);
pending_tool_use
}
pub fn stream_completion(
@@ -1898,6 +1906,7 @@ impl Thread {
cx.emit(ThreadEvent::StreamedCompletion);
cx.notify();
thread.auto_capture_telemetry(cx);
Ok(())
})??;
@@ -1965,9 +1974,11 @@ impl Thread {
if let Some(prev_message) =
thread.messages.get(ix - 1)
&& prev_message.role == Role::Assistant {
{
if prev_message.role == Role::Assistant {
break;
}
}
}
}
@@ -2070,6 +2081,8 @@ impl Thread {
request_callback(request, response_events);
}
thread.auto_capture_telemetry(cx);
if let Ok(initial_usage) = initial_token_usage {
let usage = thread.cumulative_token_usage - initial_usage;
@@ -2425,10 +2438,12 @@ impl Thread {
return;
}
let added_user_message = include_str!("./prompts/summarize_thread_detailed_prompt.txt");
let request = self.to_summarize_request(
&model,
CompletionIntent::ThreadContextSummarization,
SUMMARIZE_THREAD_DETAILED_PROMPT.into(),
added_user_message.into(),
cx,
);
@@ -2470,13 +2485,13 @@ impl Thread {
.ok()?;
// Save thread so its summary can be reused later
if let Some(thread) = thread.upgrade()
&& let Ok(Ok(save_task)) = cx.update(|cx| {
if let Some(thread) = thread.upgrade() {
if let Ok(Ok(save_task)) = cx.update(|cx| {
thread_store
.update(cx, |thread_store, cx| thread_store.save_thread(&thread, cx))
})
{
save_task.await.log_err();
}) {
save_task.await.log_err();
}
}
Some(())
@@ -2521,6 +2536,7 @@ impl Thread {
model: Arc<dyn LanguageModel>,
cx: &mut Context<Self>,
) -> Vec<PendingToolUse> {
self.auto_capture_telemetry(cx);
let request =
Arc::new(self.to_completion_request(model.clone(), CompletionIntent::ToolResults, cx));
let pending_tool_uses = self
@@ -2724,11 +2740,13 @@ impl Thread {
window: Option<AnyWindowHandle>,
cx: &mut Context<Self>,
) {
if self.all_tools_finished()
&& let Some(ConfiguredModel { model, .. }) = self.configured_model.as_ref()
&& !canceled
{
self.send_to_model(model.clone(), CompletionIntent::ToolResults, window, cx);
if self.all_tools_finished() {
if let Some(ConfiguredModel { model, .. }) = self.configured_model.as_ref() {
if !canceled {
self.send_to_model(model.clone(), CompletionIntent::ToolResults, window, cx);
}
self.auto_capture_telemetry(cx);
}
}
cx.emit(ThreadEvent::ToolFinished {
@@ -2784,6 +2802,10 @@ impl Thread {
cx.emit(ThreadEvent::CancelEditing);
}
pub fn feedback(&self) -> Option<ThreadFeedback> {
self.feedback
}
pub fn message_feedback(&self, message_id: MessageId) -> Option<ThreadFeedback> {
self.message_feedback.get(&message_id).copied()
}
@@ -2816,7 +2838,7 @@ impl Thread {
let message_content = self
.message(message_id)
.map(|msg| msg.to_message_content())
.map(|msg| msg.to_string())
.unwrap_or_default();
cx.background_spawn(async move {
@@ -2845,6 +2867,52 @@ impl Thread {
})
}
pub fn report_feedback(
&mut self,
feedback: ThreadFeedback,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let last_assistant_message_id = self
.messages
.iter()
.rev()
.find(|msg| msg.role == Role::Assistant)
.map(|msg| msg.id);
if let Some(message_id) = last_assistant_message_id {
self.report_message_feedback(message_id, feedback, cx)
} else {
let final_project_snapshot = Self::project_snapshot(self.project.clone(), cx);
let serialized_thread = self.serialize(cx);
let thread_id = self.id().clone();
let client = self.project.read(cx).client();
self.feedback = Some(feedback);
cx.notify();
cx.background_spawn(async move {
let final_project_snapshot = final_project_snapshot.await;
let serialized_thread = serialized_thread.await?;
let thread_data = serde_json::to_value(serialized_thread)
.unwrap_or_else(|_| serde_json::Value::Null);
let rating = match feedback {
ThreadFeedback::Positive => "positive",
ThreadFeedback::Negative => "negative",
};
telemetry::event!(
"Assistant Thread Rated",
rating,
thread_id,
thread_data,
final_project_snapshot
);
client.telemetry().flush_events().await;
Ok(())
})
}
}
/// Create a snapshot of the current project state including git information and unsaved buffers.
fn project_snapshot(
project: Entity<Project>,
@@ -2865,11 +2933,11 @@ impl Thread {
let buffer_store = project.read(app_cx).buffer_store();
for buffer_handle in buffer_store.read(app_cx).buffers() {
let buffer = buffer_handle.read(app_cx);
if buffer.is_dirty()
&& let Some(file) = buffer.file()
{
let path = file.path().to_string_lossy().to_string();
unsaved_buffers.push(path);
if buffer.is_dirty() {
if let Some(file) = buffer.file() {
let path = file.path().to_string_lossy().to_string();
unsaved_buffers.push(path);
}
}
}
})
@@ -3079,6 +3147,50 @@ impl Thread {
&self.project
}
pub fn auto_capture_telemetry(&mut self, cx: &mut Context<Self>) {
if !cx.has_flag::<feature_flags::ThreadAutoCaptureFeatureFlag>() {
return;
}
let now = Instant::now();
if let Some(last) = self.last_auto_capture_at {
if now.duration_since(last).as_secs() < 10 {
return;
}
}
self.last_auto_capture_at = Some(now);
let thread_id = self.id().clone();
let github_login = self
.project
.read(cx)
.user_store()
.read(cx)
.current_user()
.map(|user| user.github_login.clone());
let client = self.project.read(cx).client();
let serialize_task = self.serialize(cx);
cx.background_executor()
.spawn(async move {
if let Ok(serialized_thread) = serialize_task.await {
if let Ok(thread_data) = serde_json::to_value(serialized_thread) {
telemetry::event!(
"Agent Thread Auto-Captured",
thread_id = thread_id.to_string(),
thread_data = thread_data,
auto_capture_reason = "tracked_user",
github_login = github_login
);
client.telemetry().flush_events().await;
}
}
})
.detach();
}
pub fn cumulative_token_usage(&self) -> TokenUsage {
self.cumulative_token_usage
}
@@ -3121,13 +3233,13 @@ impl Thread {
.model
.max_token_count_for_mode(self.completion_mode().into());
if let Some(exceeded_error) = &self.exceeded_window_error
&& model.model.id() == exceeded_error.model_id
{
return Some(TotalTokenUsage {
total: exceeded_error.token_count,
max,
});
if let Some(exceeded_error) = &self.exceeded_window_error {
if model.model.id() == exceeded_error.model_id {
return Some(TotalTokenUsage {
total: exceeded_error.token_count,
max,
});
}
}
let total = self
@@ -3188,7 +3300,7 @@ impl Thread {
self.configured_model.as_ref(),
self.completion_mode,
);
self.tool_finished(tool_use_id, None, true, window, cx);
self.tool_finished(tool_use_id.clone(), None, true, window, cx);
}
}
@@ -3820,7 +3932,7 @@ fn main() {{
AgentSettings {
model_parameters: vec![LanguageModelParameters {
provider: Some(model.provider_id().0.to_string().into()),
model: Some(model.id().0),
model: Some(model.id().0.clone()),
temperature: Some(0.66),
}],
..AgentSettings::get_global(cx).clone()
@@ -3840,7 +3952,7 @@ fn main() {{
AgentSettings {
model_parameters: vec![LanguageModelParameters {
provider: None,
model: Some(model.id().0),
model: Some(model.id().0.clone()),
temperature: Some(0.66),
}],
..AgentSettings::get_global(cx).clone()
@@ -3880,7 +3992,7 @@ fn main() {{
AgentSettings {
model_parameters: vec![LanguageModelParameters {
provider: Some("anthropic".into()),
model: Some(model.id().0),
model: Some(model.id().0.clone()),
temperature: Some(0.66),
}],
..AgentSettings::get_global(cx).clone()

View File

@@ -42,7 +42,7 @@ use std::{
use util::ResultExt as _;
pub static ZED_STATELESS: std::sync::LazyLock<bool> =
std::sync::LazyLock::new(|| std::env::var("ZED_STATELESS").is_ok_and(|v| !v.is_empty()));
std::sync::LazyLock::new(|| std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty()));
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DataType {
@@ -74,7 +74,7 @@ impl Column for DataType {
}
}
const RULES_FILE_NAMES: [&str; 9] = [
const RULES_FILE_NAMES: [&'static str; 9] = [
".rules",
".cursorrules",
".windsurfrules",
@@ -581,32 +581,33 @@ impl ThreadStore {
return;
};
if protocol.capable(context_server::protocol::ServerCapability::Tools)
&& let Some(response) = protocol
if protocol.capable(context_server::protocol::ServerCapability::Tools) {
if let Some(response) = protocol
.request::<context_server::types::requests::ListTools>(())
.await
.log_err()
{
let tool_ids = tool_working_set
.update(cx, |tool_working_set, cx| {
tool_working_set.extend(
response.tools.into_iter().map(|tool| {
Arc::new(ContextServerTool::new(
context_server_store.clone(),
server.id(),
tool,
)) as Arc<dyn Tool>
}),
cx,
)
})
.log_err();
{
let tool_ids = tool_working_set
.update(cx, |tool_working_set, cx| {
tool_working_set.extend(
response.tools.into_iter().map(|tool| {
Arc::new(ContextServerTool::new(
context_server_store.clone(),
server.id(),
tool,
)) as Arc<dyn Tool>
}),
cx,
)
})
.log_err();
if let Some(tool_ids) = tool_ids {
this.update(cx, |this, _| {
this.context_server_tool_ids.insert(server_id, tool_ids);
})
.log_err();
if let Some(tool_ids) = tool_ids {
this.update(cx, |this, _| {
this.context_server_tool_ids.insert(server_id, tool_ids);
})
.log_err();
}
}
}
})
@@ -696,14 +697,13 @@ impl SerializedThreadV0_1_0 {
let mut messages: Vec<SerializedMessage> = Vec::with_capacity(self.0.messages.len());
for message in self.0.messages {
if message.role == Role::User
&& !message.tool_results.is_empty()
&& let Some(last_message) = messages.last_mut()
{
debug_assert!(last_message.role == Role::Assistant);
if message.role == Role::User && !message.tool_results.is_empty() {
if let Some(last_message) = messages.last_mut() {
debug_assert!(last_message.role == Role::Assistant);
last_message.tool_results = message.tool_results;
continue;
last_message.tool_results = message.tool_results;
continue;
}
}
messages.push(message);
@@ -895,17 +895,6 @@ impl ThreadsDatabase {
let connection = if *ZED_STATELESS {
Connection::open_memory(Some("THREAD_FALLBACK_DB"))
} else if cfg!(any(feature = "test-support", test)) {
// rust stores the name of the test on the current thread.
// We use this to automatically create a database that will
// be shared within the test (for the test_retrieve_old_thread)
// but not with concurrent tests.
let thread = std::thread::current();
let test_name = thread.name();
Connection::open_memory(Some(&format!(
"THREAD_FALLBACK_{}",
test_name.unwrap_or_default()
)))
} else {
Connection::open_file(&sqlite_path.to_string_lossy())
};

View File

@@ -112,13 +112,19 @@ impl ToolUseState {
},
);
if let Some(window) = &mut window
&& let Some(tool) = this.tools.read(cx).tool(tool_use, cx)
&& let Some(output) = tool_result.output.clone()
&& let Some(card) =
tool.deserialize_card(output, project.clone(), window, cx)
{
this.tool_result_cards.insert(tool_use_id, card);
if let Some(window) = &mut window {
if let Some(tool) = this.tools.read(cx).tool(tool_use, cx) {
if let Some(output) = tool_result.output.clone() {
if let Some(card) = tool.deserialize_card(
output,
project.clone(),
window,
cx,
) {
this.tool_result_cards.insert(tool_use_id, card);
}
}
}
}
}
}
@@ -275,7 +281,7 @@ impl ToolUseState {
pub fn message_has_tool_results(&self, assistant_message_id: MessageId) -> bool {
self.tool_uses_by_assistant_message
.get(&assistant_message_id)
.is_some_and(|results| !results.is_empty())
.map_or(false, |results| !results.is_empty())
}
pub fn tool_result(

View File

@@ -8,33 +8,24 @@ license = "GPL-3.0-or-later"
[lib]
path = "src/agent2.rs"
[features]
test-support = ["db/test-support"]
e2e = []
[lints]
workspace = true
[dependencies]
acp_thread.workspace = true
action_log.workspace = true
agent.workspace = true
agent-client-protocol.workspace = true
agent_servers.workspace = true
agent_settings.workspace = true
anyhow.workspace = true
assistant_context.workspace = true
assistant_tool.workspace = true
assistant_tools.workspace = true
chrono.workspace = true
client.workspace = true
cloud_llm_client.workspace = true
collections.workspace = true
context_server.workspace = true
db.workspace = true
fs.workspace = true
futures.workspace = true
git.workspace = true
gpui.workspace = true
handlebars = { workspace = true, features = ["rust-embed"] }
html_to_markdown.workspace = true
@@ -46,8 +37,8 @@ language_model.workspace = true
language_models.workspace = true
log.workspace = true
open.workspace = true
parking_lot.workspace = true
paths.workspace = true
portable-pty.workspace = true
project.workspace = true
prompt_store.workspace = true
rust-embed.workspace = true
@@ -56,33 +47,25 @@ serde.workspace = true
serde_json.workspace = true
settings.workspace = true
smol.workspace = true
sqlez.workspace = true
task.workspace = true
telemetry.workspace = true
terminal.workspace = true
thiserror.workspace = true
text.workspace = true
ui.workspace = true
util.workspace = true
uuid.workspace = true
watch.workspace = true
web_search.workspace = true
which.workspace = true
workspace-hack.workspace = true
zstd.workspace = true
[dev-dependencies]
agent = { workspace = true, "features" = ["test-support"] }
agent_servers = { workspace = true, "features" = ["test-support"] }
assistant_context = { workspace = true, "features" = ["test-support"] }
ctor.workspace = true
client = { workspace = true, "features" = ["test-support"] }
clock = { workspace = true, "features" = ["test-support"] }
context_server = { workspace = true, "features" = ["test-support"] }
db = { workspace = true, "features" = ["test-support"] }
editor = { workspace = true, "features" = ["test-support"] }
env_logger.workspace = true
fs = { workspace = true, "features" = ["test-support"] }
git = { workspace = true, "features" = ["test-support"] }
gpui = { workspace = true, "features" = ["test-support"] }
gpui_tokio.workspace = true
language = { workspace = true, "features" = ["test-support"] }

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +1,13 @@
mod agent;
mod db;
mod history_store;
mod native_agent_server;
mod templates;
mod thread;
mod tool_schema;
mod tools;
#[cfg(test)]
mod tests;
pub use agent::*;
pub use db::*;
pub use history_store::*;
pub use native_agent_server::NativeAgentServer;
pub use templates::*;
pub use thread::*;

View File

@@ -1,499 +0,0 @@
use crate::{AgentMessage, AgentMessageContent, UserMessage, UserMessageContent};
use acp_thread::UserMessageId;
use agent::{thread::DetailedSummaryState, thread_store};
use agent_client_protocol as acp;
use agent_settings::{AgentProfileId, CompletionMode};
use anyhow::{Result, anyhow};
use chrono::{DateTime, Utc};
use collections::{HashMap, IndexMap};
use futures::{FutureExt, future::Shared};
use gpui::{BackgroundExecutor, Global, Task};
use indoc::indoc;
use parking_lot::Mutex;
use serde::{Deserialize, Serialize};
use sqlez::{
bindable::{Bind, Column},
connection::Connection,
statement::Statement,
};
use std::sync::Arc;
use ui::{App, SharedString};
pub type DbMessage = crate::Message;
pub type DbSummary = DetailedSummaryState;
pub type DbLanguageModel = thread_store::SerializedLanguageModel;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DbThreadMetadata {
pub id: acp::SessionId,
#[serde(alias = "summary")]
pub title: SharedString,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DbThread {
pub title: SharedString,
pub messages: Vec<DbMessage>,
pub updated_at: DateTime<Utc>,
#[serde(default)]
pub detailed_summary: Option<SharedString>,
#[serde(default)]
pub initial_project_snapshot: Option<Arc<agent::thread::ProjectSnapshot>>,
#[serde(default)]
pub cumulative_token_usage: language_model::TokenUsage,
#[serde(default)]
pub request_token_usage: HashMap<acp_thread::UserMessageId, language_model::TokenUsage>,
#[serde(default)]
pub model: Option<DbLanguageModel>,
#[serde(default)]
pub completion_mode: Option<CompletionMode>,
#[serde(default)]
pub profile: Option<AgentProfileId>,
}
impl DbThread {
pub const VERSION: &'static str = "0.3.0";
pub fn from_json(json: &[u8]) -> Result<Self> {
let saved_thread_json = serde_json::from_slice::<serde_json::Value>(json)?;
match saved_thread_json.get("version") {
Some(serde_json::Value::String(version)) => match version.as_str() {
Self::VERSION => Ok(serde_json::from_value(saved_thread_json)?),
_ => Self::upgrade_from_agent_1(agent::SerializedThread::from_json(json)?),
},
_ => Self::upgrade_from_agent_1(agent::SerializedThread::from_json(json)?),
}
}
fn upgrade_from_agent_1(thread: agent::SerializedThread) -> Result<Self> {
let mut messages = Vec::new();
let mut request_token_usage = HashMap::default();
let mut last_user_message_id = None;
for (ix, msg) in thread.messages.into_iter().enumerate() {
let message = match msg.role {
language_model::Role::User => {
let mut content = Vec::new();
// Convert segments to content
for segment in msg.segments {
match segment {
thread_store::SerializedMessageSegment::Text { text } => {
content.push(UserMessageContent::Text(text));
}
thread_store::SerializedMessageSegment::Thinking { text, .. } => {
// User messages don't have thinking segments, but handle gracefully
content.push(UserMessageContent::Text(text));
}
thread_store::SerializedMessageSegment::RedactedThinking { .. } => {
// User messages don't have redacted thinking, skip.
}
}
}
// If no content was added, add context as text if available
if content.is_empty() && !msg.context.is_empty() {
content.push(UserMessageContent::Text(msg.context));
}
let id = UserMessageId::new();
last_user_message_id = Some(id.clone());
crate::Message::User(UserMessage {
// MessageId from old format can't be meaningfully converted, so generate a new one
id,
content,
})
}
language_model::Role::Assistant => {
let mut content = Vec::new();
// Convert segments to content
for segment in msg.segments {
match segment {
thread_store::SerializedMessageSegment::Text { text } => {
content.push(AgentMessageContent::Text(text));
}
thread_store::SerializedMessageSegment::Thinking {
text,
signature,
} => {
content.push(AgentMessageContent::Thinking { text, signature });
}
thread_store::SerializedMessageSegment::RedactedThinking { data } => {
content.push(AgentMessageContent::RedactedThinking(data));
}
}
}
// Convert tool uses
let mut tool_names_by_id = HashMap::default();
for tool_use in msg.tool_uses {
tool_names_by_id.insert(tool_use.id.clone(), tool_use.name.clone());
content.push(AgentMessageContent::ToolUse(
language_model::LanguageModelToolUse {
id: tool_use.id,
name: tool_use.name.into(),
raw_input: serde_json::to_string(&tool_use.input)
.unwrap_or_default(),
input: tool_use.input,
is_input_complete: true,
},
));
}
// Convert tool results
let mut tool_results = IndexMap::default();
for tool_result in msg.tool_results {
let name = tool_names_by_id
.remove(&tool_result.tool_use_id)
.unwrap_or_else(|| SharedString::from("unknown"));
tool_results.insert(
tool_result.tool_use_id.clone(),
language_model::LanguageModelToolResult {
tool_use_id: tool_result.tool_use_id,
tool_name: name.into(),
is_error: tool_result.is_error,
content: tool_result.content,
output: tool_result.output,
},
);
}
if let Some(last_user_message_id) = &last_user_message_id
&& let Some(token_usage) = thread.request_token_usage.get(ix).copied()
{
request_token_usage.insert(last_user_message_id.clone(), token_usage);
}
crate::Message::Agent(AgentMessage {
content,
tool_results,
})
}
language_model::Role::System => {
// Skip system messages as they're not supported in the new format
continue;
}
};
messages.push(message);
}
Ok(Self {
title: thread.summary,
messages,
updated_at: thread.updated_at,
detailed_summary: match thread.detailed_summary_state {
DetailedSummaryState::NotGenerated | DetailedSummaryState::Generating { .. } => {
None
}
DetailedSummaryState::Generated { text, .. } => Some(text),
},
initial_project_snapshot: thread.initial_project_snapshot,
cumulative_token_usage: thread.cumulative_token_usage,
request_token_usage,
model: thread.model,
completion_mode: thread.completion_mode,
profile: thread.profile,
})
}
}
pub static ZED_STATELESS: std::sync::LazyLock<bool> =
std::sync::LazyLock::new(|| std::env::var("ZED_STATELESS").is_ok_and(|v| !v.is_empty()));
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DataType {
#[serde(rename = "json")]
Json,
#[serde(rename = "zstd")]
Zstd,
}
impl Bind for DataType {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
let value = match self {
DataType::Json => "json",
DataType::Zstd => "zstd",
};
value.bind(statement, start_index)
}
}
impl Column for DataType {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let (value, next_index) = String::column(statement, start_index)?;
let data_type = match value.as_str() {
"json" => DataType::Json,
"zstd" => DataType::Zstd,
_ => anyhow::bail!("Unknown data type: {}", value),
};
Ok((data_type, next_index))
}
}
pub(crate) struct ThreadsDatabase {
executor: BackgroundExecutor,
connection: Arc<Mutex<Connection>>,
}
struct GlobalThreadsDatabase(Shared<Task<Result<Arc<ThreadsDatabase>, Arc<anyhow::Error>>>>);
impl Global for GlobalThreadsDatabase {}
impl ThreadsDatabase {
pub fn connect(cx: &mut App) -> Shared<Task<Result<Arc<ThreadsDatabase>, Arc<anyhow::Error>>>> {
if cx.has_global::<GlobalThreadsDatabase>() {
return cx.global::<GlobalThreadsDatabase>().0.clone();
}
let executor = cx.background_executor().clone();
let task = executor
.spawn({
let executor = executor.clone();
async move {
match ThreadsDatabase::new(executor) {
Ok(db) => Ok(Arc::new(db)),
Err(err) => Err(Arc::new(err)),
}
}
})
.shared();
cx.set_global(GlobalThreadsDatabase(task.clone()));
task
}
pub fn new(executor: BackgroundExecutor) -> Result<Self> {
let connection = if *ZED_STATELESS {
Connection::open_memory(Some("THREAD_FALLBACK_DB"))
} else if cfg!(any(feature = "test-support", test)) {
// rust stores the name of the test on the current thread.
// We use this to automatically create a database that will
// be shared within the test (for the test_retrieve_old_thread)
// but not with concurrent tests.
let thread = std::thread::current();
let test_name = thread.name();
Connection::open_memory(Some(&format!(
"THREAD_FALLBACK_{}",
test_name.unwrap_or_default()
)))
} else {
let threads_dir = paths::data_dir().join("threads");
std::fs::create_dir_all(&threads_dir)?;
let sqlite_path = threads_dir.join("threads.db");
Connection::open_file(&sqlite_path.to_string_lossy())
};
connection.exec(indoc! {"
CREATE TABLE IF NOT EXISTS threads (
id TEXT PRIMARY KEY,
summary TEXT NOT NULL,
updated_at TEXT NOT NULL,
data_type TEXT NOT NULL,
data BLOB NOT NULL
)
"})?()
.map_err(|e| anyhow!("Failed to create threads table: {}", e))?;
let db = Self {
executor,
connection: Arc::new(Mutex::new(connection)),
};
Ok(db)
}
fn save_thread_sync(
connection: &Arc<Mutex<Connection>>,
id: acp::SessionId,
thread: DbThread,
) -> Result<()> {
const COMPRESSION_LEVEL: i32 = 3;
#[derive(Serialize)]
struct SerializedThread {
#[serde(flatten)]
thread: DbThread,
version: &'static str,
}
let title = thread.title.to_string();
let updated_at = thread.updated_at.to_rfc3339();
let json_data = serde_json::to_string(&SerializedThread {
thread,
version: DbThread::VERSION,
})?;
let connection = connection.lock();
let compressed = zstd::encode_all(json_data.as_bytes(), COMPRESSION_LEVEL)?;
let data_type = DataType::Zstd;
let data = compressed;
let mut insert = connection.exec_bound::<(Arc<str>, String, String, DataType, Vec<u8>)>(indoc! {"
INSERT OR REPLACE INTO threads (id, summary, updated_at, data_type, data) VALUES (?, ?, ?, ?, ?)
"})?;
insert((id.0, title, updated_at, data_type, data))?;
Ok(())
}
pub fn list_threads(&self) -> Task<Result<Vec<DbThreadMetadata>>> {
let connection = self.connection.clone();
self.executor.spawn(async move {
let connection = connection.lock();
let mut select =
connection.select_bound::<(), (Arc<str>, String, String)>(indoc! {"
SELECT id, summary, updated_at FROM threads ORDER BY updated_at DESC
"})?;
let rows = select(())?;
let mut threads = Vec::new();
for (id, summary, updated_at) in rows {
threads.push(DbThreadMetadata {
id: acp::SessionId(id),
title: summary.into(),
updated_at: DateTime::parse_from_rfc3339(&updated_at)?.with_timezone(&Utc),
});
}
Ok(threads)
})
}
pub fn load_thread(&self, id: acp::SessionId) -> Task<Result<Option<DbThread>>> {
let connection = self.connection.clone();
self.executor.spawn(async move {
let connection = connection.lock();
let mut select = connection.select_bound::<Arc<str>, (DataType, Vec<u8>)>(indoc! {"
SELECT data_type, data FROM threads WHERE id = ? LIMIT 1
"})?;
let rows = select(id.0)?;
if let Some((data_type, data)) = rows.into_iter().next() {
let json_data = match data_type {
DataType::Zstd => {
let decompressed = zstd::decode_all(&data[..])?;
String::from_utf8(decompressed)?
}
DataType::Json => String::from_utf8(data)?,
};
let thread = DbThread::from_json(json_data.as_bytes())?;
Ok(Some(thread))
} else {
Ok(None)
}
})
}
pub fn save_thread(&self, id: acp::SessionId, thread: DbThread) -> Task<Result<()>> {
let connection = self.connection.clone();
self.executor
.spawn(async move { Self::save_thread_sync(&connection, id, thread) })
}
pub fn delete_thread(&self, id: acp::SessionId) -> Task<Result<()>> {
let connection = self.connection.clone();
self.executor.spawn(async move {
let connection = connection.lock();
let mut delete = connection.exec_bound::<Arc<str>>(indoc! {"
DELETE FROM threads WHERE id = ?
"})?;
delete(id.0)?;
Ok(())
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use agent::MessageSegment;
use agent::context::LoadedContext;
use client::Client;
use fs::FakeFs;
use gpui::AppContext;
use gpui::TestAppContext;
use http_client::FakeHttpClient;
use language_model::Role;
use project::Project;
use settings::SettingsStore;
fn init_test(cx: &mut TestAppContext) {
env_logger::try_init().ok();
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
Project::init_settings(cx);
language::init(cx);
let http_client = FakeHttpClient::with_404_response();
let clock = Arc::new(clock::FakeSystemClock::new());
let client = Client::new(clock, http_client, cx);
agent::init(cx);
agent_settings::init(cx);
language_model::init(client, cx);
});
}
#[gpui::test]
async fn test_retrieving_old_thread(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, [], cx).await;
// Save a thread using the old agent.
let thread_store = cx.new(|cx| agent::ThreadStore::fake(project, cx));
let thread = thread_store.update(cx, |thread_store, cx| thread_store.create_thread(cx));
thread.update(cx, |thread, cx| {
thread.insert_message(
Role::User,
vec![MessageSegment::Text("Hey!".into())],
LoadedContext::default(),
vec![],
false,
cx,
);
thread.insert_message(
Role::Assistant,
vec![MessageSegment::Text("How're you doing?".into())],
LoadedContext::default(),
vec![],
false,
cx,
)
});
thread_store
.update(cx, |thread_store, cx| thread_store.save_thread(&thread, cx))
.await
.unwrap();
// Open that same thread using the new agent.
let db = cx.update(ThreadsDatabase::connect).await.unwrap();
let threads = db.list_threads().await.unwrap();
assert_eq!(threads.len(), 1);
let thread = db
.load_thread(threads[0].id.clone())
.await
.unwrap()
.unwrap();
assert_eq!(thread.messages[0].to_markdown(), "## User\n\nHey!\n");
assert_eq!(
thread.messages[1].to_markdown(),
"## Assistant\n\nHow're you doing?\n"
);
}
}

View File

@@ -1,357 +0,0 @@
use crate::{DbThreadMetadata, ThreadsDatabase};
use acp_thread::MentionUri;
use agent_client_protocol as acp;
use anyhow::{Context as _, Result, anyhow};
use assistant_context::{AssistantContext, SavedContextMetadata};
use chrono::{DateTime, Utc};
use db::kvp::KEY_VALUE_STORE;
use gpui::{App, AsyncApp, Entity, SharedString, Task, prelude::*};
use itertools::Itertools;
use paths::contexts_dir;
use serde::{Deserialize, Serialize};
use std::{collections::VecDeque, path::Path, sync::Arc, time::Duration};
use ui::ElementId;
use util::ResultExt as _;
const MAX_RECENTLY_OPENED_ENTRIES: usize = 6;
const RECENTLY_OPENED_THREADS_KEY: &str = "recent-agent-threads";
const SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE: Duration = Duration::from_millis(50);
const DEFAULT_TITLE: &SharedString = &SharedString::new_static("New Thread");
#[derive(Clone, Debug)]
pub enum HistoryEntry {
AcpThread(DbThreadMetadata),
TextThread(SavedContextMetadata),
}
impl HistoryEntry {
pub fn updated_at(&self) -> DateTime<Utc> {
match self {
HistoryEntry::AcpThread(thread) => thread.updated_at,
HistoryEntry::TextThread(context) => context.mtime.to_utc(),
}
}
pub fn id(&self) -> HistoryEntryId {
match self {
HistoryEntry::AcpThread(thread) => HistoryEntryId::AcpThread(thread.id.clone()),
HistoryEntry::TextThread(context) => HistoryEntryId::TextThread(context.path.clone()),
}
}
pub fn mention_uri(&self) -> MentionUri {
match self {
HistoryEntry::AcpThread(thread) => MentionUri::Thread {
id: thread.id.clone(),
name: thread.title.to_string(),
},
HistoryEntry::TextThread(context) => MentionUri::TextThread {
path: context.path.as_ref().to_owned(),
name: context.title.to_string(),
},
}
}
pub fn title(&self) -> &SharedString {
match self {
HistoryEntry::AcpThread(thread) if thread.title.is_empty() => DEFAULT_TITLE,
HistoryEntry::AcpThread(thread) => &thread.title,
HistoryEntry::TextThread(context) => &context.title,
}
}
}
/// Generic identifier for a history entry.
#[derive(Clone, PartialEq, Eq, Debug, Hash)]
pub enum HistoryEntryId {
AcpThread(acp::SessionId),
TextThread(Arc<Path>),
}
impl Into<ElementId> for HistoryEntryId {
fn into(self) -> ElementId {
match self {
HistoryEntryId::AcpThread(session_id) => ElementId::Name(session_id.0.into()),
HistoryEntryId::TextThread(path) => ElementId::Path(path),
}
}
}
#[derive(Serialize, Deserialize, Debug)]
enum SerializedRecentOpen {
AcpThread(String),
TextThread(String),
}
pub struct HistoryStore {
threads: Vec<DbThreadMetadata>,
entries: Vec<HistoryEntry>,
context_store: Entity<assistant_context::ContextStore>,
recently_opened_entries: VecDeque<HistoryEntryId>,
_subscriptions: Vec<gpui::Subscription>,
_save_recently_opened_entries_task: Task<()>,
}
impl HistoryStore {
pub fn new(
context_store: Entity<assistant_context::ContextStore>,
cx: &mut Context<Self>,
) -> Self {
let subscriptions = vec![cx.observe(&context_store, |this, _, cx| this.update_entries(cx))];
cx.spawn(async move |this, cx| {
let entries = Self::load_recently_opened_entries(cx).await;
this.update(cx, |this, cx| {
if let Some(entries) = entries.log_err() {
this.recently_opened_entries = entries;
}
this.reload(cx);
})
.ok();
})
.detach();
Self {
context_store,
recently_opened_entries: VecDeque::default(),
threads: Vec::default(),
entries: Vec::default(),
_subscriptions: subscriptions,
_save_recently_opened_entries_task: Task::ready(()),
}
}
pub fn thread_from_session_id(&self, session_id: &acp::SessionId) -> Option<&DbThreadMetadata> {
self.threads.iter().find(|thread| &thread.id == session_id)
}
pub fn delete_thread(
&mut self,
id: acp::SessionId,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let database_future = ThreadsDatabase::connect(cx);
cx.spawn(async move |this, cx| {
let database = database_future.await.map_err(|err| anyhow!(err))?;
database.delete_thread(id.clone()).await?;
this.update(cx, |this, cx| this.reload(cx))
})
}
pub fn delete_text_thread(
&mut self,
path: Arc<Path>,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
self.context_store.update(cx, |context_store, cx| {
context_store.delete_local_context(path, cx)
})
}
pub fn load_text_thread(
&self,
path: Arc<Path>,
cx: &mut Context<Self>,
) -> Task<Result<Entity<AssistantContext>>> {
self.context_store.update(cx, |context_store, cx| {
context_store.open_local_context(path, cx)
})
}
pub fn reload(&self, cx: &mut Context<Self>) {
let database_future = ThreadsDatabase::connect(cx);
cx.spawn(async move |this, cx| {
let threads = database_future
.await
.map_err(|err| anyhow!(err))?
.list_threads()
.await?;
this.update(cx, |this, cx| {
if this.recently_opened_entries.len() < MAX_RECENTLY_OPENED_ENTRIES {
for thread in threads
.iter()
.take(MAX_RECENTLY_OPENED_ENTRIES - this.recently_opened_entries.len())
.rev()
{
this.push_recently_opened_entry(
HistoryEntryId::AcpThread(thread.id.clone()),
cx,
)
}
}
this.threads = threads;
this.update_entries(cx);
})
})
.detach_and_log_err(cx);
}
fn update_entries(&mut self, cx: &mut Context<Self>) {
#[cfg(debug_assertions)]
if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
return;
}
let mut history_entries = Vec::new();
history_entries.extend(self.threads.iter().cloned().map(HistoryEntry::AcpThread));
history_entries.extend(
self.context_store
.read(cx)
.unordered_contexts()
.cloned()
.map(HistoryEntry::TextThread),
);
history_entries.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.updated_at()));
self.entries = history_entries;
cx.notify()
}
pub fn is_empty(&self, _cx: &App) -> bool {
self.entries.is_empty()
}
pub fn recently_opened_entries(&self, cx: &App) -> Vec<HistoryEntry> {
#[cfg(debug_assertions)]
if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
return Vec::new();
}
let thread_entries = self.threads.iter().flat_map(|thread| {
self.recently_opened_entries
.iter()
.enumerate()
.flat_map(|(index, entry)| match entry {
HistoryEntryId::AcpThread(id) if &thread.id == id => {
Some((index, HistoryEntry::AcpThread(thread.clone())))
}
_ => None,
})
});
let context_entries =
self.context_store
.read(cx)
.unordered_contexts()
.flat_map(|context| {
self.recently_opened_entries
.iter()
.enumerate()
.flat_map(|(index, entry)| match entry {
HistoryEntryId::TextThread(path) if &context.path == path => {
Some((index, HistoryEntry::TextThread(context.clone())))
}
_ => None,
})
});
thread_entries
.chain(context_entries)
// optimization to halt iteration early
.take(self.recently_opened_entries.len())
.sorted_unstable_by_key(|(index, _)| *index)
.map(|(_, entry)| entry)
.collect()
}
fn save_recently_opened_entries(&mut self, cx: &mut Context<Self>) {
let serialized_entries = self
.recently_opened_entries
.iter()
.filter_map(|entry| match entry {
HistoryEntryId::TextThread(path) => path.file_name().map(|file| {
SerializedRecentOpen::TextThread(file.to_string_lossy().to_string())
}),
HistoryEntryId::AcpThread(id) => {
Some(SerializedRecentOpen::AcpThread(id.to_string()))
}
})
.collect::<Vec<_>>();
self._save_recently_opened_entries_task = cx.spawn(async move |_, cx| {
let content = serde_json::to_string(&serialized_entries).unwrap();
cx.background_executor()
.timer(SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE)
.await;
if cfg!(any(feature = "test-support", test)) {
return;
}
KEY_VALUE_STORE
.write_kvp(RECENTLY_OPENED_THREADS_KEY.to_owned(), content)
.await
.log_err();
});
}
fn load_recently_opened_entries(cx: &AsyncApp) -> Task<Result<VecDeque<HistoryEntryId>>> {
cx.background_spawn(async move {
if cfg!(any(feature = "test-support", test)) {
anyhow::bail!("history store does not persist in tests");
}
let json = KEY_VALUE_STORE
.read_kvp(RECENTLY_OPENED_THREADS_KEY)?
.unwrap_or("[]".to_string());
let entries = serde_json::from_str::<Vec<SerializedRecentOpen>>(&json)
.context("deserializing persisted agent panel navigation history")?
.into_iter()
.take(MAX_RECENTLY_OPENED_ENTRIES)
.flat_map(|entry| match entry {
SerializedRecentOpen::AcpThread(id) => Some(HistoryEntryId::AcpThread(
acp::SessionId(id.as_str().into()),
)),
SerializedRecentOpen::TextThread(file_name) => Some(
HistoryEntryId::TextThread(contexts_dir().join(file_name).into()),
),
})
.collect();
Ok(entries)
})
}
pub fn push_recently_opened_entry(&mut self, entry: HistoryEntryId, cx: &mut Context<Self>) {
self.recently_opened_entries
.retain(|old_entry| old_entry != &entry);
self.recently_opened_entries.push_front(entry);
self.recently_opened_entries
.truncate(MAX_RECENTLY_OPENED_ENTRIES);
self.save_recently_opened_entries(cx);
}
pub fn remove_recently_opened_thread(&mut self, id: acp::SessionId, cx: &mut Context<Self>) {
self.recently_opened_entries.retain(
|entry| !matches!(entry, HistoryEntryId::AcpThread(thread_id) if thread_id == &id),
);
self.save_recently_opened_entries(cx);
}
pub fn replace_recently_opened_text_thread(
&mut self,
old_path: &Path,
new_path: &Arc<Path>,
cx: &mut Context<Self>,
) {
for entry in &mut self.recently_opened_entries {
match entry {
HistoryEntryId::TextThread(path) if path.as_ref() == old_path => {
*entry = HistoryEntryId::TextThread(new_path.clone());
break;
}
_ => {}
}
}
self.save_recently_opened_entries(cx);
}
pub fn remove_recently_opened_entry(&mut self, entry: &HistoryEntryId, cx: &mut Context<Self>) {
self.recently_opened_entries
.retain(|old_entry| old_entry != entry);
self.save_recently_opened_entries(cx);
}
pub fn entries(&self) -> impl Iterator<Item = HistoryEntry> {
self.entries.iter().cloned()
}
}

View File

@@ -1,51 +1,55 @@
use std::{any::Any, path::Path, rc::Rc, sync::Arc};
use agent_servers::{AgentServer, AgentServerDelegate};
use agent_servers::AgentServer;
use anyhow::Result;
use fs::Fs;
use gpui::{App, Entity, SharedString, Task};
use gpui::{App, Entity, Task};
use project::Project;
use prompt_store::PromptStore;
use crate::{HistoryStore, NativeAgent, NativeAgentConnection, templates::Templates};
use crate::{NativeAgent, NativeAgentConnection, templates::Templates};
#[derive(Clone)]
pub struct NativeAgentServer {
fs: Arc<dyn Fs>,
history: Entity<HistoryStore>,
}
impl NativeAgentServer {
pub fn new(fs: Arc<dyn Fs>, history: Entity<HistoryStore>) -> Self {
Self { fs, history }
pub fn new(fs: Arc<dyn Fs>) -> Self {
Self { fs }
}
}
impl AgentServer for NativeAgentServer {
fn telemetry_id(&self) -> &'static str {
"zed"
fn name(&self) -> &'static str {
"Native Agent"
}
fn name(&self) -> SharedString {
"Zed Agent".into()
fn empty_state_headline(&self) -> &'static str {
"Native Agent"
}
fn empty_state_message(&self) -> &'static str {
"How can I help you today?"
}
fn logo(&self) -> ui::IconName {
ui::IconName::ZedAgent
// Using the ZedAssistant icon as it's the native built-in agent
ui::IconName::ZedAssistant
}
fn connect(
&self,
_root_dir: &Path,
delegate: AgentServerDelegate,
project: &Entity<Project>,
cx: &mut App,
) -> Task<Result<Rc<dyn acp_thread::AgentConnection>>> {
log::debug!(
log::info!(
"NativeAgentServer::connect called for path: {:?}",
_root_dir
);
let project = delegate.project().clone();
let project = project.clone();
let fs = self.fs.clone();
let history = self.history.clone();
let prompt_store = PromptStore::global(cx);
cx.spawn(async move |cx| {
log::debug!("Creating templates for native agent");
@@ -53,12 +57,11 @@ impl AgentServer for NativeAgentServer {
let prompt_store = prompt_store.await?;
log::debug!("Creating native agent entity");
let agent =
NativeAgent::new(project, history, templates, Some(prompt_store), fs, cx).await?;
let agent = NativeAgent::new(project, templates, Some(prompt_store), fs, cx).await?;
// Create the connection wrapper
let connection = NativeAgentConnection(agent);
log::debug!("NativeAgentServer connection established successfully");
log::info!("NativeAgentServer connection established successfully");
Ok(Rc::new(connection) as Rc<dyn acp_thread::AgentConnection>)
})
@@ -68,52 +71,3 @@ impl AgentServer for NativeAgentServer {
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use assistant_context::ContextStore;
use gpui::AppContext;
agent_servers::e2e_tests::common_e2e_tests!(
async |fs, project, cx| {
let auth = cx.update(|cx| {
prompt_store::init(cx);
terminal::init(cx);
let registry = language_model::LanguageModelRegistry::read_global(cx);
let auth = registry
.provider(&language_model::ANTHROPIC_PROVIDER_ID)
.unwrap()
.authenticate(cx);
cx.spawn(async move |_| auth.await)
});
auth.await.unwrap();
cx.update(|cx| {
let registry = language_model::LanguageModelRegistry::global(cx);
registry.update(cx, |registry, cx| {
registry.select_default_model(
Some(&language_model::SelectedModel {
provider: language_model::ANTHROPIC_PROVIDER_ID,
model: language_model::LanguageModelId("claude-sonnet-4-latest".into()),
}),
cx,
);
});
});
let history = cx.update(|cx| {
let context_store = cx.new(move |cx| ContextStore::fake(project.clone(), cx));
cx.new(move |cx| HistoryStore::new(context_store, cx))
});
NativeAgentServer::new(fs.clone(), history)
},
allow_option_id = "allow"
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -16,11 +16,11 @@ impl AgentTool for EchoTool {
type Input = EchoToolInput;
type Output = String;
fn name() -> &'static str {
"echo"
fn name(&self) -> SharedString {
"echo".into()
}
fn kind() -> acp::ToolKind {
fn kind(&self) -> acp::ToolKind {
acp::ToolKind::Other
}
@@ -51,8 +51,8 @@ impl AgentTool for DelayTool {
type Input = DelayToolInput;
type Output = String;
fn name() -> &'static str {
"delay"
fn name(&self) -> SharedString {
"delay".into()
}
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
@@ -63,7 +63,7 @@ impl AgentTool for DelayTool {
}
}
fn kind() -> acp::ToolKind {
fn kind(&self) -> acp::ToolKind {
acp::ToolKind::Other
}
@@ -92,11 +92,11 @@ impl AgentTool for ToolRequiringPermission {
type Input = ToolRequiringPermissionInput;
type Output = String;
fn name() -> &'static str {
"tool_requiring_permission"
fn name(&self) -> SharedString {
"tool_requiring_permission".into()
}
fn kind() -> acp::ToolKind {
fn kind(&self) -> acp::ToolKind {
acp::ToolKind::Other
}
@@ -127,11 +127,11 @@ impl AgentTool for InfiniteTool {
type Input = InfiniteToolInput;
type Output = String;
fn name() -> &'static str {
"infinite"
fn name(&self) -> SharedString {
"infinite".into()
}
fn kind() -> acp::ToolKind {
fn kind(&self) -> acp::ToolKind {
acp::ToolKind::Other
}
@@ -178,11 +178,11 @@ impl AgentTool for WordListTool {
type Input = WordListInput;
type Output = String;
fn name() -> &'static str {
"word_list"
fn name(&self) -> SharedString {
"word_list".into()
}
fn kind() -> acp::ToolKind {
fn kind(&self) -> acp::ToolKind {
acp::ToolKind::Other
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,43 +0,0 @@
use language_model::LanguageModelToolSchemaFormat;
use schemars::{
JsonSchema, Schema,
generate::SchemaSettings,
transform::{Transform, transform_subschemas},
};
pub(crate) fn root_schema_for<T: JsonSchema>(format: LanguageModelToolSchemaFormat) -> Schema {
let mut generator = match format {
LanguageModelToolSchemaFormat::JsonSchema => SchemaSettings::draft07().into_generator(),
LanguageModelToolSchemaFormat::JsonSchemaSubset => SchemaSettings::openapi3()
.with(|settings| {
settings.meta_schema = None;
settings.inline_subschemas = true;
})
.with_transform(ToJsonSchemaSubsetTransform)
.into_generator(),
};
generator.root_schema_for::<T>()
}
#[derive(Debug, Clone)]
struct ToJsonSchemaSubsetTransform;
impl Transform for ToJsonSchemaSubsetTransform {
fn transform(&mut self, schema: &mut Schema) {
// Ensure that the type field is not an array, this happens when we use
// Option<T>, the type will be [T, "null"].
if let Some(type_field) = schema.get_mut("type")
&& let Some(types) = type_field.as_array()
&& let Some(first_type) = types.first()
{
*type_field = first_type.clone();
}
// oneOf is not supported, use anyOf instead
if let Some(one_of) = schema.remove("oneOf") {
schema.insert("anyOf".to_string(), one_of);
}
transform_subschemas(self, schema);
}
}

View File

@@ -16,29 +16,6 @@ mod terminal_tool;
mod thinking_tool;
mod web_search_tool;
/// A list of all built in tool names, for use in deduplicating MCP tool names
pub fn default_tool_names() -> impl Iterator<Item = &'static str> {
[
CopyPathTool::name(),
CreateDirectoryTool::name(),
DeletePathTool::name(),
DiagnosticsTool::name(),
EditFileTool::name(),
FetchTool::name(),
FindPathTool::name(),
GrepTool::name(),
ListDirectoryTool::name(),
MovePathTool::name(),
NowTool::name(),
OpenTool::name(),
ReadFileTool::name(),
TerminalTool::name(),
ThinkingTool::name(),
WebSearchTool::name(),
]
.into_iter()
}
pub use context_server_registry::*;
pub use copy_path_tool::*;
pub use create_directory_tool::*;
@@ -56,5 +33,3 @@ pub use read_file_tool::*;
pub use terminal_tool::*;
pub use thinking_tool::*;
pub use web_search_tool::*;
use crate::AgentTool;

View File

@@ -169,23 +169,22 @@ impl AnyAgentTool for ContextServerTool {
fn run(
self: Arc<Self>,
input: serde_json::Value,
event_stream: ToolCallEventStream,
_event_stream: ToolCallEventStream,
cx: &mut App,
) -> Task<Result<AgentToolOutput>> {
let Some(server) = self.store.read(cx).get_running_server(&self.server_id) else {
return Task::ready(Err(anyhow!("Context server not found")));
};
let tool_name = self.tool.name.clone();
let authorize = event_stream.authorize(self.initial_title(input.clone()), cx);
let server_clone = server.clone();
let input_clone = input.clone();
cx.spawn(async move |_cx| {
authorize.await?;
let Some(protocol) = server.client() else {
let Some(protocol) = server_clone.client() else {
bail!("Context server not initialized");
};
let arguments = if let serde_json::Value::Object(map) = input {
let arguments = if let serde_json::Value::Object(map) = input_clone {
Some(map.into_iter().collect())
} else {
None
@@ -229,14 +228,4 @@ impl AnyAgentTool for ContextServerTool {
})
})
}
fn replay(
&self,
_input: serde_json::Value,
_output: serde_json::Value,
_event_stream: ToolCallEventStream,
_cx: &mut App,
) -> Result<()> {
Ok(())
}
}

View File

@@ -1,18 +1,23 @@
use crate::{AgentTool, ToolCallEventStream};
use agent_client_protocol::ToolKind;
use anyhow::{Context as _, Result, anyhow};
use gpui::{App, AppContext, Entity, Task};
use gpui::{App, AppContext, Entity, SharedString, Task};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use util::markdown::MarkdownInlineCode;
/// Copies a file or directory in the project, and returns confirmation that the copy succeeded.
/// Copies a file or directory in the project, and returns confirmation that the
/// copy succeeded.
///
/// Directory contents will be copied recursively (like `cp -r`).
///
/// This tool should be used when it's desirable to create a copy of a file or directory without modifying the original.
/// It's much more efficient than doing this by separately reading and then writing the file or directory's contents, so this tool should be preferred over that approach whenever copying is the goal.
/// This tool should be used when it's desirable to create a copy of a file or
/// directory without modifying the original. It's much more efficient than
/// doing this by separately reading and then writing the file or directory's
/// contents, so this tool should be preferred over that approach whenever
/// copying is the goal.
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct CopyPathToolInput {
/// The source path of the file or directory to copy.
@@ -28,10 +33,12 @@ pub struct CopyPathToolInput {
/// You can copy the first file by providing a source_path of "directory1/a/something.txt"
/// </example>
pub source_path: String,
/// The destination path where the file or directory should be copied to.
///
/// <example>
/// To copy "directory1/a/something.txt" to "directory2/b/copy.txt", provide a destination_path of "directory2/b/copy.txt"
/// To copy "directory1/a/something.txt" to "directory2/b/copy.txt",
/// provide a destination_path of "directory2/b/copy.txt"
/// </example>
pub destination_path: String,
}
@@ -50,11 +57,11 @@ impl AgentTool for CopyPathTool {
type Input = CopyPathToolInput;
type Output = String;
fn name() -> &'static str {
"copy_path"
fn name(&self) -> SharedString {
"copy_path".into()
}
fn kind() -> ToolKind {
fn kind(&self) -> ToolKind {
ToolKind::Move
}

View File

@@ -9,9 +9,12 @@ use util::markdown::MarkdownInlineCode;
use crate::{AgentTool, ToolCallEventStream};
/// Creates a new directory at the specified path within the project. Returns confirmation that the directory was created.
/// Creates a new directory at the specified path within the project. Returns
/// confirmation that the directory was created.
///
/// This tool creates a directory and all necessary parent directories (similar to `mkdir -p`). It should be used whenever you need to create new directories within the project.
/// This tool creates a directory and all necessary parent directories (similar
/// to `mkdir -p`). It should be used whenever you need to create new
/// directories within the project.
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct CreateDirectoryToolInput {
/// The path of the new directory.
@@ -41,11 +44,11 @@ impl AgentTool for CreateDirectoryTool {
type Input = CreateDirectoryToolInput;
type Output = String;
fn name() -> &'static str {
"create_directory"
fn name(&self) -> SharedString {
"create_directory".into()
}
fn kind() -> ToolKind {
fn kind(&self) -> ToolKind {
ToolKind::Read
}

View File

@@ -9,7 +9,8 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
/// Deletes the file or directory (and the directory's contents, recursively) at the specified path in the project, and returns confirmation of the deletion.
/// Deletes the file or directory (and the directory's contents, recursively) at
/// the specified path in the project, and returns confirmation of the deletion.
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct DeletePathToolInput {
/// The path of the file or directory to delete.
@@ -44,11 +45,11 @@ impl AgentTool for DeletePathTool {
type Input = DeletePathToolInput;
type Output = String;
fn name() -> &'static str {
"delete_path"
fn name(&self) -> SharedString {
"delete_path".into()
}
fn kind() -> ToolKind {
fn kind(&self) -> ToolKind {
ToolKind::Delete
}

View File

@@ -63,11 +63,11 @@ impl AgentTool for DiagnosticsTool {
type Input = DiagnosticsToolInput;
type Output = String;
fn name() -> &'static str {
"diagnostics"
fn name(&self) -> SharedString {
"diagnostics".into()
}
fn kind() -> acp::ToolKind {
fn kind(&self) -> acp::ToolKind {
acp::ToolKind::Read
}

View File

@@ -5,10 +5,10 @@ use anyhow::{Context as _, Result, anyhow};
use assistant_tools::edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent, EditFormat};
use cloud_llm_client::CompletionIntent;
use collections::HashSet;
use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity};
use gpui::{App, AppContext, AsyncApp, Entity, Task};
use indoc::formatdoc;
use language::ToPoint;
use language::language_settings::{self, FormatOnSave};
use language::{LanguageRegistry, ToPoint};
use language_model::LanguageModelToolResultContent;
use paths;
use project::lsp_store::{FormatTrigger, LspFormatTarget};
@@ -34,21 +34,25 @@ const DEFAULT_UI_TEXT: &str = "Editing file";
/// - Use the `list_directory` tool to verify the parent directory exists and is the correct location
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct EditFileToolInput {
/// A one-line, user-friendly markdown description of the edit. This will be shown in the UI and also passed to another model to perform the edit.
/// A one-line, user-friendly markdown description of the edit. This will be
/// shown in the UI and also passed to another model to perform the edit.
///
/// Be terse, but also descriptive in what you want to achieve with this edit. Avoid generic instructions.
/// Be terse, but also descriptive in what you want to achieve with this
/// edit. Avoid generic instructions.
///
/// NEVER mention the file path in this description.
///
/// <example>Fix API endpoint URLs</example>
/// <example>Update copyright year in `page_footer`</example>
///
/// Make sure to include this field before all the others in the input object so that we can display it immediately.
/// Make sure to include this field before all the others in the input object
/// so that we can display it immediately.
pub display_description: String,
/// The full path of the file to create or modify in the project.
///
/// WARNING: When specifying which file path need changing, you MUST start each path with one of the project's root directories.
/// WARNING: When specifying which file path need changing, you MUST
/// start each path with one of the project's root directories.
///
/// The following examples assume we have two root directories in the project:
/// - /a/b/backend
@@ -57,19 +61,22 @@ pub struct EditFileToolInput {
/// <example>
/// `backend/src/main.rs`
///
/// Notice how the file path starts with `backend`. Without that, the path would be ambiguous and the call would fail!
/// Notice how the file path starts with `backend`. Without that, the path
/// would be ambiguous and the call would fail!
/// </example>
///
/// <example>
/// `frontend/db.js`
/// </example>
pub path: PathBuf,
/// The mode of operation on the file. Possible values:
/// - 'edit': Make granular edits to an existing file.
/// - 'create': Create a new file if it doesn't exist.
/// - 'overwrite': Replace the entire contents of an existing file.
///
/// When a file already exists or you just created it, prefer editing it as opposed to recreating it from scratch.
/// When a file already exists or you just created it, prefer editing
/// it as opposed to recreating it from scratch.
pub mode: EditFileMode,
}
@@ -91,13 +98,11 @@ pub enum EditFileMode {
#[derive(Debug, Serialize, Deserialize)]
pub struct EditFileToolOutput {
#[serde(alias = "original_path")]
input_path: PathBuf,
project_path: PathBuf,
new_text: String,
old_text: Arc<String>,
#[serde(default)]
diff: String,
#[serde(alias = "raw_output")]
edit_agent_output: EditAgentOutput,
}
@@ -117,16 +122,12 @@ impl From<EditFileToolOutput> for LanguageModelToolResultContent {
}
pub struct EditFileTool {
thread: WeakEntity<Thread>,
language_registry: Arc<LanguageRegistry>,
thread: Entity<Thread>,
}
impl EditFileTool {
pub fn new(thread: WeakEntity<Thread>, language_registry: Arc<LanguageRegistry>) -> Self {
Self {
thread,
language_registry,
}
pub fn new(thread: Entity<Thread>) -> Self {
Self { thread }
}
fn authorize(
@@ -155,22 +156,19 @@ impl EditFileTool {
// It's also possible that the global config dir is configured to be inside the project,
// so check for that edge case too.
if let Ok(canonical_path) = std::fs::canonicalize(&input.path)
&& canonical_path.starts_with(paths::config_dir())
{
return event_stream.authorize(
format!("{} (global settings)", input.display_description),
cx,
);
if let Ok(canonical_path) = std::fs::canonicalize(&input.path) {
if canonical_path.starts_with(paths::config_dir()) {
return event_stream.authorize(
format!("{} (global settings)", input.display_description),
cx,
);
}
}
// Check if path is inside the global config directory
// First check if it's already inside project - if not, try to canonicalize
let Ok(project_path) = self.thread.read_with(cx, |thread, cx| {
thread.project().read(cx).find_project_path(&input.path, cx)
}) else {
return Task::ready(Err(anyhow!("thread was dropped")));
};
let thread = self.thread.read(cx);
let project_path = thread.project().read(cx).find_project_path(&input.path, cx);
// If the path is inside the project, and it's not one of the above edge cases,
// then no confirmation is necessary. Otherwise, confirmation is necessary.
@@ -186,11 +184,11 @@ impl AgentTool for EditFileTool {
type Input = EditFileToolInput;
type Output = EditFileToolOutput;
fn name() -> &'static str {
"edit_file"
fn name(&self) -> SharedString {
"edit_file".into()
}
fn kind() -> acp::ToolKind {
fn kind(&self) -> acp::ToolKind {
acp::ToolKind::Edit
}
@@ -223,12 +221,7 @@ impl AgentTool for EditFileTool {
event_stream: ToolCallEventStream,
cx: &mut App,
) -> Task<Result<Self::Output>> {
let Ok(project) = self
.thread
.read_with(cx, |thread, _cx| thread.project().clone())
else {
return Task::ready(Err(anyhow!("thread was dropped")));
};
let project = self.thread.read(cx).project().clone();
let project_path = match resolve_path(&input, project.clone(), cx) {
Ok(path) => path,
Err(err) => return Task::ready(Err(anyhow!(err))),
@@ -244,17 +237,23 @@ impl AgentTool for EditFileTool {
});
}
let Some(request) = self.thread.update(cx, |thread, cx| {
thread
.build_completion_request(CompletionIntent::ToolResults, cx)
.ok()
}) else {
return Task::ready(Err(anyhow!("Failed to build completion request")));
};
let thread = self.thread.read(cx);
let Some(model) = thread.model().cloned() else {
return Task::ready(Err(anyhow!("No language model configured")));
};
let action_log = thread.action_log().clone();
let authorize = self.authorize(&input, &event_stream, cx);
cx.spawn(async move |cx: &mut AsyncApp| {
authorize.await?;
let (request, model, action_log) = self.thread.update(cx, |thread, cx| {
let request = thread.build_completion_request(CompletionIntent::ToolResults, cx);
(request, thread.model().cloned(), thread.action_log().clone())
})?;
let request = request?;
let model = model.context("No language model configured")?;
let edit_format = EditFormat::from_model(model.clone())?;
let edit_agent = EditAgent::new(
model,
@@ -273,13 +272,6 @@ impl AgentTool for EditFileTool {
let diff = cx.new(|cx| Diff::new(buffer.clone(), cx))?;
event_stream.update_diff(diff.clone());
let _finalize_diff = util::defer({
let diff = diff.downgrade();
let mut cx = cx.clone();
move || {
diff.update(&mut cx, |diff, cx| diff.finalize(cx)).ok();
}
});
let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
let old_text = cx
@@ -396,6 +388,8 @@ impl AgentTool for EditFileTool {
})
.await;
diff.update(cx, |diff, cx| diff.finalize(cx)).ok();
let input_path = input.path.display();
if unified_diff.is_empty() {
anyhow::ensure!(
@@ -425,32 +419,14 @@ impl AgentTool for EditFileTool {
Ok(EditFileToolOutput {
input_path: input.path,
new_text,
project_path: project_path.path.to_path_buf(),
new_text: new_text.clone(),
old_text,
diff: unified_diff,
edit_agent_output,
})
})
}
fn replay(
&self,
_input: Self::Input,
output: Self::Output,
event_stream: ToolCallEventStream,
cx: &mut App,
) -> Result<()> {
event_stream.update_diff(cx.new(|cx| {
Diff::finalized(
output.input_path,
Some(output.old_text.to_string()),
output.new_text,
self.language_registry.clone(),
cx,
)
}));
Ok(())
}
}
/// Validate that the file path is valid, meaning:
@@ -522,6 +498,7 @@ fn resolve_path(
mod tests {
use super::*;
use crate::{ContextServerRegistry, Templates};
use action_log::ActionLog;
use client::TelemetrySettings;
use fs::Fs;
use gpui::{TestAppContext, UpdateGlobal};
@@ -538,7 +515,7 @@ mod tests {
let fs = project::FakeFs::new(cx.executor());
fs.insert_tree("/root", json!({})).await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
@@ -547,6 +524,7 @@ mod tests {
project,
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
action_log,
Templates::new(),
Some(model),
cx,
@@ -559,11 +537,7 @@ mod tests {
path: "root/nonexistent_file.txt".into(),
mode: EditFileMode::Edit,
};
Arc::new(EditFileTool::new(thread.downgrade(), language_registry)).run(
input,
ToolCallEventStream::test().0,
cx,
)
Arc::new(EditFileTool { thread }).run(input, ToolCallEventStream::test().0, cx)
})
.await;
assert_eq!(
@@ -650,7 +624,8 @@ mod tests {
mode: mode.clone(),
};
cx.update(|cx| resolve_path(&input, project, cx))
let result = cx.update(|cx| resolve_path(&input, project, cx));
result
}
fn assert_resolved_path_eq(path: anyhow::Result<ProjectPath>, expected: &str) {
@@ -737,6 +712,7 @@ mod tests {
}
});
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
@@ -745,6 +721,7 @@ mod tests {
project,
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
action_log.clone(),
Templates::new(),
Some(model.clone()),
cx,
@@ -773,10 +750,9 @@ mod tests {
path: "root/src/main.rs".into(),
mode: EditFileMode::Overwrite,
};
Arc::new(EditFileTool::new(
thread.downgrade(),
language_registry.clone(),
))
Arc::new(EditFileTool {
thread: thread.clone(),
})
.run(input, ToolCallEventStream::test().0, cx)
});
@@ -801,9 +777,7 @@ mod tests {
"Code should be formatted when format_on_save is enabled"
);
let stale_buffer_count = thread
.read_with(cx, |thread, _cx| thread.action_log.clone())
.read_with(cx, |log, cx| log.stale_buffers(cx).count());
let stale_buffer_count = action_log.read_with(cx, |log, cx| log.stale_buffers(cx).count());
assert_eq!(
stale_buffer_count, 0,
@@ -832,11 +806,7 @@ mod tests {
path: "root/src/main.rs".into(),
mode: EditFileMode::Overwrite,
};
Arc::new(EditFileTool::new(thread.downgrade(), language_registry)).run(
input,
ToolCallEventStream::test().0,
cx,
)
Arc::new(EditFileTool { thread }).run(input, ToolCallEventStream::test().0, cx)
});
// Stream the unformatted content
@@ -880,13 +850,14 @@ mod tests {
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project,
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
action_log.clone(),
Templates::new(),
Some(model.clone()),
cx,
@@ -916,10 +887,9 @@ mod tests {
path: "root/src/main.rs".into(),
mode: EditFileMode::Overwrite,
};
Arc::new(EditFileTool::new(
thread.downgrade(),
language_registry.clone(),
))
Arc::new(EditFileTool {
thread: thread.clone(),
})
.run(input, ToolCallEventStream::test().0, cx)
});
@@ -968,11 +938,10 @@ mod tests {
path: "root/src/main.rs".into(),
mode: EditFileMode::Overwrite,
};
Arc::new(EditFileTool::new(thread.downgrade(), language_registry)).run(
input,
ToolCallEventStream::test().0,
cx,
)
Arc::new(EditFileTool {
thread: thread.clone(),
})
.run(input, ToolCallEventStream::test().0, cx)
});
// Stream the content with trailing whitespace
@@ -1007,19 +976,20 @@ mod tests {
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project,
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
action_log.clone(),
Templates::new(),
Some(model.clone()),
cx,
)
});
let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
let tool = Arc::new(EditFileTool { thread });
fs.insert_tree("/root", json!({})).await;
// Test 1: Path with .zed component should require confirmation
@@ -1141,21 +1111,22 @@ mod tests {
let fs = project::FakeFs::new(cx.executor());
fs.insert_tree("/project", json!({})).await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project,
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
action_log.clone(),
Templates::new(),
Some(model.clone()),
cx,
)
});
let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
let tool = Arc::new(EditFileTool { thread });
// Test global config paths - these should require confirmation if they exist and are outside the project
let test_cases = vec![
@@ -1249,7 +1220,8 @@ mod tests {
cx,
)
.await;
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
@@ -1258,12 +1230,13 @@ mod tests {
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry.clone(),
action_log.clone(),
Templates::new(),
Some(model.clone()),
cx,
)
});
let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
let tool = Arc::new(EditFileTool { thread });
// Test files in different worktrees
let test_cases = vec![
@@ -1329,7 +1302,7 @@ mod tests {
)
.await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
@@ -1338,12 +1311,13 @@ mod tests {
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry.clone(),
action_log.clone(),
Templates::new(),
Some(model.clone()),
cx,
)
});
let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
let tool = Arc::new(EditFileTool { thread });
// Test edge cases
let test_cases = vec![
@@ -1412,7 +1386,7 @@ mod tests {
)
.await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
@@ -1421,12 +1395,13 @@ mod tests {
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry.clone(),
action_log.clone(),
Templates::new(),
Some(model.clone()),
cx,
)
});
let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
let tool = Arc::new(EditFileTool { thread });
// Test different EditFileMode values
let modes = vec![
@@ -1492,7 +1467,7 @@ mod tests {
init_test(cx);
let fs = project::FakeFs::new(cx.executor());
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
@@ -1501,12 +1476,13 @@ mod tests {
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
action_log.clone(),
Templates::new(),
Some(model.clone()),
cx,
)
});
let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
let tool = Arc::new(EditFileTool { thread });
assert_eq!(
tool.initial_title(Err(json!({
@@ -1550,100 +1526,6 @@ mod tests {
);
}
#[gpui::test]
async fn test_diff_finalization(cx: &mut TestAppContext) {
init_test(cx);
let fs = project::FakeFs::new(cx.executor());
fs.insert_tree("/", json!({"main.rs": ""})).await;
let project = Project::test(fs.clone(), [path!("/").as_ref()], cx).await;
let languages = project.read_with(cx, |project, _cx| project.languages().clone());
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry.clone(),
Templates::new(),
Some(model.clone()),
cx,
)
});
// Ensure the diff is finalized after the edit completes.
{
let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone()));
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
let edit = cx.update(|cx| {
tool.run(
EditFileToolInput {
display_description: "Edit file".into(),
path: path!("/main.rs").into(),
mode: EditFileMode::Edit,
},
stream_tx,
cx,
)
});
stream_rx.expect_update_fields().await;
let diff = stream_rx.expect_diff().await;
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_))));
cx.run_until_parked();
model.end_last_completion_stream();
edit.await.unwrap();
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_))));
}
// Ensure the diff is finalized if an error occurs while editing.
{
model.forbid_requests();
let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone()));
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
let edit = cx.update(|cx| {
tool.run(
EditFileToolInput {
display_description: "Edit file".into(),
path: path!("/main.rs").into(),
mode: EditFileMode::Edit,
},
stream_tx,
cx,
)
});
stream_rx.expect_update_fields().await;
let diff = stream_rx.expect_diff().await;
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_))));
edit.await.unwrap_err();
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_))));
model.allow_requests();
}
// Ensure the diff is finalized if the tool call gets dropped.
{
let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone()));
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
let edit = cx.update(|cx| {
tool.run(
EditFileToolInput {
display_description: "Edit file".into(),
path: path!("/main.rs").into(),
mode: EditFileMode::Edit,
},
stream_tx,
cx,
)
});
stream_rx.expect_update_fields().await;
let diff = stream_rx.expect_diff().await;
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_))));
drop(edit);
cx.run_until_parked();
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_))));
}
}
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);

View File

@@ -118,11 +118,11 @@ impl AgentTool for FetchTool {
type Input = FetchToolInput;
type Output = String;
fn name() -> &'static str {
"fetch"
fn name(&self) -> SharedString {
"fetch".into()
}
fn kind() -> acp::ToolKind {
fn kind(&self) -> acp::ToolKind {
acp::ToolKind::Fetch
}
@@ -136,17 +136,12 @@ impl AgentTool for FetchTool {
fn run(
self: Arc<Self>,
input: Self::Input,
event_stream: ToolCallEventStream,
_event_stream: ToolCallEventStream,
cx: &mut App,
) -> Task<Result<Self::Output>> {
let authorize = event_stream.authorize(input.url.clone(), cx);
let text = cx.background_spawn({
let http_client = self.http_client.clone();
async move {
authorize.await?;
Self::build_message(http_client, &input.url).await
}
async move { Self::build_message(http_client, &input.url).await }
});
cx.foreground_executor().spawn(async move {

View File

@@ -31,6 +31,7 @@ pub struct FindPathToolInput {
/// You can get back the first two paths by providing a glob of "*thing*.txt"
/// </example>
pub glob: String,
/// Optional starting position for paginated results (0-based).
/// When not provided, starts from the beginning.
#[serde(default)]
@@ -85,11 +86,11 @@ impl AgentTool for FindPathTool {
type Input = FindPathToolInput;
type Output = FindPathToolOutput;
fn name() -> &'static str {
"find_path"
fn name(&self) -> SharedString {
"find_path".into()
}
fn kind() -> acp::ToolKind {
fn kind(&self) -> acp::ToolKind {
acp::ToolKind::Search
}
@@ -115,7 +116,7 @@ impl AgentTool for FindPathTool {
..cmp::min(input.offset + RESULTS_PER_PAGE, matches.len())];
event_stream.update_fields(acp::ToolCallUpdateFields {
title: Some(if paginated_matches.is_empty() {
title: Some(if paginated_matches.len() == 0 {
"No matches".into()
} else if paginated_matches.len() == 1 {
"1 match".into()
@@ -165,17 +166,16 @@ fn search_paths(glob: &str, project: Entity<Project>, cx: &mut App) -> Task<Resu
.collect();
cx.background_spawn(async move {
let mut results = Vec::new();
for snapshot in snapshots {
for entry in snapshot.entries(false, 0) {
Ok(snapshots
.iter()
.flat_map(|snapshot| {
let root_name = PathBuf::from(snapshot.root_name());
if path_matcher.is_match(root_name.join(&entry.path)) {
results.push(snapshot.abs_path().join(entry.path.as_ref()));
}
}
}
Ok(results)
snapshot
.entries(false, 0)
.map(move |entry| root_name.join(&entry.path))
.filter(|path| path_matcher.is_match(&path))
})
.collect())
})
}
@@ -216,8 +216,8 @@ mod test {
assert_eq!(
matches,
&[
PathBuf::from(path!("/root/apple/banana/carrot")),
PathBuf::from(path!("/root/apple/bandana/carbonara"))
PathBuf::from("root/apple/banana/carrot"),
PathBuf::from("root/apple/bandana/carbonara")
]
);
@@ -228,8 +228,8 @@ mod test {
assert_eq!(
matches,
&[
PathBuf::from(path!("/root/apple/banana/carrot")),
PathBuf::from(path!("/root/apple/bandana/carbonara"))
PathBuf::from("root/apple/banana/carrot"),
PathBuf::from("root/apple/bandana/carbonara")
]
);
}

View File

@@ -27,7 +27,8 @@ use util::paths::PathMatcher;
/// - DO NOT use HTML entities solely to escape characters in the tool parameters.
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct GrepToolInput {
/// A regex pattern to search for in the entire project. Note that the regex will be parsed by the Rust `regex` crate.
/// A regex pattern to search for in the entire project. Note that the regex
/// will be parsed by the Rust `regex` crate.
///
/// Do NOT specify a path here! This will only be matched against the code **content**.
pub regex: String,
@@ -67,11 +68,11 @@ impl AgentTool for GrepTool {
type Input = GrepToolInput;
type Output = String;
fn name() -> &'static str {
"grep"
fn name(&self) -> SharedString {
"grep".into()
}
fn kind() -> acp::ToolKind {
fn kind(&self) -> acp::ToolKind {
acp::ToolKind::Search
}
@@ -178,14 +179,15 @@ impl AgentTool for GrepTool {
// Check if this file should be excluded based on its worktree settings
if let Ok(Some(project_path)) = project.read_with(cx, |project, cx| {
project.find_project_path(&path, cx)
})
&& cx.update(|cx| {
}) {
if cx.update(|cx| {
let worktree_settings = WorktreeSettings::get(Some((&project_path).into()), cx);
worktree_settings.is_path_excluded(&project_path.path)
|| worktree_settings.is_path_private(&project_path.path)
}).unwrap_or(false) {
continue;
}
}
while *parse_status.borrow() != ParseStatus::Idle {
parse_status.changed().await?;
@@ -273,11 +275,12 @@ impl AgentTool for GrepTool {
output.extend(snapshot.text_for_range(range));
output.push_str("\n```\n");
if let Some(ancestor_range) = ancestor_range
&& end_row < ancestor_range.end.row {
if let Some(ancestor_range) = ancestor_range {
if end_row < ancestor_range.end.row {
let remaining_lines = ancestor_range.end.row - end_row;
writeln!(output, "\n{} lines remaining in ancestor node. Read the file to see all.", remaining_lines)?;
}
}
matches_found += 1;
}
@@ -317,7 +320,7 @@ mod tests {
init_test(cx);
cx.executor().allow_parking();
let fs = FakeFs::new(cx.executor());
let fs = FakeFs::new(cx.executor().clone());
fs.insert_tree(
path!("/root"),
serde_json::json!({
@@ -402,7 +405,7 @@ mod tests {
init_test(cx);
cx.executor().allow_parking();
let fs = FakeFs::new(cx.executor());
let fs = FakeFs::new(cx.executor().clone());
fs.insert_tree(
path!("/root"),
serde_json::json!({
@@ -477,7 +480,7 @@ mod tests {
init_test(cx);
cx.executor().allow_parking();
let fs = FakeFs::new(cx.executor());
let fs = FakeFs::new(cx.executor().clone());
// Create test file with syntax structures
fs.insert_tree(
@@ -762,7 +765,7 @@ mod tests {
if cfg!(windows) {
result.replace("root\\", "root/")
} else {
result
result.to_string()
}
}
Err(e) => panic!("Failed to run grep tool: {}", e),

View File

@@ -10,12 +10,14 @@ use std::fmt::Write;
use std::{path::Path, sync::Arc};
use util::markdown::MarkdownInlineCode;
/// Lists files and directories in a given path. Prefer the `grep` or `find_path` tools when searching the codebase.
/// Lists files and directories in a given path. Prefer the `grep` or
/// `find_path` tools when searching the codebase.
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ListDirectoryToolInput {
/// The fully-qualified path of the directory to list in the project.
///
/// This path should never be absolute, and the first component of the path should always be a root directory in a project.
/// This path should never be absolute, and the first component
/// of the path should always be a root directory in a project.
///
/// <example>
/// If the project has the following root directories:
@@ -51,11 +53,11 @@ impl AgentTool for ListDirectoryTool {
type Input = ListDirectoryToolInput;
type Output = String;
fn name() -> &'static str {
"list_directory"
fn name(&self) -> SharedString {
"list_directory".into()
}
fn kind() -> ToolKind {
fn kind(&self) -> ToolKind {
ToolKind::Read
}

View File

@@ -8,11 +8,14 @@ use serde::{Deserialize, Serialize};
use std::{path::Path, sync::Arc};
use util::markdown::MarkdownInlineCode;
/// Moves or rename a file or directory in the project, and returns confirmation that the move succeeded.
/// Moves or rename a file or directory in the project, and returns confirmation
/// that the move succeeded.
///
/// If the source and destination directories are the same, but the filename is different, this performs a rename. Otherwise, it performs a move.
/// If the source and destination directories are the same, but the filename is
/// different, this performs a rename. Otherwise, it performs a move.
///
/// This tool should be used when it's desirable to move or rename a file or directory without changing its contents at all.
/// This tool should be used when it's desirable to move or rename a file or
/// directory without changing its contents at all.
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct MovePathToolInput {
/// The source path of the file or directory to move/rename.
@@ -52,11 +55,11 @@ impl AgentTool for MovePathTool {
type Input = MovePathToolInput;
type Output = String;
fn name() -> &'static str {
"move_path"
fn name(&self) -> SharedString {
"move_path".into()
}
fn kind() -> ToolKind {
fn kind(&self) -> ToolKind {
ToolKind::Move
}

View File

@@ -32,11 +32,11 @@ impl AgentTool for NowTool {
type Input = NowToolInput;
type Output = String;
fn name() -> &'static str {
"now"
fn name(&self) -> SharedString {
"now".into()
}
fn kind() -> acp::ToolKind {
fn kind(&self) -> acp::ToolKind {
acp::ToolKind::Other
}

View File

@@ -8,15 +8,19 @@ use serde::{Deserialize, Serialize};
use std::{path::PathBuf, sync::Arc};
use util::markdown::MarkdownEscaped;
/// This tool opens a file or URL with the default application associated with it on the user's operating system:
/// This tool opens a file or URL with the default application associated with
/// it on the user's operating system:
///
/// - On macOS, it's equivalent to the `open` command
/// - On Windows, it's equivalent to `start`
/// - On Linux, it uses something like `xdg-open`, `gio open`, `gnome-open`, `kde-open`, `wslview` as appropriate
///
/// For example, it can open a web browser with a URL, open a PDF file with the default PDF viewer, etc.
/// For example, it can open a web browser with a URL, open a PDF file with the
/// default PDF viewer, etc.
///
/// You MUST ONLY use this tool when the user has explicitly requested opening something. You MUST NEVER assume that the user would like for you to use this tool.
/// You MUST ONLY use this tool when the user has explicitly requested opening
/// something. You MUST NEVER assume that the user would like for you to use
/// this tool.
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct OpenToolInput {
/// The path or URL to open with the default application.
@@ -37,11 +41,11 @@ impl AgentTool for OpenTool {
type Input = OpenToolInput;
type Output = String;
fn name() -> &'static str {
"open"
fn name(&self) -> SharedString {
"open".into()
}
fn kind() -> ToolKind {
fn kind(&self) -> ToolKind {
ToolKind::Execute
}

View File

@@ -10,8 +10,7 @@ use project::{AgentLocation, ImageItem, Project, WorktreeSettings, image_store};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
use std::{path::Path, sync::Arc};
use util::markdown::MarkdownCodeBlock;
use std::sync::Arc;
use crate::{AgentTool, ToolCallEventStream};
@@ -22,7 +21,8 @@ use crate::{AgentTool, ToolCallEventStream};
pub struct ReadFileToolInput {
/// The relative path of the file to read.
///
/// This path should never be absolute, and the first component of the path should always be a root directory in a project.
/// This path should never be absolute, and the first component
/// of the path should always be a root directory in a project.
///
/// <example>
/// If the project has the following root directories:
@@ -34,9 +34,11 @@ pub struct ReadFileToolInput {
/// If you want to access `file.txt` in `directory2`, you should use the path `directory2/file.txt`.
/// </example>
pub path: String,
/// Optional line number to start reading on (1-based index)
#[serde(default)]
pub start_line: Option<u32>,
/// Optional line number to end reading on (1-based index, inclusive)
#[serde(default)]
pub end_line: Option<u32>,
@@ -60,21 +62,36 @@ impl AgentTool for ReadFileTool {
type Input = ReadFileToolInput;
type Output = LanguageModelToolResultContent;
fn name() -> &'static str {
"read_file"
fn name(&self) -> SharedString {
"read_file".into()
}
fn kind() -> acp::ToolKind {
fn kind(&self) -> acp::ToolKind {
acp::ToolKind::Read
}
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
input
.ok()
.as_ref()
.and_then(|input| Path::new(&input.path).file_name())
.map(|file_name| file_name.to_string_lossy().to_string().into())
.unwrap_or_default()
if let Ok(input) = input {
let path = &input.path;
match (input.start_line, input.end_line) {
(Some(start), Some(end)) => {
format!(
"[Read file `{}` (lines {}-{})](@selection:{}:({}-{}))",
path, start, end, path, start, end
)
}
(Some(start), None) => {
format!(
"[Read file `{}` (from line {})](@selection:{}:({}-{}))",
path, start, path, start, start
)
}
_ => format!("[Read file `{}`](@file:{})", path, path),
}
.into()
} else {
"Read file".into()
}
}
fn run(
@@ -158,7 +175,7 @@ impl AgentTool for ReadFileTool {
buffer
.file()
.as_ref()
.is_none_or(|file| !file.disk_state().exists())
.map_or(true, |file| !file.disk_state().exists())
})? {
anyhow::bail!("{file_path} not found");
}
@@ -244,19 +261,6 @@ impl AgentTool for ReadFileTool {
}]),
..Default::default()
});
if let Ok(LanguageModelToolResultContent::Text(text)) = &result {
let markdown = MarkdownCodeBlock {
tag: &input.path,
text,
}
.to_string();
event_stream.update_fields(ToolCallUpdateFields {
content: Some(vec![acp::ToolCallContent::Content {
content: markdown.into(),
}]),
..Default::default()
})
}
}
})?;

View File

@@ -1,19 +1,19 @@
use agent_client_protocol as acp;
use anyhow::Result;
use gpui::{App, Entity, SharedString, Task};
use project::Project;
use futures::{FutureExt as _, future::Shared};
use gpui::{App, AppContext, Entity, SharedString, Task};
use project::{Project, terminals::TerminalKind};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{
path::{Path, PathBuf},
rc::Rc,
sync::Arc,
};
use util::markdown::MarkdownInlineCode;
use util::{ResultExt, get_system_shell, markdown::MarkdownInlineCode};
use crate::{AgentTool, ThreadEnvironment, ToolCallEventStream};
use crate::{AgentTool, ToolCallEventStream};
const COMMAND_OUTPUT_LIMIT: u64 = 16 * 1024;
const COMMAND_OUTPUT_LIMIT: usize = 16 * 1024;
/// Executes a shell one-liner and returns the combined output.
///
@@ -36,14 +36,28 @@ pub struct TerminalToolInput {
pub struct TerminalTool {
project: Entity<Project>,
environment: Rc<dyn ThreadEnvironment>,
determine_shell: Shared<Task<String>>,
}
impl TerminalTool {
pub fn new(project: Entity<Project>, environment: Rc<dyn ThreadEnvironment>) -> Self {
pub fn new(project: Entity<Project>, cx: &mut App) -> Self {
let determine_shell = cx.background_spawn(async move {
if cfg!(windows) {
return get_system_shell();
}
if which::which("bash").is_ok() {
log::info!("agent selected bash for terminal tool");
"bash".into()
} else {
let shell = get_system_shell();
log::info!("agent selected {shell} for terminal tool");
shell
}
});
Self {
project,
environment,
determine_shell: determine_shell.shared(),
}
}
}
@@ -52,11 +66,11 @@ impl AgentTool for TerminalTool {
type Input = TerminalToolInput;
type Output = String;
fn name() -> &'static str {
"terminal"
fn name(&self) -> SharedString {
"terminal".into()
}
fn kind() -> acp::ToolKind {
fn kind(&self) -> acp::ToolKind {
acp::ToolKind::Execute
}
@@ -88,49 +102,128 @@ impl AgentTool for TerminalTool {
event_stream: ToolCallEventStream,
cx: &mut App,
) -> Task<Result<Self::Output>> {
let language_registry = self.project.read(cx).languages().clone();
let working_dir = match working_dir(&input, &self.project, cx) {
Ok(dir) => dir,
Err(err) => return Task::ready(Err(err)),
};
let program = self.determine_shell.clone();
let command = if cfg!(windows) {
format!("$null | & {{{}}}", input.command.replace("\"", "'"))
} else if let Some(cwd) = working_dir
.as_ref()
.and_then(|cwd| cwd.as_os_str().to_str())
{
// Make sure once we're *inside* the shell, we cd into `cwd`
format!("(cd {cwd}; {}) </dev/null", input.command)
} else {
format!("({}) </dev/null", input.command)
};
let args = vec!["-c".into(), command];
let env = match &working_dir {
Some(dir) => self.project.update(cx, |project, cx| {
project.directory_environment(dir.as_path().into(), cx)
}),
None => Task::ready(None).shared(),
};
let env = cx.spawn(async move |_| {
let mut env = env.await.unwrap_or_default();
if cfg!(unix) {
env.insert("PAGER".into(), "cat".into());
}
env
});
let authorize = event_stream.authorize(self.initial_title(Ok(input.clone())), cx);
cx.spawn(async move |cx| {
authorize.await?;
let terminal = self
.environment
.create_terminal(
input.command.clone(),
working_dir,
Some(COMMAND_OUTPUT_LIMIT),
cx,
)
.await?;
cx.spawn({
async move |cx| {
authorize.await?;
let terminal_id = terminal.id(cx)?;
event_stream.update_fields(acp::ToolCallUpdateFields {
content: Some(vec![acp::ToolCallContent::Terminal { terminal_id }]),
..Default::default()
});
let program = program.await;
let env = env.await;
let terminal = self
.project
.update(cx, |project, cx| {
project.create_terminal(
TerminalKind::Task(task::SpawnInTerminal {
command: Some(program),
args,
cwd: working_dir.clone(),
env,
..Default::default()
}),
cx,
)
})?
.await?;
let acp_terminal = cx.new(|cx| {
acp_thread::Terminal::new(
input.command.clone(),
working_dir.clone(),
terminal.clone(),
language_registry,
cx,
)
})?;
event_stream.update_terminal(acp_terminal.clone());
let exit_status = terminal.wait_for_exit(cx)?.await;
let output = terminal.current_output(cx)?;
let exit_status = terminal
.update(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
.await;
let (content, content_line_count) = terminal.read_with(cx, |terminal, _| {
(terminal.get_content(), terminal.total_lines())
})?;
Ok(process_content(output, &input.command, exit_status))
let (processed_content, finished_with_empty_output) = process_content(
&content,
&input.command,
exit_status.map(portable_pty::ExitStatus::from),
);
acp_terminal
.update(cx, |terminal, cx| {
terminal.finish(
exit_status,
content.len(),
processed_content.len(),
content_line_count,
finished_with_empty_output,
cx,
);
})
.log_err();
Ok(processed_content)
}
})
}
}
fn process_content(
output: acp::TerminalOutputResponse,
content: &str,
command: &str,
exit_status: acp::TerminalExitStatus,
) -> String {
let content = output.output.trim();
let is_empty = content.is_empty();
exit_status: Option<portable_pty::ExitStatus>,
) -> (String, bool) {
let should_truncate = content.len() > COMMAND_OUTPUT_LIMIT;
let content = if should_truncate {
let mut end_ix = COMMAND_OUTPUT_LIMIT.min(content.len());
while !content.is_char_boundary(end_ix) {
end_ix -= 1;
}
// Don't truncate mid-line, clear the remainder of the last line
end_ix = content[..end_ix].rfind('\n').unwrap_or(end_ix);
&content[..end_ix]
} else {
content
};
let content = content.trim();
let is_empty = content.is_empty();
let content = format!("```\n{content}\n```");
let content = if output.truncated {
let content = if should_truncate {
format!(
"Command output too long. The first {} bytes:\n\n{content}",
content.len(),
@@ -139,21 +232,24 @@ fn process_content(
content
};
let content = match exit_status.exit_code {
Some(0) => {
let content = match exit_status {
Some(exit_status) if exit_status.success() => {
if is_empty {
"Command executed successfully.".to_string()
} else {
content
content.to_string()
}
}
Some(exit_code) => {
Some(exit_status) => {
if is_empty {
format!("Command \"{command}\" failed with exit code {}.", exit_code)
format!(
"Command \"{command}\" failed with exit code {}.",
exit_status.exit_code()
)
} else {
format!(
"Command \"{command}\" failed with exit code {}.\n\n{content}",
exit_code
exit_status.exit_code()
)
}
}
@@ -164,7 +260,7 @@ fn process_content(
)
}
};
content
(content, is_empty)
}
fn working_dir(
@@ -175,7 +271,7 @@ fn working_dir(
let project = project.read(cx);
let cd = &input.cd;
if cd == "." || cd.is_empty() {
if cd == "." || cd == "" {
// Accept "." or "" as meaning "the one worktree" if we only have one worktree.
let mut worktrees = project.worktrees(cx);
@@ -200,10 +296,178 @@ fn working_dir(
{
return Ok(Some(input_path.into()));
}
} else if let Some(worktree) = project.worktree_for_root_name(cd, cx) {
return Ok(Some(worktree.read(cx).abs_path().to_path_buf()));
} else {
if let Some(worktree) = project.worktree_for_root_name(cd, cx) {
return Ok(Some(worktree.read(cx).abs_path().to_path_buf()));
}
}
anyhow::bail!("`cd` directory {cd:?} was not in any of the project's worktrees.");
}
}
#[cfg(test)]
mod tests {
use agent_settings::AgentSettings;
use editor::EditorSettings;
use fs::RealFs;
use gpui::{BackgroundExecutor, TestAppContext};
use pretty_assertions::assert_eq;
use serde_json::json;
use settings::{Settings, SettingsStore};
use terminal::terminal_settings::TerminalSettings;
use theme::ThemeSettings;
use util::test::TempTree;
use crate::AgentResponseEvent;
use super::*;
fn init_test(executor: &BackgroundExecutor, cx: &mut TestAppContext) {
zlog::init_test();
executor.allow_parking();
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
Project::init_settings(cx);
ThemeSettings::register(cx);
TerminalSettings::register(cx);
EditorSettings::register(cx);
AgentSettings::register(cx);
});
}
#[gpui::test]
async fn test_interactive_command(executor: BackgroundExecutor, cx: &mut TestAppContext) {
if cfg!(windows) {
return;
}
init_test(&executor, cx);
let fs = Arc::new(RealFs::new(None, executor));
let tree = TempTree::new(json!({
"project": {},
}));
let project: Entity<Project> =
Project::test(fs, [tree.path().join("project").as_path()], cx).await;
let input = TerminalToolInput {
command: "cat".to_owned(),
cd: tree
.path()
.join("project")
.as_path()
.to_string_lossy()
.to_string(),
};
let (event_stream_tx, mut event_stream_rx) = ToolCallEventStream::test();
let result = cx
.update(|cx| Arc::new(TerminalTool::new(project, cx)).run(input, event_stream_tx, cx));
let auth = event_stream_rx.expect_authorization().await;
auth.response.send(auth.options[0].id.clone()).unwrap();
event_stream_rx.expect_terminal().await;
assert_eq!(result.await.unwrap(), "Command executed successfully.");
}
#[gpui::test]
async fn test_working_directory(executor: BackgroundExecutor, cx: &mut TestAppContext) {
if cfg!(windows) {
return;
}
init_test(&executor, cx);
let fs = Arc::new(RealFs::new(None, executor));
let tree = TempTree::new(json!({
"project": {},
"other-project": {},
}));
let project: Entity<Project> =
Project::test(fs, [tree.path().join("project").as_path()], cx).await;
let check = |input, expected, cx: &mut TestAppContext| {
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
let result = cx.update(|cx| {
Arc::new(TerminalTool::new(project.clone(), cx)).run(input, stream_tx, cx)
});
cx.run_until_parked();
let event = stream_rx.try_next();
if let Ok(Some(Ok(AgentResponseEvent::ToolCallAuthorization(auth)))) = event {
auth.response.send(auth.options[0].id.clone()).unwrap();
}
cx.spawn(async move |_| {
let output = result.await;
assert_eq!(output.ok(), expected);
})
};
check(
TerminalToolInput {
command: "pwd".into(),
cd: ".".into(),
},
Some(format!(
"```\n{}\n```",
tree.path().join("project").display()
)),
cx,
)
.await;
check(
TerminalToolInput {
command: "pwd".into(),
cd: "other-project".into(),
},
None, // other-project is a dir, but *not* a worktree (yet)
cx,
)
.await;
// Absolute path above the worktree root
check(
TerminalToolInput {
command: "pwd".into(),
cd: tree.path().to_string_lossy().into(),
},
None,
cx,
)
.await;
project
.update(cx, |project, cx| {
project.create_worktree(tree.path().join("other-project"), true, cx)
})
.await
.unwrap();
check(
TerminalToolInput {
command: "pwd".into(),
cd: "other-project".into(),
},
Some(format!(
"```\n{}\n```",
tree.path().join("other-project").display()
)),
cx,
)
.await;
check(
TerminalToolInput {
command: "pwd".into(),
cd: ".".into(),
},
None,
cx,
)
.await;
}
}

View File

@@ -11,7 +11,8 @@ use crate::{AgentTool, ToolCallEventStream};
/// Use this tool when you need to work through complex problems, develop strategies, or outline approaches before taking action.
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ThinkingToolInput {
/// Content to think about. This should be a description of what to think about or a problem to solve.
/// Content to think about. This should be a description of what to think about or
/// a problem to solve.
content: String,
}
@@ -21,11 +22,11 @@ impl AgentTool for ThinkingTool {
type Input = ThinkingToolInput;
type Output = String;
fn name() -> &'static str {
"thinking"
fn name(&self) -> SharedString {
"thinking".into()
}
fn kind() -> acp::ToolKind {
fn kind(&self) -> acp::ToolKind {
acp::ToolKind::Think
}

View File

@@ -14,7 +14,7 @@ use ui::prelude::*;
use web_search::WebSearchRegistry;
/// Search the web for information using your query.
/// Use this when you need real-time information, facts, or data that might not be in your training.
/// Use this when you need real-time information, facts, or data that might not be in your training. \
/// Results will include snippets and links from relevant web pages.
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct WebSearchToolInput {
@@ -40,11 +40,11 @@ impl AgentTool for WebSearchTool {
type Input = WebSearchToolInput;
type Output = WebSearchToolOutput;
fn name() -> &'static str {
"web_search"
fn name(&self) -> SharedString {
"web_search".into()
}
fn kind() -> acp::ToolKind {
fn kind(&self) -> acp::ToolKind {
acp::ToolKind::Fetch
}
@@ -80,48 +80,33 @@ impl AgentTool for WebSearchTool {
}
};
emit_update(&response, &event_stream);
let result_text = if response.results.len() == 1 {
"1 result".to_string()
} else {
format!("{} results", response.results.len())
};
event_stream.update_fields(acp::ToolCallUpdateFields {
title: Some(format!("Searched the web: {result_text}")),
content: Some(
response
.results
.iter()
.map(|result| acp::ToolCallContent::Content {
content: acp::ContentBlock::ResourceLink(acp::ResourceLink {
name: result.title.clone(),
uri: result.url.clone(),
title: Some(result.title.clone()),
description: Some(result.text.clone()),
mime_type: None,
annotations: None,
size: None,
}),
})
.collect(),
),
..Default::default()
});
Ok(WebSearchToolOutput(response))
})
}
fn replay(
&self,
_input: Self::Input,
output: Self::Output,
event_stream: ToolCallEventStream,
_cx: &mut App,
) -> Result<()> {
emit_update(&output.0, &event_stream);
Ok(())
}
}
fn emit_update(response: &WebSearchResponse, event_stream: &ToolCallEventStream) {
let result_text = if response.results.len() == 1 {
"1 result".to_string()
} else {
format!("{} results", response.results.len())
};
event_stream.update_fields(acp::ToolCallUpdateFields {
title: Some(format!("Searched the web: {result_text}")),
content: Some(
response
.results
.iter()
.map(|result| acp::ToolCallContent::Content {
content: acp::ContentBlock::ResourceLink(acp::ResourceLink {
name: result.title.clone(),
uri: result.url.clone(),
title: Some(result.title.clone()),
description: Some(result.text.clone()),
mime_type: None,
annotations: None,
size: None,
}),
})
.collect(),
),
..Default::default()
});
}

View File

@@ -6,7 +6,7 @@ publish.workspace = true
license = "GPL-3.0-or-later"
[features]
test-support = ["acp_thread/test-support", "gpui/test-support", "project/test-support", "dep:env_logger", "client/test-support", "dep:gpui_tokio", "reqwest_client/test-support"]
test-support = ["acp_thread/test-support", "gpui/test-support", "project/test-support"]
e2e = []
[lints]
@@ -17,38 +17,34 @@ path = "src/agent_servers.rs"
doctest = false
[dependencies]
acp_tools.workspace = true
acp_thread.workspace = true
action_log.workspace = true
agent-client-protocol.workspace = true
agent_settings.workspace = true
agentic-coding-protocol.workspace = true
anyhow.workspace = true
client = { workspace = true, optional = true }
collections.workspace = true
env_logger = { workspace = true, optional = true }
fs.workspace = true
context_server.workspace = true
futures.workspace = true
gpui.workspace = true
gpui_tokio = { workspace = true, optional = true }
indoc.workspace = true
language.workspace = true
itertools.workspace = true
language_model.workspace = true
language_models.workspace = true
log.workspace = true
node_runtime.workspace = true
paths.workspace = true
project.workspace = true
reqwest_client = { workspace = true, optional = true }
rand.workspace = true
schemars.workspace = true
semver.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
smol.workspace = true
strum.workspace = true
tempfile.workspace = true
thiserror.workspace = true
ui.workspace = true
util.workspace = true
uuid.workspace = true
watch.workspace = true
which.workspace = true
workspace-hack.workspace = true
@@ -58,12 +54,8 @@ libc.workspace = true
nix.workspace = true
[dev-dependencies]
client = { workspace = true, features = ["test-support"] }
env_logger.workspace = true
fs.workspace = true
language.workspace = true
indoc.workspace = true
acp_thread = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
gpui_tokio.workspace = true
reqwest_client = { workspace = true, features = ["test-support"] }

View File

@@ -1,483 +1,34 @@
use std::{path::Path, rc::Rc};
use crate::AgentServerCommand;
use acp_thread::AgentConnection;
use acp_tools::AcpConnectionRegistry;
use action_log::ActionLog;
use agent_client_protocol::{self as acp, Agent as _, ErrorCode};
use anyhow::anyhow;
use collections::HashMap;
use futures::AsyncBufReadExt as _;
use futures::io::BufReader;
use project::Project;
use serde::Deserialize;
use std::{any::Any, cell::RefCell};
use std::{path::Path, rc::Rc};
use anyhow::Result;
use gpui::AsyncApp;
use thiserror::Error;
use anyhow::{Context as _, Result};
use gpui::{App, AppContext as _, AsyncApp, Entity, SharedString, Task, WeakEntity};
use acp_thread::{AcpThread, AuthRequired, LoadError};
mod v0;
mod v1;
#[derive(Debug, Error)]
#[error("Unsupported version")]
pub struct UnsupportedVersion;
pub struct AcpConnection {
server_name: SharedString,
connection: Rc<acp::ClientSideConnection>,
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
auth_methods: Vec<acp::AuthMethod>,
agent_capabilities: acp::AgentCapabilities,
_io_task: Task<Result<()>>,
_wait_task: Task<Result<()>>,
_stderr_task: Task<Result<()>>,
}
pub struct AcpSession {
thread: WeakEntity<AcpThread>,
suppress_abort_err: bool,
}
pub async fn connect(
server_name: SharedString,
server_name: &'static str,
command: AgentServerCommand,
root_dir: &Path,
cx: &mut AsyncApp,
) -> Result<Rc<dyn AgentConnection>> {
let conn = AcpConnection::stdio(server_name, command.clone(), root_dir, cx).await?;
Ok(Rc::new(conn) as _)
}
let conn = v1::AcpConnection::stdio(server_name, command.clone(), root_dir, cx).await;
const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::V1;
impl AcpConnection {
pub async fn stdio(
server_name: SharedString,
command: AgentServerCommand,
root_dir: &Path,
cx: &mut AsyncApp,
) -> Result<Self> {
let mut child = util::command::new_smol_command(command.path)
.args(command.args.iter().map(|arg| arg.as_str()))
.envs(command.env.iter().flatten())
.current_dir(root_dir)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.kill_on_drop(true)
.spawn()?;
let stdout = child.stdout.take().context("Failed to take stdout")?;
let stdin = child.stdin.take().context("Failed to take stdin")?;
let stderr = child.stderr.take().context("Failed to take stderr")?;
log::trace!("Spawned (pid: {})", child.id());
let sessions = Rc::new(RefCell::new(HashMap::default()));
let client = ClientDelegate {
sessions: sessions.clone(),
cx: cx.clone(),
};
let (connection, io_task) = acp::ClientSideConnection::new(client, stdin, stdout, {
let foreground_executor = cx.foreground_executor().clone();
move |fut| {
foreground_executor.spawn(fut).detach();
}
});
let io_task = cx.background_spawn(io_task);
let stderr_task = cx.background_spawn(async move {
let mut stderr = BufReader::new(stderr);
let mut line = String::new();
while let Ok(n) = stderr.read_line(&mut line).await
&& n > 0
{
log::warn!("agent stderr: {}", &line);
line.clear();
}
Ok(())
});
let wait_task = cx.spawn({
let sessions = sessions.clone();
async move |cx| {
let status = child.status().await?;
for session in sessions.borrow().values() {
session
.thread
.update(cx, |thread, cx| {
thread.emit_load_error(LoadError::Exited { status }, cx)
})
.ok();
}
anyhow::Ok(())
}
});
let connection = Rc::new(connection);
cx.update(|cx| {
AcpConnectionRegistry::default_global(cx).update(cx, |registry, cx| {
registry.set_active_connection(server_name.clone(), &connection, cx)
});
})?;
let response = connection
.initialize(acp::InitializeRequest {
protocol_version: acp::VERSION,
client_capabilities: acp::ClientCapabilities {
fs: acp::FileSystemCapability {
read_text_file: true,
write_text_file: true,
},
terminal: true,
},
})
.await?;
if response.protocol_version < MINIMUM_SUPPORTED_VERSION {
return Err(UnsupportedVersion.into());
match conn {
Ok(conn) => Ok(Rc::new(conn) as _),
Err(err) if err.is::<UnsupportedVersion>() => {
// Consider re-using initialize response and subprocess when adding another version here
let conn: Rc<dyn AgentConnection> =
Rc::new(v0::AcpConnection::stdio(server_name, command, root_dir, cx).await?);
Ok(conn)
}
Ok(Self {
auth_methods: response.auth_methods,
connection,
server_name,
sessions,
agent_capabilities: response.agent_capabilities,
_io_task: io_task,
_wait_task: wait_task,
_stderr_task: stderr_task,
})
}
pub fn prompt_capabilities(&self) -> &acp::PromptCapabilities {
&self.agent_capabilities.prompt_capabilities
}
}
impl AgentConnection for AcpConnection {
fn new_thread(
self: Rc<Self>,
project: Entity<Project>,
cwd: &Path,
cx: &mut App,
) -> Task<Result<Entity<AcpThread>>> {
let conn = self.connection.clone();
let sessions = self.sessions.clone();
let cwd = cwd.to_path_buf();
let context_server_store = project.read(cx).context_server_store().read(cx);
let mcp_servers = context_server_store
.configured_server_ids()
.iter()
.filter_map(|id| {
let configuration = context_server_store.configuration_for_server(id)?;
let command = configuration.command();
Some(acp::McpServer {
name: id.0.to_string(),
command: command.path.clone(),
args: command.args.clone(),
env: if let Some(env) = command.env.as_ref() {
env.iter()
.map(|(name, value)| acp::EnvVariable {
name: name.clone(),
value: value.clone(),
})
.collect()
} else {
vec![]
},
})
})
.collect();
cx.spawn(async move |cx| {
let response = conn
.new_session(acp::NewSessionRequest { mcp_servers, cwd })
.await
.map_err(|err| {
if err.code == acp::ErrorCode::AUTH_REQUIRED.code {
let mut error = AuthRequired::new();
if err.message != acp::ErrorCode::AUTH_REQUIRED.message {
error = error.with_description(err.message);
}
anyhow!(error)
} else {
anyhow!(err)
}
})?;
let session_id = response.session_id;
let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
let thread = cx.new(|cx| {
AcpThread::new(
self.server_name.clone(),
self.clone(),
project,
action_log,
session_id.clone(),
// ACP doesn't currently support per-session prompt capabilities or changing capabilities dynamically.
watch::Receiver::constant(self.agent_capabilities.prompt_capabilities),
response.available_commands,
cx,
)
})?;
let session = AcpSession {
thread: thread.downgrade(),
suppress_abort_err: false,
};
sessions.borrow_mut().insert(session_id, session);
Ok(thread)
})
}
fn auth_methods(&self) -> &[acp::AuthMethod] {
&self.auth_methods
}
fn authenticate(&self, method_id: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>> {
let conn = self.connection.clone();
cx.foreground_executor().spawn(async move {
let result = conn
.authenticate(acp::AuthenticateRequest {
method_id: method_id.clone(),
})
.await?;
Ok(result)
})
}
fn prompt(
&self,
_id: Option<acp_thread::UserMessageId>,
params: acp::PromptRequest,
cx: &mut App,
) -> Task<Result<acp::PromptResponse>> {
let conn = self.connection.clone();
let sessions = self.sessions.clone();
let session_id = params.session_id.clone();
cx.foreground_executor().spawn(async move {
let result = conn.prompt(params).await;
let mut suppress_abort_err = false;
if let Some(session) = sessions.borrow_mut().get_mut(&session_id) {
suppress_abort_err = session.suppress_abort_err;
session.suppress_abort_err = false;
}
match result {
Ok(response) => Ok(response),
Err(err) => {
if err.code != ErrorCode::INTERNAL_ERROR.code {
anyhow::bail!(err)
}
let Some(data) = &err.data else {
anyhow::bail!(err)
};
// Temporary workaround until the following PR is generally available:
// https://github.com/google-gemini/gemini-cli/pull/6656
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct ErrorDetails {
details: Box<str>,
}
match serde_json::from_value(data.clone()) {
Ok(ErrorDetails { details }) => {
if suppress_abort_err
&& (details.contains("This operation was aborted")
|| details.contains("The user aborted a request"))
{
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::Cancelled,
})
} else {
Err(anyhow!(details))
}
}
Err(_) => Err(anyhow!(err)),
}
}
}
})
}
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
if let Some(session) = self.sessions.borrow_mut().get_mut(session_id) {
session.suppress_abort_err = true;
}
let conn = self.connection.clone();
let params = acp::CancelNotification {
session_id: session_id.clone(),
};
cx.foreground_executor()
.spawn(async move { conn.cancel(params).await })
.detach();
}
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
self
}
}
struct ClientDelegate {
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
cx: AsyncApp,
}
impl acp::Client for ClientDelegate {
async fn request_permission(
&self,
arguments: acp::RequestPermissionRequest,
) -> Result<acp::RequestPermissionResponse, acp::Error> {
let cx = &mut self.cx.clone();
let task = self
.session_thread(&arguments.session_id)?
.update(cx, |thread, cx| {
thread.request_tool_call_authorization(arguments.tool_call, arguments.options, cx)
})??;
let outcome = task.await;
Ok(acp::RequestPermissionResponse { outcome })
}
async fn write_text_file(
&self,
arguments: acp::WriteTextFileRequest,
) -> Result<(), acp::Error> {
let cx = &mut self.cx.clone();
let task = self
.session_thread(&arguments.session_id)?
.update(cx, |thread, cx| {
thread.write_text_file(arguments.path, arguments.content, cx)
})?;
task.await?;
Ok(())
}
async fn read_text_file(
&self,
arguments: acp::ReadTextFileRequest,
) -> Result<acp::ReadTextFileResponse, acp::Error> {
let task = self.session_thread(&arguments.session_id)?.update(
&mut self.cx.clone(),
|thread, cx| {
thread.read_text_file(arguments.path, arguments.line, arguments.limit, false, cx)
},
)?;
let content = task.await?;
Ok(acp::ReadTextFileResponse { content })
}
async fn session_notification(
&self,
notification: acp::SessionNotification,
) -> Result<(), acp::Error> {
self.session_thread(&notification.session_id)?
.update(&mut self.cx.clone(), |thread, cx| {
thread.handle_session_update(notification.update, cx)
})??;
Ok(())
}
async fn create_terminal(
&self,
args: acp::CreateTerminalRequest,
) -> Result<acp::CreateTerminalResponse, acp::Error> {
let terminal = self
.session_thread(&args.session_id)?
.update(&mut self.cx.clone(), |thread, cx| {
thread.create_terminal(
args.command,
args.args,
args.env,
args.cwd,
args.output_byte_limit,
cx,
)
})?
.await?;
Ok(
terminal.read_with(&self.cx, |terminal, _| acp::CreateTerminalResponse {
terminal_id: terminal.id().clone(),
})?,
)
}
async fn kill_terminal(&self, args: acp::KillTerminalRequest) -> Result<(), acp::Error> {
self.session_thread(&args.session_id)?
.update(&mut self.cx.clone(), |thread, cx| {
thread.kill_terminal(args.terminal_id, cx)
})??;
Ok(())
}
async fn release_terminal(&self, args: acp::ReleaseTerminalRequest) -> Result<(), acp::Error> {
self.session_thread(&args.session_id)?
.update(&mut self.cx.clone(), |thread, cx| {
thread.release_terminal(args.terminal_id, cx)
})??;
Ok(())
}
async fn terminal_output(
&self,
args: acp::TerminalOutputRequest,
) -> Result<acp::TerminalOutputResponse, acp::Error> {
self.session_thread(&args.session_id)?
.read_with(&mut self.cx.clone(), |thread, cx| {
let out = thread
.terminal(args.terminal_id)?
.read(cx)
.current_output(cx);
Ok(out)
})?
}
async fn wait_for_terminal_exit(
&self,
args: acp::WaitForTerminalExitRequest,
) -> Result<acp::WaitForTerminalExitResponse, acp::Error> {
let exit_status = self
.session_thread(&args.session_id)?
.update(&mut self.cx.clone(), |thread, cx| {
anyhow::Ok(thread.terminal(args.terminal_id)?.read(cx).wait_for_exit())
})??
.await;
Ok(acp::WaitForTerminalExitResponse { exit_status })
}
}
impl ClientDelegate {
fn session_thread(&self, session_id: &acp::SessionId) -> Result<WeakEntity<AcpThread>> {
let sessions = self.sessions.borrow();
sessions
.get(session_id)
.context("Failed to get session")
.map(|session| session.thread.clone())
Err(err) => Err(err),
}
}

View File

@@ -0,0 +1,514 @@
// Translates old acp agents into the new schema
use agent_client_protocol as acp;
use agentic_coding_protocol::{self as acp_old, AgentRequest as _};
use anyhow::{Context as _, Result, anyhow};
use futures::channel::oneshot;
use gpui::{AppContext as _, AsyncApp, Entity, Task, WeakEntity};
use project::Project;
use std::{any::Any, cell::RefCell, path::Path, rc::Rc};
use ui::App;
use util::ResultExt as _;
use crate::AgentServerCommand;
use acp_thread::{AcpThread, AgentConnection, AuthRequired};
#[derive(Clone)]
struct OldAcpClientDelegate {
thread: Rc<RefCell<WeakEntity<AcpThread>>>,
cx: AsyncApp,
next_tool_call_id: Rc<RefCell<u64>>,
// sent_buffer_versions: HashMap<Entity<Buffer>, HashMap<u64, BufferSnapshot>>,
}
impl OldAcpClientDelegate {
fn new(thread: Rc<RefCell<WeakEntity<AcpThread>>>, cx: AsyncApp) -> Self {
Self {
thread,
cx,
next_tool_call_id: Rc::new(RefCell::new(0)),
}
}
}
impl acp_old::Client for OldAcpClientDelegate {
async fn stream_assistant_message_chunk(
&self,
params: acp_old::StreamAssistantMessageChunkParams,
) -> Result<(), acp_old::Error> {
let cx = &mut self.cx.clone();
cx.update(|cx| {
self.thread
.borrow()
.update(cx, |thread, cx| match params.chunk {
acp_old::AssistantMessageChunk::Text { text } => {
thread.push_assistant_content_block(text.into(), false, cx)
}
acp_old::AssistantMessageChunk::Thought { thought } => {
thread.push_assistant_content_block(thought.into(), true, cx)
}
})
.log_err();
})?;
Ok(())
}
async fn request_tool_call_confirmation(
&self,
request: acp_old::RequestToolCallConfirmationParams,
) -> Result<acp_old::RequestToolCallConfirmationResponse, acp_old::Error> {
let cx = &mut self.cx.clone();
let old_acp_id = *self.next_tool_call_id.borrow() + 1;
self.next_tool_call_id.replace(old_acp_id);
let tool_call = into_new_tool_call(
acp::ToolCallId(old_acp_id.to_string().into()),
request.tool_call,
);
let mut options = match request.confirmation {
acp_old::ToolCallConfirmation::Edit { .. } => vec![(
acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
acp::PermissionOptionKind::AllowAlways,
"Always Allow Edits".to_string(),
)],
acp_old::ToolCallConfirmation::Execute { root_command, .. } => vec![(
acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
acp::PermissionOptionKind::AllowAlways,
format!("Always Allow {}", root_command),
)],
acp_old::ToolCallConfirmation::Mcp {
server_name,
tool_name,
..
} => vec![
(
acp_old::ToolCallConfirmationOutcome::AlwaysAllowMcpServer,
acp::PermissionOptionKind::AllowAlways,
format!("Always Allow {}", server_name),
),
(
acp_old::ToolCallConfirmationOutcome::AlwaysAllowTool,
acp::PermissionOptionKind::AllowAlways,
format!("Always Allow {}", tool_name),
),
],
acp_old::ToolCallConfirmation::Fetch { .. } => vec![(
acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
acp::PermissionOptionKind::AllowAlways,
"Always Allow".to_string(),
)],
acp_old::ToolCallConfirmation::Other { .. } => vec![(
acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
acp::PermissionOptionKind::AllowAlways,
"Always Allow".to_string(),
)],
};
options.extend([
(
acp_old::ToolCallConfirmationOutcome::Allow,
acp::PermissionOptionKind::AllowOnce,
"Allow".to_string(),
),
(
acp_old::ToolCallConfirmationOutcome::Reject,
acp::PermissionOptionKind::RejectOnce,
"Reject".to_string(),
),
]);
let mut outcomes = Vec::with_capacity(options.len());
let mut acp_options = Vec::with_capacity(options.len());
for (index, (outcome, kind, label)) in options.into_iter().enumerate() {
outcomes.push(outcome);
acp_options.push(acp::PermissionOption {
id: acp::PermissionOptionId(index.to_string().into()),
name: label,
kind,
})
}
let response = cx
.update(|cx| {
self.thread.borrow().update(cx, |thread, cx| {
thread.request_tool_call_authorization(tool_call.into(), acp_options, cx)
})
})??
.context("Failed to update thread")?
.await;
let outcome = match response {
Ok(option_id) => outcomes[option_id.0.parse::<usize>().unwrap_or(0)],
Err(oneshot::Canceled) => acp_old::ToolCallConfirmationOutcome::Cancel,
};
Ok(acp_old::RequestToolCallConfirmationResponse {
id: acp_old::ToolCallId(old_acp_id),
outcome: outcome,
})
}
async fn push_tool_call(
&self,
request: acp_old::PushToolCallParams,
) -> Result<acp_old::PushToolCallResponse, acp_old::Error> {
let cx = &mut self.cx.clone();
let old_acp_id = *self.next_tool_call_id.borrow() + 1;
self.next_tool_call_id.replace(old_acp_id);
cx.update(|cx| {
self.thread.borrow().update(cx, |thread, cx| {
thread.upsert_tool_call(
into_new_tool_call(acp::ToolCallId(old_acp_id.to_string().into()), request),
cx,
)
})
})??
.context("Failed to update thread")?;
Ok(acp_old::PushToolCallResponse {
id: acp_old::ToolCallId(old_acp_id),
})
}
async fn update_tool_call(
&self,
request: acp_old::UpdateToolCallParams,
) -> Result<(), acp_old::Error> {
let cx = &mut self.cx.clone();
cx.update(|cx| {
self.thread.borrow().update(cx, |thread, cx| {
thread.update_tool_call(
acp::ToolCallUpdate {
id: acp::ToolCallId(request.tool_call_id.0.to_string().into()),
fields: acp::ToolCallUpdateFields {
status: Some(into_new_tool_call_status(request.status)),
content: Some(
request
.content
.into_iter()
.map(into_new_tool_call_content)
.collect::<Vec<_>>(),
),
..Default::default()
},
},
cx,
)
})
})?
.context("Failed to update thread")??;
Ok(())
}
async fn update_plan(&self, request: acp_old::UpdatePlanParams) -> Result<(), acp_old::Error> {
let cx = &mut self.cx.clone();
cx.update(|cx| {
self.thread.borrow().update(cx, |thread, cx| {
thread.update_plan(
acp::Plan {
entries: request
.entries
.into_iter()
.map(into_new_plan_entry)
.collect(),
},
cx,
)
})
})?
.context("Failed to update thread")?;
Ok(())
}
async fn read_text_file(
&self,
acp_old::ReadTextFileParams { path, line, limit }: acp_old::ReadTextFileParams,
) -> Result<acp_old::ReadTextFileResponse, acp_old::Error> {
let content = self
.cx
.update(|cx| {
self.thread.borrow().update(cx, |thread, cx| {
thread.read_text_file(path, line, limit, false, cx)
})
})?
.context("Failed to update thread")?
.await?;
Ok(acp_old::ReadTextFileResponse { content })
}
async fn write_text_file(
&self,
acp_old::WriteTextFileParams { path, content }: acp_old::WriteTextFileParams,
) -> Result<(), acp_old::Error> {
self.cx
.update(|cx| {
self.thread
.borrow()
.update(cx, |thread, cx| thread.write_text_file(path, content, cx))
})?
.context("Failed to update thread")?
.await?;
Ok(())
}
}
fn into_new_tool_call(id: acp::ToolCallId, request: acp_old::PushToolCallParams) -> acp::ToolCall {
acp::ToolCall {
id: id,
title: request.label,
kind: acp_kind_from_old_icon(request.icon),
status: acp::ToolCallStatus::InProgress,
content: request
.content
.into_iter()
.map(into_new_tool_call_content)
.collect(),
locations: request
.locations
.into_iter()
.map(into_new_tool_call_location)
.collect(),
raw_input: None,
raw_output: None,
}
}
fn acp_kind_from_old_icon(icon: acp_old::Icon) -> acp::ToolKind {
match icon {
acp_old::Icon::FileSearch => acp::ToolKind::Search,
acp_old::Icon::Folder => acp::ToolKind::Search,
acp_old::Icon::Globe => acp::ToolKind::Search,
acp_old::Icon::Hammer => acp::ToolKind::Other,
acp_old::Icon::LightBulb => acp::ToolKind::Think,
acp_old::Icon::Pencil => acp::ToolKind::Edit,
acp_old::Icon::Regex => acp::ToolKind::Search,
acp_old::Icon::Terminal => acp::ToolKind::Execute,
}
}
fn into_new_tool_call_status(status: acp_old::ToolCallStatus) -> acp::ToolCallStatus {
match status {
acp_old::ToolCallStatus::Running => acp::ToolCallStatus::InProgress,
acp_old::ToolCallStatus::Finished => acp::ToolCallStatus::Completed,
acp_old::ToolCallStatus::Error => acp::ToolCallStatus::Failed,
}
}
fn into_new_tool_call_content(content: acp_old::ToolCallContent) -> acp::ToolCallContent {
match content {
acp_old::ToolCallContent::Markdown { markdown } => markdown.into(),
acp_old::ToolCallContent::Diff { diff } => acp::ToolCallContent::Diff {
diff: into_new_diff(diff),
},
}
}
fn into_new_diff(diff: acp_old::Diff) -> acp::Diff {
acp::Diff {
path: diff.path,
old_text: diff.old_text,
new_text: diff.new_text,
}
}
fn into_new_tool_call_location(location: acp_old::ToolCallLocation) -> acp::ToolCallLocation {
acp::ToolCallLocation {
path: location.path,
line: location.line,
}
}
fn into_new_plan_entry(entry: acp_old::PlanEntry) -> acp::PlanEntry {
acp::PlanEntry {
content: entry.content,
priority: into_new_plan_priority(entry.priority),
status: into_new_plan_status(entry.status),
}
}
fn into_new_plan_priority(priority: acp_old::PlanEntryPriority) -> acp::PlanEntryPriority {
match priority {
acp_old::PlanEntryPriority::Low => acp::PlanEntryPriority::Low,
acp_old::PlanEntryPriority::Medium => acp::PlanEntryPriority::Medium,
acp_old::PlanEntryPriority::High => acp::PlanEntryPriority::High,
}
}
fn into_new_plan_status(status: acp_old::PlanEntryStatus) -> acp::PlanEntryStatus {
match status {
acp_old::PlanEntryStatus::Pending => acp::PlanEntryStatus::Pending,
acp_old::PlanEntryStatus::InProgress => acp::PlanEntryStatus::InProgress,
acp_old::PlanEntryStatus::Completed => acp::PlanEntryStatus::Completed,
}
}
pub struct AcpConnection {
pub name: &'static str,
pub connection: acp_old::AgentConnection,
pub _child_status: Task<Result<()>>,
pub current_thread: Rc<RefCell<WeakEntity<AcpThread>>>,
}
impl AcpConnection {
pub fn stdio(
name: &'static str,
command: AgentServerCommand,
root_dir: &Path,
cx: &mut AsyncApp,
) -> Task<Result<Self>> {
let root_dir = root_dir.to_path_buf();
cx.spawn(async move |cx| {
let mut child = util::command::new_smol_command(&command.path)
.args(command.args.iter())
.current_dir(root_dir)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::inherit())
.kill_on_drop(true)
.spawn()?;
let stdin = child.stdin.take().unwrap();
let stdout = child.stdout.take().unwrap();
log::trace!("Spawned (pid: {})", child.id());
let foreground_executor = cx.foreground_executor().clone();
let thread_rc = Rc::new(RefCell::new(WeakEntity::new_invalid()));
let (connection, io_fut) = acp_old::AgentConnection::connect_to_agent(
OldAcpClientDelegate::new(thread_rc.clone(), cx.clone()),
stdin,
stdout,
move |fut| foreground_executor.spawn(fut).detach(),
);
let io_task = cx.background_spawn(async move {
io_fut.await.log_err();
});
let child_status = cx.background_spawn(async move {
let result = match child.status().await {
Err(e) => Err(anyhow!(e)),
Ok(result) if result.success() => Ok(()),
Ok(result) => Err(anyhow!(result)),
};
drop(io_task);
result
});
Ok(Self {
name,
connection,
_child_status: child_status,
current_thread: thread_rc,
})
})
}
}
impl AgentConnection for AcpConnection {
fn new_thread(
self: Rc<Self>,
project: Entity<Project>,
_cwd: &Path,
cx: &mut App,
) -> Task<Result<Entity<AcpThread>>> {
let task = self.connection.request_any(
acp_old::InitializeParams {
protocol_version: acp_old::ProtocolVersion::latest(),
}
.into_any(),
);
let current_thread = self.current_thread.clone();
cx.spawn(async move |cx| {
let result = task.await?;
let result = acp_old::InitializeParams::response_from_any(result)?;
if !result.is_authenticated {
anyhow::bail!(AuthRequired::new())
}
cx.update(|cx| {
let thread = cx.new(|cx| {
let session_id = acp::SessionId("acp-old-no-id".into());
AcpThread::new(self.name, self.clone(), project, session_id, cx)
});
current_thread.replace(thread.downgrade());
thread
})
})
}
fn auth_methods(&self) -> &[acp::AuthMethod] {
&[]
}
fn authenticate(&self, _method_id: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>> {
let task = self
.connection
.request_any(acp_old::AuthenticateParams.into_any());
cx.foreground_executor().spawn(async move {
task.await?;
Ok(())
})
}
fn prompt(
&self,
_id: Option<acp_thread::UserMessageId>,
params: acp::PromptRequest,
cx: &mut App,
) -> Task<Result<acp::PromptResponse>> {
let chunks = params
.prompt
.into_iter()
.filter_map(|block| match block {
acp::ContentBlock::Text(text) => {
Some(acp_old::UserMessageChunk::Text { text: text.text })
}
acp::ContentBlock::ResourceLink(link) => Some(acp_old::UserMessageChunk::Path {
path: link.uri.into(),
}),
_ => None,
})
.collect();
let task = self
.connection
.request_any(acp_old::SendUserMessageParams { chunks }.into_any());
cx.foreground_executor().spawn(async move {
task.await?;
anyhow::Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
})
})
}
fn cancel(&self, _session_id: &acp::SessionId, cx: &mut App) {
let task = self
.connection
.request_any(acp_old::CancelSendMessageParams.into_any());
cx.foreground_executor()
.spawn(async move {
task.await?;
anyhow::Ok(())
})
.detach_and_log_err(cx)
}
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
self
}
}

View File

@@ -0,0 +1,308 @@
use agent_client_protocol::{self as acp, Agent as _};
use anyhow::anyhow;
use collections::HashMap;
use futures::AsyncBufReadExt as _;
use futures::channel::oneshot;
use futures::io::BufReader;
use project::Project;
use std::path::Path;
use std::rc::Rc;
use std::{any::Any, cell::RefCell};
use anyhow::{Context as _, Result};
use gpui::{App, AppContext as _, AsyncApp, Entity, Task, WeakEntity};
use crate::{AgentServerCommand, acp::UnsupportedVersion};
use acp_thread::{AcpThread, AgentConnection, AuthRequired};
pub struct AcpConnection {
server_name: &'static str,
connection: Rc<acp::ClientSideConnection>,
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
auth_methods: Vec<acp::AuthMethod>,
_io_task: Task<Result<()>>,
}
pub struct AcpSession {
thread: WeakEntity<AcpThread>,
}
const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::V1;
impl AcpConnection {
pub async fn stdio(
server_name: &'static str,
command: AgentServerCommand,
root_dir: &Path,
cx: &mut AsyncApp,
) -> Result<Self> {
let mut child = util::command::new_smol_command(&command.path)
.args(command.args.iter().map(|arg| arg.as_str()))
.envs(command.env.iter().flatten())
.current_dir(root_dir)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.kill_on_drop(true)
.spawn()?;
let stdout = child.stdout.take().context("Failed to take stdout")?;
let stdin = child.stdin.take().context("Failed to take stdin")?;
let stderr = child.stderr.take().context("Failed to take stderr")?;
log::trace!("Spawned (pid: {})", child.id());
let sessions = Rc::new(RefCell::new(HashMap::default()));
let client = ClientDelegate {
sessions: sessions.clone(),
cx: cx.clone(),
};
let (connection, io_task) = acp::ClientSideConnection::new(client, stdin, stdout, {
let foreground_executor = cx.foreground_executor().clone();
move |fut| {
foreground_executor.spawn(fut).detach();
}
});
let io_task = cx.background_spawn(io_task);
cx.background_spawn(async move {
let mut stderr = BufReader::new(stderr);
let mut line = String::new();
while let Ok(n) = stderr.read_line(&mut line).await
&& n > 0
{
log::warn!("agent stderr: {}", &line);
line.clear();
}
})
.detach();
cx.spawn({
let sessions = sessions.clone();
async move |cx| {
let status = child.status().await?;
for session in sessions.borrow().values() {
session
.thread
.update(cx, |thread, cx| thread.emit_server_exited(status, cx))
.ok();
}
anyhow::Ok(())
}
})
.detach();
let response = connection
.initialize(acp::InitializeRequest {
protocol_version: acp::VERSION,
client_capabilities: acp::ClientCapabilities {
fs: acp::FileSystemCapability {
read_text_file: true,
write_text_file: true,
},
},
})
.await?;
if response.protocol_version < MINIMUM_SUPPORTED_VERSION {
return Err(UnsupportedVersion.into());
}
Ok(Self {
auth_methods: response.auth_methods,
connection: connection.into(),
server_name,
sessions,
_io_task: io_task,
})
}
}
impl AgentConnection for AcpConnection {
fn new_thread(
self: Rc<Self>,
project: Entity<Project>,
cwd: &Path,
cx: &mut App,
) -> Task<Result<Entity<AcpThread>>> {
let conn = self.connection.clone();
let sessions = self.sessions.clone();
let cwd = cwd.to_path_buf();
cx.spawn(async move |cx| {
let response = conn
.new_session(acp::NewSessionRequest {
mcp_servers: vec![],
cwd,
})
.await
.map_err(|err| {
if err.code == acp::ErrorCode::AUTH_REQUIRED.code {
let mut error = AuthRequired::new();
if err.message != acp::ErrorCode::AUTH_REQUIRED.message {
error = error.with_description(err.message);
}
anyhow!(error)
} else {
anyhow!(err)
}
})?;
let session_id = response.session_id;
let thread = cx.new(|cx| {
AcpThread::new(
self.server_name,
self.clone(),
project,
session_id.clone(),
cx,
)
})?;
let session = AcpSession {
thread: thread.downgrade(),
};
sessions.borrow_mut().insert(session_id, session);
Ok(thread)
})
}
fn auth_methods(&self) -> &[acp::AuthMethod] {
&self.auth_methods
}
fn authenticate(&self, method_id: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>> {
let conn = self.connection.clone();
cx.foreground_executor().spawn(async move {
let result = conn
.authenticate(acp::AuthenticateRequest {
method_id: method_id.clone(),
})
.await?;
Ok(result)
})
}
fn prompt(
&self,
_id: Option<acp_thread::UserMessageId>,
params: acp::PromptRequest,
cx: &mut App,
) -> Task<Result<acp::PromptResponse>> {
let conn = self.connection.clone();
cx.foreground_executor().spawn(async move {
let response = conn.prompt(params).await?;
Ok(response)
})
}
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
let conn = self.connection.clone();
let params = acp::CancelNotification {
session_id: session_id.clone(),
};
cx.foreground_executor()
.spawn(async move { conn.cancel(params).await })
.detach();
}
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
self
}
}
struct ClientDelegate {
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
cx: AsyncApp,
}
impl acp::Client for ClientDelegate {
async fn request_permission(
&self,
arguments: acp::RequestPermissionRequest,
) -> Result<acp::RequestPermissionResponse, acp::Error> {
let cx = &mut self.cx.clone();
let rx = self
.sessions
.borrow()
.get(&arguments.session_id)
.context("Failed to get session")?
.thread
.update(cx, |thread, cx| {
thread.request_tool_call_authorization(arguments.tool_call, arguments.options, cx)
})?;
let result = rx?.await;
let outcome = match result {
Ok(option) => acp::RequestPermissionOutcome::Selected { option_id: option },
Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Canceled,
};
Ok(acp::RequestPermissionResponse { outcome })
}
async fn write_text_file(
&self,
arguments: acp::WriteTextFileRequest,
) -> Result<(), acp::Error> {
let cx = &mut self.cx.clone();
let task = self
.sessions
.borrow()
.get(&arguments.session_id)
.context("Failed to get session")?
.thread
.update(cx, |thread, cx| {
thread.write_text_file(arguments.path, arguments.content, cx)
})?;
task.await?;
Ok(())
}
async fn read_text_file(
&self,
arguments: acp::ReadTextFileRequest,
) -> Result<acp::ReadTextFileResponse, acp::Error> {
let cx = &mut self.cx.clone();
let task = self
.sessions
.borrow()
.get(&arguments.session_id)
.context("Failed to get session")?
.thread
.update(cx, |thread, cx| {
thread.read_text_file(arguments.path, arguments.line, arguments.limit, false, cx)
})?;
let content = task.await?;
Ok(acp::ReadTextFileResponse { content })
}
async fn session_notification(
&self,
notification: acp::SessionNotification,
) -> Result<(), acp::Error> {
let cx = &mut self.cx.clone();
let sessions = self.sessions.borrow();
let session = sessions
.get(&notification.session_id)
.context("Failed to get session")?;
session.thread.update(cx, |thread, cx| {
thread.handle_session_update(notification.update, cx)
})??;
Ok(())
}
}

View File

@@ -1,35 +1,22 @@
mod acp;
mod claude;
mod custom;
mod gemini;
mod settings;
#[cfg(any(test, feature = "test-support"))]
pub mod e2e_tests;
#[cfg(test)]
mod e2e_tests;
use anyhow::Context as _;
pub use claude::*;
pub use custom::*;
use fs::Fs;
use fs::RemoveOptions;
use fs::RenameOptions;
use futures::StreamExt as _;
pub use gemini::*;
use gpui::AppContext;
use node_runtime::NodeRuntime;
pub use settings::*;
use acp_thread::AgentConnection;
use acp_thread::LoadError;
use anyhow::Result;
use anyhow::anyhow;
use collections::HashMap;
use gpui::{App, AsyncApp, Entity, SharedString, Task};
use project::Project;
use schemars::JsonSchema;
use semver::Version;
use serde::{Deserialize, Serialize};
use std::str::FromStr as _;
use std::{
any::Any,
path::{Path, PathBuf},
@@ -42,201 +29,16 @@ pub fn init(cx: &mut App) {
settings::init(cx);
}
pub struct AgentServerDelegate {
project: Entity<Project>,
status_tx: Option<watch::Sender<SharedString>>,
}
impl AgentServerDelegate {
pub fn new(project: Entity<Project>, status_tx: Option<watch::Sender<SharedString>>) -> Self {
Self { project, status_tx }
}
pub fn project(&self) -> &Entity<Project> {
&self.project
}
fn get_or_npm_install_builtin_agent(
self,
binary_name: SharedString,
package_name: SharedString,
entrypoint_path: PathBuf,
ignore_system_version: bool,
minimum_version: Option<Version>,
cx: &mut App,
) -> Task<Result<AgentServerCommand>> {
let project = self.project;
let fs = project.read(cx).fs().clone();
let Some(node_runtime) = project.read(cx).node_runtime().cloned() else {
return Task::ready(Err(anyhow!(
"External agents are not yet available in remote projects."
)));
};
let status_tx = self.status_tx;
cx.spawn(async move |cx| {
if !ignore_system_version {
if let Some(bin) = find_bin_in_path(binary_name.clone(), &project, cx).await {
return Ok(AgentServerCommand {
path: bin,
args: Vec::new(),
env: Default::default(),
});
}
}
cx.spawn(async move |cx| {
let node_path = node_runtime.binary_path().await?;
let dir = paths::data_dir()
.join("external_agents")
.join(binary_name.as_str());
fs.create_dir(&dir).await?;
let mut stream = fs.read_dir(&dir).await?;
let mut versions = Vec::new();
let mut to_delete = Vec::new();
while let Some(entry) = stream.next().await {
let Ok(entry) = entry else { continue };
let Some(file_name) = entry.file_name() else {
continue;
};
if let Some(version) = file_name
.to_str()
.and_then(|name| semver::Version::from_str(&name).ok())
{
versions.push((version, file_name.to_owned()));
} else {
to_delete.push(file_name.to_owned())
}
}
versions.sort();
let newest_version = if let Some((version, file_name)) = versions.last().cloned()
&& minimum_version.is_none_or(|minimum_version| version >= minimum_version)
{
versions.pop();
Some(file_name)
} else {
None
};
log::debug!("existing version of {package_name}: {newest_version:?}");
to_delete.extend(versions.into_iter().map(|(_, file_name)| file_name));
cx.background_spawn({
let fs = fs.clone();
let dir = dir.clone();
async move {
for file_name in to_delete {
fs.remove_dir(
&dir.join(file_name),
RemoveOptions {
recursive: true,
ignore_if_not_exists: false,
},
)
.await
.ok();
}
}
})
.detach();
let version = if let Some(file_name) = newest_version {
cx.background_spawn({
let file_name = file_name.clone();
let dir = dir.clone();
async move {
let latest_version =
node_runtime.npm_package_latest_version(&package_name).await;
if let Ok(latest_version) = latest_version
&& &latest_version != &file_name.to_string_lossy()
{
Self::download_latest_version(
fs,
dir.clone(),
node_runtime,
package_name,
)
.await
.log_err();
}
}
})
.detach();
file_name
} else {
if let Some(mut status_tx) = status_tx {
status_tx.send("Installing…".into()).ok();
}
let dir = dir.clone();
cx.background_spawn(Self::download_latest_version(
fs,
dir.clone(),
node_runtime,
package_name,
))
.await?
.into()
};
anyhow::Ok(AgentServerCommand {
path: node_path,
args: vec![
dir.join(version)
.join(entrypoint_path)
.to_string_lossy()
.to_string(),
],
env: Default::default(),
})
})
.await
.map_err(|e| LoadError::FailedToInstall(e.to_string().into()).into())
})
}
async fn download_latest_version(
fs: Arc<dyn Fs>,
dir: PathBuf,
node_runtime: NodeRuntime,
package_name: SharedString,
) -> Result<String> {
log::debug!("downloading latest version of {package_name}");
let tmp_dir = tempfile::tempdir_in(&dir)?;
node_runtime
.npm_install_packages(tmp_dir.path(), &[(&package_name, "latest")])
.await?;
let version = node_runtime
.npm_package_installed_version(tmp_dir.path(), &package_name)
.await?
.context("expected package to be installed")?;
fs.rename(
&tmp_dir.keep(),
&dir.join(&version),
RenameOptions {
ignore_if_exists: true,
overwrite: false,
},
)
.await?;
anyhow::Ok(version)
}
}
pub trait AgentServer: Send {
fn logo(&self) -> ui::IconName;
fn name(&self) -> SharedString;
fn telemetry_id(&self) -> &'static str;
fn name(&self) -> &'static str;
fn empty_state_headline(&self) -> &'static str;
fn empty_state_message(&self) -> &'static str;
fn connect(
&self,
root_dir: &Path,
delegate: AgentServerDelegate,
project: &Entity<Project>,
cx: &mut App,
) -> Task<Result<Rc<dyn AgentConnection>>>;
@@ -274,6 +76,15 @@ impl std::fmt::Debug for AgentServerCommand {
}
}
pub enum AgentServerVersion {
Supported,
Unsupported {
error_message: SharedString,
upgrade_message: SharedString,
upgrade_command: String,
},
}
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)]
pub struct AgentServerCommand {
#[serde(rename = "command")]
@@ -284,20 +95,27 @@ pub struct AgentServerCommand {
}
impl AgentServerCommand {
pub async fn resolve(
pub(crate) async fn resolve(
path_bin_name: &'static str,
extra_args: &[&'static str],
fallback_path: Option<&Path>,
settings: Option<BuiltinAgentServerSettings>,
settings: Option<AgentServerSettings>,
project: &Entity<Project>,
cx: &mut AsyncApp,
) -> Option<Self> {
if let Some(settings) = settings
&& let Some(command) = settings.custom_command()
{
Some(command)
if let Some(agent_settings) = settings {
return Some(Self {
path: agent_settings.command.path,
args: agent_settings
.command
.args
.into_iter()
.chain(extra_args.iter().map(|arg| arg.to_string()))
.collect(),
env: agent_settings.command.env,
});
} else {
match find_bin_in_path(path_bin_name.into(), project, cx).await {
match find_bin_in_path(path_bin_name, project, cx).await {
Some(path) => Some(Self {
path,
args: extra_args.iter().map(|arg| arg.to_string()).collect(),
@@ -320,7 +138,7 @@ impl AgentServerCommand {
}
async fn find_bin_in_path(
bin_name: SharedString,
bin_name: &'static str,
project: &Entity<Project>,
cx: &mut AsyncApp,
) -> Option<PathBuf> {
@@ -350,11 +168,11 @@ async fn find_bin_in_path(
cx.background_executor()
.spawn(async move {
let which_result = if cfg!(windows) {
which::which(bin_name.as_str())
which::which(bin_name)
} else {
let env = env_task.await.unwrap_or_default();
let shell_path = env.get("PATH").cloned();
which::which_in(bin_name.as_str(), shell_path.as_ref(), root_dir.as_ref())
which::which_in(bin_name, shell_path.as_ref(), root_dir.as_ref())
};
if let Err(which::Error::CannotFindBinaryPath) = which_result {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,352 @@
use std::path::PathBuf;
use std::sync::Arc;
use crate::claude::tools::{ClaudeTool, EditToolParams, ReadToolParams};
use acp_thread::AcpThread;
use agent_client_protocol as acp;
use agent_settings::AgentSettings;
use anyhow::{Context, Result};
use collections::HashMap;
use context_server::listener::{McpServerTool, ToolResponse};
use context_server::types::{
Implementation, InitializeParams, InitializeResponse, ProtocolVersion, ServerCapabilities,
ToolAnnotations, ToolResponseContent, ToolsCapabilities, requests,
};
use gpui::{App, AsyncApp, Task, WeakEntity};
use project::Fs;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings as _, update_settings_file};
use util::debug_panic;
pub struct ClaudeZedMcpServer {
server: context_server::listener::McpServer,
}
pub const SERVER_NAME: &str = "zed";
impl ClaudeZedMcpServer {
pub async fn new(
thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
fs: Arc<dyn Fs>,
cx: &AsyncApp,
) -> Result<Self> {
let mut mcp_server = context_server::listener::McpServer::new(cx).await?;
mcp_server.handle_request::<requests::Initialize>(Self::handle_initialize);
mcp_server.add_tool(PermissionTool {
thread_rx: thread_rx.clone(),
fs: fs.clone(),
});
mcp_server.add_tool(ReadTool {
thread_rx: thread_rx.clone(),
});
mcp_server.add_tool(EditTool {
thread_rx: thread_rx.clone(),
});
Ok(Self { server: mcp_server })
}
pub fn server_config(&self) -> Result<McpServerConfig> {
#[cfg(not(test))]
let zed_path = std::env::current_exe()
.context("finding current executable path for use in mcp_server")?;
#[cfg(test)]
let zed_path = crate::e2e_tests::get_zed_path();
Ok(McpServerConfig {
command: zed_path,
args: vec![
"--nc".into(),
self.server.socket_path().display().to_string(),
],
env: None,
})
}
fn handle_initialize(_: InitializeParams, cx: &App) -> Task<Result<InitializeResponse>> {
cx.foreground_executor().spawn(async move {
Ok(InitializeResponse {
protocol_version: ProtocolVersion("2025-06-18".into()),
capabilities: ServerCapabilities {
experimental: None,
logging: None,
completions: None,
prompts: None,
resources: None,
tools: Some(ToolsCapabilities {
list_changed: Some(false),
}),
},
server_info: Implementation {
name: SERVER_NAME.into(),
version: "0.1.0".into(),
},
meta: None,
})
})
}
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct McpConfig {
pub mcp_servers: HashMap<String, McpServerConfig>,
}
#[derive(Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct McpServerConfig {
pub command: PathBuf,
pub args: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub env: Option<HashMap<String, String>>,
}
// Tools
#[derive(Clone)]
pub struct PermissionTool {
fs: Arc<dyn Fs>,
thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
}
#[derive(Deserialize, JsonSchema, Debug)]
pub struct PermissionToolParams {
tool_name: String,
input: serde_json::Value,
tool_use_id: Option<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PermissionToolResponse {
behavior: PermissionToolBehavior,
updated_input: serde_json::Value,
}
#[derive(Serialize)]
#[serde(rename_all = "snake_case")]
enum PermissionToolBehavior {
Allow,
Deny,
}
impl McpServerTool for PermissionTool {
type Input = PermissionToolParams;
type Output = ();
const NAME: &'static str = "Confirmation";
fn description(&self) -> &'static str {
"Request permission for tool calls"
}
async fn run(
&self,
input: Self::Input,
cx: &mut AsyncApp,
) -> Result<ToolResponse<Self::Output>> {
if agent_settings::AgentSettings::try_read_global(cx, |settings| {
settings.always_allow_tool_actions
})
.unwrap_or(false)
{
let response = PermissionToolResponse {
behavior: PermissionToolBehavior::Allow,
updated_input: input.input,
};
return Ok(ToolResponse {
content: vec![ToolResponseContent::Text {
text: serde_json::to_string(&response)?,
}],
structured_content: (),
});
}
let mut thread_rx = self.thread_rx.clone();
let Some(thread) = thread_rx.recv().await?.upgrade() else {
anyhow::bail!("Thread closed");
};
let claude_tool = ClaudeTool::infer(&input.tool_name, input.input.clone());
let tool_call_id = acp::ToolCallId(input.tool_use_id.context("Tool ID required")?.into());
const ALWAYS_ALLOW: &'static str = "always_allow";
const ALLOW: &'static str = "allow";
const REJECT: &'static str = "reject";
let chosen_option = thread
.update(cx, |thread, cx| {
thread.request_tool_call_authorization(
claude_tool.as_acp(tool_call_id).into(),
vec![
acp::PermissionOption {
id: acp::PermissionOptionId(ALWAYS_ALLOW.into()),
name: "Always Allow".into(),
kind: acp::PermissionOptionKind::AllowAlways,
},
acp::PermissionOption {
id: acp::PermissionOptionId(ALLOW.into()),
name: "Allow".into(),
kind: acp::PermissionOptionKind::AllowOnce,
},
acp::PermissionOption {
id: acp::PermissionOptionId(REJECT.into()),
name: "Reject".into(),
kind: acp::PermissionOptionKind::RejectOnce,
},
],
cx,
)
})??
.await?;
let response = match chosen_option.0.as_ref() {
ALWAYS_ALLOW => {
cx.update(|cx| {
update_settings_file::<AgentSettings>(self.fs.clone(), cx, |settings, _| {
settings.set_always_allow_tool_actions(true);
});
})?;
PermissionToolResponse {
behavior: PermissionToolBehavior::Allow,
updated_input: input.input,
}
}
ALLOW => PermissionToolResponse {
behavior: PermissionToolBehavior::Allow,
updated_input: input.input,
},
REJECT => PermissionToolResponse {
behavior: PermissionToolBehavior::Deny,
updated_input: input.input,
},
opt => {
debug_panic!("Unexpected option: {}", opt);
PermissionToolResponse {
behavior: PermissionToolBehavior::Deny,
updated_input: input.input,
}
}
};
Ok(ToolResponse {
content: vec![ToolResponseContent::Text {
text: serde_json::to_string(&response)?,
}],
structured_content: (),
})
}
}
#[derive(Clone)]
pub struct ReadTool {
thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
}
impl McpServerTool for ReadTool {
type Input = ReadToolParams;
type Output = ();
const NAME: &'static str = "Read";
fn description(&self) -> &'static str {
"Read the contents of a file. In sessions with mcp__zed__Read always use it instead of Read as it contains the most up-to-date contents."
}
fn annotations(&self) -> ToolAnnotations {
ToolAnnotations {
title: Some("Read file".to_string()),
read_only_hint: Some(true),
destructive_hint: Some(false),
open_world_hint: Some(false),
idempotent_hint: None,
}
}
async fn run(
&self,
input: Self::Input,
cx: &mut AsyncApp,
) -> Result<ToolResponse<Self::Output>> {
let mut thread_rx = self.thread_rx.clone();
let Some(thread) = thread_rx.recv().await?.upgrade() else {
anyhow::bail!("Thread closed");
};
let content = thread
.update(cx, |thread, cx| {
thread.read_text_file(input.abs_path, input.offset, input.limit, false, cx)
})?
.await?;
Ok(ToolResponse {
content: vec![ToolResponseContent::Text { text: content }],
structured_content: (),
})
}
}
#[derive(Clone)]
pub struct EditTool {
thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
}
impl McpServerTool for EditTool {
type Input = EditToolParams;
type Output = ();
const NAME: &'static str = "Edit";
fn description(&self) -> &'static str {
"Edits a file. In sessions with mcp__zed__Edit always use it instead of Edit as it will show the diff to the user better."
}
fn annotations(&self) -> ToolAnnotations {
ToolAnnotations {
title: Some("Edit file".to_string()),
read_only_hint: Some(false),
destructive_hint: Some(false),
open_world_hint: Some(false),
idempotent_hint: Some(false),
}
}
async fn run(
&self,
input: Self::Input,
cx: &mut AsyncApp,
) -> Result<ToolResponse<Self::Output>> {
let mut thread_rx = self.thread_rx.clone();
let Some(thread) = thread_rx.recv().await?.upgrade() else {
anyhow::bail!("Thread closed");
};
let content = thread
.update(cx, |thread, cx| {
thread.read_text_file(input.abs_path.clone(), None, None, true, cx)
})?
.await?;
let new_content = content.replace(&input.old_text, &input.new_text);
if new_content == content {
return Err(anyhow::anyhow!("The old_text was not found in the content"));
}
thread
.update(cx, |thread, cx| {
thread.write_text_file(input.abs_path, new_content, cx)
})?
.await?;
Ok(ToolResponse {
content: vec![],
structured_content: (),
})
}
}

View File

@@ -0,0 +1,661 @@
use std::path::PathBuf;
use agent_client_protocol as acp;
use itertools::Itertools;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use util::ResultExt;
pub enum ClaudeTool {
Task(Option<TaskToolParams>),
NotebookRead(Option<NotebookReadToolParams>),
NotebookEdit(Option<NotebookEditToolParams>),
Edit(Option<EditToolParams>),
MultiEdit(Option<MultiEditToolParams>),
ReadFile(Option<ReadToolParams>),
Write(Option<WriteToolParams>),
Ls(Option<LsToolParams>),
Glob(Option<GlobToolParams>),
Grep(Option<GrepToolParams>),
Terminal(Option<BashToolParams>),
WebFetch(Option<WebFetchToolParams>),
WebSearch(Option<WebSearchToolParams>),
TodoWrite(Option<TodoWriteToolParams>),
ExitPlanMode(Option<ExitPlanModeToolParams>),
Other {
name: String,
input: serde_json::Value,
},
}
impl ClaudeTool {
pub fn infer(tool_name: &str, input: serde_json::Value) -> Self {
match tool_name {
// Known tools
"mcp__zed__Read" => Self::ReadFile(serde_json::from_value(input).log_err()),
"mcp__zed__Edit" => Self::Edit(serde_json::from_value(input).log_err()),
"MultiEdit" => Self::MultiEdit(serde_json::from_value(input).log_err()),
"Write" => Self::Write(serde_json::from_value(input).log_err()),
"LS" => Self::Ls(serde_json::from_value(input).log_err()),
"Glob" => Self::Glob(serde_json::from_value(input).log_err()),
"Grep" => Self::Grep(serde_json::from_value(input).log_err()),
"Bash" => Self::Terminal(serde_json::from_value(input).log_err()),
"WebFetch" => Self::WebFetch(serde_json::from_value(input).log_err()),
"WebSearch" => Self::WebSearch(serde_json::from_value(input).log_err()),
"TodoWrite" => Self::TodoWrite(serde_json::from_value(input).log_err()),
"exit_plan_mode" => Self::ExitPlanMode(serde_json::from_value(input).log_err()),
"Task" => Self::Task(serde_json::from_value(input).log_err()),
"NotebookRead" => Self::NotebookRead(serde_json::from_value(input).log_err()),
"NotebookEdit" => Self::NotebookEdit(serde_json::from_value(input).log_err()),
// Inferred from name
_ => {
let tool_name = tool_name.to_lowercase();
if tool_name.contains("edit") || tool_name.contains("write") {
Self::Edit(None)
} else if tool_name.contains("terminal") {
Self::Terminal(None)
} else {
Self::Other {
name: tool_name.to_string(),
input,
}
}
}
}
}
pub fn label(&self) -> String {
match &self {
Self::Task(Some(params)) => params.description.clone(),
Self::Task(None) => "Task".into(),
Self::NotebookRead(Some(params)) => {
format!("Read Notebook {}", params.notebook_path.display())
}
Self::NotebookRead(None) => "Read Notebook".into(),
Self::NotebookEdit(Some(params)) => {
format!("Edit Notebook {}", params.notebook_path.display())
}
Self::NotebookEdit(None) => "Edit Notebook".into(),
Self::Terminal(Some(params)) => format!("`{}`", params.command),
Self::Terminal(None) => "Terminal".into(),
Self::ReadFile(_) => "Read File".into(),
Self::Ls(Some(params)) => {
format!("List Directory {}", params.path.display())
}
Self::Ls(None) => "List Directory".into(),
Self::Edit(Some(params)) => {
format!("Edit {}", params.abs_path.display())
}
Self::Edit(None) => "Edit".into(),
Self::MultiEdit(Some(params)) => {
format!("Multi Edit {}", params.file_path.display())
}
Self::MultiEdit(None) => "Multi Edit".into(),
Self::Write(Some(params)) => {
format!("Write {}", params.file_path.display())
}
Self::Write(None) => "Write".into(),
Self::Glob(Some(params)) => {
format!("Glob `{params}`")
}
Self::Glob(None) => "Glob".into(),
Self::Grep(Some(params)) => format!("`{params}`"),
Self::Grep(None) => "Grep".into(),
Self::WebFetch(Some(params)) => format!("Fetch {}", params.url),
Self::WebFetch(None) => "Fetch".into(),
Self::WebSearch(Some(params)) => format!("Web Search: {}", params),
Self::WebSearch(None) => "Web Search".into(),
Self::TodoWrite(Some(params)) => format!(
"Update TODOs: {}",
params.todos.iter().map(|todo| &todo.content).join(", ")
),
Self::TodoWrite(None) => "Update TODOs".into(),
Self::ExitPlanMode(_) => "Exit Plan Mode".into(),
Self::Other { name, .. } => name.clone(),
}
}
pub fn content(&self) -> Vec<acp::ToolCallContent> {
match &self {
Self::Other { input, .. } => vec![
format!(
"```json\n{}```",
serde_json::to_string_pretty(&input).unwrap_or("{}".to_string())
)
.into(),
],
Self::Task(Some(params)) => vec![params.prompt.clone().into()],
Self::NotebookRead(Some(params)) => {
vec![params.notebook_path.display().to_string().into()]
}
Self::NotebookEdit(Some(params)) => vec![params.new_source.clone().into()],
Self::Terminal(Some(params)) => vec![
format!(
"`{}`\n\n{}",
params.command,
params.description.as_deref().unwrap_or_default()
)
.into(),
],
Self::ReadFile(Some(params)) => vec![params.abs_path.display().to_string().into()],
Self::Ls(Some(params)) => vec![params.path.display().to_string().into()],
Self::Glob(Some(params)) => vec![params.to_string().into()],
Self::Grep(Some(params)) => vec![format!("`{params}`").into()],
Self::WebFetch(Some(params)) => vec![params.prompt.clone().into()],
Self::WebSearch(Some(params)) => vec![params.to_string().into()],
Self::ExitPlanMode(Some(params)) => vec![params.plan.clone().into()],
Self::Edit(Some(params)) => vec![acp::ToolCallContent::Diff {
diff: acp::Diff {
path: params.abs_path.clone(),
old_text: Some(params.old_text.clone()),
new_text: params.new_text.clone(),
},
}],
Self::Write(Some(params)) => vec![acp::ToolCallContent::Diff {
diff: acp::Diff {
path: params.file_path.clone(),
old_text: None,
new_text: params.content.clone(),
},
}],
Self::MultiEdit(Some(params)) => {
// todo: show multiple edits in a multibuffer?
params
.edits
.first()
.map(|edit| {
vec![acp::ToolCallContent::Diff {
diff: acp::Diff {
path: params.file_path.clone(),
old_text: Some(edit.old_string.clone()),
new_text: edit.new_string.clone(),
},
}]
})
.unwrap_or_default()
}
Self::TodoWrite(Some(_)) => {
// These are mapped to plan updates later
vec![]
}
Self::Task(None)
| Self::NotebookRead(None)
| Self::NotebookEdit(None)
| Self::Terminal(None)
| Self::ReadFile(None)
| Self::Ls(None)
| Self::Glob(None)
| Self::Grep(None)
| Self::WebFetch(None)
| Self::WebSearch(None)
| Self::TodoWrite(None)
| Self::ExitPlanMode(None)
| Self::Edit(None)
| Self::Write(None)
| Self::MultiEdit(None) => vec![],
}
}
pub fn kind(&self) -> acp::ToolKind {
match self {
Self::Task(_) => acp::ToolKind::Think,
Self::NotebookRead(_) => acp::ToolKind::Read,
Self::NotebookEdit(_) => acp::ToolKind::Edit,
Self::Edit(_) => acp::ToolKind::Edit,
Self::MultiEdit(_) => acp::ToolKind::Edit,
Self::Write(_) => acp::ToolKind::Edit,
Self::ReadFile(_) => acp::ToolKind::Read,
Self::Ls(_) => acp::ToolKind::Search,
Self::Glob(_) => acp::ToolKind::Search,
Self::Grep(_) => acp::ToolKind::Search,
Self::Terminal(_) => acp::ToolKind::Execute,
Self::WebSearch(_) => acp::ToolKind::Search,
Self::WebFetch(_) => acp::ToolKind::Fetch,
Self::TodoWrite(_) => acp::ToolKind::Think,
Self::ExitPlanMode(_) => acp::ToolKind::Think,
Self::Other { .. } => acp::ToolKind::Other,
}
}
pub fn locations(&self) -> Vec<acp::ToolCallLocation> {
match &self {
Self::Edit(Some(EditToolParams { abs_path, .. })) => vec![acp::ToolCallLocation {
path: abs_path.clone(),
line: None,
}],
Self::MultiEdit(Some(MultiEditToolParams { file_path, .. })) => {
vec![acp::ToolCallLocation {
path: file_path.clone(),
line: None,
}]
}
Self::Write(Some(WriteToolParams { file_path, .. })) => {
vec![acp::ToolCallLocation {
path: file_path.clone(),
line: None,
}]
}
Self::ReadFile(Some(ReadToolParams {
abs_path, offset, ..
})) => vec![acp::ToolCallLocation {
path: abs_path.clone(),
line: *offset,
}],
Self::NotebookRead(Some(NotebookReadToolParams { notebook_path, .. })) => {
vec![acp::ToolCallLocation {
path: notebook_path.clone(),
line: None,
}]
}
Self::NotebookEdit(Some(NotebookEditToolParams { notebook_path, .. })) => {
vec![acp::ToolCallLocation {
path: notebook_path.clone(),
line: None,
}]
}
Self::Glob(Some(GlobToolParams {
path: Some(path), ..
})) => vec![acp::ToolCallLocation {
path: path.clone(),
line: None,
}],
Self::Ls(Some(LsToolParams { path, .. })) => vec![acp::ToolCallLocation {
path: path.clone(),
line: None,
}],
Self::Grep(Some(GrepToolParams {
path: Some(path), ..
})) => vec![acp::ToolCallLocation {
path: PathBuf::from(path),
line: None,
}],
Self::Task(_)
| Self::NotebookRead(None)
| Self::NotebookEdit(None)
| Self::Edit(None)
| Self::MultiEdit(None)
| Self::Write(None)
| Self::ReadFile(None)
| Self::Ls(None)
| Self::Glob(_)
| Self::Grep(_)
| Self::Terminal(_)
| Self::WebFetch(_)
| Self::WebSearch(_)
| Self::TodoWrite(_)
| Self::ExitPlanMode(_)
| Self::Other { .. } => vec![],
}
}
pub fn as_acp(&self, id: acp::ToolCallId) -> acp::ToolCall {
acp::ToolCall {
id,
kind: self.kind(),
status: acp::ToolCallStatus::InProgress,
title: self.label(),
content: self.content(),
locations: self.locations(),
raw_input: None,
raw_output: None,
}
}
}
#[derive(Deserialize, JsonSchema, Debug)]
pub struct EditToolParams {
/// The absolute path to the file to read.
pub abs_path: PathBuf,
/// The old text to replace (must be unique in the file)
pub old_text: String,
/// The new text.
pub new_text: String,
}
#[derive(Deserialize, JsonSchema, Debug)]
pub struct ReadToolParams {
/// The absolute path to the file to read.
pub abs_path: PathBuf,
/// Which line to start reading from. Omit to start from the beginning.
#[serde(skip_serializing_if = "Option::is_none")]
pub offset: Option<u32>,
/// How many lines to read. Omit for the whole file.
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
}
#[derive(Deserialize, JsonSchema, Debug)]
pub struct WriteToolParams {
/// Absolute path for new file
pub file_path: PathBuf,
/// File content
pub content: String,
}
#[derive(Deserialize, JsonSchema, Debug)]
pub struct BashToolParams {
/// Shell command to execute
pub command: String,
/// 5-10 word description of what command does
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
/// Timeout in ms (max 600000ms/10min, default 120000ms)
#[serde(skip_serializing_if = "Option::is_none")]
pub timeout: Option<u32>,
}
#[derive(Deserialize, JsonSchema, Debug)]
pub struct GlobToolParams {
/// Glob pattern like **/*.js or src/**/*.ts
pub pattern: String,
/// Directory to search in (omit for current directory)
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<PathBuf>,
}
impl std::fmt::Display for GlobToolParams {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(path) = &self.path {
write!(f, "{}", path.display())?;
}
write!(f, "{}", self.pattern)
}
}
#[derive(Deserialize, JsonSchema, Debug)]
pub struct LsToolParams {
/// Absolute path to directory
pub path: PathBuf,
/// Array of glob patterns to ignore
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub ignore: Vec<String>,
}
#[derive(Deserialize, JsonSchema, Debug)]
pub struct GrepToolParams {
/// Regex pattern to search for
pub pattern: String,
/// File/directory to search (defaults to current directory)
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
/// "content" (shows lines), "files_with_matches" (default), "count"
#[serde(skip_serializing_if = "Option::is_none")]
pub output_mode: Option<GrepOutputMode>,
/// Filter files with glob pattern like "*.js"
#[serde(skip_serializing_if = "Option::is_none")]
pub glob: Option<String>,
/// File type filter like "js", "py", "rust"
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub file_type: Option<String>,
/// Case insensitive search
#[serde(rename = "-i", default, skip_serializing_if = "is_false")]
pub case_insensitive: bool,
/// Show line numbers (content mode only)
#[serde(rename = "-n", default, skip_serializing_if = "is_false")]
pub line_numbers: bool,
/// Lines after match (content mode only)
#[serde(rename = "-A", skip_serializing_if = "Option::is_none")]
pub after_context: Option<u32>,
/// Lines before match (content mode only)
#[serde(rename = "-B", skip_serializing_if = "Option::is_none")]
pub before_context: Option<u32>,
/// Lines before and after match (content mode only)
#[serde(rename = "-C", skip_serializing_if = "Option::is_none")]
pub context: Option<u32>,
/// Enable multiline/cross-line matching
#[serde(default, skip_serializing_if = "is_false")]
pub multiline: bool,
/// Limit output to first N results
#[serde(skip_serializing_if = "Option::is_none")]
pub head_limit: Option<u32>,
}
impl std::fmt::Display for GrepToolParams {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "grep")?;
// Boolean flags
if self.case_insensitive {
write!(f, " -i")?;
}
if self.line_numbers {
write!(f, " -n")?;
}
// Context options
if let Some(after) = self.after_context {
write!(f, " -A {}", after)?;
}
if let Some(before) = self.before_context {
write!(f, " -B {}", before)?;
}
if let Some(context) = self.context {
write!(f, " -C {}", context)?;
}
// Output mode
if let Some(mode) = &self.output_mode {
match mode {
GrepOutputMode::FilesWithMatches => write!(f, " -l")?,
GrepOutputMode::Count => write!(f, " -c")?,
GrepOutputMode::Content => {} // Default mode
}
}
// Head limit
if let Some(limit) = self.head_limit {
write!(f, " | head -{}", limit)?;
}
// Glob pattern
if let Some(glob) = &self.glob {
write!(f, " --include=\"{}\"", glob)?;
}
// File type
if let Some(file_type) = &self.file_type {
write!(f, " --type={}", file_type)?;
}
// Multiline
if self.multiline {
write!(f, " -P")?; // Perl-compatible regex for multiline
}
// Pattern (escaped if contains special characters)
write!(f, " \"{}\"", self.pattern)?;
// Path
if let Some(path) = &self.path {
write!(f, " {}", path)?;
}
Ok(())
}
}
#[derive(Default, Deserialize, Serialize, JsonSchema, strum::Display, Debug)]
#[serde(rename_all = "snake_case")]
pub enum TodoPriority {
High,
#[default]
Medium,
Low,
}
impl Into<acp::PlanEntryPriority> for TodoPriority {
fn into(self) -> acp::PlanEntryPriority {
match self {
TodoPriority::High => acp::PlanEntryPriority::High,
TodoPriority::Medium => acp::PlanEntryPriority::Medium,
TodoPriority::Low => acp::PlanEntryPriority::Low,
}
}
}
#[derive(Deserialize, Serialize, JsonSchema, Debug)]
#[serde(rename_all = "snake_case")]
pub enum TodoStatus {
Pending,
InProgress,
Completed,
}
impl Into<acp::PlanEntryStatus> for TodoStatus {
fn into(self) -> acp::PlanEntryStatus {
match self {
TodoStatus::Pending => acp::PlanEntryStatus::Pending,
TodoStatus::InProgress => acp::PlanEntryStatus::InProgress,
TodoStatus::Completed => acp::PlanEntryStatus::Completed,
}
}
}
#[derive(Deserialize, Serialize, JsonSchema, Debug)]
pub struct Todo {
/// Task description
pub content: String,
/// Current status of the todo
pub status: TodoStatus,
/// Priority level of the todo
#[serde(default)]
pub priority: TodoPriority,
}
impl Into<acp::PlanEntry> for Todo {
fn into(self) -> acp::PlanEntry {
acp::PlanEntry {
content: self.content,
priority: self.priority.into(),
status: self.status.into(),
}
}
}
#[derive(Deserialize, JsonSchema, Debug)]
pub struct TodoWriteToolParams {
pub todos: Vec<Todo>,
}
#[derive(Deserialize, JsonSchema, Debug)]
pub struct ExitPlanModeToolParams {
/// Implementation plan in markdown format
pub plan: String,
}
#[derive(Deserialize, JsonSchema, Debug)]
pub struct TaskToolParams {
/// Short 3-5 word description of task
pub description: String,
/// Detailed task for agent to perform
pub prompt: String,
}
#[derive(Deserialize, JsonSchema, Debug)]
pub struct NotebookReadToolParams {
/// Absolute path to .ipynb file
pub notebook_path: PathBuf,
/// Specific cell ID to read
#[serde(skip_serializing_if = "Option::is_none")]
pub cell_id: Option<String>,
}
#[derive(Deserialize, Serialize, JsonSchema, Debug)]
#[serde(rename_all = "snake_case")]
pub enum CellType {
Code,
Markdown,
}
#[derive(Deserialize, Serialize, JsonSchema, Debug)]
#[serde(rename_all = "snake_case")]
pub enum EditMode {
Replace,
Insert,
Delete,
}
#[derive(Deserialize, JsonSchema, Debug)]
pub struct NotebookEditToolParams {
/// Absolute path to .ipynb file
pub notebook_path: PathBuf,
/// New cell content
pub new_source: String,
/// Cell ID to edit
#[serde(skip_serializing_if = "Option::is_none")]
pub cell_id: Option<String>,
/// Type of cell (code or markdown)
#[serde(skip_serializing_if = "Option::is_none")]
pub cell_type: Option<CellType>,
/// Edit operation mode
#[serde(skip_serializing_if = "Option::is_none")]
pub edit_mode: Option<EditMode>,
}
#[derive(Deserialize, Serialize, JsonSchema, Debug)]
pub struct MultiEditItem {
/// The text to search for and replace
pub old_string: String,
/// The replacement text
pub new_string: String,
/// Whether to replace all occurrences or just the first
#[serde(default, skip_serializing_if = "is_false")]
pub replace_all: bool,
}
#[derive(Deserialize, JsonSchema, Debug)]
pub struct MultiEditToolParams {
/// Absolute path to file
pub file_path: PathBuf,
/// List of edits to apply
pub edits: Vec<MultiEditItem>,
}
fn is_false(v: &bool) -> bool {
!*v
}
#[derive(Deserialize, JsonSchema, Debug)]
#[serde(rename_all = "snake_case")]
pub enum GrepOutputMode {
Content,
FilesWithMatches,
Count,
}
#[derive(Deserialize, JsonSchema, Debug)]
pub struct WebFetchToolParams {
/// Valid URL to fetch
#[serde(rename = "url")]
pub url: String,
/// What to extract from content
pub prompt: String,
}
#[derive(Deserialize, JsonSchema, Debug)]
pub struct WebSearchToolParams {
/// Search query (min 2 chars)
pub query: String,
/// Only include these domains
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub allowed_domains: Vec<String>,
/// Exclude these domains
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub blocked_domains: Vec<String>,
}
impl std::fmt::Display for WebSearchToolParams {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "\"{}\"", self.query)?;
if !self.allowed_domains.is_empty() {
write!(f, " (allowed: {})", self.allowed_domains.join(", "))?;
}
if !self.blocked_domains.is_empty() {
write!(f, " (blocked: {})", self.blocked_domains.join(", "))?;
}
Ok(())
}
}

View File

@@ -1,48 +0,0 @@
use crate::{AgentServerCommand, AgentServerDelegate};
use acp_thread::AgentConnection;
use anyhow::Result;
use gpui::{App, SharedString, Task};
use std::{path::Path, rc::Rc};
use ui::IconName;
/// A generic agent server implementation for custom user-defined agents
pub struct CustomAgentServer {
name: SharedString,
command: AgentServerCommand,
}
impl CustomAgentServer {
pub fn new(name: SharedString, command: AgentServerCommand) -> Self {
Self { name, command }
}
}
impl crate::AgentServer for CustomAgentServer {
fn telemetry_id(&self) -> &'static str {
"custom"
}
fn name(&self) -> SharedString {
self.name.clone()
}
fn logo(&self) -> IconName {
IconName::Terminal
}
fn connect(
&self,
root_dir: &Path,
_delegate: AgentServerDelegate,
cx: &mut App,
) -> Task<Result<Rc<dyn AgentConnection>>> {
let server_name = self.name();
let command = self.command.clone();
let root_dir = root_dir.to_path_buf();
cx.spawn(async move |cx| crate::acp::connect(server_name, command, &root_dir, cx).await)
}
fn into_any(self: Rc<Self>) -> Rc<dyn std::any::Any> {
self
}
}

View File

@@ -1,33 +1,24 @@
use crate::{AgentServer, AgentServerDelegate};
#[cfg(test)]
use crate::{AgentServerCommand, CustomAgentServerSettings};
use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus};
use agent_client_protocol as acp;
use futures::{FutureExt, StreamExt, channel::mpsc, select};
use gpui::{AppContext, Entity, TestAppContext};
use indoc::indoc;
use project::{FakeFs, Project};
use std::{
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
use crate::{AgentServer, AgentServerSettings, AllAgentServersSettings};
use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus};
use agent_client_protocol as acp;
use futures::{FutureExt, StreamExt, channel::mpsc, select};
use gpui::{Entity, TestAppContext};
use indoc::indoc;
use project::{FakeFs, Project};
use settings::{Settings, SettingsStore};
use util::path;
pub async fn test_basic<T, F>(server: F, cx: &mut TestAppContext)
where
T: AgentServer + 'static,
F: AsyncFn(&Arc<dyn fs::Fs>, &Entity<Project>, &mut TestAppContext) -> T,
{
let fs = init_test(cx).await as Arc<dyn fs::Fs>;
let project = Project::test(fs.clone(), [], cx).await;
let thread = new_test_thread(
server(&fs, &project, cx).await,
project.clone(),
"/private/tmp",
cx,
)
.await;
pub async fn test_basic(server: impl AgentServer + 'static, cx: &mut TestAppContext) {
let fs = init_test(cx).await;
let project = Project::test(fs, [], cx).await;
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
thread
.update(cx, |thread, cx| thread.send_raw("Hello from Zed!", cx))
@@ -51,12 +42,8 @@ where
});
}
pub async fn test_path_mentions<T, F>(server: F, cx: &mut TestAppContext)
where
T: AgentServer + 'static,
F: AsyncFn(&Arc<dyn fs::Fs>, &Entity<Project>, &mut TestAppContext) -> T,
{
let fs = init_test(cx).await as _;
pub async fn test_path_mentions(server: impl AgentServer + 'static, cx: &mut TestAppContext) {
let _fs = init_test(cx).await;
let tempdir = tempfile::tempdir().unwrap();
std::fs::write(
@@ -69,13 +56,7 @@ where
)
.expect("failed to write file");
let project = Project::example([tempdir.path()], &mut cx.to_async()).await;
let thread = new_test_thread(
server(&fs, &project, cx).await,
project.clone(),
tempdir.path(),
cx,
)
.await;
let thread = new_test_thread(server, project.clone(), tempdir.path(), cx).await;
thread
.update(cx, |thread, cx| {
thread.send(
@@ -129,25 +110,15 @@ where
drop(tempdir);
}
pub async fn test_tool_call<T, F>(server: F, cx: &mut TestAppContext)
where
T: AgentServer + 'static,
F: AsyncFn(&Arc<dyn fs::Fs>, &Entity<Project>, &mut TestAppContext) -> T,
{
let fs = init_test(cx).await as _;
pub async fn test_tool_call(server: impl AgentServer + 'static, cx: &mut TestAppContext) {
let _fs = init_test(cx).await;
let tempdir = tempfile::tempdir().unwrap();
let foo_path = tempdir.path().join("foo");
std::fs::write(&foo_path, "Lorem ipsum dolor").expect("failed to write file");
let project = Project::example([tempdir.path()], &mut cx.to_async()).await;
let thread = new_test_thread(
server(&fs, &project, cx).await,
project.clone(),
"/private/tmp",
cx,
)
.await;
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
thread
.update(cx, |thread, cx| {
@@ -181,23 +152,14 @@ where
drop(tempdir);
}
pub async fn test_tool_call_with_permission<T, F>(
server: F,
pub async fn test_tool_call_with_permission(
server: impl AgentServer + 'static,
allow_option_id: acp::PermissionOptionId,
cx: &mut TestAppContext,
) where
T: AgentServer + 'static,
F: AsyncFn(&Arc<dyn fs::Fs>, &Entity<Project>, &mut TestAppContext) -> T,
{
let fs = init_test(cx).await as Arc<dyn fs::Fs>;
let project = Project::test(fs.clone(), [path!("/private/tmp").as_ref()], cx).await;
let thread = new_test_thread(
server(&fs, &project, cx).await,
project.clone(),
"/private/tmp",
cx,
)
.await;
) {
let fs = init_test(cx).await;
let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await;
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
let full_turn = thread.update(cx, |thread, cx| {
thread.send_raw(
r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#,
@@ -285,21 +247,11 @@ pub async fn test_tool_call_with_permission<T, F>(
});
}
pub async fn test_cancel<T, F>(server: F, cx: &mut TestAppContext)
where
T: AgentServer + 'static,
F: AsyncFn(&Arc<dyn fs::Fs>, &Entity<Project>, &mut TestAppContext) -> T,
{
let fs = init_test(cx).await as Arc<dyn fs::Fs>;
pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppContext) {
let fs = init_test(cx).await;
let project = Project::test(fs.clone(), [path!("/private/tmp").as_ref()], cx).await;
let thread = new_test_thread(
server(&fs, &project, cx).await,
project.clone(),
"/private/tmp",
cx,
)
.await;
let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await;
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
let _ = thread.update(cx, |thread, cx| {
thread.send_raw(
r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#,
@@ -364,20 +316,10 @@ where
});
}
pub async fn test_thread_drop<T, F>(server: F, cx: &mut TestAppContext)
where
T: AgentServer + 'static,
F: AsyncFn(&Arc<dyn fs::Fs>, &Entity<Project>, &mut TestAppContext) -> T,
{
let fs = init_test(cx).await as Arc<dyn fs::Fs>;
let project = Project::test(fs.clone(), [], cx).await;
let thread = new_test_thread(
server(&fs, &project, cx).await,
project.clone(),
"/private/tmp",
cx,
)
.await;
pub async fn test_thread_drop(server: impl AgentServer + 'static, cx: &mut TestAppContext) {
let fs = init_test(cx).await;
let project = Project::test(fs, [], cx).await;
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
thread
.update(cx, |thread, cx| thread.send_raw("Hello from test!", cx))
@@ -444,44 +386,27 @@ macro_rules! common_e2e_tests {
}
};
}
pub use common_e2e_tests;
// Helpers
pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
#[cfg(test)]
use settings::Settings;
env_logger::try_init().ok();
cx.update(|cx| {
let settings_store = settings::SettingsStore::test(cx);
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
Project::init_settings(cx);
language::init(cx);
gpui_tokio::init(cx);
let http_client = reqwest_client::ReqwestClient::user_agent("agent tests").unwrap();
cx.set_http_client(Arc::new(http_client));
client::init_settings(cx);
let client = client::Client::production(cx);
let user_store = cx.new(|cx| client::UserStore::new(client.clone(), cx));
language_model::init(client.clone(), cx);
language_models::init(user_store, client, cx);
agent_settings::init(cx);
crate::settings::init(cx);
#[cfg(test)]
crate::AllAgentServersSettings::override_global(
crate::AllAgentServersSettings {
claude: Some(CustomAgentServerSettings {
command: AgentServerCommand {
path: "claude-code-acp".into(),
args: vec![],
env: None,
},
AllAgentServersSettings {
claude: Some(AgentServerSettings {
command: crate::claude::tests::local_command(),
}),
gemini: Some(AgentServerSettings {
command: crate::gemini::tests::local_command(),
}),
gemini: Some(crate::gemini::tests::local_command().into()),
custom: collections::HashMap::default(),
},
cx,
);
@@ -498,16 +423,17 @@ pub async fn new_test_thread(
current_dir: impl AsRef<Path>,
cx: &mut TestAppContext,
) -> Entity<AcpThread> {
let delegate = AgentServerDelegate::new(project.clone(), None);
let connection = cx
.update(|cx| server.connect(current_dir.as_ref(), delegate, cx))
.update(|cx| server.connect(current_dir.as_ref(), &project, cx))
.await
.unwrap();
cx.update(|cx| connection.new_thread(project.clone(), current_dir.as_ref(), cx))
let thread = cx
.update(|cx| connection.new_thread(project.clone(), current_dir.as_ref(), cx))
.await
.unwrap()
.unwrap();
thread
}
pub async fn run_until_first_tool_call(
@@ -545,7 +471,7 @@ pub fn get_zed_path() -> PathBuf {
while zed_path
.file_name()
.is_none_or(|name| name.to_string_lossy() != "debug")
.map_or(true, |name| name.to_string_lossy() != "debug")
{
if !zed_path.pop() {
panic!("Could not find target directory");

View File

@@ -1,13 +1,13 @@
use std::rc::Rc;
use std::{any::Any, path::Path};
use crate::acp::AcpConnection;
use crate::{AgentServer, AgentServerDelegate};
use crate::{AgentServer, AgentServerCommand};
use acp_thread::{AgentConnection, LoadError};
use anyhow::Result;
use gpui::{App, AppContext as _, SharedString, Task};
use language_models::provider::google::GoogleLanguageModelProvider;
use gpui::{Entity, Task};
use project::Project;
use settings::SettingsStore;
use ui::App;
use crate::AllAgentServersSettings;
@@ -17,12 +17,16 @@ pub struct Gemini;
const ACP_ARG: &str = "--experimental-acp";
impl AgentServer for Gemini {
fn telemetry_id(&self) -> &'static str {
"gemini-cli"
fn name(&self) -> &'static str {
"Gemini"
}
fn name(&self) -> SharedString {
"Gemini CLI".into()
fn empty_state_headline(&self) -> &'static str {
"Welcome to Gemini"
}
fn empty_state_message(&self) -> &'static str {
"Ask questions, edit files, run commands.\nBe specific for the best results."
}
fn logo(&self) -> ui::IconName {
@@ -32,108 +36,51 @@ impl AgentServer for Gemini {
fn connect(
&self,
root_dir: &Path,
delegate: AgentServerDelegate,
project: &Entity<Project>,
cx: &mut App,
) -> Task<Result<Rc<dyn AgentConnection>>> {
let project = project.clone();
let root_dir = root_dir.to_path_buf();
let server_name = self.name();
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<AllAgentServersSettings>(None).gemini.clone()
});
cx.spawn(async move |cx| {
let ignore_system_version = settings
.as_ref()
.and_then(|settings| settings.ignore_system_version)
.unwrap_or(true);
let mut command = if let Some(settings) = settings
&& let Some(command) = settings.custom_command()
{
command
} else {
cx.update(|cx| {
delegate.get_or_npm_install_builtin_agent(
Self::BINARY_NAME.into(),
Self::PACKAGE_NAME.into(),
format!("node_modules/{}/dist/index.js", Self::PACKAGE_NAME).into(),
ignore_system_version,
Some(Self::MINIMUM_VERSION.parse().unwrap()),
cx,
)
})?
.await?
};
if !command.args.contains(&ACP_ARG.into()) {
command.args.push(ACP_ARG.into());
}
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<AllAgentServersSettings>(None).gemini.clone()
})?;
if let Some(api_key) = cx.update(GoogleLanguageModelProvider::api_key)?.await.ok() {
command
.env
.get_or_insert_default()
.insert("GEMINI_API_KEY".to_owned(), api_key.key);
}
let Some(command) =
AgentServerCommand::resolve("gemini", &[ACP_ARG], None, settings, &project, cx).await
else {
anyhow::bail!("Failed to find gemini binary");
};
let result = crate::acp::connect(server_name, command.clone(), &root_dir, cx).await;
match &result {
Ok(connection) => {
if let Some(connection) = connection.clone().downcast::<AcpConnection>()
&& !connection.prompt_capabilities().image
{
let version_output = util::command::new_smol_command(&command.path)
.args(command.args.iter())
.arg("--version")
.kill_on_drop(true)
.output()
.await;
let current_version =
String::from_utf8(version_output?.stdout)?.trim().to_owned();
if result.is_err() {
let version_fut = util::command::new_smol_command(&command.path)
.args(command.args.iter())
.arg("--version")
.kill_on_drop(true)
.output();
log::error!("connected to gemini, but missing prompt_capabilities.image (version is {current_version})");
return Err(LoadError::Unsupported {
current_version: current_version.into(),
command: command.path.to_string_lossy().to_string().into(),
minimum_version: Self::MINIMUM_VERSION.into(),
}
.into());
}
}
Err(e) => {
let version_fut = util::command::new_smol_command(&command.path)
.args(command.args.iter())
.arg("--version")
.kill_on_drop(true)
.output();
let help_fut = util::command::new_smol_command(&command.path)
.args(command.args.iter())
.arg("--help")
.kill_on_drop(true)
.output();
let help_fut = util::command::new_smol_command(&command.path)
.args(command.args.iter())
.arg("--help")
.kill_on_drop(true)
.output();
let (version_output, help_output) = futures::future::join(version_fut, help_fut).await;
let (version_output, help_output) =
futures::future::join(version_fut, help_fut).await;
let Some(version_output) = version_output.ok().and_then(|output| String::from_utf8(output.stdout).ok()) else {
return result;
};
let Some((help_stdout, help_stderr)) = help_output.ok().and_then(|output| String::from_utf8(output.stdout).ok().zip(String::from_utf8(output.stderr).ok())) else {
return result;
};
let current_version = String::from_utf8(version_output?.stdout)?;
let supported = String::from_utf8(help_output?.stdout)?.contains(ACP_ARG);
let current_version = version_output.trim().to_string();
let supported = help_stdout.contains(ACP_ARG) || current_version.parse::<semver::Version>().is_ok_and(|version| version >= Self::MINIMUM_VERSION.parse::<semver::Version>().unwrap());
log::error!("failed to create ACP connection to gemini (version is {current_version}, supported: {supported}): {e}");
log::debug!("gemini --help stdout: {help_stdout:?}");
log::debug!("gemini --help stderr: {help_stderr:?}");
if !supported {
return Err(LoadError::Unsupported {
current_version: current_version.into(),
command: command.path.to_string_lossy().to_string().into(),
minimum_version: Self::MINIMUM_VERSION.into(),
}
.into());
}
if !supported {
return Err(LoadError::Unsupported {
error_message: format!(
"Your installed version of Gemini {} doesn't support the Agentic Coding Protocol (ACP).",
current_version
).into(),
upgrade_message: "Upgrade Gemini to Latest".into(),
upgrade_command: "npm install -g @google/gemini-cli@latest".into(),
}.into())
}
}
result
@@ -145,21 +92,13 @@ impl AgentServer for Gemini {
}
}
impl Gemini {
const PACKAGE_NAME: &str = "@google/gemini-cli";
const MINIMUM_VERSION: &str = "0.2.1";
const BINARY_NAME: &str = "gemini";
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::AgentServerCommand;
use std::path::Path;
crate::common_e2e_tests!(async |_, _, _| Gemini, allow_option_id = "proceed_once");
crate::common_e2e_tests!(Gemini, allow_option_id = "proceed_once");
pub fn local_command() -> AgentServerCommand {
let cli_path = Path::new(env!("CARGO_MANIFEST_DIR"))

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