Merge branch 'main' into ui-scrollbar-teardown

This commit is contained in:
MrSubidubi 2025-08-25 22:54:12 +02:00
commit b4223d318f
557 changed files with 15692 additions and 9036 deletions

View file

@ -19,14 +19,27 @@ self-hosted-runner:
- namespace-profile-16x32-ubuntu-2004-arm - namespace-profile-16x32-ubuntu-2004-arm
- namespace-profile-32x64-ubuntu-2004-arm - namespace-profile-32x64-ubuntu-2004-arm
# Namespace Ubuntu 22.04 (Everything else) # Namespace Ubuntu 22.04 (Everything else)
- namespace-profile-2x4-ubuntu-2204
- namespace-profile-4x8-ubuntu-2204 - namespace-profile-4x8-ubuntu-2204
- namespace-profile-8x16-ubuntu-2204 - namespace-profile-8x16-ubuntu-2204
- namespace-profile-16x32-ubuntu-2204 - namespace-profile-16x32-ubuntu-2204
- namespace-profile-32x64-ubuntu-2204 - namespace-profile-32x64-ubuntu-2204
# Namespace Ubuntu 24.04 (like ubuntu-latest)
- namespace-profile-2x4-ubuntu-2404
# Namespace Limited Preview # Namespace Limited Preview
- namespace-profile-8x16-ubuntu-2004-arm-m4 - namespace-profile-8x16-ubuntu-2004-arm-m4
- namespace-profile-8x32-ubuntu-2004-arm-m4 - namespace-profile-8x32-ubuntu-2004-arm-m4
# Self Hosted Runners # Self Hosted Runners
- self-mini-macos - self-mini-macos
- self-32vcpu-windows-2022 - 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

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

View file

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

View file

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

View file

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

View file

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

141
Cargo.lock generated
View file

@ -39,6 +39,26 @@ dependencies = [
"workspace-hack", "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",
]
[[package]] [[package]]
name = "action_log" name = "action_log"
version = "0.1.0" version = "0.1.0"
@ -171,11 +191,12 @@ dependencies = [
[[package]] [[package]]
name = "agent-client-protocol" name = "agent-client-protocol"
version = "0.0.26" version = "0.0.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "160971bb53ca0b2e70ebc857c21e24eb448745f1396371015f4c59e9a9e51ed0" checksum = "289eb34ee17213dadcca47eedadd386a5e7678094095414e475965d1bcca2860"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-broadcast",
"futures 0.3.31", "futures 0.3.31",
"log", "log",
"parking_lot", "parking_lot",
@ -239,10 +260,12 @@ dependencies = [
"smol", "smol",
"sqlez", "sqlez",
"task", "task",
"telemetry",
"tempfile", "tempfile",
"terminal", "terminal",
"text", "text",
"theme", "theme",
"thiserror 2.0.12",
"tree-sitter-rust", "tree-sitter-rust",
"ui", "ui",
"unindent", "unindent",
@ -262,16 +285,19 @@ name = "agent_servers"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"acp_thread", "acp_thread",
"acp_tools",
"action_log", "action_log",
"agent-client-protocol", "agent-client-protocol",
"agent_settings", "agent_settings",
"agentic-coding-protocol",
"anyhow", "anyhow",
"client",
"collections", "collections",
"context_server", "context_server",
"env_logger 0.11.8", "env_logger 0.11.8",
"fs",
"futures 0.3.31", "futures 0.3.31",
"gpui", "gpui",
"gpui_tokio",
"indoc", "indoc",
"itertools 0.14.0", "itertools 0.14.0",
"language", "language",
@ -283,6 +309,7 @@ dependencies = [
"paths", "paths",
"project", "project",
"rand 0.8.5", "rand 0.8.5",
"reqwest_client",
"schemars", "schemars",
"semver", "semver",
"serde", "serde",
@ -376,6 +403,7 @@ dependencies = [
"parking_lot", "parking_lot",
"paths", "paths",
"picker", "picker",
"postage",
"pretty_assertions", "pretty_assertions",
"project", "project",
"prompt_store", "prompt_store",
@ -415,24 +443,6 @@ dependencies = [
"zed_actions", "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]] [[package]]
name = "ahash" name = "ahash"
version = "0.7.8" version = "0.7.8"
@ -848,7 +858,7 @@ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
"collections", "collections",
"derive_more 0.99.19", "derive_more",
"extension", "extension",
"futures 0.3.31", "futures 0.3.31",
"gpui", "gpui",
@ -911,7 +921,7 @@ dependencies = [
"clock", "clock",
"collections", "collections",
"ctor", "ctor",
"derive_more 0.99.19", "derive_more",
"gpui", "gpui",
"icons", "icons",
"indoc", "indoc",
@ -948,7 +958,7 @@ dependencies = [
"cloud_llm_client", "cloud_llm_client",
"collections", "collections",
"component", "component",
"derive_more 0.99.19", "derive_more",
"diffy", "diffy",
"editor", "editor",
"feature_flags", "feature_flags",
@ -1374,10 +1384,11 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"collections", "collections",
"derive_more 0.99.19",
"gpui", "gpui",
"parking_lot",
"rodio", "rodio",
"schemars",
"serde",
"settings",
"util", "util",
"workspace-hack", "workspace-hack",
] ]
@ -3060,7 +3071,7 @@ dependencies = [
"cocoa 0.26.0", "cocoa 0.26.0",
"collections", "collections",
"credentials_provider", "credentials_provider",
"derive_more 0.99.19", "derive_more",
"feature_flags", "feature_flags",
"fs", "fs",
"futures 0.3.31", "futures 0.3.31",
@ -3492,7 +3503,7 @@ name = "command_palette_hooks"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"collections", "collections",
"derive_more 0.99.19", "derive_more",
"gpui", "gpui",
"workspace-hack", "workspace-hack",
] ]
@ -4043,6 +4054,7 @@ dependencies = [
name = "crashes" name = "crashes"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"bincode",
"crash-handler", "crash-handler",
"log", "log",
"mach2 0.5.0", "mach2 0.5.0",
@ -4052,6 +4064,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"smol", "smol",
"system_specs",
"workspace-hack", "workspace-hack",
] ]
@ -4653,27 +4666,6 @@ dependencies = [
"syn 2.0.101", "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]] [[package]]
name = "derive_refineable" name = "derive_refineable"
version = "0.1.0" version = "0.1.0"
@ -4694,7 +4686,6 @@ dependencies = [
"component", "component",
"ctor", "ctor",
"editor", "editor",
"futures 0.3.31",
"gpui", "gpui",
"indoc", "indoc",
"language", "language",
@ -5731,14 +5722,10 @@ dependencies = [
name = "feedback" name = "feedback"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"client",
"editor", "editor",
"gpui", "gpui",
"human_bytes",
"menu", "menu",
"release_channel", "system_specs",
"serde",
"sysinfo",
"ui", "ui",
"urlencoding", "urlencoding",
"util", "util",
@ -6414,7 +6401,7 @@ dependencies = [
"askpass", "askpass",
"async-trait", "async-trait",
"collections", "collections",
"derive_more 0.99.19", "derive_more",
"futures 0.3.31", "futures 0.3.31",
"git2", "git2",
"gpui", "gpui",
@ -7444,7 +7431,7 @@ dependencies = [
"core-video", "core-video",
"cosmic-text", "cosmic-text",
"ctor", "ctor",
"derive_more 0.99.19", "derive_more",
"embed-resource", "embed-resource",
"env_logger 0.11.8", "env_logger 0.11.8",
"etagere", "etagere",
@ -7532,6 +7519,7 @@ dependencies = [
name = "gpui_tokio" name = "gpui_tokio"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow",
"gpui", "gpui",
"tokio", "tokio",
"util", "util",
@ -7968,7 +7956,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes 1.10.1", "bytes 1.10.1",
"derive_more 0.99.19", "derive_more",
"futures 0.3.31", "futures 0.3.31",
"http 1.3.1", "http 1.3.1",
"http-body 1.0.1", "http-body 1.0.1",
@ -8480,6 +8468,7 @@ dependencies = [
"theme", "theme",
"ui", "ui",
"util", "util",
"util_macros",
"workspace", "workspace",
"workspace-hack", "workspace-hack",
"zed_actions", "zed_actions",
@ -9616,6 +9605,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
"audio",
"collections", "collections",
"core-foundation 0.10.0", "core-foundation 0.10.0",
"core-video", "core-video",
@ -9638,6 +9628,7 @@ dependencies = [
"scap", "scap",
"serde", "serde",
"serde_json", "serde_json",
"settings",
"sha2", "sha2",
"simplelog", "simplelog",
"smallvec", "smallvec",
@ -11624,6 +11615,12 @@ dependencies = [
"hmac", "hmac",
] ]
[[package]]
name = "pciid-parser"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0008e816fcdaf229cdd540e9b6ca2dc4a10d65c31624abb546c6420a02846e61"
[[package]] [[package]]
name = "pem" name = "pem"
version = "3.0.5" version = "3.0.5"
@ -13524,6 +13521,7 @@ dependencies = [
"smol", "smol",
"sysinfo", "sysinfo",
"telemetry_events", "telemetry_events",
"thiserror 2.0.12",
"toml 0.8.20", "toml 0.8.20",
"unindent", "unindent",
"util", "util",
@ -14363,12 +14361,10 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe8c9d1c68d67dd9f97ecbc6f932b60eb289c5dbddd8aa1405484a8fd2fcd984" checksum = "fe8c9d1c68d67dd9f97ecbc6f932b60eb289c5dbddd8aa1405484a8fd2fcd984"
dependencies = [ dependencies = [
"chrono",
"dyn-clone", "dyn-clone",
"indexmap", "indexmap",
"ref-cast", "ref-cast",
"schemars_derive", "schemars_derive",
"semver",
"serde", "serde",
"serde_json", "serde_json",
] ]
@ -16144,6 +16140,21 @@ dependencies = [
"winx", "winx",
] ]
[[package]]
name = "system_specs"
version = "0.1.0"
dependencies = [
"anyhow",
"client",
"gpui",
"human_bytes",
"pciid-parser",
"release_channel",
"serde",
"sysinfo",
"workspace-hack",
]
[[package]] [[package]]
name = "tab_switcher" name = "tab_switcher"
version = "0.1.0" version = "0.1.0"
@ -16437,7 +16448,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"collections", "collections",
"derive_more 0.99.19", "derive_more",
"fs", "fs",
"futures 0.3.31", "futures 0.3.31",
"gpui", "gpui",
@ -19779,7 +19790,6 @@ dependencies = [
"any_vec", "any_vec",
"anyhow", "anyhow",
"async-recursion", "async-recursion",
"bincode",
"call", "call",
"client", "client",
"clock", "clock",
@ -19798,6 +19808,7 @@ dependencies = [
"node_runtime", "node_runtime",
"parking_lot", "parking_lot",
"postage", "postage",
"pretty_assertions",
"project", "project",
"remote", "remote",
"schemars", "schemars",
@ -19953,7 +19964,6 @@ dependencies = [
"rustix 1.0.7", "rustix 1.0.7",
"rustls 0.23.26", "rustls 0.23.26",
"rustls-webpki 0.103.1", "rustls-webpki 0.103.1",
"schemars",
"scopeguard", "scopeguard",
"sea-orm", "sea-orm",
"sea-query-binder", "sea-query-binder",
@ -20387,8 +20397,9 @@ dependencies = [
[[package]] [[package]]
name = "zed" name = "zed"
version = "0.201.0" version = "0.202.0"
dependencies = [ dependencies = [
"acp_tools",
"activity_indicator", "activity_indicator",
"agent", "agent",
"agent_servers", "agent_servers",
@ -20404,6 +20415,7 @@ dependencies = [
"auto_update", "auto_update",
"auto_update_ui", "auto_update_ui",
"backtrace", "backtrace",
"bincode",
"breadcrumbs", "breadcrumbs",
"call", "call",
"channel", "channel",
@ -20502,6 +20514,7 @@ dependencies = [
"supermaven", "supermaven",
"svg_preview", "svg_preview",
"sysinfo", "sysinfo",
"system_specs",
"tab_switcher", "tab_switcher",
"task", "task",
"tasks_ui", "tasks_ui",

View file

@ -1,6 +1,7 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
members = [ members = [
"crates/acp_tools",
"crates/acp_thread", "crates/acp_thread",
"crates/action_log", "crates/action_log",
"crates/activity_indicator", "crates/activity_indicator",
@ -155,6 +156,7 @@ members = [
"crates/streaming_diff", "crates/streaming_diff",
"crates/sum_tree", "crates/sum_tree",
"crates/supermaven", "crates/supermaven",
"crates/system_specs",
"crates/supermaven_api", "crates/supermaven_api",
"crates/svg_preview", "crates/svg_preview",
"crates/tab_switcher", "crates/tab_switcher",
@ -226,6 +228,7 @@ edition = "2024"
# Workspace member crates # Workspace member crates
# #
acp_tools = { path = "crates/acp_tools" }
acp_thread = { path = "crates/acp_thread" } acp_thread = { path = "crates/acp_thread" }
action_log = { path = "crates/action_log" } action_log = { path = "crates/action_log" }
agent = { path = "crates/agent" } agent = { path = "crates/agent" }
@ -381,6 +384,7 @@ streaming_diff = { path = "crates/streaming_diff" }
sum_tree = { path = "crates/sum_tree" } sum_tree = { path = "crates/sum_tree" }
supermaven = { path = "crates/supermaven" } supermaven = { path = "crates/supermaven" }
supermaven_api = { path = "crates/supermaven_api" } supermaven_api = { path = "crates/supermaven_api" }
system_specs = { path = "crates/system_specs" }
tab_switcher = { path = "crates/tab_switcher" } tab_switcher = { path = "crates/tab_switcher" }
task = { path = "crates/task" } task = { path = "crates/task" }
tasks_ui = { path = "crates/tasks_ui" } tasks_ui = { path = "crates/tasks_ui" }
@ -422,8 +426,7 @@ zlog_settings = { path = "crates/zlog_settings" }
# External crates # External crates
# #
agentic-coding-protocol = "0.0.10" agent-client-protocol = "0.0.31"
agent-client-protocol = "0.0.26"
aho-corasick = "1.1" aho-corasick = "1.1"
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" } alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
any_vec = "0.14" any_vec = "0.14"
@ -450,6 +453,7 @@ aws-sdk-bedrockruntime = { version = "1.80.0", features = [
aws-smithy-runtime-api = { version = "1.7.4", features = ["http-1x", "client"] } aws-smithy-runtime-api = { version = "1.7.4", features = ["http-1x", "client"] }
aws-smithy-types = { version = "1.3.0", features = ["http-body-1-x"] } aws-smithy-types = { version = "1.3.0", features = ["http-body-1-x"] }
base64 = "0.22" base64 = "0.22"
bincode = "1.2.1"
bitflags = "2.6.0" bitflags = "2.6.0"
blade-graphics = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" } blade-graphics = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" } blade-macros = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
@ -493,6 +497,7 @@ handlebars = "4.3"
heck = "0.5" heck = "0.5"
heed = { version = "0.21.0", features = ["read-txn-no-tls"] } heed = { version = "0.21.0", features = ["read-txn-no-tls"] }
hex = "0.4.3" hex = "0.4.3"
human_bytes = "0.4.1"
html5ever = "0.27.0" html5ever = "0.27.0"
http = "1.1" http = "1.1"
http-body = "1.0" http-body = "1.0"
@ -532,6 +537,7 @@ palette = { version = "0.7.5", default-features = false, features = ["std"] }
parking_lot = "0.12.1" parking_lot = "0.12.1"
partial-json-fixer = "0.5.3" partial-json-fixer = "0.5.3"
parse_int = "0.9" parse_int = "0.9"
pciid-parser = "0.8.0"
pathdiff = "0.2" pathdiff = "0.2"
pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" } 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" } pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
@ -802,60 +808,32 @@ unexpected_cfgs = { level = "allow" }
dbg_macro = "deny" dbg_macro = "deny"
todo = "deny" todo = "deny"
# Motivation: We use `vec![a..b]` a lot when dealing with ranges in text, so # This is not a style lint, see https://github.com/rust-lang/rust-clippy/pull/15454
# warning on this rule produces a lot of noise. # Remove when the lint gets promoted to `suspicious`.
single_range_in_vec_init = "allow" declare_interior_mutable_const = "deny"
# These are all of the rules that currently have violations in the Zed redundant_clone = "deny"
# 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 # We currently do not restrict any style rules
# allow all of those, for now. # 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 } style = { level = "allow", priority = -1 }
# Temporary list of style lints that we've fixed so far.
comparison_to_empty = "warn"
into_iter_on_ref = "warn"
iter_cloned_collect = "warn"
iter_next_slice = "warn"
iter_nth = "warn"
iter_nth_zero = "warn"
iter_skip_next = "warn"
let_and_return = "warn"
module_inception = { level = "deny" }
question_mark = { level = "deny" }
single_match = "warn"
redundant_closure = { level = "deny" }
redundant_static_lifetimes = { level = "warn" }
redundant_pattern_matching = "warn"
redundant_field_names = "warn"
declare_interior_mutable_const = { level = "deny" }
collapsible_if = { level = "warn"}
collapsible_else_if = { level = "warn" }
needless_borrow = { level = "warn"}
needless_return = { level = "warn" }
unnecessary_mut_passed = {level = "warn"}
unnecessary_map_or = { level = "warn" }
unused_unit = "warn"
# Individual rules that have violations in the codebase: # Individual rules that have violations in the codebase:
type_complexity = "allow" 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" let_underscore_future = "allow"
# It doesn't make sense to implement `Default` unilaterally.
new_without_default = "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"
# in Rust it can be very tedious to reduce argument count without # in Rust it can be very tedious to reduce argument count without
# running afoul of the borrow checker. # running afoul of the borrow checker.
@ -864,10 +842,6 @@ too_many_arguments = "allow"
# We often have large enum variants yet we rarely actually bother with splitting them up. # We often have large enum variants yet we rarely actually bother with splitting them up.
large_enum_variant = "allow" large_enum_variant = "allow"
# `enum_variant_names` fires for all enums, even when they derive serde traits.
# Adhering to this lint would be a breaking change.
enum_variant_names = "allow"
[workspace.metadata.cargo-machete] [workspace.metadata.cargo-machete]
ignored = [ ignored = [
"bindgen", "bindgen",

2
Procfile.web Normal file
View file

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

3
assets/icons/attach.svg Normal file
View file

@ -0,0 +1,3 @@
<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>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -1 +1,4 @@
<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> <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>

Before

Width:  |  Height:  |  Size: 515 B

After

Width:  |  Height:  |  Size: 802 B

Before After
Before After

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"> <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"/> <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>

Before

Width:  |  Height:  |  Size: 227 B

After

Width:  |  Height:  |  Size: 251 B

Before After
Before After

View file

@ -0,0 +1,6 @@
<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>

After

Width:  |  Height:  |  Size: 1.1 KiB

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"> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<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"/> <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"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 768 B

After

Width:  |  Height:  |  Size: 762 B

Before After
Before After

View file

@ -1,27 +1,27 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <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.2"/> <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 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 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 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 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 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 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 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 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 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 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 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 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 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="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 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 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 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 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="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 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 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 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.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 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="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 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="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 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 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 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 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 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 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 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 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 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 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="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 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"/> <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> </svg>

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Before After
Before After

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"> <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="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.25" 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.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.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 1 KiB

Before After
Before After

View file

@ -16,7 +16,6 @@
"up": "menu::SelectPrevious", "up": "menu::SelectPrevious",
"enter": "menu::Confirm", "enter": "menu::Confirm",
"ctrl-enter": "menu::SecondaryConfirm", "ctrl-enter": "menu::SecondaryConfirm",
"ctrl-escape": "menu::Cancel",
"ctrl-c": "menu::Cancel", "ctrl-c": "menu::Cancel",
"escape": "menu::Cancel", "escape": "menu::Cancel",
"alt-shift-enter": "menu::Restart", "alt-shift-enter": "menu::Restart",
@ -138,7 +137,7 @@
"find": "buffer_search::Deploy", "find": "buffer_search::Deploy",
"ctrl-f": "buffer_search::Deploy", "ctrl-f": "buffer_search::Deploy",
"ctrl-h": "buffer_search::DeployReplace", "ctrl-h": "buffer_search::DeployReplace",
"ctrl->": "assistant::QuoteSelection", "ctrl->": "agent::QuoteSelection",
"ctrl-<": "assistant::InsertIntoEditor", "ctrl-<": "assistant::InsertIntoEditor",
"ctrl-alt-e": "editor::SelectEnclosingSymbol", "ctrl-alt-e": "editor::SelectEnclosingSymbol",
"ctrl-shift-backspace": "editor::GoToPreviousChange", "ctrl-shift-backspace": "editor::GoToPreviousChange",
@ -241,7 +240,7 @@
"ctrl-shift-i": "agent::ToggleOptionsMenu", "ctrl-shift-i": "agent::ToggleOptionsMenu",
"ctrl-alt-shift-n": "agent::ToggleNewThreadMenu", "ctrl-alt-shift-n": "agent::ToggleNewThreadMenu",
"shift-alt-escape": "agent::ExpandMessageEditor", "shift-alt-escape": "agent::ExpandMessageEditor",
"ctrl->": "assistant::QuoteSelection", "ctrl->": "agent::QuoteSelection",
"ctrl-alt-e": "agent::RemoveAllContext", "ctrl-alt-e": "agent::RemoveAllContext",
"ctrl-shift-e": "project_panel::ToggleFocus", "ctrl-shift-e": "project_panel::ToggleFocus",
"ctrl-shift-enter": "agent::ContinueThread", "ctrl-shift-enter": "agent::ContinueThread",
@ -856,7 +855,7 @@
"ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }], "ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }],
"ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }], "ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }],
"alt-ctrl-r": "project_panel::RevealInFileManager", "alt-ctrl-r": "project_panel::RevealInFileManager",
"ctrl-shift-enter": "project_panel::OpenWithSystem", "ctrl-shift-enter": "workspace::OpenWithSystem",
"alt-d": "project_panel::CompareMarkedFiles", "alt-d": "project_panel::CompareMarkedFiles",
"shift-find": "project_panel::NewSearchInDirectory", "shift-find": "project_panel::NewSearchInDirectory",
"ctrl-alt-shift-f": "project_panel::NewSearchInDirectory", "ctrl-alt-shift-f": "project_panel::NewSearchInDirectory",
@ -1195,9 +1194,16 @@
"ctrl-1": "onboarding::ActivateBasicsPage", "ctrl-1": "onboarding::ActivateBasicsPage",
"ctrl-2": "onboarding::ActivateEditingPage", "ctrl-2": "onboarding::ActivateEditingPage",
"ctrl-3": "onboarding::ActivateAISetupPage", "ctrl-3": "onboarding::ActivateAISetupPage",
"ctrl-escape": "onboarding::Finish", "ctrl-enter": "onboarding::Finish",
"alt-tab": "onboarding::SignIn", "alt-shift-l": "onboarding::SignIn",
"alt-shift-a": "onboarding::OpenAccount" "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-f": "buffer_search::DeployReplace",
"cmd-alt-l": ["buffer_search::Deploy", { "selection_search_enabled": true }], "cmd-alt-l": ["buffer_search::Deploy", { "selection_search_enabled": true }],
"cmd-e": ["buffer_search::Deploy", { "focus": false }], "cmd-e": ["buffer_search::Deploy", { "focus": false }],
"cmd->": "assistant::QuoteSelection", "cmd->": "agent::QuoteSelection",
"cmd-<": "assistant::InsertIntoEditor", "cmd-<": "assistant::InsertIntoEditor",
"cmd-alt-e": "editor::SelectEnclosingSymbol", "cmd-alt-e": "editor::SelectEnclosingSymbol",
"alt-enter": "editor::OpenSelectionsInMultibuffer" "alt-enter": "editor::OpenSelectionsInMultibuffer"
@ -281,7 +281,7 @@
"cmd-shift-i": "agent::ToggleOptionsMenu", "cmd-shift-i": "agent::ToggleOptionsMenu",
"cmd-alt-shift-n": "agent::ToggleNewThreadMenu", "cmd-alt-shift-n": "agent::ToggleNewThreadMenu",
"shift-alt-escape": "agent::ExpandMessageEditor", "shift-alt-escape": "agent::ExpandMessageEditor",
"cmd->": "assistant::QuoteSelection", "cmd->": "agent::QuoteSelection",
"cmd-alt-e": "agent::RemoveAllContext", "cmd-alt-e": "agent::RemoveAllContext",
"cmd-shift-e": "project_panel::ToggleFocus", "cmd-shift-e": "project_panel::ToggleFocus",
"cmd-ctrl-b": "agent::ToggleBurnMode", "cmd-ctrl-b": "agent::ToggleBurnMode",
@ -915,7 +915,7 @@
"cmd-backspace": ["project_panel::Trash", { "skip_prompt": true }], "cmd-backspace": ["project_panel::Trash", { "skip_prompt": true }],
"cmd-delete": ["project_panel::Delete", { "skip_prompt": false }], "cmd-delete": ["project_panel::Delete", { "skip_prompt": false }],
"alt-cmd-r": "project_panel::RevealInFileManager", "alt-cmd-r": "project_panel::RevealInFileManager",
"ctrl-shift-enter": "project_panel::OpenWithSystem", "ctrl-shift-enter": "workspace::OpenWithSystem",
"alt-d": "project_panel::CompareMarkedFiles", "alt-d": "project_panel::CompareMarkedFiles",
"cmd-alt-backspace": ["project_panel::Delete", { "skip_prompt": false }], "cmd-alt-backspace": ["project_panel::Delete", { "skip_prompt": false }],
"cmd-alt-shift-f": "project_panel::NewSearchInDirectory", "cmd-alt-shift-f": "project_panel::NewSearchInDirectory",
@ -1301,5 +1301,12 @@
"alt-tab": "onboarding::SignIn", "alt-tab": "onboarding::SignIn",
"alt-shift-a": "onboarding::OpenAccount" "alt-shift-a": "onboarding::OpenAccount"
} }
},
{
"context": "InvalidBuffer",
"use_key_equivalents": true,
"bindings": {
"ctrl-shift-enter": "workspace::OpenWithSystem"
}
} }
] ]

View file

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

View file

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

View file

@ -819,7 +819,7 @@
"v": "project_panel::OpenPermanent", "v": "project_panel::OpenPermanent",
"p": "project_panel::Open", "p": "project_panel::Open",
"x": "project_panel::RevealInFileManager", "x": "project_panel::RevealInFileManager",
"s": "project_panel::OpenWithSystem", "s": "workspace::OpenWithSystem",
"z d": "project_panel::CompareMarkedFiles", "z d": "project_panel::CompareMarkedFiles",
"] c": "project_panel::SelectNextGitEntry", "] c": "project_panel::SelectNextGitEntry",
"[ c": "project_panel::SelectPrevGitEntry", "[ c": "project_panel::SelectPrevGitEntry",

View file

@ -162,6 +162,12 @@
// 2. Always quit the application // 2. Always quit the application
// "on_last_window_closed": "quit_app", // "on_last_window_closed": "quit_app",
"on_last_window_closed": "platform_default", "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. // Whether to use the system provided dialogs for Open and Save As.
// When set to false, Zed will use the built-in keyboard-first pickers. // When set to false, Zed will use the built-in keyboard-first pickers.
"use_system_path_prompts": true, "use_system_path_prompts": true,
@ -1133,11 +1139,6 @@
// The minimum severity of the diagnostics to show inline. // The minimum severity of the diagnostics to show inline.
// Inherits editor's diagnostics' max severity settings when `null`. // Inherits editor's diagnostics' max severity settings when `null`.
"max_severity": 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 // Files or globs of files that will be excluded by Zed entirely. They will be skipped during file
@ -1503,6 +1504,11 @@
// //
// Default: fallback // Default: fallback
"words": "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. // Whether to fetch LSP completions or not.
// //
// Default: true // Default: true
@ -1629,6 +1635,9 @@
"allowed": true "allowed": true
} }
}, },
"Kotlin": {
"language_servers": ["kotlin-language-server", "!kotlin-lsp", "..."]
},
"LaTeX": { "LaTeX": {
"formatter": "language_server", "formatter": "language_server",
"language_servers": ["texlab", "..."], "language_servers": ["texlab", "..."],
@ -1642,9 +1651,6 @@
"use_on_type_format": false, "use_on_type_format": false,
"allow_rewrap": "anywhere", "allow_rewrap": "anywhere",
"soft_wrap": "editor_width", "soft_wrap": "editor_width",
"completions": {
"words": "disabled"
},
"prettier": { "prettier": {
"allowed": true "allowed": true
} }
@ -1658,9 +1664,6 @@
} }
}, },
"Plain Text": { "Plain Text": {
"completions": {
"words": "disabled"
},
"allow_rewrap": "anywhere" "allow_rewrap": "anywhere"
}, },
"Python": { "Python": {

View file

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

View file

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

View file

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

View file

@ -183,16 +183,15 @@ impl ToolCall {
language_registry: Arc<LanguageRegistry>, language_registry: Arc<LanguageRegistry>,
cx: &mut App, cx: &mut App,
) -> Self { ) -> Self {
let title = if let Some((first_line, _)) = tool_call.title.split_once("\n") {
first_line.to_owned() + ""
} else {
tool_call.title
};
Self { Self {
id: tool_call.id, id: tool_call.id,
label: cx.new(|cx| { label: cx
Markdown::new( .new(|cx| Markdown::new(title.into(), Some(language_registry.clone()), None, cx)),
tool_call.title.into(),
Some(language_registry.clone()),
None,
cx,
)
}),
kind: tool_call.kind, kind: tool_call.kind,
content: tool_call content: tool_call
.content .content
@ -233,15 +232,30 @@ impl ToolCall {
if let Some(title) = title { if let Some(title) = title {
self.label.update(cx, |label, cx| { self.label.update(cx, |label, cx| {
label.replace(title, cx); if let Some((first_line, _)) = title.split_once("\n") {
label.replace(first_line.to_owned() + "", cx)
} else {
label.replace(title, cx);
}
}); });
} }
if let Some(content) = content { if let Some(content) = content {
self.content = content let new_content_len = content.len();
.into_iter() let mut content = content.into_iter();
.map(|chunk| ToolCallContent::from_acp(chunk, language_registry.clone(), cx))
.collect(); // Reuse existing content if we can
for (old, new) in self.content.iter_mut().zip(content.by_ref()) {
old.update_from_acp(new, language_registry.clone(), cx);
}
for new in content {
self.content.push(ToolCallContent::from_acp(
new,
language_registry.clone(),
cx,
))
}
self.content.truncate(new_content_len);
} }
if let Some(locations) = locations { if let Some(locations) = locations {
@ -301,11 +315,9 @@ impl ToolCall {
) -> Option<AgentLocation> { ) -> Option<AgentLocation> {
let buffer = project let buffer = project
.update(cx, |project, cx| { .update(cx, |project, cx| {
if let Some(path) = project.project_path_for_absolute_path(&location.path, cx) { project
Some(project.open_buffer(path, cx)) .project_path_for_absolute_path(&location.path, cx)
} else { .map(|path| project.open_buffer(path, cx))
None
}
}) })
.ok()??; .ok()??;
let buffer = buffer.await.log_err()?; let buffer = buffer.await.log_err()?;
@ -471,7 +483,7 @@ impl ContentBlock {
fn block_string_contents(&self, block: acp::ContentBlock) -> String { fn block_string_contents(&self, block: acp::ContentBlock) -> String {
match block { match block {
acp::ContentBlock::Text(text_content) => text_content.text.clone(), acp::ContentBlock::Text(text_content) => text_content.text,
acp::ContentBlock::ResourceLink(resource_link) => { acp::ContentBlock::ResourceLink(resource_link) => {
Self::resource_link_md(&resource_link.uri) Self::resource_link_md(&resource_link.uri)
} }
@ -500,7 +512,7 @@ impl ContentBlock {
"`Image`".into() "`Image`".into()
} }
fn to_markdown<'a>(&'a self, cx: &'a App) -> &'a str { pub fn to_markdown<'a>(&'a self, cx: &'a App) -> &'a str {
match self { match self {
ContentBlock::Empty => "", ContentBlock::Empty => "",
ContentBlock::Markdown { markdown } => markdown.read(cx).source(), ContentBlock::Markdown { markdown } => markdown.read(cx).source(),
@ -553,6 +565,28 @@ impl ToolCallContent {
} }
} }
pub fn update_from_acp(
&mut self,
new: acp::ToolCallContent,
language_registry: Arc<LanguageRegistry>,
cx: &mut App,
) {
let needs_update = match (&self, &new) {
(Self::Diff(old_diff), acp::ToolCallContent::Diff { diff: new_diff }) => {
old_diff.read(cx).needs_update(
new_diff.old_text.as_deref().unwrap_or(""),
&new_diff.new_text,
cx,
)
}
_ => true,
};
if needs_update {
*self = Self::from_acp(new, language_registry, cx);
}
}
pub fn to_markdown(&self, cx: &App) -> String { pub fn to_markdown(&self, cx: &App) -> String {
match self { match self {
Self::ContentBlock(content) => content.to_markdown(cx).to_string(), Self::ContentBlock(content) => content.to_markdown(cx).to_string(),
@ -674,6 +708,37 @@ pub struct TokenUsage {
pub used_tokens: u64, pub used_tokens: u64,
} }
impl TokenUsage {
pub fn ratio(&self) -> TokenUsageRatio {
#[cfg(debug_assertions)]
let warning_threshold: f32 = std::env::var("ZED_THREAD_WARNING_THRESHOLD")
.unwrap_or("0.8".to_string())
.parse()
.unwrap();
#[cfg(not(debug_assertions))]
let warning_threshold: f32 = 0.8;
// When the maximum is unknown because there is no selected model,
// avoid showing the token limit warning.
if self.max_tokens == 0 {
TokenUsageRatio::Normal
} else if self.used_tokens >= self.max_tokens {
TokenUsageRatio::Exceeded
} else if self.used_tokens as f32 / self.max_tokens as f32 >= warning_threshold {
TokenUsageRatio::Warning
} else {
TokenUsageRatio::Normal
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TokenUsageRatio {
Normal,
Warning,
Exceeded,
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct RetryStatus { pub struct RetryStatus {
pub last_error: SharedString, pub last_error: SharedString,
@ -694,6 +759,8 @@ pub struct AcpThread {
connection: Rc<dyn AgentConnection>, connection: Rc<dyn AgentConnection>,
session_id: acp::SessionId, session_id: acp::SessionId,
token_usage: Option<TokenUsage>, token_usage: Option<TokenUsage>,
prompt_capabilities: acp::PromptCapabilities,
_observe_prompt_capabilities: Task<anyhow::Result<()>>,
} }
#[derive(Debug)] #[derive(Debug)]
@ -708,11 +775,12 @@ pub enum AcpThreadEvent {
Stopped, Stopped,
Error, Error,
LoadError(LoadError), LoadError(LoadError),
PromptCapabilitiesUpdated,
} }
impl EventEmitter<AcpThreadEvent> for AcpThread {} impl EventEmitter<AcpThreadEvent> for AcpThread {}
#[derive(PartialEq, Eq)] #[derive(PartialEq, Eq, Debug)]
pub enum ThreadStatus { pub enum ThreadStatus {
Idle, Idle,
WaitingForToolConfirmation, WaitingForToolConfirmation,
@ -759,7 +827,20 @@ impl AcpThread {
project: Entity<Project>, project: Entity<Project>,
action_log: Entity<ActionLog>, action_log: Entity<ActionLog>,
session_id: acp::SessionId, session_id: acp::SessionId,
mut prompt_capabilities_rx: watch::Receiver<acp::PromptCapabilities>,
cx: &mut Context<Self>,
) -> Self { ) -> Self {
let prompt_capabilities = *prompt_capabilities_rx.borrow();
let task = cx.spawn::<_, anyhow::Result<()>>(async move |this, cx| {
loop {
let caps = prompt_capabilities_rx.recv().await?;
this.update(cx, |this, cx| {
this.prompt_capabilities = caps;
cx.emit(AcpThreadEvent::PromptCapabilitiesUpdated);
})?;
}
});
Self { Self {
action_log, action_log,
shared_buffers: Default::default(), shared_buffers: Default::default(),
@ -771,9 +852,15 @@ impl AcpThread {
connection, connection,
session_id, session_id,
token_usage: None, token_usage: None,
prompt_capabilities,
_observe_prompt_capabilities: task,
} }
} }
pub fn prompt_capabilities(&self) -> acp::PromptCapabilities {
self.prompt_capabilities
}
pub fn connection(&self) -> &Rc<dyn AgentConnection> { pub fn connection(&self) -> &Rc<dyn AgentConnection> {
&self.connection &self.connection
} }
@ -958,10 +1045,19 @@ impl AcpThread {
cx.emit(AcpThreadEvent::NewEntry); cx.emit(AcpThreadEvent::NewEntry);
} }
pub fn update_title(&mut self, title: SharedString, cx: &mut Context<Self>) -> Result<()> { pub fn can_set_title(&mut self, cx: &mut Context<Self>) -> bool {
self.title = title; self.connection.set_title(&self.session_id, cx).is_some()
cx.emit(AcpThreadEvent::TitleUpdated); }
Ok(())
pub fn set_title(&mut self, title: SharedString, cx: &mut Context<Self>) -> Task<Result<()>> {
if title != self.title {
self.title = title.clone();
cx.emit(AcpThreadEvent::TitleUpdated);
if let Some(set_title) = self.connection.set_title(&self.session_id, cx) {
return set_title.run(title, cx);
}
}
Task::ready(Ok(()))
} }
pub fn update_token_usage(&mut self, usage: Option<TokenUsage>, cx: &mut Context<Self>) { pub fn update_token_usage(&mut self, usage: Option<TokenUsage>, cx: &mut Context<Self>) {
@ -989,7 +1085,7 @@ impl AcpThread {
let location_updated = update.fields.locations.is_some(); let location_updated = update.fields.locations.is_some();
current_call.update_fields(update.fields, languages, cx); current_call.update_fields(update.fields, languages, cx);
if location_updated { if location_updated {
self.resolve_locations(update.id.clone(), cx); self.resolve_locations(update.id, cx);
} }
} }
ToolCallUpdate::UpdateDiff(update) => { ToolCallUpdate::UpdateDiff(update) => {
@ -1264,11 +1360,7 @@ impl AcpThread {
}; };
let git_store = self.project.read(cx).git_store().clone(); let git_store = self.project.read(cx).git_store().clone();
let message_id = if self let message_id = if self.connection.truncate(&self.session_id, cx).is_some() {
.connection
.session_editor(&self.session_id, cx)
.is_some()
{
Some(UserMessageId::new()) Some(UserMessageId::new())
} else { } else {
None None
@ -1306,6 +1398,10 @@ impl AcpThread {
}) })
} }
pub fn can_resume(&self, cx: &App) -> bool {
self.connection.resume(&self.session_id, cx).is_some()
}
pub fn resume(&mut self, cx: &mut Context<Self>) -> BoxFuture<'static, Result<()>> { pub fn resume(&mut self, cx: &mut Context<Self>) -> BoxFuture<'static, Result<()>> {
self.run_turn(cx, async move |this, cx| { self.run_turn(cx, async move |this, cx| {
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
@ -1352,7 +1448,7 @@ impl AcpThread {
let canceled = matches!( let canceled = matches!(
result, result,
Ok(Ok(acp::PromptResponse { Ok(Ok(acp::PromptResponse {
stop_reason: acp::StopReason::Canceled stop_reason: acp::StopReason::Cancelled
})) }))
); );
@ -1365,6 +1461,17 @@ impl AcpThread {
this.send_task.take(); this.send_task.take();
} }
// Truncate entries if the last prompt was refused.
if let Ok(Ok(acp::PromptResponse {
stop_reason: acp::StopReason::Refusal,
})) = result
&& let Some((ix, _)) = this.last_user_message()
{
let range = ix..this.entries.len();
this.entries.truncate(ix);
cx.emit(AcpThreadEvent::EntriesRemoved(range));
}
cx.emit(AcpThreadEvent::Stopped); cx.emit(AcpThreadEvent::Stopped);
Ok(()) Ok(())
} }
@ -1403,7 +1510,7 @@ impl AcpThread {
/// Rewinds this thread to before the entry at `index`, removing it and all /// Rewinds this thread to before the entry at `index`, removing it and all
/// subsequent entries while reverting any changes made from that point. /// subsequent entries while reverting any changes made from that point.
pub fn rewind(&mut self, id: UserMessageId, cx: &mut Context<Self>) -> Task<Result<()>> { pub fn rewind(&mut self, id: UserMessageId, cx: &mut Context<Self>) -> Task<Result<()>> {
let Some(session_editor) = self.connection.session_editor(&self.session_id, cx) else { let Some(truncate) = self.connection.truncate(&self.session_id, cx) else {
return Task::ready(Err(anyhow!("not supported"))); return Task::ready(Err(anyhow!("not supported")));
}; };
let Some(message) = self.user_message(&id) else { let Some(message) = self.user_message(&id) else {
@ -1423,8 +1530,7 @@ impl AcpThread {
.await?; .await?;
} }
cx.update(|cx| session_editor.truncate(id.clone(), cx))? cx.update(|cx| truncate.run(id.clone(), cx))?.await?;
.await?;
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
if let Some((ix, _)) = this.user_message_mut(&id) { if let Some((ix, _)) = this.user_message_mut(&id) {
let range = ix..this.entries.len(); let range = ix..this.entries.len();
@ -2340,6 +2446,92 @@ mod tests {
assert_eq!(fs.files(), vec![Path::new(path!("/test/file-0"))]); assert_eq!(fs.files(), vec![Path::new(path!("/test/file-0"))]);
} }
#[gpui::test]
async fn test_refusal(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.background_executor.clone());
fs.insert_tree(path!("/"), json!({})).await;
let project = Project::test(fs.clone(), [path!("/").as_ref()], cx).await;
let refuse_next = Arc::new(AtomicBool::new(false));
let connection = Rc::new(FakeAgentConnection::new().on_user_message({
let refuse_next = refuse_next.clone();
move |request, thread, mut cx| {
let refuse_next = refuse_next.clone();
async move {
if refuse_next.load(SeqCst) {
return Ok(acp::PromptResponse {
stop_reason: acp::StopReason::Refusal,
});
}
let acp::ContentBlock::Text(content) = &request.prompt[0] else {
panic!("expected text content block");
};
thread.update(&mut cx, |thread, cx| {
thread
.handle_session_update(
acp::SessionUpdate::AgentMessageChunk {
content: content.text.to_uppercase().into(),
},
cx,
)
.unwrap();
})?;
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
})
}
.boxed_local()
}
}));
let thread = cx
.update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx))
.await
.unwrap();
cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["hello".into()], cx)))
.await
.unwrap();
thread.read_with(cx, |thread, cx| {
assert_eq!(
thread.to_markdown(cx),
indoc! {"
## User
hello
## Assistant
HELLO
"}
);
});
// Simulate refusing the second message, ensuring the conversation gets
// truncated to before sending it.
refuse_next.store(true, SeqCst);
cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["world".into()], cx)))
.await
.unwrap();
thread.read_with(cx, |thread, cx| {
assert_eq!(
thread.to_markdown(cx),
indoc! {"
## User
hello
## Assistant
HELLO
"}
);
});
}
async fn run_until_first_tool_call( async fn run_until_first_tool_call(
thread: &Entity<AcpThread>, thread: &Entity<AcpThread>,
cx: &mut TestAppContext, cx: &mut TestAppContext,
@ -2432,13 +2624,19 @@ mod tests {
.into(), .into(),
); );
let action_log = cx.new(|_| ActionLog::new(project.clone())); let action_log = cx.new(|_| ActionLog::new(project.clone()));
let thread = cx.new(|_cx| { let thread = cx.new(|cx| {
AcpThread::new( AcpThread::new(
"Test", "Test",
self.clone(), self.clone(),
project, project,
action_log, action_log,
session_id.clone(), session_id.clone(),
watch::Receiver::constant(acp::PromptCapabilities {
image: true,
audio: true,
embedded_context: true,
}),
cx,
) )
}); });
self.sessions.lock().insert(session_id, thread.downgrade()); self.sessions.lock().insert(session_id, thread.downgrade());
@ -2485,11 +2683,11 @@ mod tests {
.detach(); .detach();
} }
fn session_editor( fn truncate(
&self, &self,
session_id: &acp::SessionId, session_id: &acp::SessionId,
_cx: &mut App, _cx: &App,
) -> Option<Rc<dyn AgentSessionEditor>> { ) -> Option<Rc<dyn AgentSessionTruncate>> {
Some(Rc::new(FakeAgentSessionEditor { Some(Rc::new(FakeAgentSessionEditor {
_session_id: session_id.clone(), _session_id: session_id.clone(),
})) }))
@ -2504,8 +2702,8 @@ mod tests {
_session_id: acp::SessionId, _session_id: acp::SessionId,
} }
impl AgentSessionEditor for FakeAgentSessionEditor { impl AgentSessionTruncate for FakeAgentSessionEditor {
fn truncate(&self, _message_id: UserMessageId, _cx: &mut App) -> Task<Result<()>> { fn run(&self, _message_id: UserMessageId, _cx: &mut App) -> Task<Result<()>> {
Task::ready(Ok(())) Task::ready(Ok(()))
} }
} }

View file

@ -41,18 +41,26 @@ pub trait AgentConnection {
fn resume( fn resume(
&self, &self,
_session_id: &acp::SessionId, _session_id: &acp::SessionId,
_cx: &mut App, _cx: &App,
) -> Option<Rc<dyn AgentSessionResume>> { ) -> Option<Rc<dyn AgentSessionResume>> {
None None
} }
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App); fn cancel(&self, session_id: &acp::SessionId, cx: &mut App);
fn session_editor( fn truncate(
&self, &self,
_session_id: &acp::SessionId, _session_id: &acp::SessionId,
_cx: &mut App, _cx: &App,
) -> Option<Rc<dyn AgentSessionEditor>> { ) -> Option<Rc<dyn AgentSessionTruncate>> {
None
}
fn set_title(
&self,
_session_id: &acp::SessionId,
_cx: &App,
) -> Option<Rc<dyn AgentSessionSetTitle>> {
None None
} }
@ -64,6 +72,10 @@ pub trait AgentConnection {
None None
} }
fn telemetry(&self) -> Option<Rc<dyn AgentTelemetry>> {
None
}
fn into_any(self: Rc<Self>) -> Rc<dyn Any>; fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
} }
@ -73,14 +85,31 @@ impl dyn AgentConnection {
} }
} }
pub trait AgentSessionEditor { pub trait AgentSessionTruncate {
fn truncate(&self, message_id: UserMessageId, cx: &mut App) -> Task<Result<()>>; fn run(&self, message_id: UserMessageId, cx: &mut App) -> Task<Result<()>>;
} }
pub trait AgentSessionResume { pub trait AgentSessionResume {
fn run(&self, cx: &mut App) -> Task<Result<acp::PromptResponse>>; 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)] #[derive(Debug)]
pub struct AuthRequired { pub struct AuthRequired {
pub description: Option<String>, pub description: Option<String>,
@ -298,13 +327,19 @@ mod test_support {
) -> Task<gpui::Result<Entity<AcpThread>>> { ) -> Task<gpui::Result<Entity<AcpThread>>> {
let session_id = acp::SessionId(self.sessions.lock().len().to_string().into()); let session_id = acp::SessionId(self.sessions.lock().len().to_string().into());
let action_log = cx.new(|_| ActionLog::new(project.clone())); let action_log = cx.new(|_| ActionLog::new(project.clone()));
let thread = cx.new(|_cx| { let thread = cx.new(|cx| {
AcpThread::new( AcpThread::new(
"Test", "Test",
self.clone(), self.clone(),
project, project,
action_log, action_log,
session_id.clone(), session_id.clone(),
watch::Receiver::constant(acp::PromptCapabilities {
image: true,
audio: true,
embedded_context: true,
}),
cx,
) )
}); });
self.sessions.lock().insert( self.sessions.lock().insert(
@ -393,15 +428,15 @@ mod test_support {
.response_tx .response_tx
.take() .take()
{ {
end_turn_tx.send(acp::StopReason::Canceled).unwrap(); end_turn_tx.send(acp::StopReason::Cancelled).unwrap();
} }
} }
fn session_editor( fn truncate(
&self, &self,
_session_id: &agent_client_protocol::SessionId, _session_id: &agent_client_protocol::SessionId,
_cx: &mut App, _cx: &App,
) -> Option<Rc<dyn AgentSessionEditor>> { ) -> Option<Rc<dyn AgentSessionTruncate>> {
Some(Rc::new(StubAgentSessionEditor)) Some(Rc::new(StubAgentSessionEditor))
} }
@ -412,8 +447,8 @@ mod test_support {
struct StubAgentSessionEditor; struct StubAgentSessionEditor;
impl AgentSessionEditor for StubAgentSessionEditor { impl AgentSessionTruncate for StubAgentSessionEditor {
fn truncate(&self, _: UserMessageId, _: &mut App) -> Task<Result<()>> { fn run(&self, _: UserMessageId, _: &mut App) -> Task<Result<()>> {
Task::ready(Ok(())) Task::ready(Ok(()))
} }
} }

View file

@ -28,57 +28,46 @@ impl Diff {
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Self { ) -> Self {
let multibuffer = cx.new(|_cx| MultiBuffer::without_headers(Capability::ReadOnly)); let multibuffer = cx.new(|_cx| MultiBuffer::without_headers(Capability::ReadOnly));
let new_buffer = cx.new(|cx| Buffer::local(new_text, cx)); let new_buffer = cx.new(|cx| Buffer::local(new_text, cx));
let old_buffer = cx.new(|cx| Buffer::local(old_text.unwrap_or("".into()), cx)); let base_text = old_text.clone().unwrap_or(String::new()).into();
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 task = cx.spawn({
let multibuffer = multibuffer.clone(); let multibuffer = multibuffer.clone();
let path = path.clone(); let path = path.clone();
let buffer = new_buffer.clone();
async move |_, cx| { async move |_, cx| {
let language = language_registry let language = language_registry
.language_for_file_path(&path) .language_for_file_path(&path)
.await .await
.log_err(); .log_err();
new_buffer.update(cx, |buffer, cx| buffer.set_language(language.clone(), cx))?; buffer.update(cx, |buffer, cx| buffer.set_language(language.clone(), cx))?;
let old_buffer_snapshot = old_buffer.update(cx, |buffer, cx| { let diff = build_buffer_diff(
buffer.set_language(language, cx); old_text.unwrap_or("".into()).into(),
buffer.snapshot() &buffer,
})?; Some(language_registry.clone()),
cx,
buffer_diff )
.update(cx, |diff, cx| { .await?;
diff.set_base_text(
old_buffer_snapshot,
Some(language_registry),
new_buffer_snapshot,
cx,
)
})?
.await?;
multibuffer multibuffer
.update(cx, |multibuffer, cx| { .update(cx, |multibuffer, cx| {
let hunk_ranges = { let hunk_ranges = {
let buffer = new_buffer.read(cx); let buffer = buffer.read(cx);
let diff = buffer_diff.read(cx); let diff = diff.read(cx);
diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer, cx) diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer, cx)
.map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer)) .map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer))
.collect::<Vec<_>>() .collect::<Vec<_>>()
}; };
multibuffer.set_excerpts_for_path( multibuffer.set_excerpts_for_path(
PathKey::for_buffer(&new_buffer, cx), PathKey::for_buffer(&buffer, cx),
new_buffer.clone(), buffer.clone(),
hunk_ranges, hunk_ranges,
editor::DEFAULT_MULTIBUFFER_CONTEXT, editor::DEFAULT_MULTIBUFFER_CONTEXT,
cx, cx,
); );
multibuffer.add_diff(buffer_diff, cx); multibuffer.add_diff(diff, cx);
}) })
.log_err(); .log_err();
@ -89,23 +78,26 @@ impl Diff {
Self::Finalized(FinalizedDiff { Self::Finalized(FinalizedDiff {
multibuffer, multibuffer,
path, path,
base_text,
new_buffer,
_update_diff: task, _update_diff: task,
}) })
} }
pub fn new(buffer: Entity<Buffer>, cx: &mut Context<Self>) -> Self { pub fn new(buffer: Entity<Buffer>, cx: &mut Context<Self>) -> Self {
let buffer_snapshot = buffer.read(cx).snapshot(); let buffer_text_snapshot = buffer.read(cx).text_snapshot();
let base_text = buffer_snapshot.text(); let base_text_snapshot = buffer.read(cx).snapshot();
let language_registry = buffer.read(cx).language_registry(); let base_text = base_text_snapshot.text();
let text_snapshot = buffer.read(cx).text_snapshot(); debug_assert_eq!(buffer_text_snapshot.text(), base_text);
let buffer_diff = cx.new(|cx| { let buffer_diff = cx.new(|cx| {
let mut diff = BufferDiff::new(&text_snapshot, cx); let mut diff = BufferDiff::new_unchanged(&buffer_text_snapshot, base_text_snapshot);
let _ = diff.set_base_text( let snapshot = diff.snapshot(cx);
buffer_snapshot.clone(), let secondary_diff = cx.new(|cx| {
language_registry, let mut diff = BufferDiff::new(&buffer_text_snapshot, cx);
text_snapshot, diff.set_snapshot(snapshot, &buffer_text_snapshot, cx);
cx, diff
); });
diff.set_secondary_diff(secondary_diff);
diff diff
}); });
@ -123,7 +115,7 @@ impl Diff {
diff.update(cx); diff.update(cx);
} }
}), }),
buffer, new_buffer: buffer,
diff: buffer_diff, diff: buffer_diff,
revealed_ranges: Vec::new(), revealed_ranges: Vec::new(),
update_diff: Task::ready(Ok(())), update_diff: Task::ready(Ok(())),
@ -158,9 +150,9 @@ impl Diff {
.map(|buffer| buffer.read(cx).text()) .map(|buffer| buffer.read(cx).text())
.join("\n"); .join("\n");
let path = match self { let path = match self {
Diff::Pending(PendingDiff { buffer, .. }) => { Diff::Pending(PendingDiff {
buffer.read(cx).file().map(|file| file.path().as_ref()) new_buffer: buffer, ..
} }) => buffer.read(cx).file().map(|file| file.path().as_ref()),
Diff::Finalized(FinalizedDiff { path, .. }) => Some(path.as_path()), Diff::Finalized(FinalizedDiff { path, .. }) => Some(path.as_path()),
}; };
format!( format!(
@ -173,12 +165,33 @@ impl Diff {
pub fn has_revealed_range(&self, cx: &App) -> bool { pub fn has_revealed_range(&self, cx: &App) -> bool {
self.multibuffer().read(cx).excerpt_paths().next().is_some() 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 { pub struct PendingDiff {
multibuffer: Entity<MultiBuffer>, multibuffer: Entity<MultiBuffer>,
base_text: Arc<String>, base_text: Arc<String>,
buffer: Entity<Buffer>, new_buffer: Entity<Buffer>,
diff: Entity<BufferDiff>, diff: Entity<BufferDiff>,
revealed_ranges: Vec<Range<Anchor>>, revealed_ranges: Vec<Range<Anchor>>,
_subscription: Subscription, _subscription: Subscription,
@ -187,7 +200,7 @@ pub struct PendingDiff {
impl PendingDiff { impl PendingDiff {
pub fn update(&mut self, cx: &mut Context<Diff>) { pub fn update(&mut self, cx: &mut Context<Diff>) {
let buffer = self.buffer.clone(); let buffer = self.new_buffer.clone();
let buffer_diff = self.diff.clone(); let buffer_diff = self.diff.clone();
let base_text = self.base_text.clone(); let base_text = self.base_text.clone();
self.update_diff = cx.spawn(async move |diff, cx| { self.update_diff = cx.spawn(async move |diff, cx| {
@ -204,7 +217,10 @@ impl PendingDiff {
) )
.await?; .await?;
buffer_diff.update(cx, |diff, cx| { buffer_diff.update(cx, |diff, cx| {
diff.set_snapshot(diff_snapshot, &text_snapshot, 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.update(cx, |diff, cx| { diff.update(cx, |diff, cx| {
if let Diff::Pending(diff) = diff { if let Diff::Pending(diff) = diff {
@ -222,10 +238,10 @@ impl PendingDiff {
fn finalize(&self, cx: &mut Context<Diff>) -> FinalizedDiff { fn finalize(&self, cx: &mut Context<Diff>) -> FinalizedDiff {
let ranges = self.excerpt_ranges(cx); let ranges = self.excerpt_ranges(cx);
let base_text = self.base_text.clone(); let base_text = self.base_text.clone();
let language_registry = self.buffer.read(cx).language_registry().clone(); let language_registry = self.new_buffer.read(cx).language_registry();
let path = self let path = self
.buffer .new_buffer
.read(cx) .read(cx)
.file() .file()
.map(|file| file.path().as_ref()) .map(|file| file.path().as_ref())
@ -234,12 +250,12 @@ impl PendingDiff {
// Replace the buffer in the multibuffer with the snapshot // Replace the buffer in the multibuffer with the snapshot
let buffer = cx.new(|cx| { let buffer = cx.new(|cx| {
let language = self.buffer.read(cx).language().cloned(); let language = self.new_buffer.read(cx).language().cloned();
let buffer = TextBuffer::new_normalized( let buffer = TextBuffer::new_normalized(
0, 0,
cx.entity_id().as_non_zero_u64().into(), cx.entity_id().as_non_zero_u64().into(),
self.buffer.read(cx).line_ending(), self.new_buffer.read(cx).line_ending(),
self.buffer.read(cx).as_rope().clone(), self.new_buffer.read(cx).as_rope().clone(),
); );
let mut buffer = Buffer::build(buffer, None, Capability::ReadWrite); let mut buffer = Buffer::build(buffer, None, Capability::ReadWrite);
buffer.set_language(language, cx); buffer.set_language(language, cx);
@ -248,7 +264,6 @@ impl PendingDiff {
let buffer_diff = cx.spawn({ let buffer_diff = cx.spawn({
let buffer = buffer.clone(); let buffer = buffer.clone();
let language_registry = language_registry.clone();
async move |_this, cx| { async move |_this, cx| {
build_buffer_diff(base_text, &buffer, language_registry, cx).await build_buffer_diff(base_text, &buffer, language_registry, cx).await
} }
@ -276,7 +291,9 @@ impl PendingDiff {
FinalizedDiff { FinalizedDiff {
path, path,
base_text: self.base_text.clone(),
multibuffer: self.multibuffer.clone(), multibuffer: self.multibuffer.clone(),
new_buffer: self.new_buffer.clone(),
_update_diff: update_diff, _update_diff: update_diff,
} }
} }
@ -285,8 +302,8 @@ impl PendingDiff {
let ranges = self.excerpt_ranges(cx); let ranges = self.excerpt_ranges(cx);
self.multibuffer.update(cx, |multibuffer, cx| { self.multibuffer.update(cx, |multibuffer, cx| {
multibuffer.set_excerpts_for_path( multibuffer.set_excerpts_for_path(
PathKey::for_buffer(&self.buffer, cx), PathKey::for_buffer(&self.new_buffer, cx),
self.buffer.clone(), self.new_buffer.clone(),
ranges, ranges,
editor::DEFAULT_MULTIBUFFER_CONTEXT, editor::DEFAULT_MULTIBUFFER_CONTEXT,
cx, cx,
@ -298,7 +315,7 @@ impl PendingDiff {
} }
fn excerpt_ranges(&self, cx: &App) -> Vec<Range<Point>> { fn excerpt_ranges(&self, cx: &App) -> Vec<Range<Point>> {
let buffer = self.buffer.read(cx); let buffer = self.new_buffer.read(cx);
let diff = self.diff.read(cx); let diff = self.diff.read(cx);
let mut ranges = diff let mut ranges = diff
.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer, cx) .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer, cx)
@ -332,6 +349,8 @@ impl PendingDiff {
pub struct FinalizedDiff { pub struct FinalizedDiff {
path: PathBuf, path: PathBuf,
base_text: Arc<String>,
new_buffer: Entity<Buffer>,
multibuffer: Entity<MultiBuffer>, multibuffer: Entity<MultiBuffer>,
_update_diff: Task<Result<()>>, _update_diff: Task<Result<()>>,
} }
@ -385,3 +404,21 @@ async fn build_buffer_diff(
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

@ -5,7 +5,7 @@ use prompt_store::{PromptId, UserPromptId};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{
fmt, fmt,
ops::Range, ops::RangeInclusive,
path::{Path, PathBuf}, path::{Path, PathBuf},
str::FromStr, str::FromStr,
}; };
@ -17,13 +17,14 @@ pub enum MentionUri {
File { File {
abs_path: PathBuf, abs_path: PathBuf,
}, },
PastedImage,
Directory { Directory {
abs_path: PathBuf, abs_path: PathBuf,
}, },
Symbol { Symbol {
path: PathBuf, abs_path: PathBuf,
name: String, name: String,
line_range: Range<u32>, line_range: RangeInclusive<u32>,
}, },
Thread { Thread {
id: acp::SessionId, id: acp::SessionId,
@ -38,8 +39,9 @@ pub enum MentionUri {
name: String, name: String,
}, },
Selection { Selection {
path: PathBuf, #[serde(default, skip_serializing_if = "Option::is_none")]
line_range: Range<u32>, abs_path: Option<PathBuf>,
line_range: RangeInclusive<u32>,
}, },
Fetch { Fetch {
url: Url, url: Url,
@ -48,36 +50,44 @@ pub enum MentionUri {
impl MentionUri { impl MentionUri {
pub fn parse(input: &str) -> Result<Self> { 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 url = url::Url::parse(input)?;
let path = url.path(); let path = url.path();
match url.scheme() { match url.scheme() {
"file" => { "file" => {
let path = url.to_file_path().ok().context("Extracting file path")?; let path = url.to_file_path().ok().context("Extracting file path")?;
if let Some(fragment) = url.fragment() { if let Some(fragment) = url.fragment() {
let range = fragment let line_range = parse_line_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")? { if let Some(name) = single_query_param(&url, "symbol")? {
Ok(Self::Symbol { Ok(Self::Symbol {
name, name,
path, abs_path: path,
line_range, line_range,
}) })
} else { } else {
Ok(Self::Selection { path, line_range }) Ok(Self::Selection {
abs_path: Some(path),
line_range,
})
} }
} else if input.ends_with("/") { } else if input.ends_with("/") {
Ok(Self::Directory { abs_path: path }) Ok(Self::Directory { abs_path: path })
@ -105,6 +115,17 @@ impl MentionUri {
id: rule_id.into(), id: rule_id.into(),
name, 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 { } else {
bail!("invalid zed url: {:?}", input); bail!("invalid zed url: {:?}", input);
} }
@ -121,13 +142,16 @@ impl MentionUri {
.unwrap_or_default() .unwrap_or_default()
.to_string_lossy() .to_string_lossy()
.into_owned(), .into_owned(),
MentionUri::PastedImage => "Image".to_string(),
MentionUri::Symbol { name, .. } => name.clone(), MentionUri::Symbol { name, .. } => name.clone(),
MentionUri::Thread { name, .. } => name.clone(), MentionUri::Thread { name, .. } => name.clone(),
MentionUri::TextThread { name, .. } => name.clone(), MentionUri::TextThread { name, .. } => name.clone(),
MentionUri::Rule { name, .. } => name.clone(), MentionUri::Rule { name, .. } => name.clone(),
MentionUri::Selection { MentionUri::Selection {
path, line_range, .. abs_path: path,
} => selection_name(path, line_range), line_range,
..
} => selection_name(path.as_deref(), line_range),
MentionUri::Fetch { url } => url.to_string(), MentionUri::Fetch { url } => url.to_string(),
} }
} }
@ -137,6 +161,7 @@ impl MentionUri {
MentionUri::File { abs_path } => { MentionUri::File { abs_path } => {
FileIcons::get_icon(abs_path, cx).unwrap_or_else(|| IconName::File.path().into()) 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) MentionUri::Directory { .. } => FileIcons::get_folder_icon(false, cx)
.unwrap_or_else(|| IconName::Folder.path().into()), .unwrap_or_else(|| IconName::Folder.path().into()),
MentionUri::Symbol { .. } => IconName::Code.path().into(), MentionUri::Symbol { .. } => IconName::Code.path().into(),
@ -157,29 +182,40 @@ impl MentionUri {
MentionUri::File { abs_path } => { MentionUri::File { abs_path } => {
Url::from_file_path(abs_path).expect("mention path should be absolute") Url::from_file_path(abs_path).expect("mention path should be absolute")
} }
MentionUri::PastedImage => Url::parse("zed:///agent/pasted-image").unwrap(),
MentionUri::Directory { abs_path } => { MentionUri::Directory { abs_path } => {
Url::from_directory_path(abs_path).expect("mention path should be absolute") Url::from_directory_path(abs_path).expect("mention path should be absolute")
} }
MentionUri::Symbol { MentionUri::Symbol {
path, abs_path,
name, name,
line_range, line_range,
} => { } => {
let mut url = Url::from_file_path(path).expect("mention path should be absolute"); let mut url =
Url::from_file_path(abs_path).expect("mention path should be absolute");
url.query_pairs_mut().append_pair("symbol", name); url.query_pairs_mut().append_pair("symbol", name);
url.set_fragment(Some(&format!( url.set_fragment(Some(&format!(
"L{}:{}", "L{}:{}",
line_range.start + 1, line_range.start() + 1,
line_range.end + 1 line_range.end() + 1
))); )));
url url
} }
MentionUri::Selection { path, line_range } => { MentionUri::Selection {
let mut url = Url::from_file_path(path).expect("mention path should be absolute"); 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
};
url.set_fragment(Some(&format!( url.set_fragment(Some(&format!(
"L{}:{}", "L{}:{}",
line_range.start + 1, line_range.start() + 1,
line_range.end + 1 line_range.end() + 1
))); )));
url url
} }
@ -191,7 +227,10 @@ impl MentionUri {
} }
MentionUri::TextThread { path, name } => { MentionUri::TextThread { path, name } => {
let mut url = Url::parse("zed:///").unwrap(); let mut url = Url::parse("zed:///").unwrap();
url.set_path(&format!("/agent/text-thread/{}", path.to_string_lossy())); url.set_path(&format!(
"/agent/text-thread/{}",
path.to_string_lossy().trim_start_matches('/')
));
url.query_pairs_mut().append_pair("name", name); url.query_pairs_mut().append_pair("name", name);
url url
} }
@ -237,12 +276,14 @@ fn single_query_param(url: &Url, name: &'static str) -> Result<Option<String>> {
} }
} }
pub fn selection_name(path: &Path, line_range: &Range<u32>) -> String { pub fn selection_name(path: Option<&Path>, line_range: &RangeInclusive<u32>) -> String {
format!( format!(
"{} ({}:{})", "{} ({}:{})",
path.file_name().unwrap_or_default().display(), path.and_then(|path| path.file_name())
line_range.start + 1, .unwrap_or("Untitled".as_ref())
line_range.end + 1 .display(),
*line_range.start() + 1,
*line_range.end() + 1
) )
} }
@ -302,14 +343,14 @@ mod tests {
let parsed = MentionUri::parse(symbol_uri).unwrap(); let parsed = MentionUri::parse(symbol_uri).unwrap();
match &parsed { match &parsed {
MentionUri::Symbol { MentionUri::Symbol {
path, abs_path: path,
name, name,
line_range, line_range,
} => { } => {
assert_eq!(path.to_str().unwrap(), path!("/path/to/file.rs")); assert_eq!(path.to_str().unwrap(), path!("/path/to/file.rs"));
assert_eq!(name, "MySymbol"); assert_eq!(name, "MySymbol");
assert_eq!(line_range.start, 9); assert_eq!(line_range.start(), &9);
assert_eq!(line_range.end, 19); assert_eq!(line_range.end(), &19);
} }
_ => panic!("Expected Symbol variant"), _ => panic!("Expected Symbol variant"),
} }
@ -321,16 +362,39 @@ mod tests {
let selection_uri = uri!("file:///path/to/file.rs#L5:15"); let selection_uri = uri!("file:///path/to/file.rs#L5:15");
let parsed = MentionUri::parse(selection_uri).unwrap(); let parsed = MentionUri::parse(selection_uri).unwrap();
match &parsed { match &parsed {
MentionUri::Selection { path, line_range } => { MentionUri::Selection {
assert_eq!(path.to_str().unwrap(), path!("/path/to/file.rs")); abs_path: path,
assert_eq!(line_range.start, 4); line_range,
assert_eq!(line_range.end, 14); } => {
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);
} }
_ => panic!("Expected Selection variant"), _ => panic!("Expected Selection variant"),
} }
assert_eq!(parsed.to_uri().to_string(), selection_uri); 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] #[test]
fn test_parse_thread_uri() { fn test_parse_thread_uri() {
let thread_uri = "zed:///agent/thread/session123?name=Thread+name"; let thread_uri = "zed:///agent/thread/session123?name=Thread+name";

View file

@ -0,0 +1,30 @@
[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

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

View file

@ -0,0 +1,494 @@
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!(acp, [OpenDebugTools]);
pub fn init(cx: &mut App) {
cx.observe_new(
|workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
workspace.register_action(|workspace, _: &OpenDebugTools, 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

@ -161,7 +161,7 @@ impl ActionLog {
diff_base, diff_base,
last_seen_base, last_seen_base,
unreviewed_edits, unreviewed_edits,
snapshot: text_snapshot.clone(), snapshot: text_snapshot,
status, status,
version: buffer.read(cx).version(), version: buffer.read(cx).version(),
diff, diff,
@ -190,7 +190,7 @@ impl ActionLog {
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
match event { match event {
BufferEvent::Edited { .. } => self.handle_buffer_edited(buffer, cx), BufferEvent::Edited => self.handle_buffer_edited(buffer, cx),
BufferEvent::FileHandleChanged => { BufferEvent::FileHandleChanged => {
self.handle_buffer_file_changed(buffer, cx); self.handle_buffer_file_changed(buffer, cx);
} }
@ -461,7 +461,7 @@ impl ActionLog {
anyhow::Ok(( anyhow::Ok((
tracked_buffer.diff.clone(), tracked_buffer.diff.clone(),
buffer.read(cx).language().cloned(), buffer.read(cx).language().cloned(),
buffer.read(cx).language_registry().clone(), buffer.read(cx).language_registry(),
)) ))
})??; })??;
let diff_snapshot = BufferDiff::update_diff( let diff_snapshot = BufferDiff::update_diff(
@ -529,12 +529,12 @@ impl ActionLog {
/// Mark a buffer as created by agent, so we can refresh it in the context /// 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>) { pub fn buffer_created(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
self.track_buffer_internal(buffer.clone(), true, cx); self.track_buffer_internal(buffer, true, cx);
} }
/// Mark a buffer as edited by agent, so we can refresh it in the context /// 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>) { pub fn buffer_edited(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
let tracked_buffer = self.track_buffer_internal(buffer.clone(), false, cx); let tracked_buffer = self.track_buffer_internal(buffer, false, cx);
if let TrackedBufferStatus::Deleted = tracked_buffer.status { if let TrackedBufferStatus::Deleted = tracked_buffer.status {
tracked_buffer.status = TrackedBufferStatus::Modified; tracked_buffer.status = TrackedBufferStatus::Modified;
} }
@ -2425,7 +2425,7 @@ mod tests {
assert_eq!( assert_eq!(
unreviewed_hunks(&action_log, cx), unreviewed_hunks(&action_log, cx),
vec![( vec![(
buffer.clone(), buffer,
vec![ vec![
HunkStatus { HunkStatus {
range: Point::new(6, 0)..Point::new(7, 0), range: Point::new(6, 0)..Point::new(7, 0),

View file

@ -104,7 +104,7 @@ impl ActivityIndicator {
&workspace_handle, &workspace_handle,
window, window,
|activity_indicator, _, event, window, cx| { |activity_indicator, _, event, window, cx| {
if let workspace::Event::ClearActivityIndicator { .. } = event if let workspace::Event::ClearActivityIndicator = event
&& activity_indicator.statuses.pop().is_some() && activity_indicator.statuses.pop().is_some()
{ {
activity_indicator.dismiss_error_message(&DismissErrorMessage, window, cx); activity_indicator.dismiss_error_message(&DismissErrorMessage, window, cx);

View file

@ -132,7 +132,7 @@ mod tests {
}); });
let tool_set = default_tool_set(cx); let tool_set = default_tool_set(cx);
let profile = AgentProfile::new(id.clone(), tool_set); let profile = AgentProfile::new(id, tool_set);
let mut enabled_tools = cx let mut enabled_tools = cx
.read(|cx| profile.enabled_tools(cx)) .read(|cx| profile.enabled_tools(cx))
@ -169,7 +169,7 @@ mod tests {
}); });
let tool_set = default_tool_set(cx); let tool_set = default_tool_set(cx);
let profile = AgentProfile::new(id.clone(), tool_set); let profile = AgentProfile::new(id, tool_set);
let mut enabled_tools = cx let mut enabled_tools = cx
.read(|cx| profile.enabled_tools(cx)) .read(|cx| profile.enabled_tools(cx))
@ -202,7 +202,7 @@ mod tests {
}); });
let tool_set = default_tool_set(cx); let tool_set = default_tool_set(cx);
let profile = AgentProfile::new(id.clone(), tool_set); let profile = AgentProfile::new(id, tool_set);
let mut enabled_tools = cx let mut enabled_tools = cx
.read(|cx| profile.enabled_tools(cx)) .read(|cx| profile.enabled_tools(cx))

View file

@ -362,7 +362,7 @@ impl Display for DirectoryContext {
let mut is_first = true; let mut is_first = true;
for descendant in &self.descendants { for descendant in &self.descendants {
if !is_first { if !is_first {
write!(f, "\n")?; writeln!(f)?;
} else { } else {
is_first = false; is_first = false;
} }
@ -650,7 +650,7 @@ impl TextThreadContextHandle {
impl Display for TextThreadContext { impl Display for TextThreadContext {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
// TODO: escape title? // TODO: escape title?
write!(f, "<text_thread title=\"{}\">\n", self.title)?; writeln!(f, "<text_thread title=\"{}\">", self.title)?;
write!(f, "{}", self.text.trim())?; write!(f, "{}", self.text.trim())?;
write!(f, "\n</text_thread>") write!(f, "\n</text_thread>")
} }
@ -716,7 +716,7 @@ impl RulesContextHandle {
impl Display for RulesContext { impl Display for RulesContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(title) = &self.title { if let Some(title) = &self.title {
write!(f, "Rules title: {}\n", title)?; writeln!(f, "Rules title: {}", title)?;
} }
let code_block = MarkdownCodeBlock { let code_block = MarkdownCodeBlock {
tag: "", tag: "",

View file

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

View file

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

View file

@ -181,7 +181,7 @@ impl Message {
} }
} }
pub fn to_string(&self) -> String { pub fn to_message_content(&self) -> String {
let mut result = String::new(); let mut result = String::new();
if !self.loaded_context.text.is_empty() { if !self.loaded_context.text.is_empty() {
@ -387,7 +387,6 @@ pub struct Thread {
cumulative_token_usage: TokenUsage, cumulative_token_usage: TokenUsage,
exceeded_window_error: Option<ExceededWindowError>, exceeded_window_error: Option<ExceededWindowError>,
tool_use_limit_reached: bool, tool_use_limit_reached: bool,
feedback: Option<ThreadFeedback>,
retry_state: Option<RetryState>, retry_state: Option<RetryState>,
message_feedback: HashMap<MessageId, ThreadFeedback>, message_feedback: HashMap<MessageId, ThreadFeedback>,
last_received_chunk_at: Option<Instant>, last_received_chunk_at: Option<Instant>,
@ -487,14 +486,13 @@ impl Thread {
cumulative_token_usage: TokenUsage::default(), cumulative_token_usage: TokenUsage::default(),
exceeded_window_error: None, exceeded_window_error: None,
tool_use_limit_reached: false, tool_use_limit_reached: false,
feedback: None,
retry_state: None, retry_state: None,
message_feedback: HashMap::default(), message_feedback: HashMap::default(),
last_error_context: None, last_error_context: None,
last_received_chunk_at: None, last_received_chunk_at: None,
request_callback: None, request_callback: None,
remaining_turns: u32::MAX, remaining_turns: u32::MAX,
configured_model: configured_model.clone(), configured_model,
profile: AgentProfile::new(profile_id, tools), profile: AgentProfile::new(profile_id, tools),
} }
} }
@ -532,7 +530,7 @@ impl Thread {
.and_then(|model| { .and_then(|model| {
let model = SelectedModel { let model = SelectedModel {
provider: model.provider.clone().into(), provider: model.provider.clone().into(),
model: model.model.clone().into(), model: model.model.into(),
}; };
registry.select_model(&model, cx) registry.select_model(&model, cx)
}) })
@ -612,7 +610,6 @@ impl Thread {
cumulative_token_usage: serialized.cumulative_token_usage, cumulative_token_usage: serialized.cumulative_token_usage,
exceeded_window_error: None, exceeded_window_error: None,
tool_use_limit_reached: serialized.tool_use_limit_reached, tool_use_limit_reached: serialized.tool_use_limit_reached,
feedback: None,
message_feedback: HashMap::default(), message_feedback: HashMap::default(),
last_error_context: None, last_error_context: None,
last_received_chunk_at: None, last_received_chunk_at: None,
@ -667,7 +664,7 @@ impl Thread {
} }
pub fn get_or_init_configured_model(&mut self, cx: &App) -> Option<ConfiguredModel> { pub fn get_or_init_configured_model(&mut self, cx: &App) -> Option<ConfiguredModel> {
if self.configured_model.is_none() { if self.configured_model.is_none() || self.messages.is_empty() {
self.configured_model = LanguageModelRegistry::read_global(cx).default_model(); self.configured_model = LanguageModelRegistry::read_global(cx).default_model();
} }
self.configured_model.clone() self.configured_model.clone()
@ -1646,10 +1643,10 @@ impl Thread {
}; };
self.tool_use self.tool_use
.request_tool_use(tool_message_id, tool_use, tool_use_metadata.clone(), cx); .request_tool_use(tool_message_id, tool_use, tool_use_metadata, cx);
self.tool_use.insert_tool_output( self.tool_use.insert_tool_output(
tool_use_id.clone(), tool_use_id,
tool_name, tool_name,
tool_output, tool_output,
self.configured_model.as_ref(), self.configured_model.as_ref(),
@ -2100,7 +2097,7 @@ impl Thread {
} }
pub fn summarize(&mut self, cx: &mut Context<Self>) { pub fn summarize(&mut self, cx: &mut Context<Self>) {
let Some(model) = LanguageModelRegistry::read_global(cx).thread_summary_model() else { let Some(model) = LanguageModelRegistry::read_global(cx).thread_summary_model(cx) else {
println!("No thread summary model"); println!("No thread summary model");
return; return;
}; };
@ -2419,7 +2416,7 @@ impl Thread {
} }
let Some(ConfiguredModel { model, provider }) = let Some(ConfiguredModel { model, provider }) =
LanguageModelRegistry::read_global(cx).thread_summary_model() LanguageModelRegistry::read_global(cx).thread_summary_model(cx)
else { else {
return; return;
}; };
@ -2787,10 +2784,6 @@ impl Thread {
cx.emit(ThreadEvent::CancelEditing); cx.emit(ThreadEvent::CancelEditing);
} }
pub fn feedback(&self) -> Option<ThreadFeedback> {
self.feedback
}
pub fn message_feedback(&self, message_id: MessageId) -> Option<ThreadFeedback> { pub fn message_feedback(&self, message_id: MessageId) -> Option<ThreadFeedback> {
self.message_feedback.get(&message_id).copied() self.message_feedback.get(&message_id).copied()
} }
@ -2823,7 +2816,7 @@ impl Thread {
let message_content = self let message_content = self
.message(message_id) .message(message_id)
.map(|msg| msg.to_string()) .map(|msg| msg.to_message_content())
.unwrap_or_default(); .unwrap_or_default();
cx.background_spawn(async move { cx.background_spawn(async move {
@ -2852,52 +2845,6 @@ 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. /// Create a snapshot of the current project state including git information and unsaved buffers.
fn project_snapshot( fn project_snapshot(
project: Entity<Project>, project: Entity<Project>,
@ -3241,7 +3188,7 @@ impl Thread {
self.configured_model.as_ref(), self.configured_model.as_ref(),
self.completion_mode, self.completion_mode,
); );
self.tool_finished(tool_use_id.clone(), None, true, window, cx); self.tool_finished(tool_use_id, None, true, window, cx);
} }
} }
@ -3873,7 +3820,7 @@ fn main() {{
AgentSettings { AgentSettings {
model_parameters: vec![LanguageModelParameters { model_parameters: vec![LanguageModelParameters {
provider: Some(model.provider_id().0.to_string().into()), provider: Some(model.provider_id().0.to_string().into()),
model: Some(model.id().0.clone()), model: Some(model.id().0),
temperature: Some(0.66), temperature: Some(0.66),
}], }],
..AgentSettings::get_global(cx).clone() ..AgentSettings::get_global(cx).clone()
@ -3893,7 +3840,7 @@ fn main() {{
AgentSettings { AgentSettings {
model_parameters: vec![LanguageModelParameters { model_parameters: vec![LanguageModelParameters {
provider: None, provider: None,
model: Some(model.id().0.clone()), model: Some(model.id().0),
temperature: Some(0.66), temperature: Some(0.66),
}], }],
..AgentSettings::get_global(cx).clone() ..AgentSettings::get_global(cx).clone()
@ -3933,7 +3880,7 @@ fn main() {{
AgentSettings { AgentSettings {
model_parameters: vec![LanguageModelParameters { model_parameters: vec![LanguageModelParameters {
provider: Some("anthropic".into()), provider: Some("anthropic".into()),
model: Some(model.id().0.clone()), model: Some(model.id().0),
temperature: Some(0.66), temperature: Some(0.66),
}], }],
..AgentSettings::get_global(cx).clone() ..AgentSettings::get_global(cx).clone()
@ -5463,13 +5410,10 @@ fn main() {{
}), }),
cx, cx,
); );
registry.set_thread_summary_model( registry.set_thread_summary_model(Some(ConfiguredModel {
Some(ConfiguredModel { provider,
provider, model: model.clone(),
model: model.clone(), }));
}),
cx,
);
}) })
}); });

View file

@ -893,8 +893,19 @@ impl ThreadsDatabase {
let needs_migration_from_heed = mdb_path.exists(); let needs_migration_from_heed = mdb_path.exists();
let connection = if *ZED_STATELESS || cfg!(any(feature = "test-support", test)) { let connection = if *ZED_STATELESS {
Connection::open_memory(Some("THREAD_FALLBACK_DB")) 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 { } else {
Connection::open_file(&sqlite_path.to_string_lossy()) Connection::open_file(&sqlite_path.to_string_lossy())
}; };

View file

@ -10,6 +10,7 @@ path = "src/agent2.rs"
[features] [features]
test-support = ["db/test-support"] test-support = ["db/test-support"]
e2e = []
[lints] [lints]
workspace = true workspace = true
@ -26,6 +27,7 @@ assistant_context.workspace = true
assistant_tool.workspace = true assistant_tool.workspace = true
assistant_tools.workspace = true assistant_tools.workspace = true
chrono.workspace = true chrono.workspace = true
client.workspace = true
cloud_llm_client.workspace = true cloud_llm_client.workspace = true
collections.workspace = true collections.workspace = true
context_server.workspace = true context_server.workspace = true
@ -57,7 +59,9 @@ settings.workspace = true
smol.workspace = true smol.workspace = true
sqlez.workspace = true sqlez.workspace = true
task.workspace = true task.workspace = true
telemetry.workspace = true
terminal.workspace = true terminal.workspace = true
thiserror.workspace = true
text.workspace = true text.workspace = true
ui.workspace = true ui.workspace = true
util.workspace = true util.workspace = true
@ -70,6 +74,7 @@ zstd.workspace = true
[dev-dependencies] [dev-dependencies]
agent = { workspace = true, "features" = ["test-support"] } agent = { workspace = true, "features" = ["test-support"] }
agent_servers = { workspace = true, "features" = ["test-support"] }
assistant_context = { workspace = true, "features" = ["test-support"] } assistant_context = { workspace = true, "features" = ["test-support"] }
ctor.workspace = true ctor.workspace = true
client = { workspace = true, "features" = ["test-support"] } client = { workspace = true, "features" = ["test-support"] }

View file

@ -1,8 +1,8 @@
use crate::HistoryStore;
use crate::{ use crate::{
ContextServerRegistry, Thread, ThreadEvent, ThreadsDatabase, ToolCallAuthorization, ContextServerRegistry, Thread, ThreadEvent, ThreadsDatabase, ToolCallAuthorization,
UserMessageContent, templates::Templates, UserMessageContent, templates::Templates,
}; };
use crate::{HistoryStore, TitleUpdated, TokenUsageUpdated};
use acp_thread::{AcpThread, AgentModelSelector}; use acp_thread::{AcpThread, AgentModelSelector};
use action_log::ActionLog; use action_log::ActionLog;
use agent_client_protocol as acp; use agent_client_protocol as acp;
@ -180,7 +180,7 @@ impl NativeAgent {
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
cx: &mut AsyncApp, cx: &mut AsyncApp,
) -> Result<Entity<NativeAgent>> { ) -> Result<Entity<NativeAgent>> {
log::info!("Creating new NativeAgent"); log::debug!("Creating new NativeAgent");
let project_context = cx let project_context = cx
.update(|cx| Self::build_project_context(&project, prompt_store.as_ref(), cx))? .update(|cx| Self::build_project_context(&project, prompt_store.as_ref(), cx))?
@ -228,7 +228,7 @@ impl NativeAgent {
) -> Entity<AcpThread> { ) -> Entity<AcpThread> {
let connection = Rc::new(NativeAgentConnection(cx.entity())); let connection = Rc::new(NativeAgentConnection(cx.entity()));
let registry = LanguageModelRegistry::read_global(cx); let registry = LanguageModelRegistry::read_global(cx);
let summarization_model = registry.thread_summary_model().map(|c| c.model); let summarization_model = registry.thread_summary_model(cx).map(|c| c.model);
thread_handle.update(cx, |thread, cx| { thread_handle.update(cx, |thread, cx| {
thread.set_summarization_model(summarization_model, cx); thread.set_summarization_model(summarization_model, cx);
@ -240,21 +240,26 @@ impl NativeAgent {
let title = thread.title(); let title = thread.title();
let project = thread.project.clone(); let project = thread.project.clone();
let action_log = thread.action_log.clone(); let action_log = thread.action_log.clone();
let acp_thread = cx.new(|_cx| { let prompt_capabilities_rx = thread.prompt_capabilities_rx.clone();
let acp_thread = cx.new(|cx| {
acp_thread::AcpThread::new( acp_thread::AcpThread::new(
title, title,
connection, connection,
project.clone(), project.clone(),
action_log.clone(), action_log.clone(),
session_id.clone(), session_id.clone(),
prompt_capabilities_rx,
cx,
) )
}); });
let subscriptions = vec![ let subscriptions = vec![
cx.observe_release(&acp_thread, |this, acp_thread, _cx| { cx.observe_release(&acp_thread, |this, acp_thread, _cx| {
this.sessions.remove(acp_thread.session_id()); this.sessions.remove(acp_thread.session_id());
}), }),
cx.subscribe(&thread_handle, Self::handle_thread_title_updated),
cx.subscribe(&thread_handle, Self::handle_thread_token_usage_updated),
cx.observe(&thread_handle, move |this, thread, cx| { cx.observe(&thread_handle, move |this, thread, cx| {
this.save_thread(thread.clone(), cx) this.save_thread(thread, cx)
}), }),
]; ];
@ -440,6 +445,43 @@ impl NativeAgent {
}) })
} }
fn handle_thread_title_updated(
&mut self,
thread: Entity<Thread>,
_: &TitleUpdated,
cx: &mut Context<Self>,
) {
let session_id = thread.read(cx).id();
let Some(session) = self.sessions.get(session_id) else {
return;
};
let thread = thread.downgrade();
let acp_thread = session.acp_thread.clone();
cx.spawn(async move |_, cx| {
let title = thread.read_with(cx, |thread, _| thread.title())?;
let task = acp_thread.update(cx, |acp_thread, cx| acp_thread.set_title(title, cx))?;
task.await
})
.detach_and_log_err(cx);
}
fn handle_thread_token_usage_updated(
&mut self,
thread: Entity<Thread>,
usage: &TokenUsageUpdated,
cx: &mut Context<Self>,
) {
let Some(session) = self.sessions.get(thread.read(cx).id()) else {
return;
};
session
.acp_thread
.update(cx, |acp_thread, cx| {
acp_thread.update_token_usage(usage.0.clone(), cx);
})
.ok();
}
fn handle_project_event( fn handle_project_event(
&mut self, &mut self,
_project: Entity<Project>, _project: Entity<Project>,
@ -481,8 +523,8 @@ impl NativeAgent {
self.models.refresh_list(cx); self.models.refresh_list(cx);
let registry = LanguageModelRegistry::read_global(cx); let registry = LanguageModelRegistry::read_global(cx);
let default_model = registry.default_model().map(|m| m.model.clone()); let default_model = registry.default_model().map(|m| m.model);
let summarization_model = registry.thread_summary_model().map(|m| m.model.clone()); let summarization_model = registry.thread_summary_model(cx).map(|m| m.model);
for session in self.sessions.values_mut() { for session in self.sessions.values_mut() {
session.thread.update(cx, |thread, cx| { session.thread.update(cx, |thread, cx| {
@ -559,6 +601,10 @@ impl NativeAgent {
} }
fn save_thread(&mut self, thread: Entity<Thread>, cx: &mut Context<Self>) { fn save_thread(&mut self, thread: Entity<Thread>, cx: &mut Context<Self>) {
if thread.read(cx).is_empty() {
return;
}
let database_future = ThreadsDatabase::connect(cx); let database_future = ThreadsDatabase::connect(cx);
let (id, db_thread) = let (id, db_thread) =
thread.update(cx, |thread, cx| (thread.id().clone(), thread.to_db(cx))); thread.update(cx, |thread, cx| (thread.id().clone(), thread.to_db(cx)));
@ -695,15 +741,6 @@ impl NativeAgentConnection {
thread.update_tool_call(update, cx) thread.update_tool_call(update, cx)
})??; })??;
} }
ThreadEvent::TokenUsageUpdate(usage) => {
acp_thread.update(cx, |thread, cx| {
thread.update_token_usage(Some(usage), cx)
})?;
}
ThreadEvent::TitleUpdate(title) => {
acp_thread
.update(cx, |thread, cx| thread.update_title(title, cx))??;
}
ThreadEvent::Retry(status) => { ThreadEvent::Retry(status) => {
acp_thread.update(cx, |thread, cx| { acp_thread.update(cx, |thread, cx| {
thread.update_retry_status(status, cx) thread.update_retry_status(status, cx)
@ -722,7 +759,7 @@ impl NativeAgentConnection {
} }
} }
log::info!("Response stream completed"); log::debug!("Response stream completed");
anyhow::Ok(acp::PromptResponse { anyhow::Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn, stop_reason: acp::StopReason::EndTurn,
}) })
@ -747,7 +784,7 @@ impl AgentModelSelector for NativeAgentConnection {
model_id: acp_thread::AgentModelId, model_id: acp_thread::AgentModelId,
cx: &mut App, cx: &mut App,
) -> Task<Result<()>> { ) -> Task<Result<()>> {
log::info!("Setting model for session {}: {}", session_id, model_id); log::debug!("Setting model for session {}: {}", session_id, model_id);
let Some(thread) = self let Some(thread) = self
.0 .0
.read(cx) .read(cx)
@ -818,12 +855,11 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
cx: &mut App, cx: &mut App,
) -> Task<Result<Entity<acp_thread::AcpThread>>> { ) -> Task<Result<Entity<acp_thread::AcpThread>>> {
let agent = self.0.clone(); let agent = self.0.clone();
log::info!("Creating new thread for project at: {:?}", cwd); log::debug!("Creating new thread for project at: {:?}", cwd);
cx.spawn(async move |cx| { cx.spawn(async move |cx| {
log::debug!("Starting thread creation in async context"); log::debug!("Starting thread creation in async context");
let action_log = cx.new(|_cx| ActionLog::new(project.clone()))?;
// Create Thread // Create Thread
let thread = agent.update( let thread = agent.update(
cx, cx,
@ -839,20 +875,16 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
.models .models
.model_from_id(&LanguageModels::model_id(&default_model.model)) .model_from_id(&LanguageModels::model_id(&default_model.model))
}); });
Ok(cx.new(|cx| {
let thread = cx.new(|cx| {
Thread::new( Thread::new(
project.clone(), project.clone(),
agent.project_context.clone(), agent.project_context.clone(),
agent.context_server_registry.clone(), agent.context_server_registry.clone(),
action_log.clone(),
agent.templates.clone(), agent.templates.clone(),
default_model, default_model,
cx, cx,
) )
}); }))
Ok(thread)
}, },
)??; )??;
agent.update(cx, |agent, cx| agent.register_session(thread, cx)) agent.update(cx, |agent, cx| agent.register_session(thread, cx))
@ -888,7 +920,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
.into_iter() .into_iter()
.map(Into::into) .map(Into::into)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
log::info!("Converted prompt to message: {} chars", content.len()); log::debug!("Converted prompt to message: {} chars", content.len());
log::debug!("Message id: {:?}", id); log::debug!("Message id: {:?}", id);
log::debug!("Message content: {:?}", content); log::debug!("Message content: {:?}", content);
@ -899,7 +931,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
fn resume( fn resume(
&self, &self,
session_id: &acp::SessionId, session_id: &acp::SessionId,
_cx: &mut App, _cx: &App,
) -> Option<Rc<dyn acp_thread::AgentSessionResume>> { ) -> Option<Rc<dyn acp_thread::AgentSessionResume>> {
Some(Rc::new(NativeAgentSessionResume { Some(Rc::new(NativeAgentSessionResume {
connection: self.clone(), connection: self.clone(),
@ -916,12 +948,12 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
}); });
} }
fn session_editor( fn truncate(
&self, &self,
session_id: &agent_client_protocol::SessionId, session_id: &agent_client_protocol::SessionId,
cx: &mut App, cx: &App,
) -> Option<Rc<dyn acp_thread::AgentSessionEditor>> { ) -> Option<Rc<dyn acp_thread::AgentSessionTruncate>> {
self.0.update(cx, |agent, _cx| { self.0.read_with(cx, |agent, _cx| {
agent.sessions.get(session_id).map(|session| { agent.sessions.get(session_id).map(|session| {
Rc::new(NativeAgentSessionEditor { Rc::new(NativeAgentSessionEditor {
thread: session.thread.clone(), thread: session.thread.clone(),
@ -931,18 +963,54 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
}) })
} }
fn set_title(
&self,
session_id: &acp::SessionId,
_cx: &App,
) -> Option<Rc<dyn acp_thread::AgentSessionSetTitle>> {
Some(Rc::new(NativeAgentSessionSetTitle {
connection: self.clone(),
session_id: session_id.clone(),
}) as _)
}
fn telemetry(&self) -> Option<Rc<dyn acp_thread::AgentTelemetry>> {
Some(Rc::new(self.clone()) as Rc<dyn acp_thread::AgentTelemetry>)
}
fn into_any(self: Rc<Self>) -> Rc<dyn Any> { fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
self self
} }
} }
impl acp_thread::AgentTelemetry for NativeAgentConnection {
fn agent_name(&self) -> String {
"Zed".into()
}
fn thread_data(
&self,
session_id: &acp::SessionId,
cx: &mut App,
) -> Task<Result<serde_json::Value>> {
let Some(session) = self.0.read(cx).sessions.get(session_id) else {
return Task::ready(Err(anyhow!("Session not found")));
};
let task = session.thread.read(cx).to_db(cx);
cx.background_spawn(async move {
serde_json::to_value(task.await).context("Failed to serialize thread")
})
}
}
struct NativeAgentSessionEditor { struct NativeAgentSessionEditor {
thread: Entity<Thread>, thread: Entity<Thread>,
acp_thread: WeakEntity<AcpThread>, acp_thread: WeakEntity<AcpThread>,
} }
impl acp_thread::AgentSessionEditor for NativeAgentSessionEditor { impl acp_thread::AgentSessionTruncate for NativeAgentSessionEditor {
fn truncate(&self, message_id: acp_thread::UserMessageId, cx: &mut App) -> Task<Result<()>> { fn run(&self, message_id: acp_thread::UserMessageId, cx: &mut App) -> Task<Result<()>> {
match self.thread.update(cx, |thread, cx| { match self.thread.update(cx, |thread, cx| {
thread.truncate(message_id.clone(), cx)?; thread.truncate(message_id.clone(), cx)?;
Ok(thread.latest_token_usage()) Ok(thread.latest_token_usage())
@ -974,14 +1042,37 @@ impl acp_thread::AgentSessionResume for NativeAgentSessionResume {
} }
} }
struct NativeAgentSessionSetTitle {
connection: NativeAgentConnection,
session_id: acp::SessionId,
}
impl acp_thread::AgentSessionSetTitle for NativeAgentSessionSetTitle {
fn run(&self, title: SharedString, cx: &mut App) -> Task<Result<()>> {
let Some(session) = self.connection.0.read(cx).sessions.get(&self.session_id) else {
return Task::ready(Err(anyhow!("session not found")));
};
let thread = session.thread.clone();
thread.update(cx, |thread, cx| thread.set_title(title, cx));
Task::ready(Ok(()))
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::HistoryEntryId;
use super::*; use super::*;
use acp_thread::{AgentConnection, AgentModelGroupName, AgentModelId, AgentModelInfo}; use acp_thread::{
AgentConnection, AgentModelGroupName, AgentModelId, AgentModelInfo, MentionUri,
};
use fs::FakeFs; use fs::FakeFs;
use gpui::TestAppContext; use gpui::TestAppContext;
use indoc::indoc;
use language_model::fake_provider::FakeLanguageModel;
use serde_json::json; use serde_json::json;
use settings::SettingsStore; use settings::SettingsStore;
use util::path;
#[gpui::test] #[gpui::test]
async fn test_maintaining_project_context(cx: &mut TestAppContext) { async fn test_maintaining_project_context(cx: &mut TestAppContext) {
@ -1166,6 +1257,158 @@ mod tests {
); );
} }
#[gpui::test]
#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows
async fn test_save_load_thread(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/",
json!({
"a": {
"b.md": "Lorem"
}
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/a").as_ref()], cx).await;
let context_store = cx.new(|cx| assistant_context::ContextStore::fake(project.clone(), cx));
let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
let agent = NativeAgent::new(
project.clone(),
history_store.clone(),
Templates::new(),
None,
fs.clone(),
&mut cx.to_async(),
)
.await
.unwrap();
let connection = Rc::new(NativeAgentConnection(agent.clone()));
let acp_thread = cx
.update(|cx| {
connection
.clone()
.new_thread(project.clone(), Path::new(""), cx)
})
.await
.unwrap();
let session_id = acp_thread.read_with(cx, |thread, _| thread.session_id().clone());
let thread = agent.read_with(cx, |agent, _| {
agent.sessions.get(&session_id).unwrap().thread.clone()
});
// Ensure empty threads are not saved, even if they get mutated.
let model = Arc::new(FakeLanguageModel::default());
let summary_model = Arc::new(FakeLanguageModel::default());
thread.update(cx, |thread, cx| {
thread.set_model(model.clone(), cx);
thread.set_summarization_model(Some(summary_model.clone()), cx);
});
cx.run_until_parked();
assert_eq!(history_entries(&history_store, cx), vec![]);
let send = acp_thread.update(cx, |thread, cx| {
thread.send(
vec![
"What does ".into(),
acp::ContentBlock::ResourceLink(acp::ResourceLink {
name: "b.md".into(),
uri: MentionUri::File {
abs_path: path!("/a/b.md").into(),
}
.to_uri()
.to_string(),
annotations: None,
description: None,
mime_type: None,
size: None,
title: None,
}),
" mean?".into(),
],
cx,
)
});
let send = cx.foreground_executor().spawn(send);
cx.run_until_parked();
model.send_last_completion_stream_text_chunk("Lorem.");
model.end_last_completion_stream();
cx.run_until_parked();
summary_model.send_last_completion_stream_text_chunk("Explaining /a/b.md");
summary_model.end_last_completion_stream();
send.await.unwrap();
acp_thread.read_with(cx, |thread, cx| {
assert_eq!(
thread.to_markdown(cx),
indoc! {"
## User
What does [@b.md](file:///a/b.md) mean?
## Assistant
Lorem.
"}
)
});
cx.run_until_parked();
// Drop the ACP thread, which should cause the session to be dropped as well.
cx.update(|_| {
drop(thread);
drop(acp_thread);
});
agent.read_with(cx, |agent, _| {
assert_eq!(agent.sessions.keys().cloned().collect::<Vec<_>>(), []);
});
// Ensure the thread can be reloaded from disk.
assert_eq!(
history_entries(&history_store, cx),
vec![(
HistoryEntryId::AcpThread(session_id.clone()),
"Explaining /a/b.md".into()
)]
);
let acp_thread = agent
.update(cx, |agent, cx| agent.open_thread(session_id.clone(), cx))
.await
.unwrap();
acp_thread.read_with(cx, |thread, cx| {
assert_eq!(
thread.to_markdown(cx),
indoc! {"
## User
What does [@b.md](file:///a/b.md) mean?
## Assistant
Lorem.
"}
)
});
}
fn history_entries(
history: &Entity<HistoryStore>,
cx: &mut TestAppContext,
) -> Vec<(HistoryEntryId, String)> {
history.read_with(cx, |history, _| {
history
.entries()
.map(|e| (e.id(), e.title().to_string()))
.collect::<Vec<_>>()
})
}
fn init_test(cx: &mut TestAppContext) { fn init_test(cx: &mut TestAppContext) {
env_logger::try_init().ok(); env_logger::try_init().ok();
cx.update(|cx| { cx.update(|cx| {

View file

@ -266,8 +266,19 @@ impl ThreadsDatabase {
} }
pub fn new(executor: BackgroundExecutor) -> Result<Self> { pub fn new(executor: BackgroundExecutor) -> Result<Self> {
let connection = if *ZED_STATELESS || cfg!(any(feature = "test-support", test)) { let connection = if *ZED_STATELESS {
Connection::open_memory(Some("THREAD_FALLBACK_DB")) 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 { } else {
let threads_dir = paths::data_dir().join("threads"); let threads_dir = paths::data_dir().join("threads");
std::fs::create_dir_all(&threads_dir)?; std::fs::create_dir_all(&threads_dir)?;
@ -287,7 +298,7 @@ impl ThreadsDatabase {
.map_err(|e| anyhow!("Failed to create threads table: {}", e))?; .map_err(|e| anyhow!("Failed to create threads table: {}", e))?;
let db = Self { let db = Self {
executor: executor.clone(), executor,
connection: Arc::new(Mutex::new(connection)), connection: Arc::new(Mutex::new(connection)),
}; };
@ -325,7 +336,7 @@ impl ThreadsDatabase {
INSERT OR REPLACE INTO threads (id, summary, updated_at, data_type, data) VALUES (?, ?, ?, ?, ?) INSERT OR REPLACE INTO threads (id, summary, updated_at, data_type, data) VALUES (?, ?, ?, ?, ?)
"})?; "})?;
insert((id.0.clone(), title, updated_at, data_type, data))?; insert((id.0, title, updated_at, data_type, data))?;
Ok(()) Ok(())
} }
@ -434,7 +445,7 @@ mod tests {
let client = Client::new(clock, http_client, cx); let client = Client::new(clock, http_client, cx);
agent::init(cx); agent::init(cx);
agent_settings::init(cx); agent_settings::init(cx);
language_model::init(client.clone(), cx); language_model::init(client, cx);
}); });
} }

View file

@ -10,6 +10,7 @@ use itertools::Itertools;
use paths::contexts_dir; use paths::contexts_dir;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{collections::VecDeque, path::Path, sync::Arc, time::Duration}; use std::{collections::VecDeque, path::Path, sync::Arc, time::Duration};
use ui::ElementId;
use util::ResultExt as _; use util::ResultExt as _;
const MAX_RECENTLY_OPENED_ENTRIES: usize = 6; const MAX_RECENTLY_OPENED_ENTRIES: usize = 6;
@ -68,6 +69,15 @@ pub enum HistoryEntryId {
TextThread(Arc<Path>), 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)] #[derive(Serialize, Deserialize, Debug)]
enum SerializedRecentOpen { enum SerializedRecentOpen {
AcpThread(String), AcpThread(String),
@ -76,6 +86,7 @@ enum SerializedRecentOpen {
pub struct HistoryStore { pub struct HistoryStore {
threads: Vec<DbThreadMetadata>, threads: Vec<DbThreadMetadata>,
entries: Vec<HistoryEntry>,
context_store: Entity<assistant_context::ContextStore>, context_store: Entity<assistant_context::ContextStore>,
recently_opened_entries: VecDeque<HistoryEntryId>, recently_opened_entries: VecDeque<HistoryEntryId>,
_subscriptions: Vec<gpui::Subscription>, _subscriptions: Vec<gpui::Subscription>,
@ -87,7 +98,7 @@ impl HistoryStore {
context_store: Entity<assistant_context::ContextStore>, context_store: Entity<assistant_context::ContextStore>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Self { ) -> Self {
let subscriptions = vec![cx.observe(&context_store, |_, _, cx| cx.notify())]; let subscriptions = vec![cx.observe(&context_store, |this, _, cx| this.update_entries(cx))];
cx.spawn(async move |this, cx| { cx.spawn(async move |this, cx| {
let entries = Self::load_recently_opened_entries(cx).await; let entries = Self::load_recently_opened_entries(cx).await;
@ -106,11 +117,16 @@ impl HistoryStore {
context_store, context_store,
recently_opened_entries: VecDeque::default(), recently_opened_entries: VecDeque::default(),
threads: Vec::default(), threads: Vec::default(),
entries: Vec::default(),
_subscriptions: subscriptions, _subscriptions: subscriptions,
_save_recently_opened_entries_task: Task::ready(()), _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( pub fn delete_thread(
&mut self, &mut self,
id: acp::SessionId, id: acp::SessionId,
@ -167,20 +183,18 @@ impl HistoryStore {
} }
} }
this.threads = threads; this.threads = threads;
cx.notify(); this.update_entries(cx);
}) })
}) })
.detach_and_log_err(cx); .detach_and_log_err(cx);
} }
pub fn entries(&self, cx: &App) -> Vec<HistoryEntry> { fn update_entries(&mut self, cx: &mut Context<Self>) {
let mut history_entries = Vec::new();
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() { if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
return history_entries; return;
} }
let mut history_entries = Vec::new();
history_entries.extend(self.threads.iter().cloned().map(HistoryEntry::AcpThread)); history_entries.extend(self.threads.iter().cloned().map(HistoryEntry::AcpThread));
history_entries.extend( history_entries.extend(
self.context_store self.context_store
@ -191,17 +205,12 @@ impl HistoryStore {
); );
history_entries.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.updated_at())); history_entries.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.updated_at()));
history_entries self.entries = history_entries;
cx.notify()
} }
pub fn is_empty(&self, cx: &App) -> bool { pub fn is_empty(&self, _cx: &App) -> bool {
self.threads.is_empty() self.entries.is_empty()
&& self
.context_store
.read(cx)
.unordered_contexts()
.next()
.is_none()
} }
pub fn recently_opened_entries(&self, cx: &App) -> Vec<HistoryEntry> { pub fn recently_opened_entries(&self, cx: &App) -> Vec<HistoryEntry> {
@ -312,10 +321,9 @@ impl HistoryStore {
} }
pub fn remove_recently_opened_thread(&mut self, id: acp::SessionId, cx: &mut Context<Self>) { pub fn remove_recently_opened_thread(&mut self, id: acp::SessionId, cx: &mut Context<Self>) {
self.recently_opened_entries.retain(|entry| match entry { self.recently_opened_entries.retain(
HistoryEntryId::AcpThread(thread_id) if thread_id == &id => false, |entry| !matches!(entry, HistoryEntryId::AcpThread(thread_id) if thread_id == &id),
_ => true, );
});
self.save_recently_opened_entries(cx); self.save_recently_opened_entries(cx);
} }
@ -342,4 +350,8 @@ impl HistoryStore {
.retain(|old_entry| old_entry != entry); .retain(|old_entry| old_entry != entry);
self.save_recently_opened_entries(cx); self.save_recently_opened_entries(cx);
} }
pub fn entries(&self) -> impl Iterator<Item = HistoryEntry> {
self.entries.iter().cloned()
}
} }

View file

@ -3,7 +3,7 @@ use std::{any::Any, path::Path, rc::Rc, sync::Arc};
use agent_servers::AgentServer; use agent_servers::AgentServer;
use anyhow::Result; use anyhow::Result;
use fs::Fs; use fs::Fs;
use gpui::{App, Entity, Task}; use gpui::{App, Entity, SharedString, Task};
use project::Project; use project::Project;
use prompt_store::PromptStore; use prompt_store::PromptStore;
@ -22,16 +22,20 @@ impl NativeAgentServer {
} }
impl AgentServer for NativeAgentServer { impl AgentServer for NativeAgentServer {
fn name(&self) -> &'static str { fn telemetry_id(&self) -> &'static str {
"Native Agent" "zed"
} }
fn empty_state_headline(&self) -> &'static str { fn name(&self) -> SharedString {
"" "Zed Agent".into()
} }
fn empty_state_message(&self) -> &'static str { fn empty_state_headline(&self) -> SharedString {
"" self.name()
}
fn empty_state_message(&self) -> SharedString {
"".into()
} }
fn logo(&self) -> ui::IconName { fn logo(&self) -> ui::IconName {
@ -44,7 +48,7 @@ impl AgentServer for NativeAgentServer {
project: &Entity<Project>, project: &Entity<Project>,
cx: &mut App, cx: &mut App,
) -> Task<Result<Rc<dyn acp_thread::AgentConnection>>> { ) -> Task<Result<Rc<dyn acp_thread::AgentConnection>>> {
log::info!( log::debug!(
"NativeAgentServer::connect called for path: {:?}", "NativeAgentServer::connect called for path: {:?}",
_root_dir _root_dir
); );
@ -63,7 +67,7 @@ impl AgentServer for NativeAgentServer {
// Create the connection wrapper // Create the connection wrapper
let connection = NativeAgentConnection(agent); let connection = NativeAgentConnection(agent);
log::info!("NativeAgentServer connection established successfully"); log::debug!("NativeAgentServer connection established successfully");
Ok(Rc::new(connection) as Rc<dyn acp_thread::AgentConnection>) Ok(Rc::new(connection) as Rc<dyn acp_thread::AgentConnection>)
}) })
@ -73,3 +77,52 @@ impl AgentServer for NativeAgentServer {
self 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 Input = EchoToolInput;
type Output = String; type Output = String;
fn name(&self) -> SharedString { fn name() -> &'static str {
"echo".into() "echo"
} }
fn kind(&self) -> acp::ToolKind { fn kind() -> acp::ToolKind {
acp::ToolKind::Other acp::ToolKind::Other
} }
@ -51,8 +51,8 @@ impl AgentTool for DelayTool {
type Input = DelayToolInput; type Input = DelayToolInput;
type Output = String; type Output = String;
fn name(&self) -> SharedString { fn name() -> &'static str {
"delay".into() "delay"
} }
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString { fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
@ -63,7 +63,7 @@ impl AgentTool for DelayTool {
} }
} }
fn kind(&self) -> acp::ToolKind { fn kind() -> acp::ToolKind {
acp::ToolKind::Other acp::ToolKind::Other
} }
@ -92,11 +92,11 @@ impl AgentTool for ToolRequiringPermission {
type Input = ToolRequiringPermissionInput; type Input = ToolRequiringPermissionInput;
type Output = String; type Output = String;
fn name(&self) -> SharedString { fn name() -> &'static str {
"tool_requiring_permission".into() "tool_requiring_permission"
} }
fn kind(&self) -> acp::ToolKind { fn kind() -> acp::ToolKind {
acp::ToolKind::Other acp::ToolKind::Other
} }
@ -127,11 +127,11 @@ impl AgentTool for InfiniteTool {
type Input = InfiniteToolInput; type Input = InfiniteToolInput;
type Output = String; type Output = String;
fn name(&self) -> SharedString { fn name() -> &'static str {
"infinite".into() "infinite"
} }
fn kind(&self) -> acp::ToolKind { fn kind() -> acp::ToolKind {
acp::ToolKind::Other acp::ToolKind::Other
} }
@ -178,11 +178,11 @@ impl AgentTool for WordListTool {
type Input = WordListInput; type Input = WordListInput;
type Output = String; type Output = String;
fn name(&self) -> SharedString { fn name() -> &'static str {
"word_list".into() "word_list"
} }
fn kind(&self) -> acp::ToolKind { fn kind() -> acp::ToolKind {
acp::ToolKind::Other acp::ToolKind::Other
} }

File diff suppressed because it is too large Load diff

View file

@ -16,6 +16,29 @@ mod terminal_tool;
mod thinking_tool; mod thinking_tool;
mod web_search_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 context_server_registry::*;
pub use copy_path_tool::*; pub use copy_path_tool::*;
pub use create_directory_tool::*; pub use create_directory_tool::*;
@ -33,3 +56,5 @@ pub use read_file_tool::*;
pub use terminal_tool::*; pub use terminal_tool::*;
pub use thinking_tool::*; pub use thinking_tool::*;
pub use web_search_tool::*; pub use web_search_tool::*;
use crate::AgentTool;

View file

@ -176,15 +176,13 @@ impl AnyAgentTool for ContextServerTool {
return Task::ready(Err(anyhow!("Context server not found"))); return Task::ready(Err(anyhow!("Context server not found")));
}; };
let tool_name = self.tool.name.clone(); let tool_name = self.tool.name.clone();
let server_clone = server.clone();
let input_clone = input.clone();
cx.spawn(async move |_cx| { cx.spawn(async move |_cx| {
let Some(protocol) = server_clone.client() else { let Some(protocol) = server.client() else {
bail!("Context server not initialized"); bail!("Context server not initialized");
}; };
let arguments = if let serde_json::Value::Object(map) = input_clone { let arguments = if let serde_json::Value::Object(map) = input {
Some(map.into_iter().collect()) Some(map.into_iter().collect())
} else { } else {
None None

View file

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

View file

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

View file

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

View file

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

View file

@ -34,25 +34,21 @@ const DEFAULT_UI_TEXT: &str = "Editing file";
/// - Use the `list_directory` tool to verify the parent directory exists and is the correct location /// - Use the `list_directory` tool to verify the parent directory exists and is the correct location
#[derive(Debug, Serialize, Deserialize, JsonSchema)] #[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct EditFileToolInput { pub struct EditFileToolInput {
/// A one-line, user-friendly markdown description of the edit. This will be /// 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.
/// 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 /// Be terse, but also descriptive in what you want to achieve with this edit. Avoid generic instructions.
/// edit. Avoid generic instructions.
/// ///
/// NEVER mention the file path in this description. /// NEVER mention the file path in this description.
/// ///
/// <example>Fix API endpoint URLs</example> /// <example>Fix API endpoint URLs</example>
/// <example>Update copyright year in `page_footer`</example> /// <example>Update copyright year in `page_footer`</example>
/// ///
/// Make sure to include this field before all the others in the input object /// Make sure to include this field before all the others in the input object so that we can display it immediately.
/// so that we can display it immediately.
pub display_description: String, pub display_description: String,
/// The full path of the file to create or modify in the project. /// The full path of the file to create or modify in the project.
/// ///
/// WARNING: When specifying which file path need changing, you MUST /// WARNING: When specifying which file path need changing, you MUST start each path with one of the project's root directories.
/// start each path with one of the project's root directories.
/// ///
/// The following examples assume we have two root directories in the project: /// The following examples assume we have two root directories in the project:
/// - /a/b/backend /// - /a/b/backend
@ -61,22 +57,19 @@ pub struct EditFileToolInput {
/// <example> /// <example>
/// `backend/src/main.rs` /// `backend/src/main.rs`
/// ///
/// Notice how the file path starts with `backend`. Without that, the path /// Notice how the file path starts with `backend`. Without that, the path would be ambiguous and the call would fail!
/// would be ambiguous and the call would fail!
/// </example> /// </example>
/// ///
/// <example> /// <example>
/// `frontend/db.js` /// `frontend/db.js`
/// </example> /// </example>
pub path: PathBuf, pub path: PathBuf,
/// The mode of operation on the file. Possible values: /// The mode of operation on the file. Possible values:
/// - 'edit': Make granular edits to an existing file. /// - 'edit': Make granular edits to an existing file.
/// - 'create': Create a new file if it doesn't exist. /// - 'create': Create a new file if it doesn't exist.
/// - 'overwrite': Replace the entire contents of an existing file. /// - 'overwrite': Replace the entire contents of an existing file.
/// ///
/// When a file already exists or you just created it, prefer editing /// When a file already exists or you just created it, prefer editing it as opposed to recreating it from scratch.
/// it as opposed to recreating it from scratch.
pub mode: EditFileMode, pub mode: EditFileMode,
} }
@ -193,11 +186,11 @@ impl AgentTool for EditFileTool {
type Input = EditFileToolInput; type Input = EditFileToolInput;
type Output = EditFileToolOutput; type Output = EditFileToolOutput;
fn name(&self) -> SharedString { fn name() -> &'static str {
"edit_file".into() "edit_file"
} }
fn kind(&self) -> acp::ToolKind { fn kind() -> acp::ToolKind {
acp::ToolKind::Edit acp::ToolKind::Edit
} }
@ -427,7 +420,7 @@ impl AgentTool for EditFileTool {
Ok(EditFileToolOutput { Ok(EditFileToolOutput {
input_path: input.path, input_path: input.path,
new_text: new_text.clone(), new_text,
old_text, old_text,
diff: unified_diff, diff: unified_diff,
edit_agent_output, edit_agent_output,
@ -524,7 +517,6 @@ fn resolve_path(
mod tests { mod tests {
use super::*; use super::*;
use crate::{ContextServerRegistry, Templates}; use crate::{ContextServerRegistry, Templates};
use action_log::ActionLog;
use client::TelemetrySettings; use client::TelemetrySettings;
use fs::Fs; use fs::Fs;
use gpui::{TestAppContext, UpdateGlobal}; use gpui::{TestAppContext, UpdateGlobal};
@ -542,7 +534,6 @@ mod tests {
fs.insert_tree("/root", json!({})).await; fs.insert_tree("/root", json!({})).await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); 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 = let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default()); let model = Arc::new(FakeLanguageModel::default());
@ -551,7 +542,6 @@ mod tests {
project, project,
cx.new(|_cx| ProjectContext::default()), cx.new(|_cx| ProjectContext::default()),
context_server_registry, context_server_registry,
action_log,
Templates::new(), Templates::new(),
Some(model), Some(model),
cx, cx,
@ -742,7 +732,6 @@ mod tests {
} }
}); });
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry = let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default()); let model = Arc::new(FakeLanguageModel::default());
@ -751,7 +740,6 @@ mod tests {
project, project,
cx.new(|_cx| ProjectContext::default()), cx.new(|_cx| ProjectContext::default()),
context_server_registry, context_server_registry,
action_log.clone(),
Templates::new(), Templates::new(),
Some(model.clone()), Some(model.clone()),
cx, cx,
@ -808,7 +796,9 @@ mod tests {
"Code should be formatted when format_on_save is enabled" "Code should be formatted when format_on_save is enabled"
); );
let stale_buffer_count = action_log.read_with(cx, |log, cx| log.stale_buffers(cx).count()); let stale_buffer_count = thread
.read_with(cx, |thread, _cx| thread.action_log.clone())
.read_with(cx, |log, cx| log.stale_buffers(cx).count());
assert_eq!( assert_eq!(
stale_buffer_count, 0, stale_buffer_count, 0,
@ -886,14 +876,12 @@ mod tests {
let context_server_registry = let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); 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 model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| { let thread = cx.new(|cx| {
Thread::new( Thread::new(
project, project,
cx.new(|_cx| ProjectContext::default()), cx.new(|_cx| ProjectContext::default()),
context_server_registry, context_server_registry,
action_log.clone(),
Templates::new(), Templates::new(),
Some(model.clone()), Some(model.clone()),
cx, cx,
@ -1015,14 +1003,12 @@ mod tests {
let context_server_registry = let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); 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 model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| { let thread = cx.new(|cx| {
Thread::new( Thread::new(
project, project,
cx.new(|_cx| ProjectContext::default()), cx.new(|_cx| ProjectContext::default()),
context_server_registry, context_server_registry,
action_log.clone(),
Templates::new(), Templates::new(),
Some(model.clone()), Some(model.clone()),
cx, cx,
@ -1153,14 +1139,12 @@ mod tests {
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
let context_server_registry = let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let model = Arc::new(FakeLanguageModel::default()); let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| { let thread = cx.new(|cx| {
Thread::new( Thread::new(
project, project,
cx.new(|_cx| ProjectContext::default()), cx.new(|_cx| ProjectContext::default()),
context_server_registry, context_server_registry,
action_log.clone(),
Templates::new(), Templates::new(),
Some(model.clone()), Some(model.clone()),
cx, cx,
@ -1261,7 +1245,6 @@ mod tests {
) )
.await; .await;
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); 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 = let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default()); let model = Arc::new(FakeLanguageModel::default());
@ -1270,7 +1253,6 @@ mod tests {
project.clone(), project.clone(),
cx.new(|_cx| ProjectContext::default()), cx.new(|_cx| ProjectContext::default()),
context_server_registry.clone(), context_server_registry.clone(),
action_log.clone(),
Templates::new(), Templates::new(),
Some(model.clone()), Some(model.clone()),
cx, cx,
@ -1343,7 +1325,6 @@ mod tests {
.await; .await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).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 language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry = let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default()); let model = Arc::new(FakeLanguageModel::default());
@ -1352,7 +1333,6 @@ mod tests {
project.clone(), project.clone(),
cx.new(|_cx| ProjectContext::default()), cx.new(|_cx| ProjectContext::default()),
context_server_registry.clone(), context_server_registry.clone(),
action_log.clone(),
Templates::new(), Templates::new(),
Some(model.clone()), Some(model.clone()),
cx, cx,
@ -1428,7 +1408,6 @@ mod tests {
.await; .await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).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 language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry = let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default()); let model = Arc::new(FakeLanguageModel::default());
@ -1437,7 +1416,6 @@ mod tests {
project.clone(), project.clone(),
cx.new(|_cx| ProjectContext::default()), cx.new(|_cx| ProjectContext::default()),
context_server_registry.clone(), context_server_registry.clone(),
action_log.clone(),
Templates::new(), Templates::new(),
Some(model.clone()), Some(model.clone()),
cx, cx,
@ -1510,7 +1488,6 @@ mod tests {
let fs = project::FakeFs::new(cx.executor()); let fs = project::FakeFs::new(cx.executor());
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).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 language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry = let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default()); let model = Arc::new(FakeLanguageModel::default());
@ -1519,7 +1496,6 @@ mod tests {
project.clone(), project.clone(),
cx.new(|_cx| ProjectContext::default()), cx.new(|_cx| ProjectContext::default()),
context_server_registry, context_server_registry,
action_log.clone(),
Templates::new(), Templates::new(),
Some(model.clone()), Some(model.clone()),
cx, cx,

View file

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

View file

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

View file

@ -27,8 +27,7 @@ use util::paths::PathMatcher;
/// - DO NOT use HTML entities solely to escape characters in the tool parameters. /// - DO NOT use HTML entities solely to escape characters in the tool parameters.
#[derive(Debug, Serialize, Deserialize, JsonSchema)] #[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct GrepToolInput { pub struct GrepToolInput {
/// A regex pattern to search for in the entire project. Note that the regex /// A regex pattern to search for in the entire project. Note that the regex will be parsed by the Rust `regex` crate.
/// will be parsed by the Rust `regex` crate.
/// ///
/// Do NOT specify a path here! This will only be matched against the code **content**. /// Do NOT specify a path here! This will only be matched against the code **content**.
pub regex: String, pub regex: String,
@ -68,11 +67,11 @@ impl AgentTool for GrepTool {
type Input = GrepToolInput; type Input = GrepToolInput;
type Output = String; type Output = String;
fn name(&self) -> SharedString { fn name() -> &'static str {
"grep".into() "grep"
} }
fn kind(&self) -> acp::ToolKind { fn kind() -> acp::ToolKind {
acp::ToolKind::Search acp::ToolKind::Search
} }
@ -318,7 +317,7 @@ mod tests {
init_test(cx); init_test(cx);
cx.executor().allow_parking(); cx.executor().allow_parking();
let fs = FakeFs::new(cx.executor().clone()); let fs = FakeFs::new(cx.executor());
fs.insert_tree( fs.insert_tree(
path!("/root"), path!("/root"),
serde_json::json!({ serde_json::json!({
@ -403,7 +402,7 @@ mod tests {
init_test(cx); init_test(cx);
cx.executor().allow_parking(); cx.executor().allow_parking();
let fs = FakeFs::new(cx.executor().clone()); let fs = FakeFs::new(cx.executor());
fs.insert_tree( fs.insert_tree(
path!("/root"), path!("/root"),
serde_json::json!({ serde_json::json!({
@ -478,7 +477,7 @@ mod tests {
init_test(cx); init_test(cx);
cx.executor().allow_parking(); cx.executor().allow_parking();
let fs = FakeFs::new(cx.executor().clone()); let fs = FakeFs::new(cx.executor());
// Create test file with syntax structures // Create test file with syntax structures
fs.insert_tree( fs.insert_tree(
@ -763,7 +762,7 @@ mod tests {
if cfg!(windows) { if cfg!(windows) {
result.replace("root\\", "root/") result.replace("root\\", "root/")
} else { } else {
result.to_string() result
} }
} }
Err(e) => panic!("Failed to run grep tool: {}", e), Err(e) => panic!("Failed to run grep tool: {}", e),

View file

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

View file

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

View file

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

View file

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

View file

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

@ -63,11 +63,11 @@ impl AgentTool for TerminalTool {
type Input = TerminalToolInput; type Input = TerminalToolInput;
type Output = String; type Output = String;
fn name(&self) -> SharedString { fn name() -> &'static str {
"terminal".into() "terminal"
} }
fn kind(&self) -> acp::ToolKind { fn kind() -> acp::ToolKind {
acp::ToolKind::Execute acp::ToolKind::Execute
} }
@ -234,7 +234,7 @@ fn process_content(
if is_empty { if is_empty {
"Command executed successfully.".to_string() "Command executed successfully.".to_string()
} else { } else {
content.to_string() content
} }
} }
Some(exit_status) => { Some(exit_status) => {

View file

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

View file

@ -14,7 +14,7 @@ use ui::prelude::*;
use web_search::WebSearchRegistry; use web_search::WebSearchRegistry;
/// Search the web for information using your query. /// 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. /// Results will include snippets and links from relevant web pages.
#[derive(Debug, Serialize, Deserialize, JsonSchema)] #[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct WebSearchToolInput { pub struct WebSearchToolInput {
@ -40,11 +40,11 @@ impl AgentTool for WebSearchTool {
type Input = WebSearchToolInput; type Input = WebSearchToolInput;
type Output = WebSearchToolOutput; type Output = WebSearchToolOutput;
fn name(&self) -> SharedString { fn name() -> &'static str {
"web_search".into() "web_search"
} }
fn kind(&self) -> acp::ToolKind { fn kind() -> acp::ToolKind {
acp::ToolKind::Fetch acp::ToolKind::Fetch
} }

View file

@ -6,7 +6,7 @@ publish.workspace = true
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
[features] [features]
test-support = ["acp_thread/test-support", "gpui/test-support", "project/test-support"] test-support = ["acp_thread/test-support", "gpui/test-support", "project/test-support", "dep:env_logger", "fs", "client/test-support", "dep:gpui_tokio", "reqwest_client/test-support"]
e2e = [] e2e = []
[lints] [lints]
@ -17,16 +17,20 @@ path = "src/agent_servers.rs"
doctest = false doctest = false
[dependencies] [dependencies]
acp_tools.workspace = true
acp_thread.workspace = true acp_thread.workspace = true
action_log.workspace = true action_log.workspace = true
agent-client-protocol.workspace = true agent-client-protocol.workspace = true
agent_settings.workspace = true agent_settings.workspace = true
agentic-coding-protocol.workspace = true
anyhow.workspace = true anyhow.workspace = true
client = { workspace = true, optional = true }
collections.workspace = true collections.workspace = true
context_server.workspace = true context_server.workspace = true
env_logger = { workspace = true, optional = true }
fs = { workspace = true, optional = true }
futures.workspace = true futures.workspace = true
gpui.workspace = true gpui.workspace = true
gpui_tokio = { workspace = true, optional = true }
indoc.workspace = true indoc.workspace = true
itertools.workspace = true itertools.workspace = true
language.workspace = true language.workspace = true
@ -36,6 +40,7 @@ log.workspace = true
paths.workspace = true paths.workspace = true
project.workspace = true project.workspace = true
rand.workspace = true rand.workspace = true
reqwest_client = { workspace = true, optional = true }
schemars.workspace = true schemars.workspace = true
semver.workspace = true semver.workspace = true
serde.workspace = true serde.workspace = true
@ -57,8 +62,12 @@ libc.workspace = true
nix.workspace = true nix.workspace = true
[dev-dependencies] [dev-dependencies]
client = { workspace = true, features = ["test-support"] }
env_logger.workspace = true env_logger.workspace = true
fs.workspace = true
language.workspace = true language.workspace = true
indoc.workspace = true indoc.workspace = true
acp_thread = { workspace = true, features = ["test-support"] } acp_thread = { workspace = true, features = ["test-support"] }
gpui = { 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,34 +1,392 @@
use std::{path::Path, rc::Rc};
use crate::AgentServerCommand; use crate::AgentServerCommand;
use acp_thread::AgentConnection; use acp_thread::AgentConnection;
use anyhow::Result; use acp_tools::AcpConnectionRegistry;
use gpui::AsyncApp; 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::channel::oneshot;
use futures::io::BufReader;
use project::Project;
use serde::Deserialize;
use std::{any::Any, cell::RefCell};
use std::{path::Path, rc::Rc};
use thiserror::Error; use thiserror::Error;
mod v0; use anyhow::{Context as _, Result};
mod v1; use gpui::{App, AppContext as _, AsyncApp, Entity, SharedString, Task, WeakEntity};
use acp_thread::{AcpThread, AuthRequired, LoadError};
#[derive(Debug, Error)] #[derive(Debug, Error)]
#[error("Unsupported version")] #[error("Unsupported version")]
pub struct UnsupportedVersion; 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>,
prompt_capabilities: acp::PromptCapabilities,
_io_task: Task<Result<()>>,
}
pub struct AcpSession {
thread: WeakEntity<AcpThread>,
suppress_abort_err: bool,
}
pub async fn connect( pub async fn connect(
server_name: &'static str, server_name: SharedString,
command: AgentServerCommand, command: AgentServerCommand,
root_dir: &Path, root_dir: &Path,
cx: &mut AsyncApp, cx: &mut AsyncApp,
) -> Result<Rc<dyn AgentConnection>> { ) -> Result<Rc<dyn AgentConnection>> {
let conn = v1::AcpConnection::stdio(server_name, command.clone(), root_dir, cx).await; let conn = AcpConnection::stdio(server_name, command.clone(), root_dir, cx).await?;
Ok(Rc::new(conn) as _)
}
match conn { const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::V1;
Ok(conn) => Ok(Rc::new(conn) as _),
Err(err) if err.is::<UnsupportedVersion>() => { impl AcpConnection {
// Consider re-using initialize response and subprocess when adding another version here pub async fn stdio(
let conn: Rc<dyn AgentConnection> = server_name: SharedString,
Rc::new(v0::AcpConnection::stdio(server_name, command, root_dir, cx).await?); command: AgentServerCommand,
Ok(conn) 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_load_error(LoadError::Exited { status }, cx)
})
.ok();
}
anyhow::Ok(())
}
})
.detach();
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,
},
},
})
.await?;
if response.protocol_version < MINIMUM_SUPPORTED_VERSION {
return Err(UnsupportedVersion.into());
} }
Err(err) => Err(err),
Ok(Self {
auth_methods: response.auth_methods,
connection,
server_name,
sessions,
prompt_capabilities: response.agent_capabilities.prompt_capabilities,
_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 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.prompt_capabilities),
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 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::Cancelled,
};
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

@ -498,6 +498,14 @@ impl AgentConnection for AcpConnection {
}) })
} }
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
acp::PromptCapabilities {
image: false,
audio: false,
embedded_context: false,
}
}
fn cancel(&self, _session_id: &acp::SessionId, cx: &mut App) { fn cancel(&self, _session_id: &acp::SessionId, cx: &mut App) {
let task = self let task = self
.connection .connection

View file

@ -1,11 +1,13 @@
use acp_tools::AcpConnectionRegistry;
use action_log::ActionLog; use action_log::ActionLog;
use agent_client_protocol::{self as acp, Agent as _}; use agent_client_protocol::{self as acp, Agent as _, ErrorCode};
use anyhow::anyhow; use anyhow::anyhow;
use collections::HashMap; use collections::HashMap;
use futures::AsyncBufReadExt as _; use futures::AsyncBufReadExt as _;
use futures::channel::oneshot; use futures::channel::oneshot;
use futures::io::BufReader; use futures::io::BufReader;
use project::Project; use project::Project;
use serde::Deserialize;
use std::path::Path; use std::path::Path;
use std::rc::Rc; use std::rc::Rc;
use std::{any::Any, cell::RefCell}; use std::{any::Any, cell::RefCell};
@ -21,11 +23,13 @@ pub struct AcpConnection {
connection: Rc<acp::ClientSideConnection>, connection: Rc<acp::ClientSideConnection>,
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>, sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
auth_methods: Vec<acp::AuthMethod>, auth_methods: Vec<acp::AuthMethod>,
prompt_capabilities: acp::PromptCapabilities,
_io_task: Task<Result<()>>, _io_task: Task<Result<()>>,
} }
pub struct AcpSession { pub struct AcpSession {
thread: WeakEntity<AcpThread>, thread: WeakEntity<AcpThread>,
suppress_abort_err: bool,
} }
const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::V1; const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::V1;
@ -98,6 +102,14 @@ impl AcpConnection {
}) })
.detach(); .detach();
let connection = Rc::new(connection);
cx.update(|cx| {
AcpConnectionRegistry::default_global(cx).update(cx, |registry, cx| {
registry.set_active_connection(server_name, &connection, cx)
});
})?;
let response = connection let response = connection
.initialize(acp::InitializeRequest { .initialize(acp::InitializeRequest {
protocol_version: acp::VERSION, protocol_version: acp::VERSION,
@ -116,9 +128,10 @@ impl AcpConnection {
Ok(Self { Ok(Self {
auth_methods: response.auth_methods, auth_methods: response.auth_methods,
connection: connection.into(), connection,
server_name, server_name,
sessions, sessions,
prompt_capabilities: response.agent_capabilities.prompt_capabilities,
_io_task: io_task, _io_task: io_task,
}) })
} }
@ -169,6 +182,7 @@ impl AgentConnection for AcpConnection {
let session = AcpSession { let session = AcpSession {
thread: thread.downgrade(), thread: thread.downgrade(),
suppress_abort_err: false,
}; };
sessions.borrow_mut().insert(session_id, session); sessions.borrow_mut().insert(session_id, session);
@ -200,13 +214,64 @@ impl AgentConnection for AcpConnection {
cx: &mut App, cx: &mut App,
) -> Task<Result<acp::PromptResponse>> { ) -> Task<Result<acp::PromptResponse>> {
let conn = self.connection.clone(); let conn = self.connection.clone();
let sessions = self.sessions.clone();
let session_id = params.session_id.clone();
cx.foreground_executor().spawn(async move { cx.foreground_executor().spawn(async move {
let response = conn.prompt(params).await?; let result = conn.prompt(params).await;
Ok(response)
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")
{
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::Cancelled,
})
} else {
Err(anyhow!(details))
}
}
Err(_) => Err(anyhow!(err)),
}
}
}
}) })
} }
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
self.prompt_capabilities
}
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { 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 conn = self.connection.clone();
let params = acp::CancelNotification { let params = acp::CancelNotification {
session_id: session_id.clone(), session_id: session_id.clone(),
@ -246,7 +311,7 @@ impl acp::Client for ClientDelegate {
let outcome = match result { let outcome = match result {
Ok(option) => acp::RequestPermissionOutcome::Selected { option_id: option }, Ok(option) => acp::RequestPermissionOutcome::Selected { option_id: option },
Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Canceled, Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Cancelled,
}; };
Ok(acp::RequestPermissionResponse { outcome }) Ok(acp::RequestPermissionResponse { outcome })

View file

@ -1,12 +1,14 @@
mod acp; mod acp;
mod claude; mod claude;
mod custom;
mod gemini; mod gemini;
mod settings; mod settings;
#[cfg(test)] #[cfg(any(test, feature = "test-support"))]
mod e2e_tests; pub mod e2e_tests;
pub use claude::*; pub use claude::*;
pub use custom::*;
pub use gemini::*; pub use gemini::*;
pub use settings::*; pub use settings::*;
@ -31,9 +33,10 @@ pub fn init(cx: &mut App) {
pub trait AgentServer: Send { pub trait AgentServer: Send {
fn logo(&self) -> ui::IconName; fn logo(&self) -> ui::IconName;
fn name(&self) -> &'static str; fn name(&self) -> SharedString;
fn empty_state_headline(&self) -> &'static str; fn empty_state_headline(&self) -> SharedString;
fn empty_state_message(&self) -> &'static str; fn empty_state_message(&self) -> SharedString;
fn telemetry_id(&self) -> &'static str;
fn connect( fn connect(
&self, &self,
@ -95,7 +98,7 @@ pub struct AgentServerCommand {
} }
impl AgentServerCommand { impl AgentServerCommand {
pub(crate) async fn resolve( pub async fn resolve(
path_bin_name: &'static str, path_bin_name: &'static str,
extra_args: &[&'static str], extra_args: &[&'static str],
fallback_path: Option<&Path>, fallback_path: Option<&Path>,

View file

@ -30,7 +30,7 @@ use futures::{
io::BufReader, io::BufReader,
select_biased, select_biased,
}; };
use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity}; use gpui::{App, AppContext, AsyncApp, Entity, SharedString, Task, WeakEntity};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use util::{ResultExt, debug_panic}; use util::{ResultExt, debug_panic};
@ -43,16 +43,20 @@ use acp_thread::{AcpThread, AgentConnection, AuthRequired, LoadError, MentionUri
pub struct ClaudeCode; pub struct ClaudeCode;
impl AgentServer for ClaudeCode { impl AgentServer for ClaudeCode {
fn name(&self) -> &'static str { fn telemetry_id(&self) -> &'static str {
"Claude Code" "claude-code"
} }
fn empty_state_headline(&self) -> &'static str { fn name(&self) -> SharedString {
"Claude Code".into()
}
fn empty_state_headline(&self) -> SharedString {
self.name() self.name()
} }
fn empty_state_message(&self) -> &'static str { fn empty_state_message(&self) -> SharedString {
"How can I help you today?" "How can I help you today?".into()
} }
fn logo(&self) -> ui::IconName { fn logo(&self) -> ui::IconName {
@ -249,13 +253,19 @@ impl AgentConnection for ClaudeAgentConnection {
}); });
let action_log = cx.new(|_| ActionLog::new(project.clone()))?; let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
let thread = cx.new(|_cx| { let thread = cx.new(|cx| {
AcpThread::new( AcpThread::new(
"Claude Code", "Claude Code",
self.clone(), self.clone(),
project, project,
action_log, action_log,
session_id.clone(), session_id.clone(),
watch::Receiver::constant(acp::PromptCapabilities {
image: true,
audio: false,
embedded_context: true,
}),
cx,
) )
})?; })?;
@ -697,7 +707,7 @@ impl ClaudeAgentSession {
let stop_reason = match subtype { let stop_reason = match subtype {
ResultErrorType::Success => acp::StopReason::EndTurn, ResultErrorType::Success => acp::StopReason::EndTurn,
ResultErrorType::ErrorMaxTurns => acp::StopReason::MaxTurnRequests, ResultErrorType::ErrorMaxTurns => acp::StopReason::MaxTurnRequests,
ResultErrorType::ErrorDuringExecution => acp::StopReason::Canceled, ResultErrorType::ErrorDuringExecution => acp::StopReason::Cancelled,
}; };
end_turn_tx end_turn_tx
.send(Ok(acp::PromptResponse { stop_reason })) .send(Ok(acp::PromptResponse { stop_reason }))
@ -787,7 +797,7 @@ impl Content {
pub fn chunks(self) -> impl Iterator<Item = ContentChunk> { pub fn chunks(self) -> impl Iterator<Item = ContentChunk> {
match self { match self {
Self::Chunks(chunks) => chunks.into_iter(), Self::Chunks(chunks) => chunks.into_iter(),
Self::UntaggedText(text) => vec![ContentChunk::Text { text: text.clone() }].into_iter(), Self::UntaggedText(text) => vec![ContentChunk::Text { text }].into_iter(),
} }
} }
} }
@ -1085,7 +1095,7 @@ pub(crate) mod tests {
use gpui::TestAppContext; use gpui::TestAppContext;
use serde_json::json; use serde_json::json;
crate::common_e2e_tests!(ClaudeCode, allow_option_id = "allow"); crate::common_e2e_tests!(async |_, _, _| ClaudeCode, allow_option_id = "allow");
pub fn local_command() -> AgentServerCommand { pub fn local_command() -> AgentServerCommand {
AgentServerCommand { AgentServerCommand {
@ -1117,7 +1127,7 @@ pub(crate) mod tests {
thread.read_with(cx, |thread, _| { thread.read_with(cx, |thread, _| {
entries_len = thread.plan().entries.len(); entries_len = thread.plan().entries.len();
assert!(thread.plan().entries.len() > 0, "Empty plan"); assert!(!thread.plan().entries.is_empty(), "Empty plan");
}); });
thread thread

View file

@ -58,7 +58,7 @@ impl ClaudeTool {
Self::Terminal(None) Self::Terminal(None)
} else { } else {
Self::Other { Self::Other {
name: tool_name.to_string(), name: tool_name,
input, input,
} }
} }

View file

@ -0,0 +1,63 @@
use crate::{AgentServerCommand, AgentServerSettings};
use acp_thread::AgentConnection;
use anyhow::Result;
use gpui::{App, Entity, SharedString, Task};
use project::Project;
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, settings: &AgentServerSettings) -> Self {
Self {
name,
command: settings.command.clone(),
}
}
}
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 empty_state_headline(&self) -> SharedString {
"No conversations yet".into()
}
fn empty_state_message(&self) -> SharedString {
format!("Start a conversation with {}", self.name).into()
}
fn connect(
&self,
root_dir: &Path,
_project: &Entity<Project>,
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 |mut cx| {
crate::acp::connect(server_name, command, &root_dir, &mut cx).await
})
}
fn into_any(self: Rc<Self>) -> Rc<dyn std::any::Any> {
self
}
}

View file

@ -1,24 +1,31 @@
use crate::AgentServer;
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::{ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::Arc, sync::Arc,
time::Duration, 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; use util::path;
pub async fn test_basic(server: impl AgentServer + 'static, cx: &mut TestAppContext) { pub async fn test_basic<T, F>(server: F, cx: &mut TestAppContext)
let fs = init_test(cx).await; where
let project = Project::test(fs, [], cx).await; T: AgentServer + 'static,
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await; 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;
thread thread
.update(cx, |thread, cx| thread.send_raw("Hello from Zed!", cx)) .update(cx, |thread, cx| thread.send_raw("Hello from Zed!", cx))
@ -42,8 +49,12 @@ pub async fn test_basic(server: impl AgentServer + 'static, cx: &mut TestAppCont
}); });
} }
pub async fn test_path_mentions(server: impl AgentServer + 'static, cx: &mut TestAppContext) { pub async fn test_path_mentions<T, F>(server: F, cx: &mut TestAppContext)
let _fs = init_test(cx).await; where
T: AgentServer + 'static,
F: AsyncFn(&Arc<dyn fs::Fs>, &Entity<Project>, &mut TestAppContext) -> T,
{
let fs = init_test(cx).await as _;
let tempdir = tempfile::tempdir().unwrap(); let tempdir = tempfile::tempdir().unwrap();
std::fs::write( std::fs::write(
@ -56,7 +67,13 @@ pub async fn test_path_mentions(server: impl AgentServer + 'static, cx: &mut Tes
) )
.expect("failed to write file"); .expect("failed to write file");
let project = Project::example([tempdir.path()], &mut cx.to_async()).await; let project = Project::example([tempdir.path()], &mut cx.to_async()).await;
let thread = new_test_thread(server, project.clone(), tempdir.path(), cx).await; let thread = new_test_thread(
server(&fs, &project, cx).await,
project.clone(),
tempdir.path(),
cx,
)
.await;
thread thread
.update(cx, |thread, cx| { .update(cx, |thread, cx| {
thread.send( thread.send(
@ -110,15 +127,25 @@ pub async fn test_path_mentions(server: impl AgentServer + 'static, cx: &mut Tes
drop(tempdir); drop(tempdir);
} }
pub async fn test_tool_call(server: impl AgentServer + 'static, cx: &mut TestAppContext) { pub async fn test_tool_call<T, F>(server: F, cx: &mut TestAppContext)
let _fs = init_test(cx).await; where
T: AgentServer + 'static,
F: AsyncFn(&Arc<dyn fs::Fs>, &Entity<Project>, &mut TestAppContext) -> T,
{
let fs = init_test(cx).await as _;
let tempdir = tempfile::tempdir().unwrap(); let tempdir = tempfile::tempdir().unwrap();
let foo_path = tempdir.path().join("foo"); let foo_path = tempdir.path().join("foo");
std::fs::write(&foo_path, "Lorem ipsum dolor").expect("failed to write file"); 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 project = Project::example([tempdir.path()], &mut cx.to_async()).await;
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await; let thread = new_test_thread(
server(&fs, &project, cx).await,
project.clone(),
"/private/tmp",
cx,
)
.await;
thread thread
.update(cx, |thread, cx| { .update(cx, |thread, cx| {
@ -152,14 +179,23 @@ pub async fn test_tool_call(server: impl AgentServer + 'static, cx: &mut TestApp
drop(tempdir); drop(tempdir);
} }
pub async fn test_tool_call_with_permission( pub async fn test_tool_call_with_permission<T, F>(
server: impl AgentServer + 'static, server: F,
allow_option_id: acp::PermissionOptionId, allow_option_id: acp::PermissionOptionId,
cx: &mut TestAppContext, cx: &mut TestAppContext,
) { ) where
let fs = init_test(cx).await; T: AgentServer + 'static,
let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await; F: AsyncFn(&Arc<dyn fs::Fs>, &Entity<Project>, &mut TestAppContext) -> T,
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await; {
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 full_turn = thread.update(cx, |thread, cx| { let full_turn = thread.update(cx, |thread, cx| {
thread.send_raw( thread.send_raw(
r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#, r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#,
@ -247,11 +283,21 @@ pub async fn test_tool_call_with_permission(
}); });
} }
pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppContext) { pub async fn test_cancel<T, F>(server: F, cx: &mut TestAppContext)
let fs = init_test(cx).await; 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, [path!("/private/tmp").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/private/tmp").as_ref()], cx).await;
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await; let thread = new_test_thread(
server(&fs, &project, cx).await,
project.clone(),
"/private/tmp",
cx,
)
.await;
let _ = thread.update(cx, |thread, cx| { let _ = thread.update(cx, |thread, cx| {
thread.send_raw( thread.send_raw(
r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#, r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#,
@ -316,10 +362,20 @@ pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppCon
}); });
} }
pub async fn test_thread_drop(server: impl AgentServer + 'static, cx: &mut TestAppContext) { pub async fn test_thread_drop<T, F>(server: F, cx: &mut TestAppContext)
let fs = init_test(cx).await; where
let project = Project::test(fs, [], cx).await; T: AgentServer + 'static,
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await; 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;
thread thread
.update(cx, |thread, cx| thread.send_raw("Hello from test!", cx)) .update(cx, |thread, cx| thread.send_raw("Hello from test!", cx))
@ -386,27 +442,42 @@ macro_rules! common_e2e_tests {
} }
}; };
} }
pub use common_e2e_tests;
// Helpers // Helpers
pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> { pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
#[cfg(test)]
use settings::Settings;
env_logger::try_init().ok(); env_logger::try_init().ok();
cx.update(|cx| { cx.update(|cx| {
let settings_store = SettingsStore::test(cx); let settings_store = settings::SettingsStore::test(cx);
cx.set_global(settings_store); cx.set_global(settings_store);
Project::init_settings(cx); Project::init_settings(cx);
language::init(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); crate::settings::init(cx);
#[cfg(test)]
crate::AllAgentServersSettings::override_global( crate::AllAgentServersSettings::override_global(
AllAgentServersSettings { crate::AllAgentServersSettings {
claude: Some(AgentServerSettings { claude: Some(crate::AgentServerSettings {
command: crate::claude::tests::local_command(), command: crate::claude::tests::local_command(),
}), }),
gemini: Some(AgentServerSettings { gemini: Some(crate::AgentServerSettings {
command: crate::gemini::tests::local_command(), command: crate::gemini::tests::local_command(),
}), }),
custom: collections::HashMap::default(),
}, },
cx, cx,
); );

View file

@ -4,10 +4,10 @@ use std::{any::Any, path::Path};
use crate::{AgentServer, AgentServerCommand}; use crate::{AgentServer, AgentServerCommand};
use acp_thread::{AgentConnection, LoadError}; use acp_thread::{AgentConnection, LoadError};
use anyhow::Result; use anyhow::Result;
use gpui::{Entity, Task}; use gpui::{App, Entity, SharedString, Task};
use language_models::provider::google::GoogleLanguageModelProvider;
use project::Project; use project::Project;
use settings::SettingsStore; use settings::SettingsStore;
use ui::App;
use crate::AllAgentServersSettings; use crate::AllAgentServersSettings;
@ -17,16 +17,20 @@ pub struct Gemini;
const ACP_ARG: &str = "--experimental-acp"; const ACP_ARG: &str = "--experimental-acp";
impl AgentServer for Gemini { impl AgentServer for Gemini {
fn name(&self) -> &'static str { fn telemetry_id(&self) -> &'static str {
"Gemini" "gemini-cli"
} }
fn empty_state_headline(&self) -> &'static str { fn name(&self) -> SharedString {
"Welcome to Gemini" "Gemini CLI".into()
} }
fn empty_state_message(&self) -> &'static str { fn empty_state_headline(&self) -> SharedString {
"Ask questions, edit files, run commands" self.name()
}
fn empty_state_message(&self) -> SharedString {
"Ask questions, edit files, run commands".into()
} }
fn logo(&self) -> ui::IconName { fn logo(&self) -> ui::IconName {
@ -47,16 +51,20 @@ impl AgentServer for Gemini {
settings.get::<AllAgentServersSettings>(None).gemini.clone() settings.get::<AllAgentServersSettings>(None).gemini.clone()
})?; })?;
let Some(command) = let Some(mut command) =
AgentServerCommand::resolve("gemini", &[ACP_ARG], None, settings, &project, cx).await AgentServerCommand::resolve("gemini", &[ACP_ARG], None, settings, &project, cx).await
else { else {
return Err(LoadError::NotInstalled { return Err(LoadError::NotInstalled {
error_message: "Failed to find Gemini CLI binary".into(), error_message: "Failed to find Gemini CLI binary".into(),
install_message: "Install Gemini CLI".into(), install_message: "Install Gemini CLI".into(),
install_command: "npm install -g @google/gemini-cli@latest".into() install_command: Self::install_command().into(),
}.into()); }.into());
}; };
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 result = crate::acp::connect(server_name, command.clone(), &root_dir, cx).await; let result = crate::acp::connect(server_name, command.clone(), &root_dir, cx).await;
if result.is_err() { if result.is_err() {
let version_fut = util::command::new_smol_command(&command.path) let version_fut = util::command::new_smol_command(&command.path)
@ -84,7 +92,7 @@ impl AgentServer for Gemini {
current_version current_version
).into(), ).into(),
upgrade_message: "Upgrade Gemini CLI to latest".into(), upgrade_message: "Upgrade Gemini CLI to latest".into(),
upgrade_command: "npm install -g @google/gemini-cli@latest".into(), upgrade_command: Self::upgrade_command().into(),
}.into()) }.into())
} }
} }
@ -97,13 +105,27 @@ impl AgentServer for Gemini {
} }
} }
impl Gemini {
pub fn binary_name() -> &'static str {
"gemini"
}
pub fn install_command() -> &'static str {
"npm install -g @google/gemini-cli@preview"
}
pub fn upgrade_command() -> &'static str {
"npm install -g @google/gemini-cli@preview"
}
}
#[cfg(test)] #[cfg(test)]
pub(crate) mod tests { pub(crate) mod tests {
use super::*; use super::*;
use crate::AgentServerCommand; use crate::AgentServerCommand;
use std::path::Path; use std::path::Path;
crate::common_e2e_tests!(Gemini, allow_option_id = "proceed_once"); crate::common_e2e_tests!(async |_, _, _| Gemini, allow_option_id = "proceed_once");
pub fn local_command() -> AgentServerCommand { pub fn local_command() -> AgentServerCommand {
let cli_path = Path::new(env!("CARGO_MANIFEST_DIR")) let cli_path = Path::new(env!("CARGO_MANIFEST_DIR"))

View file

@ -1,6 +1,7 @@
use crate::AgentServerCommand; use crate::AgentServerCommand;
use anyhow::Result; use anyhow::Result;
use gpui::App; use collections::HashMap;
use gpui::{App, SharedString};
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources}; use settings::{Settings, SettingsSources};
@ -13,9 +14,13 @@ pub fn init(cx: &mut App) {
pub struct AllAgentServersSettings { pub struct AllAgentServersSettings {
pub gemini: Option<AgentServerSettings>, pub gemini: Option<AgentServerSettings>,
pub claude: Option<AgentServerSettings>, pub claude: Option<AgentServerSettings>,
/// Custom agent servers configured by the user
#[serde(flatten)]
pub custom: HashMap<SharedString, AgentServerSettings>,
} }
#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug)] #[derive(Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
pub struct AgentServerSettings { pub struct AgentServerSettings {
#[serde(flatten)] #[serde(flatten)]
pub command: AgentServerCommand, pub command: AgentServerCommand,
@ -29,13 +34,26 @@ impl settings::Settings for AllAgentServersSettings {
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> { fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
let mut settings = AllAgentServersSettings::default(); let mut settings = AllAgentServersSettings::default();
for AllAgentServersSettings { gemini, claude } in sources.defaults_and_customizations() { for AllAgentServersSettings {
gemini,
claude,
custom,
} in sources.defaults_and_customizations()
{
if gemini.is_some() { if gemini.is_some() {
settings.gemini = gemini.clone(); settings.gemini = gemini.clone();
} }
if claude.is_some() { if claude.is_some() {
settings.claude = claude.clone(); settings.claude = claude.clone();
} }
// Merge custom agents
for (name, config) in custom {
// Skip built-in agent names to avoid conflicts
if name != "gemini" && name != "claude" {
settings.custom.insert(name.clone(), config.clone());
}
}
} }
Ok(settings) Ok(settings)

View file

@ -505,9 +505,8 @@ impl Settings for AgentSettings {
} }
} }
debug_assert_eq!( debug_assert!(
sources.default.always_allow_tool_actions.unwrap_or(false), !sources.default.always_allow_tool_actions.unwrap_or(false),
false,
"For security, agent.always_allow_tool_actions should always be false in default.json. If it's true, that is a bug that should be fixed!" "For security, agent.always_allow_tool_actions should always be false in default.json. If it's true, that is a bug that should be fixed!"
); );

View file

@ -67,6 +67,7 @@ ordered-float.workspace = true
parking_lot.workspace = true parking_lot.workspace = true
paths.workspace = true paths.workspace = true
picker.workspace = true picker.workspace = true
postage.workspace = true
project.workspace = true project.workspace = true
prompt_store.workspace = true prompt_store.workspace = true
proto.workspace = true proto.workspace = true

View file

@ -1,8 +1,11 @@
use std::cell::Cell;
use std::ops::Range; use std::ops::Range;
use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicBool;
use acp_thread::MentionUri; use acp_thread::MentionUri;
use agent_client_protocol as acp;
use agent2::{HistoryEntry, HistoryStore}; use agent2::{HistoryEntry, HistoryStore};
use anyhow::Result; use anyhow::Result;
use editor::{CompletionProvider, Editor, ExcerptId}; use editor::{CompletionProvider, Editor, ExcerptId};
@ -63,6 +66,7 @@ pub struct ContextPickerCompletionProvider {
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
history_store: Entity<HistoryStore>, history_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>, prompt_store: Option<Entity<PromptStore>>,
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
} }
impl ContextPickerCompletionProvider { impl ContextPickerCompletionProvider {
@ -71,12 +75,14 @@ impl ContextPickerCompletionProvider {
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
history_store: Entity<HistoryStore>, history_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>, prompt_store: Option<Entity<PromptStore>>,
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
) -> Self { ) -> Self {
Self { Self {
message_editor, message_editor,
workspace, workspace,
history_store, history_store,
prompt_store, prompt_store,
prompt_capabilities,
} }
} }
@ -89,7 +95,7 @@ impl ContextPickerCompletionProvider {
) -> Option<Completion> { ) -> Option<Completion> {
match entry { match entry {
ContextPickerEntry::Mode(mode) => Some(Completion { ContextPickerEntry::Mode(mode) => Some(Completion {
replace_range: source_range.clone(), replace_range: source_range,
new_text: format!("@{} ", mode.keyword()), new_text: format!("@{} ", mode.keyword()),
label: CodeLabel::plain(mode.label().to_string(), None), label: CodeLabel::plain(mode.label().to_string(), None),
icon_path: Some(mode.icon().path().into()), icon_path: Some(mode.icon().path().into()),
@ -102,62 +108,7 @@ impl ContextPickerCompletionProvider {
confirm: Some(Arc::new(|_, _, _| true)), confirm: Some(Arc::new(|_, _, _| true)),
}), }),
ContextPickerEntry::Action(action) => { ContextPickerEntry::Action(action) => {
let (new_text, on_action) = match action { Self::completion_for_action(action, source_range, message_editor, workspace, cx)
ContextPickerAction::AddSelections => {
const PLACEHOLDER: &str = "selection ";
let selections = selection_ranges(workspace, cx)
.into_iter()
.enumerate()
.map(|(ix, (buffer, range))| {
(
buffer,
range,
(PLACEHOLDER.len() * ix)..(PLACEHOLDER.len() * (ix + 1) - 1),
)
})
.collect::<Vec<_>>();
let new_text: String = PLACEHOLDER.repeat(selections.len());
let callback = Arc::new({
let source_range = source_range.clone();
move |_, window: &mut Window, cx: &mut App| {
let selections = selections.clone();
let message_editor = message_editor.clone();
let source_range = source_range.clone();
window.defer(cx, move |window, cx| {
message_editor
.update(cx, |message_editor, cx| {
message_editor.confirm_mention_for_selection(
source_range,
selections,
window,
cx,
)
})
.ok();
});
false
}
});
(new_text, callback)
}
};
Some(Completion {
replace_range: source_range.clone(),
new_text,
label: CodeLabel::plain(action.label().to_string(), None),
icon_path: Some(action.icon().path().into()),
documentation: None,
source: project::CompletionSource::Custom,
insert_text_mode: None,
// This ensures that when a user accepts this completion, the
// completion menu will still be shown after "@category " is
// inserted
confirm: Some(on_action),
})
} }
} }
} }
@ -187,7 +138,7 @@ impl ContextPickerCompletionProvider {
documentation: None, documentation: None,
insert_text_mode: None, insert_text_mode: None,
source: project::CompletionSource::Custom, source: project::CompletionSource::Custom,
icon_path: Some(icon_for_completion.clone()), icon_path: Some(icon_for_completion),
confirm: Some(confirm_completion_callback( confirm: Some(confirm_completion_callback(
thread_entry.title().clone(), thread_entry.title().clone(),
source_range.start, source_range.start,
@ -218,9 +169,9 @@ impl ContextPickerCompletionProvider {
documentation: None, documentation: None,
insert_text_mode: None, insert_text_mode: None,
source: project::CompletionSource::Custom, source: project::CompletionSource::Custom,
icon_path: Some(icon_path.clone()), icon_path: Some(icon_path),
confirm: Some(confirm_completion_callback( confirm: Some(confirm_completion_callback(
rule.title.clone(), rule.title,
source_range.start, source_range.start,
new_text_len - 1, new_text_len - 1,
editor, editor,
@ -260,7 +211,7 @@ impl ContextPickerCompletionProvider {
let completion_icon_path = if is_recent { let completion_icon_path = if is_recent {
IconName::HistoryRerun.path().into() IconName::HistoryRerun.path().into()
} else { } else {
crease_icon_path.clone() crease_icon_path
}; };
let new_text = format!("{} ", uri.as_link()); let new_text = format!("{} ", uri.as_link());
@ -296,9 +247,9 @@ impl ContextPickerCompletionProvider {
let abs_path = project.read(cx).absolute_path(&symbol.path, cx)?; let abs_path = project.read(cx).absolute_path(&symbol.path, cx)?;
let uri = MentionUri::Symbol { let uri = MentionUri::Symbol {
path: abs_path, abs_path,
name: symbol.name.clone(), name: symbol.name.clone(),
line_range: symbol.range.start.0.row..symbol.range.end.0.row, line_range: symbol.range.start.0.row..=symbol.range.end.0.row,
}; };
let new_text = format!("{} ", uri.as_link()); let new_text = format!("{} ", uri.as_link());
let new_text_len = new_text.len(); let new_text_len = new_text.len();
@ -309,10 +260,10 @@ impl ContextPickerCompletionProvider {
label, label,
documentation: None, documentation: None,
source: project::CompletionSource::Custom, source: project::CompletionSource::Custom,
icon_path: Some(icon_path.clone()), icon_path: Some(icon_path),
insert_text_mode: None, insert_text_mode: None,
confirm: Some(confirm_completion_callback( confirm: Some(confirm_completion_callback(
symbol.name.clone().into(), symbol.name.into(),
source_range.start, source_range.start,
new_text_len - 1, new_text_len - 1,
message_editor, message_editor,
@ -327,7 +278,7 @@ impl ContextPickerCompletionProvider {
message_editor: WeakEntity<MessageEditor>, message_editor: WeakEntity<MessageEditor>,
cx: &mut App, cx: &mut App,
) -> Option<Completion> { ) -> Option<Completion> {
let new_text = format!("@fetch {} ", url_to_fetch.clone()); let new_text = format!("@fetch {} ", url_to_fetch);
let url_to_fetch = url::Url::parse(url_to_fetch.as_ref()) let url_to_fetch = url::Url::parse(url_to_fetch.as_ref())
.or_else(|_| url::Url::parse(&format!("https://{url_to_fetch}"))) .or_else(|_| url::Url::parse(&format!("https://{url_to_fetch}")))
.ok()?; .ok()?;
@ -341,7 +292,7 @@ impl ContextPickerCompletionProvider {
label: CodeLabel::plain(url_to_fetch.to_string(), None), label: CodeLabel::plain(url_to_fetch.to_string(), None),
documentation: None, documentation: None,
source: project::CompletionSource::Custom, source: project::CompletionSource::Custom,
icon_path: Some(icon_path.clone()), icon_path: Some(icon_path),
insert_text_mode: None, insert_text_mode: None,
confirm: Some(confirm_completion_callback( confirm: Some(confirm_completion_callback(
url_to_fetch.to_string().into(), url_to_fetch.to_string().into(),
@ -353,6 +304,71 @@ impl ContextPickerCompletionProvider {
}) })
} }
pub(crate) fn completion_for_action(
action: ContextPickerAction,
source_range: Range<Anchor>,
message_editor: WeakEntity<MessageEditor>,
workspace: &Entity<Workspace>,
cx: &mut App,
) -> Option<Completion> {
let (new_text, on_action) = match action {
ContextPickerAction::AddSelections => {
const PLACEHOLDER: &str = "selection ";
let selections = selection_ranges(workspace, cx)
.into_iter()
.enumerate()
.map(|(ix, (buffer, range))| {
(
buffer,
range,
(PLACEHOLDER.len() * ix)..(PLACEHOLDER.len() * (ix + 1) - 1),
)
})
.collect::<Vec<_>>();
let new_text: String = PLACEHOLDER.repeat(selections.len());
let callback = Arc::new({
let source_range = source_range.clone();
move |_, window: &mut Window, cx: &mut App| {
let selections = selections.clone();
let message_editor = message_editor.clone();
let source_range = source_range.clone();
window.defer(cx, move |window, cx| {
message_editor
.update(cx, |message_editor, cx| {
message_editor.confirm_mention_for_selection(
source_range,
selections,
window,
cx,
)
})
.ok();
});
false
}
});
(new_text, callback)
}
};
Some(Completion {
replace_range: source_range,
new_text,
label: CodeLabel::plain(action.label().to_string(), None),
icon_path: Some(action.icon().path().into()),
documentation: None,
source: project::CompletionSource::Custom,
insert_text_mode: None,
// This ensures that when a user accepts this completion, the
// completion menu will still be shown after "@category " is
// inserted
confirm: Some(on_action),
})
}
fn search( fn search(
&self, &self,
mode: Option<ContextPickerMode>, mode: Option<ContextPickerMode>,
@ -365,8 +381,7 @@ impl ContextPickerCompletionProvider {
}; };
match mode { match mode {
Some(ContextPickerMode::File) => { Some(ContextPickerMode::File) => {
let search_files_task = let search_files_task = search_files(query, cancellation_flag, &workspace, cx);
search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
cx.background_spawn(async move { cx.background_spawn(async move {
search_files_task search_files_task
.await .await
@ -377,8 +392,7 @@ impl ContextPickerCompletionProvider {
} }
Some(ContextPickerMode::Symbol) => { Some(ContextPickerMode::Symbol) => {
let search_symbols_task = let search_symbols_task = search_symbols(query, cancellation_flag, &workspace, cx);
search_symbols(query.clone(), cancellation_flag.clone(), &workspace, cx);
cx.background_spawn(async move { cx.background_spawn(async move {
search_symbols_task search_symbols_task
.await .await
@ -389,12 +403,8 @@ impl ContextPickerCompletionProvider {
} }
Some(ContextPickerMode::Thread) => { Some(ContextPickerMode::Thread) => {
let search_threads_task = search_threads( let search_threads_task =
query.clone(), search_threads(query, cancellation_flag, &self.history_store, cx);
cancellation_flag.clone(),
&self.history_store,
cx,
);
cx.background_spawn(async move { cx.background_spawn(async move {
search_threads_task search_threads_task
.await .await
@ -415,7 +425,7 @@ impl ContextPickerCompletionProvider {
Some(ContextPickerMode::Rules) => { Some(ContextPickerMode::Rules) => {
if let Some(prompt_store) = self.prompt_store.as_ref() { if let Some(prompt_store) = self.prompt_store.as_ref() {
let search_rules_task = let search_rules_task =
search_rules(query.clone(), cancellation_flag.clone(), prompt_store, cx); search_rules(query, cancellation_flag, prompt_store, cx);
cx.background_spawn(async move { cx.background_spawn(async move {
search_rules_task search_rules_task
.await .await
@ -448,7 +458,7 @@ impl ContextPickerCompletionProvider {
let executor = cx.background_executor().clone(); let executor = cx.background_executor().clone();
let search_files_task = let search_files_task =
search_files(query.clone(), cancellation_flag.clone(), &workspace, cx); search_files(query.clone(), cancellation_flag, &workspace, cx);
let entries = self.available_context_picker_entries(&workspace, cx); let entries = self.available_context_picker_entries(&workspace, cx);
let entry_candidates = entries let entry_candidates = entries
@ -550,17 +560,19 @@ impl ContextPickerCompletionProvider {
}), }),
); );
const RECENT_COUNT: usize = 2; if self.prompt_capabilities.get().embedded_context {
let threads = self const RECENT_COUNT: usize = 2;
.history_store let threads = self
.read(cx) .history_store
.recently_opened_entries(cx) .read(cx)
.into_iter() .recently_opened_entries(cx)
.filter(|thread| !mentions.contains(&thread.mention_uri())) .into_iter()
.take(RECENT_COUNT) .filter(|thread| !mentions.contains(&thread.mention_uri()))
.collect::<Vec<_>>(); .take(RECENT_COUNT)
.collect::<Vec<_>>();
recent.extend(threads.into_iter().map(Match::RecentThread)); recent.extend(threads.into_iter().map(Match::RecentThread));
}
recent recent
} }
@ -570,11 +582,17 @@ impl ContextPickerCompletionProvider {
workspace: &Entity<Workspace>, workspace: &Entity<Workspace>,
cx: &mut App, cx: &mut App,
) -> Vec<ContextPickerEntry> { ) -> Vec<ContextPickerEntry> {
let mut entries = vec![ let embedded_context = self.prompt_capabilities.get().embedded_context;
ContextPickerEntry::Mode(ContextPickerMode::File), let mut entries = if embedded_context {
ContextPickerEntry::Mode(ContextPickerMode::Symbol), vec![
ContextPickerEntry::Mode(ContextPickerMode::Thread), ContextPickerEntry::Mode(ContextPickerMode::File),
]; ContextPickerEntry::Mode(ContextPickerMode::Symbol),
ContextPickerEntry::Mode(ContextPickerMode::Thread),
]
} else {
// File is always available, but we don't need a mode entry
vec![]
};
let has_selection = workspace let has_selection = workspace
.read(cx) .read(cx)
@ -589,11 +607,13 @@ impl ContextPickerCompletionProvider {
)); ));
} }
if self.prompt_store.is_some() { if embedded_context {
entries.push(ContextPickerEntry::Mode(ContextPickerMode::Rules)); if self.prompt_store.is_some() {
} entries.push(ContextPickerEntry::Mode(ContextPickerMode::Rules));
}
entries.push(ContextPickerEntry::Mode(ContextPickerMode::Fetch)); entries.push(ContextPickerEntry::Mode(ContextPickerMode::Fetch));
}
entries entries
} }
@ -631,7 +651,11 @@ impl CompletionProvider for ContextPickerCompletionProvider {
let offset_to_line = buffer.point_to_offset(line_start); let offset_to_line = buffer.point_to_offset(line_start);
let mut lines = buffer.text_for_range(line_start..position).lines(); let mut lines = buffer.text_for_range(line_start..position).lines();
let line = lines.next()?; let line = lines.next()?;
MentionCompletion::try_parse(line, offset_to_line) MentionCompletion::try_parse(
self.prompt_capabilities.get().embedded_context,
line,
offset_to_line,
)
}); });
let Some(state) = state else { let Some(state) = state else {
return Task::ready(Ok(Vec::new())); return Task::ready(Ok(Vec::new()));
@ -751,12 +775,16 @@ impl CompletionProvider for ContextPickerCompletionProvider {
let offset_to_line = buffer.point_to_offset(line_start); let offset_to_line = buffer.point_to_offset(line_start);
let mut lines = buffer.text_for_range(line_start..position).lines(); let mut lines = buffer.text_for_range(line_start..position).lines();
if let Some(line) = lines.next() { if let Some(line) = lines.next() {
MentionCompletion::try_parse(line, offset_to_line) MentionCompletion::try_parse(
.map(|completion| { self.prompt_capabilities.get().embedded_context,
completion.source_range.start <= offset_to_line + position.column as usize line,
&& completion.source_range.end >= offset_to_line + position.column as usize offset_to_line,
}) )
.unwrap_or(false) .map(|completion| {
completion.source_range.start <= offset_to_line + position.column as usize
&& completion.source_range.end >= offset_to_line + position.column as usize
})
.unwrap_or(false)
} else { } else {
false false
} }
@ -777,7 +805,7 @@ pub(crate) fn search_threads(
history_store: &Entity<HistoryStore>, history_store: &Entity<HistoryStore>,
cx: &mut App, cx: &mut App,
) -> Task<Vec<HistoryEntry>> { ) -> Task<Vec<HistoryEntry>> {
let threads = history_store.read(cx).entries(cx); let threads = history_store.read(cx).entries().collect();
if query.is_empty() { if query.is_empty() {
return Task::ready(threads); return Task::ready(threads);
} }
@ -847,7 +875,7 @@ struct MentionCompletion {
} }
impl MentionCompletion { impl MentionCompletion {
fn try_parse(line: &str, offset_to_line: usize) -> Option<Self> { fn try_parse(allow_non_file_mentions: bool, line: &str, offset_to_line: usize) -> Option<Self> {
let last_mention_start = line.rfind('@')?; let last_mention_start = line.rfind('@')?;
if last_mention_start >= line.len() { if last_mention_start >= line.len() {
return Some(Self::default()); return Some(Self::default());
@ -871,7 +899,9 @@ impl MentionCompletion {
if let Some(mode_text) = parts.next() { if let Some(mode_text) = parts.next() {
end += mode_text.len(); end += mode_text.len();
if let Some(parsed_mode) = ContextPickerMode::try_from(mode_text).ok() { if let Some(parsed_mode) = ContextPickerMode::try_from(mode_text).ok()
&& (allow_non_file_mentions || matches!(parsed_mode, ContextPickerMode::File))
{
mode = Some(parsed_mode); mode = Some(parsed_mode);
} else { } else {
argument = Some(mode_text.to_string()); argument = Some(mode_text.to_string());
@ -904,10 +934,10 @@ mod tests {
#[test] #[test]
fn test_mention_completion_parse() { fn test_mention_completion_parse() {
assert_eq!(MentionCompletion::try_parse("Lorem Ipsum", 0), None); assert_eq!(MentionCompletion::try_parse(true, "Lorem Ipsum", 0), None);
assert_eq!( assert_eq!(
MentionCompletion::try_parse("Lorem @", 0), MentionCompletion::try_parse(true, "Lorem @", 0),
Some(MentionCompletion { Some(MentionCompletion {
source_range: 6..7, source_range: 6..7,
mode: None, mode: None,
@ -916,7 +946,7 @@ mod tests {
); );
assert_eq!( assert_eq!(
MentionCompletion::try_parse("Lorem @file", 0), MentionCompletion::try_parse(true, "Lorem @file", 0),
Some(MentionCompletion { Some(MentionCompletion {
source_range: 6..11, source_range: 6..11,
mode: Some(ContextPickerMode::File), mode: Some(ContextPickerMode::File),
@ -925,7 +955,7 @@ mod tests {
); );
assert_eq!( assert_eq!(
MentionCompletion::try_parse("Lorem @file ", 0), MentionCompletion::try_parse(true, "Lorem @file ", 0),
Some(MentionCompletion { Some(MentionCompletion {
source_range: 6..12, source_range: 6..12,
mode: Some(ContextPickerMode::File), mode: Some(ContextPickerMode::File),
@ -934,7 +964,7 @@ mod tests {
); );
assert_eq!( assert_eq!(
MentionCompletion::try_parse("Lorem @file main.rs", 0), MentionCompletion::try_parse(true, "Lorem @file main.rs", 0),
Some(MentionCompletion { Some(MentionCompletion {
source_range: 6..19, source_range: 6..19,
mode: Some(ContextPickerMode::File), mode: Some(ContextPickerMode::File),
@ -943,7 +973,7 @@ mod tests {
); );
assert_eq!( assert_eq!(
MentionCompletion::try_parse("Lorem @file main.rs ", 0), MentionCompletion::try_parse(true, "Lorem @file main.rs ", 0),
Some(MentionCompletion { Some(MentionCompletion {
source_range: 6..19, source_range: 6..19,
mode: Some(ContextPickerMode::File), mode: Some(ContextPickerMode::File),
@ -952,7 +982,7 @@ mod tests {
); );
assert_eq!( assert_eq!(
MentionCompletion::try_parse("Lorem @file main.rs Ipsum", 0), MentionCompletion::try_parse(true, "Lorem @file main.rs Ipsum", 0),
Some(MentionCompletion { Some(MentionCompletion {
source_range: 6..19, source_range: 6..19,
mode: Some(ContextPickerMode::File), mode: Some(ContextPickerMode::File),
@ -961,7 +991,7 @@ mod tests {
); );
assert_eq!( assert_eq!(
MentionCompletion::try_parse("Lorem @main", 0), MentionCompletion::try_parse(true, "Lorem @main", 0),
Some(MentionCompletion { Some(MentionCompletion {
source_range: 6..11, source_range: 6..11,
mode: None, mode: None,
@ -969,6 +999,28 @@ mod tests {
}) })
); );
assert_eq!(MentionCompletion::try_parse("test@", 0), None); assert_eq!(MentionCompletion::try_parse(true, "test@", 0), None);
// Allowed non-file mentions
assert_eq!(
MentionCompletion::try_parse(true, "Lorem @symbol main", 0),
Some(MentionCompletion {
source_range: 6..18,
mode: Some(ContextPickerMode::Symbol),
argument: Some("main".to_string()),
})
);
// Disallowed non-file mentions
assert_eq!(
MentionCompletion::try_parse(false, "Lorem @symbol main", 0),
Some(MentionCompletion {
source_range: 6..18,
mode: None,
argument: Some("main".to_string()),
})
);
} }
} }

View file

@ -1,6 +1,7 @@
use std::ops::Range; use std::{cell::Cell, ops::Range, rc::Rc};
use acp_thread::{AcpThread, AgentThreadEntry}; use acp_thread::{AcpThread, AgentThreadEntry};
use agent_client_protocol::{PromptCapabilities, ToolCallId};
use agent2::HistoryStore; use agent2::HistoryStore;
use collections::HashMap; use collections::HashMap;
use editor::{Editor, EditorMode, MinimapVisibility}; use editor::{Editor, EditorMode, MinimapVisibility};
@ -26,6 +27,7 @@ pub struct EntryViewState {
prompt_store: Option<Entity<PromptStore>>, prompt_store: Option<Entity<PromptStore>>,
entries: Vec<Entry>, entries: Vec<Entry>,
prevent_slash_commands: bool, prevent_slash_commands: bool,
prompt_capabilities: Rc<Cell<PromptCapabilities>>,
} }
impl EntryViewState { impl EntryViewState {
@ -34,6 +36,7 @@ impl EntryViewState {
project: Entity<Project>, project: Entity<Project>,
history_store: Entity<HistoryStore>, history_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>, prompt_store: Option<Entity<PromptStore>>,
prompt_capabilities: Rc<Cell<PromptCapabilities>>,
prevent_slash_commands: bool, prevent_slash_commands: bool,
) -> Self { ) -> Self {
Self { Self {
@ -43,6 +46,7 @@ impl EntryViewState {
prompt_store, prompt_store,
entries: Vec::new(), entries: Vec::new(),
prevent_slash_commands, prevent_slash_commands,
prompt_capabilities,
} }
} }
@ -80,6 +84,7 @@ impl EntryViewState {
self.project.clone(), self.project.clone(),
self.history_store.clone(), self.history_store.clone(),
self.prompt_store.clone(), self.prompt_store.clone(),
self.prompt_capabilities.clone(),
"Edit message @ to include context", "Edit message @ to include context",
self.prevent_slash_commands, self.prevent_slash_commands,
editor::EditorMode::AutoHeight { editor::EditorMode::AutoHeight {
@ -106,6 +111,7 @@ impl EntryViewState {
} }
} }
AgentThreadEntry::ToolCall(tool_call) => { AgentThreadEntry::ToolCall(tool_call) => {
let id = tool_call.id.clone();
let terminals = tool_call.terminals().cloned().collect::<Vec<_>>(); let terminals = tool_call.terminals().cloned().collect::<Vec<_>>();
let diffs = tool_call.diffs().cloned().collect::<Vec<_>>(); let diffs = tool_call.diffs().cloned().collect::<Vec<_>>();
@ -121,21 +127,31 @@ impl EntryViewState {
for terminal in terminals { for terminal in terminals {
views.entry(terminal.entity_id()).or_insert_with(|| { views.entry(terminal.entity_id()).or_insert_with(|| {
create_terminal( let element = create_terminal(
self.workspace.clone(), self.workspace.clone(),
self.project.clone(), self.project.clone(),
terminal.clone(), terminal.clone(),
window, window,
cx, cx,
) )
.into_any() .into_any();
cx.emit(EntryViewEvent {
entry_index: index,
view_event: ViewEvent::NewTerminal(id.clone()),
});
element
}); });
} }
for diff in diffs { for diff in diffs {
views views.entry(diff.entity_id()).or_insert_with(|| {
.entry(diff.entity_id()) let element = create_editor_diff(diff.clone(), window, cx).into_any();
.or_insert_with(|| create_editor_diff(diff.clone(), window, cx).into_any()); cx.emit(EntryViewEvent {
entry_index: index,
view_event: ViewEvent::NewDiff(id.clone()),
});
element
});
} }
} }
AgentThreadEntry::AssistantMessage(_) => { AgentThreadEntry::AssistantMessage(_) => {
@ -187,6 +203,8 @@ pub struct EntryViewEvent {
} }
pub enum ViewEvent { pub enum ViewEvent {
NewDiff(ToolCallId),
NewTerminal(ToolCallId),
MessageEditorEvent(Entity<MessageEditor>, MessageEditorEvent), MessageEditorEvent(Entity<MessageEditor>, MessageEditorEvent),
} }
@ -389,6 +407,7 @@ mod tests {
project.clone(), project.clone(),
history_store, history_store,
None, None,
Default::default(),
false, false,
) )
}); });

File diff suppressed because it is too large Load diff

View file

@ -1,19 +1,20 @@
use crate::RemoveSelectedThread; use crate::acp::AcpThreadView;
use crate::{AgentPanel, RemoveSelectedThread};
use agent2::{HistoryEntry, HistoryStore}; use agent2::{HistoryEntry, HistoryStore};
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta}; use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
use editor::{Editor, EditorEvent}; use editor::{Editor, EditorEvent};
use fuzzy::{StringMatch, StringMatchCandidate}; use fuzzy::StringMatchCandidate;
use gpui::{ use gpui::{
App, Empty, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Task, App, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Task,
UniformListScrollHandle, Window, uniform_list, UniformListScrollHandle, WeakEntity, Window, uniform_list,
}; };
use std::{fmt::Display, ops::Range, sync::Arc}; use std::{fmt::Display, ops::Range};
use text::Bias;
use time::{OffsetDateTime, UtcOffset}; use time::{OffsetDateTime, UtcOffset};
use ui::{ use ui::{
HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Tooltip, WithScrollbar, HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Tooltip, WithScrollbar,
prelude::*, prelude::*,
}; };
use util::ResultExt;
pub struct AcpThreadHistory { pub struct AcpThreadHistory {
pub(crate) history_store: Entity<HistoryStore>, pub(crate) history_store: Entity<HistoryStore>,
@ -21,36 +22,36 @@ pub struct AcpThreadHistory {
selected_index: usize, selected_index: usize,
hovered_index: Option<usize>, hovered_index: Option<usize>,
search_editor: Entity<Editor>, search_editor: Entity<Editor>,
all_entries: Arc<Vec<HistoryEntry>>, search_query: SharedString,
// When the search is empty, we display date separators between history entries
// This vector contains an enum of either a separator or an actual entry
separated_items: Vec<ListItemType>,
// Maps entry indexes to list item indexes
separated_item_indexes: Vec<u32>,
_separated_items_task: Option<Task<()>>,
search_state: SearchState,
local_timezone: UtcOffset,
_subscriptions: Vec<gpui::Subscription>,
}
enum SearchState { visible_items: Vec<ListItemType>,
Empty,
Searching { local_timezone: UtcOffset,
query: SharedString,
_task: Task<()>, _update_task: Task<()>,
}, _subscriptions: Vec<gpui::Subscription>,
Searched {
query: SharedString,
matches: Vec<StringMatch>,
},
} }
enum ListItemType { enum ListItemType {
BucketSeparator(TimeBucket), BucketSeparator(TimeBucket),
Entry { Entry {
index: usize, entry: HistoryEntry,
format: EntryTimeFormat, format: EntryTimeFormat,
}, },
SearchResult {
entry: HistoryEntry,
positions: Vec<usize>,
},
}
impl ListItemType {
fn history_entry(&self) -> Option<&HistoryEntry> {
match self {
ListItemType::Entry { entry, .. } => Some(entry),
ListItemType::SearchResult { entry, .. } => Some(entry),
_ => None,
}
}
} }
pub enum ThreadHistoryEvent { pub enum ThreadHistoryEvent {
@ -75,12 +76,15 @@ impl AcpThreadHistory {
cx.subscribe(&search_editor, |this, search_editor, event, cx| { cx.subscribe(&search_editor, |this, search_editor, event, cx| {
if let EditorEvent::BufferEdited = event { if let EditorEvent::BufferEdited = event {
let query = search_editor.read(cx).text(cx); let query = search_editor.read(cx).text(cx);
this.search(query.into(), cx); if this.search_query != query {
this.search_query = query.into();
this.update_visible_items(false, cx);
}
} }
}); });
let history_store_subscription = cx.observe(&history_store, |this, _, cx| { let history_store_subscription = cx.observe(&history_store, |this, _, cx| {
this.update_all_entries(cx); this.update_visible_items(true, cx);
}); });
let scroll_handle = UniformListScrollHandle::default(); let scroll_handle = UniformListScrollHandle::default();
@ -90,38 +94,67 @@ impl AcpThreadHistory {
scroll_handle, scroll_handle,
selected_index: 0, selected_index: 0,
hovered_index: None, hovered_index: None,
search_state: SearchState::Empty, visible_items: Default::default(),
all_entries: Default::default(),
separated_items: Default::default(),
separated_item_indexes: Default::default(),
search_editor, search_editor,
local_timezone: UtcOffset::from_whole_seconds( local_timezone: UtcOffset::from_whole_seconds(
chrono::Local::now().offset().local_minus_utc(), chrono::Local::now().offset().local_minus_utc(),
) )
.unwrap(), .unwrap(),
search_query: SharedString::default(),
_subscriptions: vec![search_editor_subscription, history_store_subscription], _subscriptions: vec![search_editor_subscription, history_store_subscription],
_separated_items_task: None, _update_task: Task::ready(()),
}; };
this.update_all_entries(cx); this.update_visible_items(false, cx);
this this
} }
fn update_all_entries(&mut self, cx: &mut Context<Self>) { fn update_visible_items(&mut self, preserve_selected_item: bool, cx: &mut Context<Self>) {
let new_entries: Arc<Vec<HistoryEntry>> = self let entries = self
.history_store .history_store
.update(cx, |store, cx| store.entries(cx)) .update(cx, |store, _| store.entries().collect());
.into(); let new_list_items = if self.search_query.is_empty() {
self.add_list_separators(entries, cx)
} else {
self.filter_search_results(entries, cx)
};
let selected_history_entry = if preserve_selected_item {
self.selected_history_entry().cloned()
} else {
None
};
self._separated_items_task.take(); self._update_task = cx.spawn(async move |this, cx| {
let new_visible_items = new_list_items.await;
this.update(cx, |this, cx| {
let new_selected_index = if let Some(history_entry) = selected_history_entry {
let history_entry_id = history_entry.id();
new_visible_items
.iter()
.position(|visible_entry| {
visible_entry
.history_entry()
.is_some_and(|entry| entry.id() == history_entry_id)
})
.unwrap_or(0)
} else {
0
};
let mut items = Vec::with_capacity(new_entries.len() + 1); this.visible_items = new_visible_items;
let mut indexes = Vec::with_capacity(new_entries.len() + 1); this.set_selected_index(new_selected_index, Bias::Right, cx);
cx.notify();
})
.ok();
});
}
let bg_task = cx.background_spawn(async move { fn add_list_separators(&self, entries: Vec<HistoryEntry>, cx: &App) -> Task<Vec<ListItemType>> {
cx.background_spawn(async move {
let mut items = Vec::with_capacity(entries.len() + 1);
let mut bucket = None; let mut bucket = None;
let today = Local::now().naive_local().date(); let today = Local::now().naive_local().date();
for (index, entry) in new_entries.iter().enumerate() { for entry in entries.into_iter() {
let entry_date = entry let entry_date = entry
.updated_at() .updated_at()
.with_timezone(&Local) .with_timezone(&Local)
@ -134,75 +167,33 @@ impl AcpThreadHistory {
items.push(ListItemType::BucketSeparator(entry_bucket)); items.push(ListItemType::BucketSeparator(entry_bucket));
} }
indexes.push(items.len() as u32);
items.push(ListItemType::Entry { items.push(ListItemType::Entry {
index, entry,
format: entry_bucket.into(), format: entry_bucket.into(),
}); });
} }
(new_entries, items, indexes) items
}); })
let task = cx.spawn(async move |this, cx| {
let (new_entries, items, indexes) = bg_task.await;
this.update(cx, |this, cx| {
let previously_selected_entry =
this.all_entries.get(this.selected_index).map(|e| e.id());
this.all_entries = new_entries;
this.separated_items = items;
this.separated_item_indexes = indexes;
match &this.search_state {
SearchState::Empty => {
if this.selected_index >= this.all_entries.len() {
this.set_selected_entry_index(
this.all_entries.len().saturating_sub(1),
cx,
);
} else if let Some(prev_id) = previously_selected_entry
&& let Some(new_ix) = this
.all_entries
.iter()
.position(|probe| probe.id() == prev_id)
{
this.set_selected_entry_index(new_ix, cx);
}
}
SearchState::Searching { query, .. } | SearchState::Searched { query, .. } => {
this.search(query.clone(), cx);
}
}
cx.notify();
})
.log_err();
});
self._separated_items_task = Some(task);
} }
fn search(&mut self, query: SharedString, cx: &mut Context<Self>) { fn filter_search_results(
if query.is_empty() { &self,
self.search_state = SearchState::Empty; entries: Vec<HistoryEntry>,
cx.notify(); cx: &App,
return; ) -> Task<Vec<ListItemType>> {
} let query = self.search_query.clone();
cx.background_spawn({
let all_entries = self.all_entries.clone();
let fuzzy_search_task = cx.background_spawn({
let query = query.clone();
let executor = cx.background_executor().clone(); let executor = cx.background_executor().clone();
async move { async move {
let mut candidates = Vec::with_capacity(all_entries.len()); let mut candidates = Vec::with_capacity(entries.len());
for (idx, entry) in all_entries.iter().enumerate() { for (idx, entry) in entries.iter().enumerate() {
candidates.push(StringMatchCandidate::new(idx, entry.title())); candidates.push(StringMatchCandidate::new(idx, entry.title()));
} }
const MAX_MATCHES: usize = 100; const MAX_MATCHES: usize = 100;
fuzzy::match_strings( let matches = fuzzy::match_strings(
&candidates, &candidates,
&query, &query,
false, false,
@ -211,74 +202,61 @@ impl AcpThreadHistory {
&Default::default(), &Default::default(),
executor, executor,
) )
.await .await;
matches
.into_iter()
.map(|search_match| ListItemType::SearchResult {
entry: entries[search_match.candidate_id].clone(),
positions: search_match.positions,
})
.collect()
} }
}); })
let task = cx.spawn({
let query = query.clone();
async move |this, cx| {
let matches = fuzzy_search_task.await;
this.update(cx, |this, cx| {
let SearchState::Searching {
query: current_query,
_task,
} = &this.search_state
else {
return;
};
if &query == current_query {
this.search_state = SearchState::Searched {
query: query.clone(),
matches,
};
this.set_selected_entry_index(0, cx);
cx.notify();
};
})
.log_err();
}
});
self.search_state = SearchState::Searching { query, _task: task };
cx.notify();
}
fn matched_count(&self) -> usize {
match &self.search_state {
SearchState::Empty => self.all_entries.len(),
SearchState::Searching { .. } => 0,
SearchState::Searched { matches, .. } => matches.len(),
}
}
fn list_item_count(&self) -> usize {
match &self.search_state {
SearchState::Empty => self.separated_items.len(),
SearchState::Searching { .. } => 0,
SearchState::Searched { matches, .. } => matches.len(),
}
} }
fn search_produced_no_matches(&self) -> bool { fn search_produced_no_matches(&self) -> bool {
match &self.search_state { self.visible_items.is_empty() && !self.search_query.is_empty()
SearchState::Empty => false,
SearchState::Searching { .. } => false,
SearchState::Searched { matches, .. } => matches.is_empty(),
}
} }
fn get_match(&self, ix: usize) -> Option<&HistoryEntry> { fn selected_history_entry(&self) -> Option<&HistoryEntry> {
match &self.search_state { self.get_history_entry(self.selected_index)
SearchState::Empty => self.all_entries.get(ix), }
SearchState::Searching { .. } => None,
SearchState::Searched { matches, .. } => matches fn get_history_entry(&self, visible_items_ix: usize) -> Option<&HistoryEntry> {
.get(ix) self.visible_items.get(visible_items_ix)?.history_entry()
.and_then(|m| self.all_entries.get(m.candidate_id)), }
fn set_selected_index(&mut self, mut index: usize, bias: Bias, cx: &mut Context<Self>) {
if self.visible_items.len() == 0 {
self.selected_index = 0;
return;
} }
while matches!(
self.visible_items.get(index),
None | Some(ListItemType::BucketSeparator(..))
) {
index = match bias {
Bias::Left => {
if index == 0 {
self.visible_items.len() - 1
} else {
index - 1
}
}
Bias::Right => {
if index >= self.visible_items.len() - 1 {
0
} else {
index + 1
}
}
};
}
self.selected_index = index;
self.scroll_handle
.scroll_to_item(index, ScrollStrategy::Top);
cx.notify()
} }
pub fn select_previous( pub fn select_previous(
@ -287,13 +265,10 @@ impl AcpThreadHistory {
_window: &mut Window, _window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let count = self.matched_count(); if self.selected_index == 0 {
if count > 0 { self.set_selected_index(self.visible_items.len() - 1, Bias::Left, cx);
if self.selected_index == 0 { } else {
self.set_selected_entry_index(count - 1, cx); self.set_selected_index(self.selected_index - 1, Bias::Left, cx);
} else {
self.set_selected_entry_index(self.selected_index - 1, cx);
}
} }
} }
@ -303,13 +278,10 @@ impl AcpThreadHistory {
_window: &mut Window, _window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let count = self.matched_count(); if self.selected_index == self.visible_items.len() - 1 {
if count > 0 { self.set_selected_index(0, Bias::Right, cx);
if self.selected_index == count - 1 { } else {
self.set_selected_entry_index(0, cx); self.set_selected_index(self.selected_index + 1, Bias::Right, cx);
} else {
self.set_selected_entry_index(self.selected_index + 1, cx);
}
} }
} }
@ -319,35 +291,11 @@ impl AcpThreadHistory {
_window: &mut Window, _window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let count = self.matched_count(); self.set_selected_index(0, Bias::Right, cx);
if count > 0 {
self.set_selected_entry_index(0, cx);
}
} }
fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) { fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
let count = self.matched_count(); self.set_selected_index(self.visible_items.len() - 1, Bias::Left, cx);
if count > 0 {
self.set_selected_entry_index(count - 1, cx);
}
}
fn set_selected_entry_index(&mut self, entry_index: usize, cx: &mut Context<Self>) {
self.selected_index = entry_index;
let scroll_ix = match self.search_state {
SearchState::Empty | SearchState::Searching { .. } => self
.separated_item_indexes
.get(entry_index)
.map(|ix| *ix as usize)
.unwrap_or(entry_index + 1),
SearchState::Searched { .. } => entry_index,
};
self.scroll_handle
.scroll_to_item(scroll_ix, ScrollStrategy::Top);
cx.notify();
} }
fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) { fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
@ -355,7 +303,7 @@ impl AcpThreadHistory {
} }
fn confirm_entry(&mut self, ix: usize, cx: &mut Context<Self>) { fn confirm_entry(&mut self, ix: usize, cx: &mut Context<Self>) {
let Some(entry) = self.get_match(ix) else { let Some(entry) = self.get_history_entry(ix) else {
return; return;
}; };
cx.emit(ThreadHistoryEvent::Open(entry.clone())); cx.emit(ThreadHistoryEvent::Open(entry.clone()));
@ -370,8 +318,8 @@ impl AcpThreadHistory {
self.remove_thread(self.selected_index, cx) self.remove_thread(self.selected_index, cx)
} }
fn remove_thread(&mut self, ix: usize, cx: &mut Context<Self>) { fn remove_thread(&mut self, visible_item_ix: usize, cx: &mut Context<Self>) {
let Some(entry) = self.get_match(ix) else { let Some(entry) = self.get_history_entry(visible_item_ix) else {
return; return;
}; };
@ -386,55 +334,33 @@ impl AcpThreadHistory {
task.detach_and_log_err(cx); task.detach_and_log_err(cx);
} }
fn list_items( fn render_list_items(
&mut self, &mut self,
range: Range<usize>, range: Range<usize>,
_window: &mut Window, _window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Vec<AnyElement> { ) -> Vec<AnyElement> {
match &self.search_state { self.visible_items
SearchState::Empty => self .get(range.clone())
.separated_items .into_iter()
.get(range) .flatten()
.iter() .enumerate()
.flat_map(|items| { .map(|(ix, item)| self.render_list_item(item, range.start + ix, cx))
items .collect()
.iter()
.map(|item| self.render_list_item(item, vec![], cx))
})
.collect(),
SearchState::Searched { matches, .. } => matches[range]
.iter()
.filter_map(|m| {
let entry = self.all_entries.get(m.candidate_id)?;
Some(self.render_history_entry(
entry,
EntryTimeFormat::DateAndTime,
m.candidate_id,
m.positions.clone(),
cx,
))
})
.collect(),
SearchState::Searching { .. } => {
vec![]
}
}
} }
fn render_list_item( fn render_list_item(&self, item: &ListItemType, ix: usize, cx: &Context<Self>) -> AnyElement {
&self,
item: &ListItemType,
highlight_positions: Vec<usize>,
cx: &Context<Self>,
) -> AnyElement {
match item { match item {
ListItemType::Entry { index, format } => match self.all_entries.get(*index) { ListItemType::Entry { entry, format } => self
Some(entry) => self .render_history_entry(entry, *format, ix, Vec::default(), cx)
.render_history_entry(entry, *format, *index, highlight_positions, cx) .into_any(),
.into_any(), ListItemType::SearchResult { entry, positions } => self.render_history_entry(
None => Empty.into_any_element(), entry,
}, EntryTimeFormat::DateAndTime,
ix,
positions.clone(),
cx,
),
ListItemType::BucketSeparator(bucket) => div() ListItemType::BucketSeparator(bucket) => div()
.px(DynamicSpacing::Base06.rems(cx)) .px(DynamicSpacing::Base06.rems(cx))
.pt_2() .pt_2()
@ -452,12 +378,12 @@ impl AcpThreadHistory {
&self, &self,
entry: &HistoryEntry, entry: &HistoryEntry,
format: EntryTimeFormat, format: EntryTimeFormat,
list_entry_ix: usize, ix: usize,
highlight_positions: Vec<usize>, highlight_positions: Vec<usize>,
cx: &Context<Self>, cx: &Context<Self>,
) -> AnyElement { ) -> AnyElement {
let selected = list_entry_ix == self.selected_index; let selected = ix == self.selected_index;
let hovered = Some(list_entry_ix) == self.hovered_index; let hovered = Some(ix) == self.hovered_index;
let timestamp = entry.updated_at().timestamp(); let timestamp = entry.updated_at().timestamp();
let thread_timestamp = format.format_timestamp(timestamp, self.local_timezone); let thread_timestamp = format.format_timestamp(timestamp, self.local_timezone);
@ -465,7 +391,7 @@ impl AcpThreadHistory {
.w_full() .w_full()
.pb_1() .pb_1()
.child( .child(
ListItem::new(list_entry_ix) ListItem::new(ix)
.rounded() .rounded()
.toggle_state(selected) .toggle_state(selected)
.spacing(ListItemSpacing::Sparse) .spacing(ListItemSpacing::Sparse)
@ -487,14 +413,14 @@ impl AcpThreadHistory {
) )
.on_hover(cx.listener(move |this, is_hovered, _window, cx| { .on_hover(cx.listener(move |this, is_hovered, _window, cx| {
if *is_hovered { if *is_hovered {
this.hovered_index = Some(list_entry_ix); this.hovered_index = Some(ix);
} else if this.hovered_index == Some(list_entry_ix) { } else if this.hovered_index == Some(ix) {
this.hovered_index = None; this.hovered_index = None;
} }
cx.notify(); cx.notify();
})) }))
.end_slot::<IconButton>(if hovered || selected { .end_slot::<IconButton>(if hovered {
Some( Some(
IconButton::new("delete", IconName::Trash) IconButton::new("delete", IconName::Trash)
.shape(IconButtonShape::Square) .shape(IconButtonShape::Square)
@ -503,16 +429,14 @@ impl AcpThreadHistory {
.tooltip(move |window, cx| { .tooltip(move |window, cx| {
Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx) Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx)
}) })
.on_click(cx.listener(move |this, _, _, cx| { .on_click(
this.remove_thread(list_entry_ix, cx) cx.listener(move |this, _, _, cx| this.remove_thread(ix, cx)),
})), ),
) )
} else { } else {
None None
}) })
.on_click( .on_click(cx.listener(move |this, _, _, cx| this.confirm_entry(ix, cx))),
cx.listener(move |this, _, _, cx| this.confirm_entry(list_entry_ix, cx)),
),
) )
.into_any_element() .into_any_element()
} }
@ -535,7 +459,7 @@ impl Render for AcpThreadHistory {
.on_action(cx.listener(Self::select_last)) .on_action(cx.listener(Self::select_last))
.on_action(cx.listener(Self::confirm)) .on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::remove_selected_thread)) .on_action(cx.listener(Self::remove_selected_thread))
.when(!self.all_entries.is_empty(), |parent| { .when(!self.history_store.read(cx).is_empty(cx), |parent| {
parent.child( parent.child(
h_flex() h_flex()
.h(px(41.)) // Match the toolbar perfectly .h(px(41.)) // Match the toolbar perfectly
@ -561,7 +485,7 @@ impl Render for AcpThreadHistory {
.overflow_hidden() .overflow_hidden()
.flex_grow(); .flex_grow();
if self.all_entries.is_empty() { if self.history_store.read(cx).is_empty(cx) {
view.justify_center() view.justify_center()
.child( .child(
h_flex().w_full().justify_center().child( h_flex().w_full().justify_center().child(
@ -579,21 +503,160 @@ impl Render for AcpThreadHistory {
view.pr_5().child( view.pr_5().child(
uniform_list( uniform_list(
"thread-history", "thread-history",
self.list_item_count(), self.visible_items.len(),
cx.processor(|this, range: Range<usize>, window, cx| { cx.processor(|this, range: Range<usize>, window, cx| {
this.list_items(range, window, cx) this.render_list_items(range, window, cx)
}), }),
) )
.p_1() .p_1()
.track_scroll(self.scroll_handle.clone()) .track_scroll(self.scroll_handle.clone())
.vertical_scrollbar_for(self.scroll_handle.clone(), window, cx) .flex_grow()
.flex_grow(), .vertical_scrollbar_for(
self.scroll_handle.clone(),
window,
cx,
),
) )
} }
}) })
} }
} }
#[derive(IntoElement)]
pub struct AcpHistoryEntryElement {
entry: HistoryEntry,
thread_view: WeakEntity<AcpThreadView>,
selected: bool,
hovered: bool,
on_hover: Box<dyn Fn(&bool, &mut Window, &mut App) + 'static>,
}
impl AcpHistoryEntryElement {
pub fn new(entry: HistoryEntry, thread_view: WeakEntity<AcpThreadView>) -> Self {
Self {
entry,
thread_view,
selected: false,
hovered: false,
on_hover: Box::new(|_, _, _| {}),
}
}
pub fn hovered(mut self, hovered: bool) -> Self {
self.hovered = hovered;
self
}
pub fn on_hover(mut self, on_hover: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self {
self.on_hover = Box::new(on_hover);
self
}
}
impl RenderOnce for AcpHistoryEntryElement {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
let id = self.entry.id();
let title = self.entry.title();
let timestamp = self.entry.updated_at();
let formatted_time = {
let now = chrono::Utc::now();
let duration = now.signed_duration_since(timestamp);
if duration.num_days() > 0 {
format!("{}d", duration.num_days())
} else if duration.num_hours() > 0 {
format!("{}h ago", duration.num_hours())
} else if duration.num_minutes() > 0 {
format!("{}m ago", duration.num_minutes())
} else {
"Just now".to_string()
}
};
ListItem::new(id)
.rounded()
.toggle_state(self.selected)
.spacing(ListItemSpacing::Sparse)
.start_slot(
h_flex()
.w_full()
.gap_2()
.justify_between()
.child(Label::new(title).size(LabelSize::Small).truncate())
.child(
Label::new(formatted_time)
.color(Color::Muted)
.size(LabelSize::XSmall),
),
)
.on_hover(self.on_hover)
.end_slot::<IconButton>(if self.hovered || self.selected {
Some(
IconButton::new("delete", IconName::Trash)
.shape(IconButtonShape::Square)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.tooltip(move |window, cx| {
Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx)
})
.on_click({
let thread_view = self.thread_view.clone();
let entry = self.entry.clone();
move |_event, _window, cx| {
if let Some(thread_view) = thread_view.upgrade() {
thread_view.update(cx, |thread_view, cx| {
thread_view.delete_history_entry(entry.clone(), cx);
});
}
}
}),
)
} else {
None
})
.on_click({
let thread_view = self.thread_view.clone();
let entry = self.entry;
move |_event, window, cx| {
if let Some(workspace) = thread_view
.upgrade()
.and_then(|view| view.read(cx).workspace().upgrade())
{
match &entry {
HistoryEntry::AcpThread(thread_metadata) => {
if let Some(panel) = workspace.read(cx).panel::<AgentPanel>(cx) {
panel.update(cx, |panel, cx| {
panel.load_agent_thread(
thread_metadata.clone(),
window,
cx,
);
});
}
}
HistoryEntry::TextThread(context) => {
if let Some(panel) = workspace.read(cx).panel::<AgentPanel>(cx) {
panel.update(cx, |panel, cx| {
panel
.open_saved_prompt_editor(
context.path.clone(),
window,
cx,
)
.detach_and_log_err(cx);
});
}
}
}
}
}
})
}
}
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub enum EntryTimeFormat { pub enum EntryTimeFormat {
DateAndTime, DateAndTime,

File diff suppressed because it is too large Load diff

View file

@ -488,7 +488,7 @@ fn render_markdown_code_block(
.on_click({ .on_click({
let active_thread = active_thread.clone(); let active_thread = active_thread.clone();
let parsed_markdown = parsed_markdown.clone(); let parsed_markdown = parsed_markdown.clone();
let code_block_range = metadata.content_range.clone(); let code_block_range = metadata.content_range;
move |_event, _window, cx| { move |_event, _window, cx| {
active_thread.update(cx, |this, cx| { active_thread.update(cx, |this, cx| {
this.copied_code_block_ids.insert((message_id, ix)); this.copied_code_block_ids.insert((message_id, ix));
@ -529,7 +529,6 @@ fn render_markdown_code_block(
"Expand Code" "Expand Code"
})) }))
.on_click({ .on_click({
let active_thread = active_thread.clone();
move |_event, _window, cx| { move |_event, _window, cx| {
active_thread.update(cx, |this, cx| { active_thread.update(cx, |this, cx| {
this.toggle_codeblock_expanded(message_id, ix); this.toggle_codeblock_expanded(message_id, ix);
@ -777,13 +776,11 @@ impl ActiveThread {
let list_state = ListState::new(0, ListAlignment::Bottom, px(2048.)); let list_state = ListState::new(0, ListAlignment::Bottom, px(2048.));
let workspace_subscription = if let Some(workspace) = workspace.upgrade() { let workspace_subscription = workspace.upgrade().map(|workspace| {
Some(cx.observe_release(&workspace, |this, _, cx| { cx.observe_release(&workspace, |this, _, cx| {
this.dismiss_notifications(cx); this.dismiss_notifications(cx);
})) })
} else { });
None
};
let mut this = Self { let mut this = Self {
language_registry, language_registry,
@ -912,7 +909,7 @@ impl ActiveThread {
) { ) {
let rendered = self let rendered = self
.rendered_tool_uses .rendered_tool_uses
.entry(tool_use_id.clone()) .entry(tool_use_id)
.or_insert_with(|| RenderedToolUse { .or_insert_with(|| RenderedToolUse {
label: cx.new(|cx| { label: cx.new(|cx| {
Markdown::new("".into(), Some(self.language_registry.clone()), None, cx) Markdown::new("".into(), Some(self.language_registry.clone()), None, cx)
@ -1214,7 +1211,7 @@ impl ActiveThread {
match AgentSettings::get_global(cx).notify_when_agent_waiting { match AgentSettings::get_global(cx).notify_when_agent_waiting {
NotifyWhenAgentWaiting::PrimaryScreen => { NotifyWhenAgentWaiting::PrimaryScreen => {
if let Some(primary) = cx.primary_display() { if let Some(primary) = cx.primary_display() {
self.pop_up(icon, caption.into(), title.clone(), window, primary, cx); self.pop_up(icon, caption.into(), title, window, primary, cx);
} }
} }
NotifyWhenAgentWaiting::AllScreens => { NotifyWhenAgentWaiting::AllScreens => {
@ -1594,11 +1591,6 @@ impl ActiveThread {
return; return;
}; };
if model.provider.must_accept_terms(cx) {
cx.notify();
return;
}
let edited_text = state.editor.read(cx).text(cx); let edited_text = state.editor.read(cx).text(cx);
let creases = state.editor.update(cx, extract_message_creases); let creases = state.editor.update(cx, extract_message_creases);
@ -1761,7 +1753,7 @@ impl ActiveThread {
.thread .thread
.read(cx) .read(cx)
.message(message_id) .message(message_id)
.map(|msg| msg.to_string()) .map(|msg| msg.to_message_content())
.unwrap_or_default(); .unwrap_or_default();
telemetry::event!( telemetry::event!(
@ -2108,7 +2100,7 @@ impl ActiveThread {
.gap_1() .gap_1()
.children(message_content) .children(message_content)
.when_some(editing_message_state, |this, state| { .when_some(editing_message_state, |this, state| {
let focus_handle = state.editor.focus_handle(cx).clone(); let focus_handle = state.editor.focus_handle(cx);
this.child( this.child(
h_flex() h_flex()
@ -2169,7 +2161,6 @@ impl ActiveThread {
.icon_color(Color::Muted) .icon_color(Color::Muted)
.icon_size(IconSize::Small) .icon_size(IconSize::Small)
.tooltip({ .tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| { move |window, cx| {
Tooltip::for_action_in( Tooltip::for_action_in(
"Regenerate", "Regenerate",
@ -2308,7 +2299,7 @@ impl ActiveThread {
.into_any_element() .into_any_element()
} else if let Some(error) = error { } else if let Some(error) = error {
restore_checkpoint_button restore_checkpoint_button
.tooltip(Tooltip::text(error.to_string())) .tooltip(Tooltip::text(error))
.into_any_element() .into_any_element()
} else { } else {
restore_checkpoint_button.into_any_element() restore_checkpoint_button.into_any_element()
@ -2349,7 +2340,6 @@ impl ActiveThread {
this.submit_feedback_message(message_id, cx); this.submit_feedback_message(message_id, cx);
cx.notify(); cx.notify();
})) }))
.on_action(cx.listener(Self::confirm_editing_message))
.mb_2() .mb_2()
.mx_4() .mx_4()
.p_2() .p_2()

View file

@ -5,6 +5,7 @@ mod tool_picker;
use std::{sync::Arc, time::Duration}; use std::{sync::Arc, time::Duration};
use agent_servers::{AgentServerCommand, AllAgentServersSettings, Gemini};
use agent_settings::AgentSettings; use agent_settings::AgentSettings;
use assistant_tool::{ToolSource, ToolWorkingSet}; use assistant_tool::{ToolSource, ToolWorkingSet};
use cloud_llm_client::Plan; use cloud_llm_client::Plan;
@ -15,7 +16,7 @@ use extension_host::ExtensionStore;
use fs::Fs; use fs::Fs;
use gpui::{ use gpui::{
Action, Animation, AnimationExt as _, AnyView, App, Corner, Entity, EventEmitter, FocusHandle, Action, Animation, AnimationExt as _, AnyView, App, Corner, Entity, EventEmitter, FocusHandle,
Focusable, ScrollHandle, Subscription, Task, Transformation, WeakEntity, percentage, Focusable, Hsla, ScrollHandle, Subscription, Task, Transformation, WeakEntity, percentage,
}; };
use language::LanguageRegistry; use language::LanguageRegistry;
use language_model::{ use language_model::{
@ -23,10 +24,11 @@ use language_model::{
}; };
use notifications::status_toast::{StatusToast, ToastIcon}; use notifications::status_toast::{StatusToast, ToastIcon};
use project::{ use project::{
Project,
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore}, context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
project_settings::{ContextServerSettings, ProjectSettings}, project_settings::{ContextServerSettings, ProjectSettings},
}; };
use settings::{Settings, update_settings_file}; use settings::{Settings, SettingsStore, update_settings_file};
use ui::{ use ui::{
Chip, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu, Chip, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu,
Switch, SwitchColor, SwitchField, Tooltip, WithScrollbar, prelude::*, Switch, SwitchColor, SwitchField, Tooltip, WithScrollbar, prelude::*,
@ -39,7 +41,7 @@ pub(crate) use configure_context_server_modal::ConfigureContextServerModal;
pub(crate) use manage_profiles_modal::ManageProfilesModal; pub(crate) use manage_profiles_modal::ManageProfilesModal;
use crate::{ use crate::{
AddContextServer, AddContextServer, ExternalAgent, NewExternalAgentThread,
agent_configuration::add_llm_provider_modal::{AddLlmProviderModal, LlmCompatibleProvider}, agent_configuration::add_llm_provider_modal::{AddLlmProviderModal, LlmCompatibleProvider},
}; };
@ -47,6 +49,7 @@ pub struct AgentConfiguration {
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
language_registry: Arc<LanguageRegistry>, language_registry: Arc<LanguageRegistry>,
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
project: WeakEntity<Project>,
focus_handle: FocusHandle, focus_handle: FocusHandle,
configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>, configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>,
context_server_store: Entity<ContextServerStore>, context_server_store: Entity<ContextServerStore>,
@ -55,6 +58,8 @@ pub struct AgentConfiguration {
tools: Entity<ToolWorkingSet>, tools: Entity<ToolWorkingSet>,
_registry_subscription: Subscription, _registry_subscription: Subscription,
scroll_handle: ScrollHandle, scroll_handle: ScrollHandle,
gemini_is_installed: bool,
_check_for_gemini: Task<()>,
} }
impl AgentConfiguration { impl AgentConfiguration {
@ -64,6 +69,7 @@ impl AgentConfiguration {
tools: Entity<ToolWorkingSet>, tools: Entity<ToolWorkingSet>,
language_registry: Arc<LanguageRegistry>, language_registry: Arc<LanguageRegistry>,
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
project: WeakEntity<Project>,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Self { ) -> Self {
@ -88,31 +94,32 @@ impl AgentConfiguration {
cx.subscribe(&context_server_store, |_, _, _, cx| cx.notify()) cx.subscribe(&context_server_store, |_, _, _, cx| cx.notify())
.detach(); .detach();
cx.observe_global_in::<SettingsStore>(window, |this, _, cx| {
this.check_for_gemini(cx);
cx.notify();
})
.detach();
let scroll_handle = ScrollHandle::new(); let scroll_handle = ScrollHandle::new();
let mut expanded_provider_configurations = HashMap::default();
if LanguageModelRegistry::read_global(cx)
.provider(&ZED_CLOUD_PROVIDER_ID)
.is_some_and(|cloud_provider| cloud_provider.must_accept_terms(cx))
{
expanded_provider_configurations.insert(ZED_CLOUD_PROVIDER_ID, true);
}
let mut this = Self { let mut this = Self {
fs, fs,
language_registry, language_registry,
workspace, workspace,
project,
focus_handle, focus_handle,
configuration_views_by_provider: HashMap::default(), configuration_views_by_provider: HashMap::default(),
context_server_store, context_server_store,
expanded_context_server_tools: HashMap::default(), expanded_context_server_tools: HashMap::default(),
expanded_provider_configurations, expanded_provider_configurations: HashMap::default(),
tools, tools,
_registry_subscription: registry_subscription, _registry_subscription: registry_subscription,
scroll_handle, scroll_handle,
gemini_is_installed: false,
_check_for_gemini: Task::ready(()),
}; };
this.build_provider_configuration_views(window, cx); this.build_provider_configuration_views(window, cx);
this.check_for_gemini(cx);
this this
} }
@ -142,6 +149,34 @@ impl AgentConfiguration {
self.configuration_views_by_provider self.configuration_views_by_provider
.insert(provider.id(), configuration_view); .insert(provider.id(), configuration_view);
} }
fn check_for_gemini(&mut self, cx: &mut Context<Self>) {
let project = self.project.clone();
let settings = AllAgentServersSettings::get_global(cx).clone();
self._check_for_gemini = cx.spawn({
async move |this, cx| {
let Some(project) = project.upgrade() else {
return;
};
let gemini_is_installed = AgentServerCommand::resolve(
Gemini::binary_name(),
&[],
// TODO expose fallback path from the Gemini/CC types so we don't have to hardcode it again here
None,
settings.gemini,
&project,
cx,
)
.await
.is_some();
this.update(cx, |this, cx| {
this.gemini_is_installed = gemini_is_installed;
cx.notify();
})
.ok();
}
});
}
} }
impl Focusable for AgentConfiguration { impl Focusable for AgentConfiguration {
@ -162,8 +197,8 @@ impl AgentConfiguration {
provider: &Arc<dyn LanguageModelProvider>, provider: &Arc<dyn LanguageModelProvider>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> impl IntoElement + use<> { ) -> impl IntoElement + use<> {
let provider_id = provider.id().0.clone(); let provider_id = provider.id().0;
let provider_name = provider.name().0.clone(); let provider_name = provider.name().0;
let provider_id_string = SharedString::from(format!("provider-disclosure-{provider_id}")); let provider_id_string = SharedString::from(format!("provider-disclosure-{provider_id}"));
let configuration_view = self let configuration_view = self
@ -189,7 +224,7 @@ impl AgentConfiguration {
let is_signed_in = self let is_signed_in = self
.workspace .workspace
.read_with(cx, |workspace, _| { .read_with(cx, |workspace, _| {
workspace.client().status().borrow().is_connected() !workspace.client().status().borrow().is_signed_out()
}) })
.unwrap_or(false); .unwrap_or(false);
@ -216,7 +251,6 @@ impl AgentConfiguration {
.child( .child(
h_flex() h_flex()
.id(provider_id_string.clone()) .id(provider_id_string.clone())
.cursor_pointer()
.px_2() .px_2()
.py_0p5() .py_0p5()
.w_full() .w_full()
@ -236,10 +270,7 @@ impl AgentConfiguration {
h_flex() h_flex()
.w_full() .w_full()
.gap_1() .gap_1()
.child( .child(Label::new(provider_name.clone()))
Label::new(provider_name.clone())
.size(LabelSize::Large),
)
.map(|this| { .map(|this| {
if is_zed_provider && is_signed_in { if is_zed_provider && is_signed_in {
this.child( this.child(
@ -266,7 +297,7 @@ impl AgentConfiguration {
.closed_icon(IconName::ChevronDown), .closed_icon(IconName::ChevronDown),
) )
.on_click(cx.listener({ .on_click(cx.listener({
let provider_id = provider.id().clone(); let provider_id = provider.id();
move |this, _event, _window, _cx| { move |this, _event, _window, _cx| {
let is_expanded = this let is_expanded = this
.expanded_provider_configurations .expanded_provider_configurations
@ -284,7 +315,7 @@ impl AgentConfiguration {
"Start New Thread", "Start New Thread",
) )
.icon_position(IconPosition::Start) .icon_position(IconPosition::Start)
.icon(IconName::Plus) .icon(IconName::Thread)
.icon_size(IconSize::Small) .icon_size(IconSize::Small)
.icon_color(Color::Muted) .icon_color(Color::Muted)
.label_size(LabelSize::Small) .label_size(LabelSize::Small)
@ -383,7 +414,7 @@ impl AgentConfiguration {
), ),
) )
.child( .child(
Label::new("Add at least one provider to use AI-powered features.") Label::new("Add at least one provider to use AI-powered features with Zed's native agent.")
.color(Color::Muted), .color(Color::Muted),
), ),
), ),
@ -524,6 +555,14 @@ impl AgentConfiguration {
} }
} }
fn card_item_bg_color(&self, cx: &mut Context<Self>) -> Hsla {
cx.theme().colors().background.opacity(0.25)
}
fn card_item_border_color(&self, cx: &mut Context<Self>) -> Hsla {
cx.theme().colors().border.opacity(0.6)
}
fn render_context_servers_section( fn render_context_servers_section(
&mut self, &mut self,
window: &mut Window, window: &mut Window,
@ -541,7 +580,12 @@ impl AgentConfiguration {
v_flex() v_flex()
.gap_0p5() .gap_0p5()
.child(Headline::new("Model Context Protocol (MCP) Servers")) .child(Headline::new("Model Context Protocol (MCP) Servers"))
.child(Label::new("Connect to context servers through the Model Context Protocol, either using Zed extensions or directly.").color(Color::Muted)), .child(
Label::new(
"All context servers connected through the Model Context Protocol.",
)
.color(Color::Muted),
),
) )
.children( .children(
context_server_ids.into_iter().map(|context_server_id| { context_server_ids.into_iter().map(|context_server_id| {
@ -551,7 +595,7 @@ impl AgentConfiguration {
.child( .child(
h_flex() h_flex()
.justify_between() .justify_between()
.gap_2() .gap_1p5()
.child( .child(
h_flex().w_full().child( h_flex().w_full().child(
Button::new("add-context-server", "Add Custom Server") Button::new("add-context-server", "Add Custom Server")
@ -642,8 +686,6 @@ impl AgentConfiguration {
.map_or([].as_slice(), |tools| tools.as_slice()); .map_or([].as_slice(), |tools| tools.as_slice());
let tool_count = tools.len(); let tool_count = tools.len();
let border_color = cx.theme().colors().border.opacity(0.6);
let (source_icon, source_tooltip) = if is_from_extension { let (source_icon, source_tooltip) = if is_from_extension {
( (
IconName::ZedMcpExtension, IconName::ZedMcpExtension,
@ -662,7 +704,7 @@ impl AgentConfiguration {
.size(IconSize::XSmall) .size(IconSize::XSmall)
.color(Color::Accent) .color(Color::Accent)
.with_animation( .with_animation(
SharedString::from(format!("{}-starting", context_server_id.0.clone(),)), SharedString::from(format!("{}-starting", context_server_id.0,)),
Animation::new(Duration::from_secs(3)).repeat(), Animation::new(Duration::from_secs(3)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))), |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
) )
@ -786,8 +828,8 @@ impl AgentConfiguration {
.id(item_id.clone()) .id(item_id.clone())
.border_1() .border_1()
.rounded_md() .rounded_md()
.border_color(border_color) .border_color(self.card_item_border_color(cx))
.bg(cx.theme().colors().background.opacity(0.2)) .bg(self.card_item_bg_color(cx))
.overflow_hidden() .overflow_hidden()
.child( .child(
h_flex() h_flex()
@ -795,7 +837,11 @@ impl AgentConfiguration {
.justify_between() .justify_between()
.when( .when(
error.is_some() || are_tools_expanded && tool_count >= 1, error.is_some() || are_tools_expanded && tool_count >= 1,
|element| element.border_b_1().border_color(border_color), |element| {
element
.border_b_1()
.border_color(self.card_item_border_color(cx))
},
) )
.child( .child(
h_flex() h_flex()
@ -862,7 +908,6 @@ impl AgentConfiguration {
.on_click({ .on_click({
let context_server_manager = let context_server_manager =
self.context_server_store.clone(); self.context_server_store.clone();
let context_server_id = context_server_id.clone();
let fs = self.fs.clone(); let fs = self.fs.clone();
move |state, _window, cx| { move |state, _window, cx| {
@ -978,6 +1023,166 @@ impl AgentConfiguration {
)) ))
}) })
} }
fn render_agent_servers_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
let settings = AllAgentServersSettings::get_global(cx).clone();
let user_defined_agents = settings
.custom
.iter()
.map(|(name, settings)| {
self.render_agent_server(
IconName::Ai,
name.clone(),
ExternalAgent::Custom {
name: name.clone(),
settings: settings.clone(),
},
None,
cx,
)
.into_any_element()
})
.collect::<Vec<_>>();
v_flex()
.border_b_1()
.border_color(cx.theme().colors().border)
.child(
v_flex()
.p(DynamicSpacing::Base16.rems(cx))
.pr(DynamicSpacing::Base20.rems(cx))
.gap_2()
.child(
v_flex()
.gap_0p5()
.child(Headline::new("External Agents"))
.child(
Label::new(
"Use the full power of Zed's UI with your favorite agent, connected via the Agent Client Protocol.",
)
.color(Color::Muted),
),
)
.child(self.render_agent_server(
IconName::AiGemini,
"Gemini CLI",
ExternalAgent::Gemini,
(!self.gemini_is_installed).then_some(Gemini::install_command().into()),
cx,
))
// TODO add CC
.children(user_defined_agents),
)
}
fn render_agent_server(
&self,
icon: IconName,
name: impl Into<SharedString>,
agent: ExternalAgent,
install_command: Option<SharedString>,
cx: &mut Context<Self>,
) -> impl IntoElement {
let name = name.into();
h_flex()
.p_1()
.pl_2()
.gap_1p5()
.justify_between()
.border_1()
.rounded_md()
.border_color(self.card_item_border_color(cx))
.bg(self.card_item_bg_color(cx))
.overflow_hidden()
.child(
h_flex()
.gap_1p5()
.child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
.child(Label::new(name.clone())),
)
.map(|this| {
if let Some(install_command) = install_command {
this.child(
Button::new(
SharedString::from(format!("install_external_agent-{name}")),
"Install Agent",
)
.label_size(LabelSize::Small)
.icon(IconName::Plus)
.icon_position(IconPosition::Start)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.tooltip(Tooltip::text(install_command.clone()))
.on_click(cx.listener(
move |this, _, window, cx| {
let Some(project) = this.project.upgrade() else {
return;
};
let Some(workspace) = this.workspace.upgrade() else {
return;
};
let cwd = project.read(cx).first_project_directory(cx);
let shell =
project.read(cx).terminal_settings(&cwd, cx).shell.clone();
let spawn_in_terminal = task::SpawnInTerminal {
id: task::TaskId(install_command.to_string()),
full_label: install_command.to_string(),
label: install_command.to_string(),
command: Some(install_command.to_string()),
args: Vec::new(),
command_label: install_command.to_string(),
cwd,
env: Default::default(),
use_new_terminal: true,
allow_concurrent_runs: true,
reveal: Default::default(),
reveal_target: Default::default(),
hide: Default::default(),
shell,
show_summary: true,
show_command: true,
show_rerun: false,
};
let task = workspace.update(cx, |workspace, cx| {
workspace.spawn_in_terminal(spawn_in_terminal, window, cx)
});
cx.spawn(async move |this, cx| {
task.await;
this.update(cx, |this, cx| {
this.check_for_gemini(cx);
})
.ok();
})
.detach();
},
)),
)
} else {
this.child(
h_flex().gap_1().child(
Button::new(
SharedString::from(format!("start_acp_thread-{name}")),
"Start New Thread",
)
.label_size(LabelSize::Small)
.icon(IconName::Thread)
.icon_position(IconPosition::Start)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.on_click(move |_, window, cx| {
window.dispatch_action(
NewExternalAgentThread {
agent: Some(agent.clone()),
}
.boxed_clone(),
cx,
);
}),
),
)
}
})
}
} }
impl Render for AgentConfiguration { impl Render for AgentConfiguration {
@ -997,6 +1202,7 @@ impl Render for AgentConfiguration {
.size_full() .size_full()
.overflow_y_scroll() .overflow_y_scroll()
.child(self.render_general_settings_section(cx)) .child(self.render_general_settings_section(cx))
.child(self.render_agent_servers_section(cx))
.child(self.render_context_servers_section(window, cx)) .child(self.render_context_servers_section(window, cx))
.child(self.render_provider_configuration_section(cx)), .child(self.render_provider_configuration_section(cx)),
) )
@ -1047,7 +1253,6 @@ fn show_unable_to_uninstall_extension_with_context_server(
cx, cx,
move |this, _cx| { move |this, _cx| {
let workspace_handle = workspace_handle.clone(); let workspace_handle = workspace_handle.clone();
let context_server_id = context_server_id.clone();
this.icon(ToastIcon::new(IconName::Warning).color(Color::Warning)) this.icon(ToastIcon::new(IconName::Warning).color(Color::Warning))
.dismiss_button(true) .dismiss_button(true)

View file

@ -668,10 +668,10 @@ mod tests {
); );
let parsed_model = model_input.parse(cx).unwrap(); let parsed_model = model_input.parse(cx).unwrap();
assert_eq!(parsed_model.capabilities.tools, true); assert!(parsed_model.capabilities.tools);
assert_eq!(parsed_model.capabilities.images, false); assert!(!parsed_model.capabilities.images);
assert_eq!(parsed_model.capabilities.parallel_tool_calls, false); assert!(!parsed_model.capabilities.parallel_tool_calls);
assert_eq!(parsed_model.capabilities.prompt_cache_key, false); assert!(!parsed_model.capabilities.prompt_cache_key);
}); });
} }
@ -693,10 +693,10 @@ mod tests {
model_input.capabilities.supports_prompt_cache_key = ToggleState::Unselected; model_input.capabilities.supports_prompt_cache_key = ToggleState::Unselected;
let parsed_model = model_input.parse(cx).unwrap(); let parsed_model = model_input.parse(cx).unwrap();
assert_eq!(parsed_model.capabilities.tools, false); assert!(!parsed_model.capabilities.tools);
assert_eq!(parsed_model.capabilities.images, false); assert!(!parsed_model.capabilities.images);
assert_eq!(parsed_model.capabilities.parallel_tool_calls, false); assert!(!parsed_model.capabilities.parallel_tool_calls);
assert_eq!(parsed_model.capabilities.prompt_cache_key, false); assert!(!parsed_model.capabilities.prompt_cache_key);
}); });
} }
@ -719,10 +719,10 @@ mod tests {
let parsed_model = model_input.parse(cx).unwrap(); let parsed_model = model_input.parse(cx).unwrap();
assert_eq!(parsed_model.name, "somemodel"); assert_eq!(parsed_model.name, "somemodel");
assert_eq!(parsed_model.capabilities.tools, true); assert!(parsed_model.capabilities.tools);
assert_eq!(parsed_model.capabilities.images, false); assert!(!parsed_model.capabilities.images);
assert_eq!(parsed_model.capabilities.parallel_tool_calls, true); assert!(parsed_model.capabilities.parallel_tool_calls);
assert_eq!(parsed_model.capabilities.prompt_cache_key, false); assert!(!parsed_model.capabilities.prompt_cache_key);
}); });
} }

View file

@ -261,7 +261,6 @@ impl ConfigureContextServerModal {
_cx: &mut Context<Workspace>, _cx: &mut Context<Workspace>,
) { ) {
workspace.register_action({ workspace.register_action({
let language_registry = language_registry.clone();
move |_workspace, _: &AddContextServer, window, cx| { move |_workspace, _: &AddContextServer, window, cx| {
let workspace_handle = cx.weak_entity(); let workspace_handle = cx.weak_entity();
let language_registry = language_registry.clone(); let language_registry = language_registry.clone();

View file

@ -464,7 +464,7 @@ impl ManageProfilesModal {
}, },
)) ))
.child(ListSeparator) .child(ListSeparator)
.child(h_flex().p_2().child(mode.name_editor.clone())) .child(h_flex().p_2().child(mode.name_editor))
} }
fn render_view_profile( fn render_view_profile(

View file

@ -185,7 +185,7 @@ impl AgentDiffPane {
let focus_handle = cx.focus_handle(); let focus_handle = cx.focus_handle();
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
let project = thread.project(cx).clone(); let project = thread.project(cx);
let editor = cx.new(|cx| { let editor = cx.new(|cx| {
let mut editor = let mut editor =
Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx); Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);
@ -196,7 +196,7 @@ impl AgentDiffPane {
editor editor
}); });
let action_log = thread.action_log(cx).clone(); let action_log = thread.action_log(cx);
let mut this = Self { let mut this = Self {
_subscriptions: vec![ _subscriptions: vec![
@ -1312,7 +1312,7 @@ impl AgentDiff {
let entity = cx.new(|_cx| Self::default()); let entity = cx.new(|_cx| Self::default());
let global = AgentDiffGlobal(entity.clone()); let global = AgentDiffGlobal(entity.clone());
cx.set_global(global); cx.set_global(global);
entity.clone() entity
}) })
} }
@ -1334,7 +1334,7 @@ impl AgentDiff {
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let action_log = thread.action_log(cx).clone(); let action_log = thread.action_log(cx);
let action_log_subscription = cx.observe_in(&action_log, window, { let action_log_subscription = cx.observe_in(&action_log, window, {
let workspace = workspace.clone(); let workspace = workspace.clone();
@ -1529,6 +1529,7 @@ impl AgentDiff {
| AcpThreadEvent::TokenUsageUpdated | AcpThreadEvent::TokenUsageUpdated
| AcpThreadEvent::EntriesRemoved(_) | AcpThreadEvent::EntriesRemoved(_)
| AcpThreadEvent::ToolAuthorizationRequired | AcpThreadEvent::ToolAuthorizationRequired
| AcpThreadEvent::PromptCapabilitiesUpdated
| AcpThreadEvent::Retry(_) => {} | AcpThreadEvent::Retry(_) => {}
} }
} }
@ -1544,7 +1545,7 @@ impl AgentDiff {
&& let Some(editor) = item.downcast::<Editor>() && let Some(editor) = item.downcast::<Editor>()
&& let Some(buffer) = Self::full_editor_buffer(editor.read(cx), cx) && let Some(buffer) = Self::full_editor_buffer(editor.read(cx), cx)
{ {
self.register_editor(workspace.downgrade(), buffer.clone(), editor, window, cx); self.register_editor(workspace.downgrade(), buffer, editor, window, cx);
} }
} }
@ -1643,7 +1644,7 @@ impl AgentDiff {
continue; continue;
}; };
for (weak_editor, _) in buffer_editors { for weak_editor in buffer_editors.keys() {
let Some(editor) = weak_editor.upgrade() else { let Some(editor) = weak_editor.upgrade() else {
continue; continue;
}; };

View file

@ -66,10 +66,8 @@ impl AgentModelSelector {
fs.clone(), fs.clone(),
cx, cx,
move |settings, _cx| { move |settings, _cx| {
settings.set_inline_assistant_model( settings
provider.clone(), .set_inline_assistant_model(provider.clone(), model_id);
model_id.clone(),
);
}, },
); );
} }

View file

@ -5,9 +5,12 @@ use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use acp_thread::AcpThread; use acp_thread::AcpThread;
use agent_servers::AgentServerSettings;
use agent2::{DbThreadMetadata, HistoryEntry}; use agent2::{DbThreadMetadata, HistoryEntry};
use db::kvp::{Dismissable, KEY_VALUE_STORE}; use db::kvp::{Dismissable, KEY_VALUE_STORE};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use zed_actions::OpenBrowser;
use zed_actions::agent::ReauthenticateAgent;
use crate::acp::{AcpThreadHistory, ThreadHistoryEvent}; use crate::acp::{AcpThreadHistory, ThreadHistoryEvent};
use crate::agent_diff::AgentDiffThread; use crate::agent_diff::AgentDiffThread;
@ -30,7 +33,7 @@ use crate::{
thread_history::{HistoryEntryElement, ThreadHistory}, thread_history::{HistoryEntryElement, ThreadHistory},
ui::{AgentOnboardingModal, EndTrialUpsell}, ui::{AgentOnboardingModal, EndTrialUpsell},
}; };
use crate::{ExternalAgent, NewExternalAgentThread}; use crate::{ExternalAgent, NewExternalAgentThread, NewNativeAgentThreadFromSummary};
use agent::{ use agent::{
Thread, ThreadError, ThreadEvent, ThreadId, ThreadSummary, TokenUsageRatio, Thread, ThreadError, ThreadEvent, ThreadId, ThreadSummary, TokenUsageRatio,
context_store::ContextStore, context_store::ContextStore,
@ -54,9 +57,7 @@ use gpui::{
Pixels, Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between, Pixels, Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between,
}; };
use language::LanguageRegistry; use language::LanguageRegistry;
use language_model::{ use language_model::{ConfigurationError, ConfiguredModel, LanguageModelRegistry};
ConfigurationError, ConfiguredModel, LanguageModelProviderTosView, LanguageModelRegistry,
};
use project::{DisableAiSettings, Project, ProjectPath, Worktree}; use project::{DisableAiSettings, Project, ProjectPath, Worktree};
use prompt_store::{PromptBuilder, PromptStore, UserPromptId}; use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
use rules_library::{RulesLibrary, open_rules_library}; use rules_library::{RulesLibrary, open_rules_library};
@ -98,6 +99,16 @@ pub fn init(cx: &mut App) {
workspace.focus_panel::<AgentPanel>(window, cx); workspace.focus_panel::<AgentPanel>(window, cx);
} }
}) })
.register_action(
|workspace, action: &NewNativeAgentThreadFromSummary, window, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
panel.update(cx, |panel, cx| {
panel.new_native_agent_thread_from_summary(action, window, cx)
});
workspace.focus_panel::<AgentPanel>(window, cx);
}
},
)
.register_action(|workspace, _: &OpenHistory, window, cx| { .register_action(|workspace, _: &OpenHistory, window, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) { if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx); workspace.focus_panel::<AgentPanel>(window, cx);
@ -120,7 +131,7 @@ pub fn init(cx: &mut App) {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) { if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx); workspace.focus_panel::<AgentPanel>(window, cx);
panel.update(cx, |panel, cx| { panel.update(cx, |panel, cx| {
panel.external_thread(action.agent, None, window, cx) panel.external_thread(action.agent.clone(), None, None, window, cx)
}); });
} }
}) })
@ -231,7 +242,8 @@ enum WhichFontSize {
None, None,
} }
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] // TODO unify this with ExternalAgent
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
pub enum AgentType { pub enum AgentType {
#[default] #[default]
Zed, Zed,
@ -239,23 +251,29 @@ pub enum AgentType {
Gemini, Gemini,
ClaudeCode, ClaudeCode,
NativeAgent, NativeAgent,
Custom {
name: SharedString,
settings: AgentServerSettings,
},
} }
impl AgentType { impl AgentType {
fn label(self) -> impl Into<SharedString> { fn label(&self) -> SharedString {
match self { match self {
Self::Zed | Self::TextThread => "Zed Agent", Self::Zed | Self::TextThread => "Zed Agent".into(),
Self::NativeAgent => "Agent 2", Self::NativeAgent => "Agent 2".into(),
Self::Gemini => "Gemini CLI", Self::Gemini => "Gemini CLI".into(),
Self::ClaudeCode => "Claude Code", Self::ClaudeCode => "Claude Code".into(),
Self::Custom { name, .. } => name.into(),
} }
} }
fn icon(self) -> Option<IconName> { fn icon(&self) -> Option<IconName> {
match self { match self {
Self::Zed | Self::NativeAgent | Self::TextThread => None, Self::Zed | Self::NativeAgent | Self::TextThread => None,
Self::Gemini => Some(IconName::AiGemini), Self::Gemini => Some(IconName::AiGemini),
Self::ClaudeCode => Some(IconName::AiClaude), Self::ClaudeCode => Some(IconName::AiClaude),
Self::Custom { .. } => Some(IconName::Terminal),
} }
} }
} }
@ -509,7 +527,7 @@ pub struct AgentPanel {
impl AgentPanel { impl AgentPanel {
fn serialize(&mut self, cx: &mut Context<Self>) { fn serialize(&mut self, cx: &mut Context<Self>) {
let width = self.width; let width = self.width;
let selected_agent = self.selected_agent; let selected_agent = self.selected_agent.clone();
self.pending_serialization = Some(cx.background_spawn(async move { self.pending_serialization = Some(cx.background_spawn(async move {
KEY_VALUE_STORE KEY_VALUE_STORE
.write_kvp( .write_kvp(
@ -523,6 +541,7 @@ impl AgentPanel {
anyhow::Ok(()) anyhow::Ok(())
})); }));
} }
pub fn load( pub fn load(
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
prompt_builder: Arc<PromptBuilder>, prompt_builder: Arc<PromptBuilder>,
@ -572,6 +591,17 @@ impl AgentPanel {
None None
}; };
// Wait for the Gemini/Native feature flag to be available.
let client = workspace.read_with(cx, |workspace, _| workspace.client().clone())?;
if !client.status().borrow().is_signed_out() {
cx.update(|_, cx| {
cx.wait_for_flag_or_timeout::<feature_flags::GeminiAndNativeFeatureFlag>(
Duration::from_secs(2),
)
})?
.await;
}
let panel = workspace.update_in(cx, |workspace, window, cx| { let panel = workspace.update_in(cx, |workspace, window, cx| {
let panel = cx.new(|cx| { let panel = cx.new(|cx| {
Self::new( Self::new(
@ -587,7 +617,7 @@ impl AgentPanel {
panel.update(cx, |panel, cx| { panel.update(cx, |panel, cx| {
panel.width = serialized_panel.width.map(|w| w.round()); panel.width = serialized_panel.width.map(|w| w.round());
if let Some(selected_agent) = serialized_panel.selected_agent { if let Some(selected_agent) = serialized_panel.selected_agent {
panel.selected_agent = selected_agent; panel.selected_agent = selected_agent.clone();
panel.new_agent_thread(selected_agent, window, cx); panel.new_agent_thread(selected_agent, window, cx);
} }
cx.notify(); cx.notify();
@ -658,6 +688,7 @@ impl AgentPanel {
this.external_thread( this.external_thread(
Some(crate::ExternalAgent::NativeAgent), Some(crate::ExternalAgent::NativeAgent),
Some(thread.clone()), Some(thread.clone()),
None,
window, window,
cx, cx,
); );
@ -880,6 +911,16 @@ impl AgentPanel {
} }
} }
fn active_thread_view(&self) -> Option<&Entity<AcpThreadView>> {
match &self.active_view {
ActiveView::ExternalAgentThread { thread_view, .. } => Some(thread_view),
ActiveView::Thread { .. }
| ActiveView::TextThread { .. }
| ActiveView::History
| ActiveView::Configuration => None,
}
}
fn new_thread(&mut self, action: &NewThread, window: &mut Window, cx: &mut Context<Self>) { fn new_thread(&mut self, action: &NewThread, window: &mut Window, cx: &mut Context<Self>) {
if cx.has_flag::<GeminiAndNativeFeatureFlag>() { if cx.has_flag::<GeminiAndNativeFeatureFlag>() {
return self.new_agent_thread(AgentType::NativeAgent, window, cx); return self.new_agent_thread(AgentType::NativeAgent, window, cx);
@ -956,13 +997,38 @@ impl AgentPanel {
message_editor.focus_handle(cx).focus(window); message_editor.focus_handle(cx).focus(window);
let thread_view = ActiveView::thread(active_thread.clone(), message_editor, window, cx); let thread_view = ActiveView::thread(active_thread, message_editor, window, cx);
self.set_active_view(thread_view, window, cx); self.set_active_view(thread_view, window, cx);
AgentDiff::set_active_thread(&self.workspace, thread.clone(), window, cx); AgentDiff::set_active_thread(&self.workspace, thread.clone(), window, cx);
} }
fn new_native_agent_thread_from_summary(
&mut self,
action: &NewNativeAgentThreadFromSummary,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(thread) = self
.acp_history_store
.read(cx)
.thread_from_session_id(&action.from_session_id)
else {
return;
};
self.external_thread(
Some(ExternalAgent::NativeAgent),
None,
Some(thread.clone()),
window,
cx,
);
}
fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
telemetry::event!("Agent Thread Started", agent = "zed-text");
let context = self let context = self
.context_store .context_store
.update(cx, |context_store, cx| context_store.create(cx)); .update(cx, |context_store, cx| context_store.create(cx));
@ -1003,6 +1069,7 @@ impl AgentPanel {
&mut self, &mut self,
agent_choice: Option<crate::ExternalAgent>, agent_choice: Option<crate::ExternalAgent>,
resume_thread: Option<DbThreadMetadata>, resume_thread: Option<DbThreadMetadata>,
summarize_thread: Option<DbThreadMetadata>,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
@ -1022,14 +1089,17 @@ impl AgentPanel {
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
let ext_agent = match agent_choice { let ext_agent = match agent_choice {
Some(agent) => { Some(agent) => {
cx.background_spawn(async move { cx.background_spawn({
if let Some(serialized) = let agent = agent.clone();
serde_json::to_string(&LastUsedExternalAgent { agent }).log_err() async move {
{ if let Some(serialized) =
KEY_VALUE_STORE serde_json::to_string(&LastUsedExternalAgent { agent }).log_err()
.write_kvp(LAST_USED_EXTERNAL_AGENT_KEY.to_string(), serialized) {
.await KEY_VALUE_STORE
.log_err(); .write_kvp(LAST_USED_EXTERNAL_AGENT_KEY.to_string(), serialized)
.await
.log_err();
}
} }
}) })
.detach(); .detach();
@ -1051,11 +1121,15 @@ impl AgentPanel {
} }
}; };
telemetry::event!("Agent Thread Started", agent = ext_agent.name());
let server = ext_agent.server(fs, history); let server = ext_agent.server(fs, history);
this.update_in(cx, |this, window, cx| { this.update_in(cx, |this, window, cx| {
match ext_agent { match ext_agent {
crate::ExternalAgent::Gemini | crate::ExternalAgent::NativeAgent => { crate::ExternalAgent::Gemini
| crate::ExternalAgent::NativeAgent
| crate::ExternalAgent::Custom { .. } => {
if !cx.has_flag::<GeminiAndNativeFeatureFlag>() { if !cx.has_flag::<GeminiAndNativeFeatureFlag>() {
return; return;
} }
@ -1071,6 +1145,7 @@ impl AgentPanel {
crate::acp::AcpThreadView::new( crate::acp::AcpThreadView::new(
server, server,
resume_thread, resume_thread,
summarize_thread,
workspace.clone(), workspace.clone(),
project, project,
this.acp_history_store.clone(), this.acp_history_store.clone(),
@ -1163,7 +1238,7 @@ impl AgentPanel {
}); });
self.set_active_view( self.set_active_view(
ActiveView::prompt_editor( ActiveView::prompt_editor(
editor.clone(), editor,
self.history_store.clone(), self.history_store.clone(),
self.acp_history_store.clone(), self.acp_history_store.clone(),
self.language_registry.clone(), self.language_registry.clone(),
@ -1236,7 +1311,7 @@ impl AgentPanel {
}); });
message_editor.focus_handle(cx).focus(window); message_editor.focus_handle(cx).focus(window);
let thread_view = ActiveView::thread(active_thread.clone(), message_editor, window, cx); let thread_view = ActiveView::thread(active_thread, message_editor, window, cx);
self.set_active_view(thread_view, window, cx); self.set_active_view(thread_view, window, cx);
AgentDiff::set_active_thread(&self.workspace, thread.clone(), window, cx); AgentDiff::set_active_thread(&self.workspace, thread.clone(), window, cx);
} }
@ -1405,6 +1480,7 @@ impl AgentPanel {
tools, tools,
self.language_registry.clone(), self.language_registry.clone(),
self.workspace.clone(), self.workspace.clone(),
self.project.downgrade(),
window, window,
cx, cx,
) )
@ -1525,7 +1601,7 @@ impl AgentPanel {
return; return;
} }
let model = thread_state.configured_model().map(|cm| cm.model.clone()); let model = thread_state.configured_model().map(|cm| cm.model);
if let Some(model) = model { if let Some(model) = model {
thread.update(cx, |active_thread, cx| { thread.update(cx, |active_thread, cx| {
active_thread.thread().update(cx, |thread, cx| { active_thread.thread().update(cx, |thread, cx| {
@ -1680,7 +1756,7 @@ impl AgentPanel {
.open_thread_by_id(&id, window, cx) .open_thread_by_id(&id, window, cx)
.detach_and_log_err(cx), .detach_and_log_err(cx),
HistoryEntryId::Context(path) => this HistoryEntryId::Context(path) => this
.open_saved_prompt_editor(path.clone(), window, cx) .open_saved_prompt_editor(path, window, cx)
.detach_and_log_err(cx), .detach_and_log_err(cx),
}) })
.ok(); .ok();
@ -1742,6 +1818,7 @@ impl AgentPanel {
agent2::HistoryEntry::AcpThread(entry) => this.external_thread( agent2::HistoryEntry::AcpThread(entry) => this.external_thread(
Some(ExternalAgent::NativeAgent), Some(ExternalAgent::NativeAgent),
Some(entry.clone()), Some(entry.clone()),
None,
window, window,
cx, cx,
), ),
@ -1782,14 +1859,14 @@ impl AgentPanel {
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
if self.selected_agent != agent { if self.selected_agent != agent {
self.selected_agent = agent; self.selected_agent = agent.clone();
self.serialize(cx); self.serialize(cx);
} }
self.new_agent_thread(agent, window, cx); self.new_agent_thread(agent, window, cx);
} }
pub fn selected_agent(&self) -> AgentType { pub fn selected_agent(&self) -> AgentType {
self.selected_agent self.selected_agent.clone()
} }
pub fn new_agent_thread( pub fn new_agent_thread(
@ -1811,15 +1888,30 @@ impl AgentPanel {
AgentType::TextThread => { AgentType::TextThread => {
window.dispatch_action(NewTextThread.boxed_clone(), cx); window.dispatch_action(NewTextThread.boxed_clone(), cx);
} }
AgentType::NativeAgent => { AgentType::NativeAgent => self.external_thread(
self.external_thread(Some(crate::ExternalAgent::NativeAgent), None, window, cx) Some(crate::ExternalAgent::NativeAgent),
} None,
None,
window,
cx,
),
AgentType::Gemini => { AgentType::Gemini => {
self.external_thread(Some(crate::ExternalAgent::Gemini), None, window, cx) self.external_thread(Some(crate::ExternalAgent::Gemini), None, None, window, cx)
}
AgentType::ClaudeCode => {
self.external_thread(Some(crate::ExternalAgent::ClaudeCode), None, window, cx)
} }
AgentType::ClaudeCode => self.external_thread(
Some(crate::ExternalAgent::ClaudeCode),
None,
None,
window,
cx,
),
AgentType::Custom { name, settings } => self.external_thread(
Some(crate::ExternalAgent::Custom { name, settings }),
None,
None,
window,
cx,
),
} }
} }
@ -1829,7 +1921,13 @@ impl AgentPanel {
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
self.external_thread(Some(ExternalAgent::NativeAgent), Some(thread), window, cx); self.external_thread(
Some(ExternalAgent::NativeAgent),
Some(thread),
None,
window,
cx,
);
} }
} }
@ -1966,11 +2064,13 @@ impl AgentPanel {
}; };
match state { match state {
ThreadSummary::Pending => Label::new(ThreadSummary::DEFAULT.clone()) ThreadSummary::Pending => Label::new(ThreadSummary::DEFAULT)
.truncate() .truncate()
.color(Color::Muted)
.into_any_element(), .into_any_element(),
ThreadSummary::Generating => Label::new(LOADING_SUMMARY_PLACEHOLDER) ThreadSummary::Generating => Label::new(LOADING_SUMMARY_PLACEHOLDER)
.truncate() .truncate()
.color(Color::Muted)
.into_any_element(), .into_any_element(),
ThreadSummary::Ready(_) => div() ThreadSummary::Ready(_) => div()
.w_full() .w_full()
@ -2002,9 +2102,33 @@ impl AgentPanel {
} }
} }
ActiveView::ExternalAgentThread { thread_view } => { ActiveView::ExternalAgentThread { thread_view } => {
Label::new(thread_view.read(cx).title(cx)) if let Some(title_editor) = thread_view.read(cx).title_editor() {
.truncate() div()
.into_any_element() .w_full()
.on_action({
let thread_view = thread_view.downgrade();
move |_: &menu::Confirm, window, cx| {
if let Some(thread_view) = thread_view.upgrade() {
thread_view.focus_handle(cx).focus(window);
}
}
})
.on_action({
let thread_view = thread_view.downgrade();
move |_: &editor::actions::Cancel, window, cx| {
if let Some(thread_view) = thread_view.upgrade() {
thread_view.focus_handle(cx).focus(window);
}
}
})
.child(title_editor)
.into_any_element()
} else {
Label::new(thread_view.read(cx).title())
.color(Color::Muted)
.truncate()
.into_any_element()
}
} }
ActiveView::TextThread { ActiveView::TextThread {
title_editor, title_editor,
@ -2015,6 +2139,7 @@ impl AgentPanel {
match summary { match summary {
ContextSummary::Pending => Label::new(ContextSummary::DEFAULT) ContextSummary::Pending => Label::new(ContextSummary::DEFAULT)
.color(Color::Muted)
.truncate() .truncate()
.into_any_element(), .into_any_element(),
ContextSummary::Content(summary) => { ContextSummary::Content(summary) => {
@ -2026,6 +2151,7 @@ impl AgentPanel {
} else { } else {
Label::new(LOADING_SUMMARY_PLACEHOLDER) Label::new(LOADING_SUMMARY_PLACEHOLDER)
.truncate() .truncate()
.color(Color::Muted)
.into_any_element() .into_any_element()
} }
} }
@ -2086,6 +2212,8 @@ impl AgentPanel {
"Enable Full Screen" "Enable Full Screen"
}; };
let selected_agent = self.selected_agent.clone();
PopoverMenu::new("agent-options-menu") PopoverMenu::new("agent-options-menu")
.trigger_with_tooltip( .trigger_with_tooltip(
IconButton::new("agent-options-menu", IconName::Ellipsis) IconButton::new("agent-options-menu", IconName::Ellipsis)
@ -2106,7 +2234,6 @@ impl AgentPanel {
.anchor(Corner::TopRight) .anchor(Corner::TopRight)
.with_handle(self.agent_panel_menu_handle.clone()) .with_handle(self.agent_panel_menu_handle.clone())
.menu({ .menu({
let focus_handle = focus_handle.clone();
move |window, cx| { move |window, cx| {
Some(ContextMenu::build(window, cx, |mut menu, _window, _| { Some(ContextMenu::build(window, cx, |mut menu, _window, _| {
menu = menu.context(focus_handle.clone()); menu = menu.context(focus_handle.clone());
@ -2166,6 +2293,11 @@ impl AgentPanel {
.action("Settings", Box::new(OpenSettings)) .action("Settings", Box::new(OpenSettings))
.separator() .separator()
.action(full_screen_label, Box::new(ToggleZoom)); .action(full_screen_label, Box::new(ToggleZoom));
if selected_agent == AgentType::Gemini {
menu = menu.action("Reauthenticate", Box::new(ReauthenticateAgent))
}
menu menu
})) }))
} }
@ -2184,7 +2316,6 @@ impl AgentPanel {
.trigger_with_tooltip( .trigger_with_tooltip(
IconButton::new("agent-nav-menu", icon).icon_size(IconSize::Small), IconButton::new("agent-nav-menu", icon).icon_size(IconSize::Small),
{ {
let focus_handle = focus_handle.clone();
move |window, cx| { move |window, cx| {
Tooltip::for_action_in( Tooltip::for_action_in(
"Toggle Recent Threads", "Toggle Recent Threads",
@ -2201,6 +2332,8 @@ impl AgentPanel {
.menu({ .menu({
let menu = self.assistant_navigation_menu.clone(); let menu = self.assistant_navigation_menu.clone();
move |window, cx| { move |window, cx| {
telemetry::event!("View Thread History Clicked");
if let Some(menu) = menu.as_ref() { if let Some(menu) = menu.as_ref() {
menu.update(cx, |_, cx| { menu.update(cx, |_, cx| {
cx.defer_in(window, |menu, window, cx| { cx.defer_in(window, |menu, window, cx| {
@ -2222,8 +2355,6 @@ impl AgentPanel {
this.go_back(&workspace::GoBack, window, cx); this.go_back(&workspace::GoBack, window, cx);
})) }))
.tooltip({ .tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| { move |window, cx| {
Tooltip::for_action_in("Go Back", &workspace::GoBack, &focus_handle, window, cx) Tooltip::for_action_in("Go Back", &workspace::GoBack, &focus_handle, window, cx)
} }
@ -2249,7 +2380,6 @@ impl AgentPanel {
.anchor(Corner::TopRight) .anchor(Corner::TopRight)
.with_handle(self.new_thread_menu_handle.clone()) .with_handle(self.new_thread_menu_handle.clone())
.menu({ .menu({
let focus_handle = focus_handle.clone();
move |window, cx| { move |window, cx| {
let active_thread = active_thread.clone(); let active_thread = active_thread.clone();
Some(ContextMenu::build(window, cx, |mut menu, _window, cx| { Some(ContextMenu::build(window, cx, |mut menu, _window, cx| {
@ -2351,8 +2481,10 @@ impl AgentPanel {
let focus_handle = self.focus_handle(cx); let focus_handle = self.focus_handle(cx);
let active_thread = match &self.active_view { let active_thread = match &self.active_view {
ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()), ActiveView::ExternalAgentThread { thread_view } => {
ActiveView::ExternalAgentThread { .. } thread_view.read(cx).as_native_thread(cx)
}
ActiveView::Thread { .. }
| ActiveView::TextThread { .. } | ActiveView::TextThread { .. }
| ActiveView::History | ActiveView::History
| ActiveView::Configuration => None, | ActiveView::Configuration => None,
@ -2377,10 +2509,11 @@ impl AgentPanel {
.anchor(Corner::TopLeft) .anchor(Corner::TopLeft)
.with_handle(self.new_thread_menu_handle.clone()) .with_handle(self.new_thread_menu_handle.clone())
.menu({ .menu({
let focus_handle = focus_handle.clone();
let workspace = self.workspace.clone(); let workspace = self.workspace.clone();
move |window, cx| { move |window, cx| {
telemetry::event!("New Thread Clicked");
let active_thread = active_thread.clone(); let active_thread = active_thread.clone();
Some(ContextMenu::build(window, cx, |mut menu, _window, cx| { Some(ContextMenu::build(window, cx, |mut menu, _window, cx| {
menu = menu menu = menu
@ -2390,15 +2523,15 @@ impl AgentPanel {
let thread = active_thread.read(cx); let thread = active_thread.read(cx);
if !thread.is_empty() { if !thread.is_empty() {
let thread_id = thread.id().clone(); let session_id = thread.id().clone();
this.item( this.item(
ContextMenuEntry::new("New From Summary") ContextMenuEntry::new("New From Summary")
.icon(IconName::ThreadFromSummary) .icon(IconName::ThreadFromSummary)
.icon_color(Color::Muted) .icon_color(Color::Muted)
.handler(move |window, cx| { .handler(move |window, cx| {
window.dispatch_action( window.dispatch_action(
Box::new(NewThread { Box::new(NewNativeAgentThreadFromSummary {
from_thread_id: Some(thread_id.clone()), from_session_id: session_id.clone(),
}), }),
cx, cx,
); );
@ -2515,13 +2648,64 @@ impl AgentPanel {
} }
}), }),
) )
})
.when(cx.has_flag::<GeminiAndNativeFeatureFlag>(), |mut menu| {
// Add custom agents from settings
let settings =
agent_servers::AllAgentServersSettings::get_global(cx);
for (agent_name, agent_settings) in &settings.custom {
menu = menu.item(
ContextMenuEntry::new(format!("New {} Thread", agent_name))
.icon(IconName::Terminal)
.icon_color(Color::Muted)
.handler({
let workspace = workspace.clone();
let agent_name = agent_name.clone();
let agent_settings = agent_settings.clone();
move |window, cx| {
if let Some(workspace) = workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
if let Some(panel) =
workspace.panel::<AgentPanel>(cx)
{
panel.update(cx, |panel, cx| {
panel.set_selected_agent(
AgentType::Custom {
name: agent_name
.clone(),
settings:
agent_settings
.clone(),
},
window,
cx,
);
});
}
});
}
}
}),
);
}
menu
})
.when(cx.has_flag::<GeminiAndNativeFeatureFlag>(), |menu| {
menu.separator().link(
"Add Your Own Agent",
OpenBrowser {
url: "https://agentclientprotocol.com/".into(),
}
.boxed_clone(),
)
}); });
menu menu
})) }))
} }
}); });
let selected_agent_label = self.selected_agent.label().into(); let selected_agent_label = self.selected_agent.label();
let selected_agent = div() let selected_agent = div()
.id("selected_agent_icon") .id("selected_agent_icon")
.when_some(self.selected_agent.icon(), |this, icon| { .when_some(self.selected_agent.icon(), |this, icon| {
@ -3015,7 +3199,7 @@ impl AgentPanel {
// TODO: Add keyboard navigation. // TODO: Add keyboard navigation.
let is_hovered = let is_hovered =
self.hovered_recent_history_item == Some(index); self.hovered_recent_history_item == Some(index);
HistoryEntryElement::new(entry.clone(), cx.entity().downgrade()) HistoryEntryElement::new(entry, cx.entity().downgrade())
.hovered(is_hovered) .hovered(is_hovered)
.on_hover(cx.listener( .on_hover(cx.listener(
move |this, is_hovered, _window, cx| { move |this, is_hovered, _window, cx| {
@ -3106,17 +3290,6 @@ impl AgentPanel {
ConfigurationError::ModelNotFound ConfigurationError::ModelNotFound
| ConfigurationError::ProviderNotAuthenticated(_) | ConfigurationError::ProviderNotAuthenticated(_)
| ConfigurationError::NoProvider => callout.into_any_element(), | ConfigurationError::NoProvider => callout.into_any_element(),
ConfigurationError::ProviderPendingTermsAcceptance(provider) => {
Banner::new()
.severity(Severity::Warning)
.child(h_flex().w_full().children(
provider.render_accept_terms(
LanguageModelProviderTosView::ThreadEmptyState,
cx,
),
))
.into_any_element()
}
} }
} }
@ -3339,7 +3512,7 @@ impl AgentPanel {
.severity(Severity::Error) .severity(Severity::Error)
.icon(IconName::XCircle) .icon(IconName::XCircle)
.title(header) .title(header)
.description(message.clone()) .description(message)
.actions_slot( .actions_slot(
h_flex() h_flex()
.gap_0p5() .gap_0p5()
@ -3359,7 +3532,7 @@ impl AgentPanel {
Callout::new() Callout::new()
.severity(Severity::Error) .severity(Severity::Error)
.title("Error") .title("Error")
.description(message.clone()) .description(message)
.actions_slot( .actions_slot(
h_flex() h_flex()
.gap_0p5() .gap_0p5()
@ -3606,6 +3779,11 @@ impl Render for AgentPanel {
} }
})) }))
.on_action(cx.listener(Self::toggle_burn_mode)) .on_action(cx.listener(Self::toggle_burn_mode))
.on_action(cx.listener(|this, _: &ReauthenticateAgent, window, cx| {
if let Some(thread_view) = this.active_thread_view() {
thread_view.update(cx, |thread_view, cx| thread_view.reauthenticate(window, cx))
}
}))
.child(self.render_toolbar(window, cx)) .child(self.render_toolbar(window, cx))
.children(self.render_onboarding(window, cx)) .children(self.render_onboarding(window, cx))
.map(|parent| match &self.active_view { .map(|parent| match &self.active_view {
@ -3823,7 +4001,11 @@ impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
// Wait to create a new context until the workspace is no longer // Wait to create a new context until the workspace is no longer
// being updated. // being updated.
cx.defer_in(window, move |panel, window, cx| { cx.defer_in(window, move |panel, window, cx| {
if let Some(message_editor) = panel.active_message_editor() { if let Some(thread_view) = panel.active_thread_view() {
thread_view.update(cx, |thread_view, cx| {
thread_view.insert_selections(window, cx);
});
} else if let Some(message_editor) = panel.active_message_editor() {
message_editor.update(cx, |message_editor, cx| { message_editor.update(cx, |message_editor, cx| {
message_editor.context_store().update(cx, |store, cx| { message_editor.context_store().update(cx, |store, cx| {
let buffer = buffer.read(cx); let buffer = buffer.read(cx);

View file

@ -28,13 +28,14 @@ use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
use agent::{Thread, ThreadId}; use agent::{Thread, ThreadId};
use agent_servers::AgentServerSettings;
use agent_settings::{AgentProfileId, AgentSettings, LanguageModelSelection}; use agent_settings::{AgentProfileId, AgentSettings, LanguageModelSelection};
use assistant_slash_command::SlashCommandRegistry; use assistant_slash_command::SlashCommandRegistry;
use client::Client; use client::Client;
use command_palette_hooks::CommandPaletteFilter; use command_palette_hooks::CommandPaletteFilter;
use feature_flags::FeatureFlagAppExt as _; use feature_flags::FeatureFlagAppExt as _;
use fs::Fs; use fs::Fs;
use gpui::{Action, App, Entity, actions}; use gpui::{Action, App, Entity, SharedString, actions};
use language::LanguageRegistry; use language::LanguageRegistry;
use language_model::{ use language_model::{
ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry,
@ -128,6 +129,12 @@ actions!(
] ]
); );
#[derive(Clone, Copy, Debug, PartialEq, Eq, Action)]
#[action(namespace = agent)]
#[action(deprecated_aliases = ["assistant::QuoteSelection"])]
/// Quotes the current selection in the agent panel's message editor.
pub struct QuoteSelection;
/// Creates a new conversation thread, optionally based on an existing thread. /// Creates a new conversation thread, optionally based on an existing thread.
#[derive(Default, Clone, PartialEq, Deserialize, JsonSchema, Action)] #[derive(Default, Clone, PartialEq, Deserialize, JsonSchema, Action)]
#[action(namespace = agent)] #[action(namespace = agent)]
@ -146,25 +153,50 @@ pub struct NewExternalAgentThread {
agent: Option<ExternalAgent>, agent: Option<ExternalAgent>,
} }
#[derive(Default, Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)] #[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
#[action(namespace = agent)]
#[serde(deny_unknown_fields)]
pub struct NewNativeAgentThreadFromSummary {
from_session_id: agent_client_protocol::SessionId,
}
// TODO unify this with AgentType
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
enum ExternalAgent { enum ExternalAgent {
#[default] #[default]
Gemini, Gemini,
ClaudeCode, ClaudeCode,
NativeAgent, NativeAgent,
Custom {
name: SharedString,
settings: AgentServerSettings,
},
} }
impl ExternalAgent { impl ExternalAgent {
fn name(&self) -> &'static str {
match self {
Self::NativeAgent => "zed",
Self::Gemini => "gemini-cli",
Self::ClaudeCode => "claude-code",
Self::Custom { .. } => "custom",
}
}
pub fn server( pub fn server(
&self, &self,
fs: Arc<dyn fs::Fs>, fs: Arc<dyn fs::Fs>,
history: Entity<agent2::HistoryStore>, history: Entity<agent2::HistoryStore>,
) -> Rc<dyn agent_servers::AgentServer> { ) -> Rc<dyn agent_servers::AgentServer> {
match self { match self {
ExternalAgent::Gemini => Rc::new(agent_servers::Gemini), Self::Gemini => Rc::new(agent_servers::Gemini),
ExternalAgent::ClaudeCode => Rc::new(agent_servers::ClaudeCode), Self::ClaudeCode => Rc::new(agent_servers::ClaudeCode),
ExternalAgent::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs, history)), Self::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs, history)),
Self::Custom { name, settings } => Rc::new(agent_servers::CustomAgentServer::new(
name.clone(),
settings,
)),
} }
} }
} }
@ -240,12 +272,7 @@ pub fn init(
client.telemetry().clone(), client.telemetry().clone(),
cx, cx,
); );
terminal_inline_assistant::init( terminal_inline_assistant::init(fs.clone(), prompt_builder, client.telemetry().clone(), cx);
fs.clone(),
prompt_builder.clone(),
client.telemetry().clone(),
cx,
);
cx.observe_new(move |workspace, window, cx| { cx.observe_new(move |workspace, window, cx| {
ConfigureContextServerModal::register(workspace, language_registry.clone(), window, cx) ConfigureContextServerModal::register(workspace, language_registry.clone(), window, cx)
}) })
@ -391,7 +418,6 @@ fn register_slash_commands(cx: &mut App) {
slash_command_registry.register_command(assistant_slash_commands::FetchSlashCommand, true); slash_command_registry.register_command(assistant_slash_commands::FetchSlashCommand, true);
cx.observe_flag::<assistant_slash_commands::StreamingExampleSlashCommandFeatureFlag, _>({ cx.observe_flag::<assistant_slash_commands::StreamingExampleSlashCommandFeatureFlag, _>({
let slash_command_registry = slash_command_registry.clone();
move |is_enabled, _cx| { move |is_enabled, _cx| {
if is_enabled { if is_enabled {
slash_command_registry.register_command( slash_command_registry.register_command(

View file

@ -1129,7 +1129,7 @@ mod tests {
) )
}); });
let chunks_tx = simulate_response_stream(codegen.clone(), cx); let chunks_tx = simulate_response_stream(&codegen, cx);
let mut new_text = concat!( let mut new_text = concat!(
" let mut x = 0;\n", " let mut x = 0;\n",
@ -1196,7 +1196,7 @@ mod tests {
) )
}); });
let chunks_tx = simulate_response_stream(codegen.clone(), cx); let chunks_tx = simulate_response_stream(&codegen, cx);
cx.background_executor.run_until_parked(); cx.background_executor.run_until_parked();
@ -1265,7 +1265,7 @@ mod tests {
) )
}); });
let chunks_tx = simulate_response_stream(codegen.clone(), cx); let chunks_tx = simulate_response_stream(&codegen, cx);
cx.background_executor.run_until_parked(); cx.background_executor.run_until_parked();
@ -1334,7 +1334,7 @@ mod tests {
) )
}); });
let chunks_tx = simulate_response_stream(codegen.clone(), cx); let chunks_tx = simulate_response_stream(&codegen, cx);
let new_text = concat!( let new_text = concat!(
"func main() {\n", "func main() {\n",
"\tx := 0\n", "\tx := 0\n",
@ -1391,7 +1391,7 @@ mod tests {
) )
}); });
let chunks_tx = simulate_response_stream(codegen.clone(), cx); let chunks_tx = simulate_response_stream(&codegen, cx);
chunks_tx chunks_tx
.unbounded_send("let mut x = 0;\nx += 1;".to_string()) .unbounded_send("let mut x = 0;\nx += 1;".to_string())
.unwrap(); .unwrap();
@ -1473,7 +1473,7 @@ mod tests {
} }
fn simulate_response_stream( fn simulate_response_stream(
codegen: Entity<CodegenAlternative>, codegen: &Entity<CodegenAlternative>,
cx: &mut TestAppContext, cx: &mut TestAppContext,
) -> mpsc::UnboundedSender<String> { ) -> mpsc::UnboundedSender<String> {
let (chunks_tx, chunks_rx) = mpsc::unbounded(); let (chunks_tx, chunks_rx) = mpsc::unbounded();

View file

@ -818,13 +818,8 @@ pub fn crease_for_mention(
let render_trailer = move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any(); let render_trailer = move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any();
Crease::inline( Crease::inline(range, placeholder, fold_toggle("mention"), render_trailer)
range, .with_metadata(CreaseMetadata { icon_path, label })
placeholder.clone(),
fold_toggle("mention"),
render_trailer,
)
.with_metadata(CreaseMetadata { icon_path, label })
} }
fn render_fold_icon_button( fn render_fold_icon_button(

View file

@ -79,8 +79,7 @@ fn search(
) -> Task<Vec<Match>> { ) -> Task<Vec<Match>> {
match mode { match mode {
Some(ContextPickerMode::File) => { Some(ContextPickerMode::File) => {
let search_files_task = let search_files_task = search_files(query, cancellation_flag, &workspace, cx);
search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
cx.background_spawn(async move { cx.background_spawn(async move {
search_files_task search_files_task
.await .await
@ -91,8 +90,7 @@ fn search(
} }
Some(ContextPickerMode::Symbol) => { Some(ContextPickerMode::Symbol) => {
let search_symbols_task = let search_symbols_task = search_symbols(query, cancellation_flag, &workspace, cx);
search_symbols(query.clone(), cancellation_flag.clone(), &workspace, cx);
cx.background_spawn(async move { cx.background_spawn(async move {
search_symbols_task search_symbols_task
.await .await
@ -108,13 +106,8 @@ fn search(
.and_then(|t| t.upgrade()) .and_then(|t| t.upgrade())
.zip(text_thread_context_store.as_ref().and_then(|t| t.upgrade())) .zip(text_thread_context_store.as_ref().and_then(|t| t.upgrade()))
{ {
let search_threads_task = search_threads( let search_threads_task =
query.clone(), search_threads(query, cancellation_flag, thread_store, context_store, cx);
cancellation_flag.clone(),
thread_store,
context_store,
cx,
);
cx.background_spawn(async move { cx.background_spawn(async move {
search_threads_task search_threads_task
.await .await
@ -137,8 +130,7 @@ fn search(
Some(ContextPickerMode::Rules) => { Some(ContextPickerMode::Rules) => {
if let Some(prompt_store) = prompt_store.as_ref() { if let Some(prompt_store) = prompt_store.as_ref() {
let search_rules_task = let search_rules_task = search_rules(query, cancellation_flag, prompt_store, cx);
search_rules(query.clone(), cancellation_flag.clone(), prompt_store, cx);
cx.background_spawn(async move { cx.background_spawn(async move {
search_rules_task search_rules_task
.await .await
@ -196,7 +188,7 @@ fn search(
let executor = cx.background_executor().clone(); let executor = cx.background_executor().clone();
let search_files_task = let search_files_task =
search_files(query.clone(), cancellation_flag.clone(), &workspace, cx); search_files(query.clone(), cancellation_flag, &workspace, cx);
let entries = let entries =
available_context_picker_entries(&prompt_store, &thread_store, &workspace, cx); available_context_picker_entries(&prompt_store, &thread_store, &workspace, cx);
@ -283,7 +275,7 @@ impl ContextPickerCompletionProvider {
) -> Option<Completion> { ) -> Option<Completion> {
match entry { match entry {
ContextPickerEntry::Mode(mode) => Some(Completion { ContextPickerEntry::Mode(mode) => Some(Completion {
replace_range: source_range.clone(), replace_range: source_range,
new_text: format!("@{} ", mode.keyword()), new_text: format!("@{} ", mode.keyword()),
label: CodeLabel::plain(mode.label().to_string(), None), label: CodeLabel::plain(mode.label().to_string(), None),
icon_path: Some(mode.icon().path().into()), icon_path: Some(mode.icon().path().into()),
@ -330,9 +322,6 @@ impl ContextPickerCompletionProvider {
); );
let callback = Arc::new({ let callback = Arc::new({
let context_store = context_store.clone();
let selections = selections.clone();
let selection_infos = selection_infos.clone();
move |_, window: &mut Window, cx: &mut App| { move |_, window: &mut Window, cx: &mut App| {
context_store.update(cx, |context_store, cx| { context_store.update(cx, |context_store, cx| {
for (buffer, range) in &selections { for (buffer, range) in &selections {
@ -441,7 +430,7 @@ impl ContextPickerCompletionProvider {
excerpt_id, excerpt_id,
source_range.start, source_range.start,
new_text_len - 1, new_text_len - 1,
editor.clone(), editor,
context_store.clone(), context_store.clone(),
move |window, cx| match &thread_entry { move |window, cx| match &thread_entry {
ThreadContextEntry::Thread { id, .. } => { ThreadContextEntry::Thread { id, .. } => {
@ -510,7 +499,7 @@ impl ContextPickerCompletionProvider {
excerpt_id, excerpt_id,
source_range.start, source_range.start,
new_text_len - 1, new_text_len - 1,
editor.clone(), editor,
context_store.clone(), context_store.clone(),
move |_, cx| { move |_, cx| {
let user_prompt_id = rules.prompt_id; let user_prompt_id = rules.prompt_id;
@ -547,7 +536,7 @@ impl ContextPickerCompletionProvider {
excerpt_id, excerpt_id,
source_range.start, source_range.start,
new_text_len - 1, new_text_len - 1,
editor.clone(), editor,
context_store.clone(), context_store.clone(),
move |_, cx| { move |_, cx| {
let context_store = context_store.clone(); let context_store = context_store.clone();
@ -704,16 +693,16 @@ impl ContextPickerCompletionProvider {
excerpt_id, excerpt_id,
source_range.start, source_range.start,
new_text_len - 1, new_text_len - 1,
editor.clone(), editor,
context_store.clone(), context_store.clone(),
move |_, cx| { move |_, cx| {
let symbol = symbol.clone(); let symbol = symbol.clone();
let context_store = context_store.clone(); let context_store = context_store.clone();
let workspace = workspace.clone(); let workspace = workspace.clone();
let result = super::symbol_context_picker::add_symbol( let result = super::symbol_context_picker::add_symbol(
symbol.clone(), symbol,
false, false,
workspace.clone(), workspace,
context_store.downgrade(), context_store.downgrade(),
cx, cx,
); );
@ -1162,7 +1151,7 @@ mod tests {
impl Focusable for AtMentionEditor { impl Focusable for AtMentionEditor {
fn focus_handle(&self, cx: &App) -> FocusHandle { fn focus_handle(&self, cx: &App) -> FocusHandle {
self.0.read(cx).focus_handle(cx).clone() self.0.read(cx).focus_handle(cx)
} }
} }
@ -1480,7 +1469,7 @@ mod tests {
let completions = editor.current_completions().expect("Missing completions"); let completions = editor.current_completions().expect("Missing completions");
completions completions
.into_iter() .into_iter()
.map(|completion| completion.label.text.to_string()) .map(|completion| completion.label.text)
.collect::<Vec<_>>() .collect::<Vec<_>>()
} }

View file

@ -1532,13 +1532,11 @@ impl InlineAssistant {
.and_then(|item| item.act_as::<Editor>(cx)) .and_then(|item| item.act_as::<Editor>(cx))
{ {
Some(InlineAssistTarget::Editor(workspace_editor)) Some(InlineAssistTarget::Editor(workspace_editor))
} else if let Some(terminal_view) = workspace
.active_item(cx)
.and_then(|item| item.act_as::<TerminalView>(cx))
{
Some(InlineAssistTarget::Terminal(terminal_view))
} else { } else {
None workspace
.active_item(cx)
.and_then(|item| item.act_as::<TerminalView>(cx))
.map(InlineAssistTarget::Terminal)
} }
} }
} }
@ -1693,7 +1691,7 @@ impl InlineAssist {
}), }),
range, range,
codegen: codegen.clone(), codegen: codegen.clone(),
workspace: workspace.clone(), workspace,
_subscriptions: vec![ _subscriptions: vec![
window.on_focus_in(&prompt_editor_focus_handle, cx, move |_, cx| { window.on_focus_in(&prompt_editor_focus_handle, cx, move |_, cx| {
InlineAssistant::update_global(cx, |this, cx| { InlineAssistant::update_global(cx, |this, cx| {

View file

@ -1229,27 +1229,27 @@ pub enum GenerationMode {
impl GenerationMode { impl GenerationMode {
fn start_label(self) -> &'static str { fn start_label(self) -> &'static str {
match self { match self {
GenerationMode::Generate { .. } => "Generate", GenerationMode::Generate => "Generate",
GenerationMode::Transform => "Transform", GenerationMode::Transform => "Transform",
} }
} }
fn tooltip_interrupt(self) -> &'static str { fn tooltip_interrupt(self) -> &'static str {
match self { match self {
GenerationMode::Generate { .. } => "Interrupt Generation", GenerationMode::Generate => "Interrupt Generation",
GenerationMode::Transform => "Interrupt Transform", GenerationMode::Transform => "Interrupt Transform",
} }
} }
fn tooltip_restart(self) -> &'static str { fn tooltip_restart(self) -> &'static str {
match self { match self {
GenerationMode::Generate { .. } => "Restart Generation", GenerationMode::Generate => "Restart Generation",
GenerationMode::Transform => "Restart Transform", GenerationMode::Transform => "Restart Transform",
} }
} }
fn tooltip_accept(self) -> &'static str { fn tooltip_accept(self) -> &'static str {
match self { match self {
GenerationMode::Generate { .. } => "Accept Generation", GenerationMode::Generate => "Accept Generation",
GenerationMode::Transform => "Accept Transform", GenerationMode::Transform => "Accept Transform",
} }
} }

View file

@ -6,8 +6,7 @@ use feature_flags::ZedProFeatureFlag;
use fuzzy::{StringMatch, StringMatchCandidate, match_strings}; use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
use gpui::{Action, AnyElement, App, BackgroundExecutor, DismissEvent, Subscription, Task}; use gpui::{Action, AnyElement, App, BackgroundExecutor, DismissEvent, Subscription, Task};
use language_model::{ use language_model::{
AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelProviderId, ConfiguredModel, LanguageModel, LanguageModelProviderId, LanguageModelRegistry,
LanguageModelRegistry,
}; };
use ordered_float::OrderedFloat; use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate}; use picker::{Picker, PickerDelegate};
@ -77,7 +76,6 @@ pub struct LanguageModelPickerDelegate {
all_models: Arc<GroupedModels>, all_models: Arc<GroupedModels>,
filtered_entries: Vec<LanguageModelPickerEntry>, filtered_entries: Vec<LanguageModelPickerEntry>,
selected_index: usize, selected_index: usize,
_authenticate_all_providers_task: Task<()>,
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
} }
@ -93,12 +91,11 @@ impl LanguageModelPickerDelegate {
let entries = models.entries(); let entries = models.entries();
Self { Self {
on_model_changed: on_model_changed.clone(), on_model_changed,
all_models: Arc::new(models), all_models: Arc::new(models),
selected_index: Self::get_active_model_index(&entries, get_active_model(cx)), selected_index: Self::get_active_model_index(&entries, get_active_model(cx)),
filtered_entries: entries, filtered_entries: entries,
get_active_model: Arc::new(get_active_model), get_active_model: Arc::new(get_active_model),
_authenticate_all_providers_task: Self::authenticate_all_providers(cx),
_subscriptions: vec![cx.subscribe_in( _subscriptions: vec![cx.subscribe_in(
&LanguageModelRegistry::global(cx), &LanguageModelRegistry::global(cx),
window, window,
@ -142,56 +139,6 @@ impl LanguageModelPickerDelegate {
.unwrap_or(0) .unwrap_or(0)
} }
/// Authenticates all providers in the [`LanguageModelRegistry`].
///
/// We do this so that we can populate the language selector with all of the
/// models from the configured providers.
fn authenticate_all_providers(cx: &mut App) -> Task<()> {
let authenticate_all_providers = LanguageModelRegistry::global(cx)
.read(cx)
.providers()
.iter()
.map(|provider| (provider.id(), provider.name(), provider.authenticate(cx)))
.collect::<Vec<_>>();
cx.spawn(async move |_cx| {
for (provider_id, provider_name, authenticate_task) in authenticate_all_providers {
if let Err(err) = authenticate_task.await {
if matches!(err, AuthenticateError::CredentialsNotFound) {
// Since we're authenticating these providers in the
// background for the purposes of populating the
// language selector, we don't care about providers
// where the credentials are not found.
} else {
// Some providers have noisy failure states that we
// don't want to spam the logs with every time the
// language model selector is initialized.
//
// Ideally these should have more clear failure modes
// that we know are safe to ignore here, like what we do
// with `CredentialsNotFound` above.
match provider_id.0.as_ref() {
"lmstudio" | "ollama" => {
// LM Studio and Ollama both make fetch requests to the local APIs to determine if they are "authenticated".
//
// These fail noisily, so we don't log them.
}
"copilot_chat" => {
// Copilot Chat returns an error if Copilot is not enabled, so we don't log those errors.
}
_ => {
log::error!(
"Failed to authenticate provider: {}: {err}",
provider_name.0
);
}
}
}
}
}
})
}
pub fn active_model(&self, cx: &App) -> Option<ConfiguredModel> { pub fn active_model(&self, cx: &App) -> Option<ConfiguredModel> {
(self.get_active_model)(cx) (self.get_active_model)(cx)
} }
@ -514,7 +461,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
.pl_0p5() .pl_0p5()
.gap_1p5() .gap_1p5()
.w(px(240.)) .w(px(240.))
.child(Label::new(model_info.model.name().0.clone()).truncate()), .child(Label::new(model_info.model.name().0).truncate()),
) )
.end_slot(div().pr_3().when(is_selected, |this| { .end_slot(div().pr_3().when(is_selected, |this| {
this.child( this.child(

View file

@ -248,7 +248,7 @@ impl MessageEditor {
editor: editor.clone(), editor: editor.clone(),
project: thread.read(cx).project().clone(), project: thread.read(cx).project().clone(),
thread, thread,
incompatible_tools_state: incompatible_tools.clone(), incompatible_tools_state: incompatible_tools,
workspace, workspace,
context_store, context_store,
prompt_store, prompt_store,
@ -378,18 +378,13 @@ impl MessageEditor {
} }
fn send_to_model(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn send_to_model(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(ConfiguredModel { model, provider }) = self let Some(ConfiguredModel { model, .. }) = self
.thread .thread
.update(cx, |thread, cx| thread.get_or_init_configured_model(cx)) .update(cx, |thread, cx| thread.get_or_init_configured_model(cx))
else { else {
return; return;
}; };
if provider.must_accept_terms(cx) {
cx.notify();
return;
}
let (user_message, user_message_creases) = self.editor.update(cx, |editor, cx| { let (user_message, user_message_creases) = self.editor.update(cx, |editor, cx| {
let creases = extract_message_creases(editor, cx); let creases = extract_message_creases(editor, cx);
let text = editor.text(cx); let text = editor.text(cx);
@ -839,7 +834,6 @@ impl MessageEditor {
.child(self.profile_selector.clone()) .child(self.profile_selector.clone())
.child(self.model_selector.clone()) .child(self.model_selector.clone())
.map({ .map({
let focus_handle = focus_handle.clone();
move |parent| { move |parent| {
if is_generating { if is_generating {
parent parent
@ -1683,7 +1677,7 @@ impl Render for MessageEditor {
let has_history = self let has_history = self
.history_store .history_store
.as_ref() .as_ref()
.and_then(|hs| hs.update(cx, |hs, cx| hs.entries(cx).len() > 0).ok()) .and_then(|hs| hs.update(cx, |hs, cx| !hs.entries(cx).is_empty()).ok())
.unwrap_or(false) .unwrap_or(false)
|| self || self
.thread .thread
@ -1696,7 +1690,7 @@ impl Render for MessageEditor {
!has_history && is_signed_out && has_configured_providers, !has_history && is_signed_out && has_configured_providers,
|this| this.child(cx.new(ApiKeysWithProviders::new)), |this| this.child(cx.new(ApiKeysWithProviders::new)),
) )
.when(changed_buffers.len() > 0, |parent| { .when(!changed_buffers.is_empty(), |parent| {
parent.child(self.render_edits_bar(&changed_buffers, window, cx)) parent.child(self.render_edits_bar(&changed_buffers, window, cx))
}) })
.child(self.render_editor(window, cx)) .child(self.render_editor(window, cx))
@ -1801,7 +1795,7 @@ impl AgentPreview for MessageEditor {
.bg(cx.theme().colors().panel_background) .bg(cx.theme().colors().panel_background)
.border_1() .border_1()
.border_color(cx.theme().colors().border) .border_color(cx.theme().colors().border)
.child(default_message_editor.clone()) .child(default_message_editor)
.into_any_element(), .into_any_element(),
)]) )])
.into_any_element(), .into_any_element(),

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