diff --git a/.github/actionlint.yml b/.github/actionlint.yml
index 0ee6af8a1d..6d8e0107e9 100644
--- a/.github/actionlint.yml
+++ b/.github/actionlint.yml
@@ -19,14 +19,27 @@ self-hosted-runner:
- namespace-profile-16x32-ubuntu-2004-arm
- namespace-profile-32x64-ubuntu-2004-arm
# Namespace Ubuntu 22.04 (Everything else)
- - namespace-profile-2x4-ubuntu-2204
- namespace-profile-4x8-ubuntu-2204
- namespace-profile-8x16-ubuntu-2204
- namespace-profile-16x32-ubuntu-2204
- namespace-profile-32x64-ubuntu-2204
+ # Namespace Ubuntu 24.04 (like ubuntu-latest)
+ - namespace-profile-2x4-ubuntu-2404
# Namespace Limited Preview
- namespace-profile-8x16-ubuntu-2004-arm-m4
- namespace-profile-8x32-ubuntu-2004-arm-m4
# Self Hosted Runners
- self-mini-macos
- self-32vcpu-windows-2022
+
+# Disable shellcheck because it doesn't like powershell
+# This should have been triggered with initial rollout of actionlint
+# but https://github.com/zed-industries/zed/pull/36693
+# somehow caused actionlint to actually check those windows jobs
+# where previously they were being skipped. Likely caused by an
+# unknown bug in actionlint where parsing of `runs-on: [ ]`
+# breaks something else. (yuck)
+paths:
+ .github/workflows/{ci,release_nightly}.yml:
+ ignore:
+ - "shellcheck"
diff --git a/.github/workflows/bump_collab_staging.yml b/.github/workflows/bump_collab_staging.yml
index d8eaa6019e..d400905b4d 100644
--- a/.github/workflows/bump_collab_staging.yml
+++ b/.github/workflows/bump_collab_staging.yml
@@ -8,7 +8,7 @@ on:
jobs:
update-collab-staging-tag:
if: github.repository_owner == 'zed-industries'
- runs-on: ubuntu-latest
+ runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f4ba227168..a34833d0fd 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -37,7 +37,7 @@ jobs:
run_nix: ${{ steps.filter.outputs.run_nix }}
run_actionlint: ${{ steps.filter.outputs.run_actionlint }}
runs-on:
- - ubuntu-latest
+ - namespace-profile-2x4-ubuntu-2404
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@@ -237,7 +237,7 @@ jobs:
uses: ./.github/actions/build_docs
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'
needs: [job_spec]
steps:
@@ -418,7 +418,7 @@ jobs:
if: |
github.repository_owner == 'zed-industries' &&
needs.job_spec.outputs.run_tests == 'true'
- runs-on: [self-hosted, Windows, X64]
+ runs-on: [self-32vcpu-windows-2022]
steps:
- name: Environment Setup
run: |
@@ -458,7 +458,7 @@ jobs:
tests_pass:
name: Tests Pass
- runs-on: ubuntu-latest
+ runs-on: namespace-profile-2x4-ubuntu-2404
needs:
- job_spec
- style
@@ -784,7 +784,7 @@ jobs:
bundle-windows-x64:
timeout-minutes: 120
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: (startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling'))
needs: [windows_tests]
diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml
index 15c82643ae..3f84179278 100644
--- a/.github/workflows/danger.yml
+++ b/.github/workflows/danger.yml
@@ -12,7 +12,7 @@ on:
jobs:
danger:
if: github.repository_owner == 'zed-industries'
- runs-on: ubuntu-latest
+ runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml
index 0cc6737a45..2026ee7b73 100644
--- a/.github/workflows/release_nightly.yml
+++ b/.github/workflows/release_nightly.yml
@@ -59,7 +59,7 @@ jobs:
timeout-minutes: 60
name: Run tests on Windows
if: github.repository_owner == 'zed-industries'
- runs-on: [self-hosted, Windows, X64]
+ runs-on: [self-32vcpu-windows-2022]
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@@ -206,9 +206,6 @@ jobs:
runs-on: github-8vcpu-ubuntu-2404
needs: tests
name: Build Zed on FreeBSD
- # env:
- # MYTOKEN : ${{ secrets.MYTOKEN }}
- # MYTOKEN2: "value2"
steps:
- uses: actions/checkout@v4
- name: Build FreeBSD remote-server
@@ -243,7 +240,6 @@ jobs:
bundle-nix:
name: Build and cache Nix package
- if: false
needs: tests
secrets: inherit
uses: ./.github/workflows/nix.yml
@@ -252,7 +248,7 @@ jobs:
timeout-minutes: 60
name: Create a Windows installer
if: github.repository_owner == 'zed-industries'
- runs-on: [self-hosted, Windows, X64]
+ runs-on: [self-32vcpu-windows-2022]
needs: windows-tests
env:
AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }}
@@ -294,7 +290,7 @@ jobs:
update-nightly-tag:
name: Update nightly tag
if: github.repository_owner == 'zed-industries'
- runs-on: ubuntu-latest
+ runs-on: namespace-profile-2x4-ubuntu-2404
needs:
- bundle-mac
- bundle-linux-x86
diff --git a/.github/workflows/script_checks.yml b/.github/workflows/script_checks.yml
index c32a433e46..5dbfc9cb7f 100644
--- a/.github/workflows/script_checks.yml
+++ b/.github/workflows/script_checks.yml
@@ -12,7 +12,7 @@ jobs:
shellcheck:
name: "ShellCheck Scripts"
if: github.repository_owner == 'zed-industries'
- runs-on: ubuntu-latest
+ runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
diff --git a/Cargo.lock b/Cargo.lock
index 6dbbccedaa..06d418a750 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -39,6 +39,26 @@ dependencies = [
"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]]
name = "action_log"
version = "0.1.0"
@@ -171,11 +191,12 @@ dependencies = [
[[package]]
name = "agent-client-protocol"
-version = "0.0.26"
+version = "0.0.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "160971bb53ca0b2e70ebc857c21e24eb448745f1396371015f4c59e9a9e51ed0"
+checksum = "289eb34ee17213dadcca47eedadd386a5e7678094095414e475965d1bcca2860"
dependencies = [
"anyhow",
+ "async-broadcast",
"futures 0.3.31",
"log",
"parking_lot",
@@ -239,10 +260,12 @@ dependencies = [
"smol",
"sqlez",
"task",
+ "telemetry",
"tempfile",
"terminal",
"text",
"theme",
+ "thiserror 2.0.12",
"tree-sitter-rust",
"ui",
"unindent",
@@ -262,16 +285,19 @@ name = "agent_servers"
version = "0.1.0"
dependencies = [
"acp_thread",
+ "acp_tools",
"action_log",
"agent-client-protocol",
"agent_settings",
- "agentic-coding-protocol",
"anyhow",
+ "client",
"collections",
"context_server",
"env_logger 0.11.8",
+ "fs",
"futures 0.3.31",
"gpui",
+ "gpui_tokio",
"indoc",
"itertools 0.14.0",
"language",
@@ -283,6 +309,7 @@ dependencies = [
"paths",
"project",
"rand 0.8.5",
+ "reqwest_client",
"schemars",
"semver",
"serde",
@@ -376,6 +403,7 @@ dependencies = [
"parking_lot",
"paths",
"picker",
+ "postage",
"pretty_assertions",
"project",
"prompt_store",
@@ -415,24 +443,6 @@ dependencies = [
"zed_actions",
]
-[[package]]
-name = "agentic-coding-protocol"
-version = "0.0.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a3e6ae951b36fa2f8d9dd6e1af6da2fcaba13d7c866cf6a9e65deda9dc6c5fe4"
-dependencies = [
- "anyhow",
- "chrono",
- "derive_more 2.0.1",
- "futures 0.3.31",
- "log",
- "parking_lot",
- "schemars",
- "semver",
- "serde",
- "serde_json",
-]
-
[[package]]
name = "ahash"
version = "0.7.8"
@@ -848,7 +858,7 @@ dependencies = [
"anyhow",
"async-trait",
"collections",
- "derive_more 0.99.19",
+ "derive_more",
"extension",
"futures 0.3.31",
"gpui",
@@ -911,7 +921,7 @@ dependencies = [
"clock",
"collections",
"ctor",
- "derive_more 0.99.19",
+ "derive_more",
"gpui",
"icons",
"indoc",
@@ -948,7 +958,7 @@ dependencies = [
"cloud_llm_client",
"collections",
"component",
- "derive_more 0.99.19",
+ "derive_more",
"diffy",
"editor",
"feature_flags",
@@ -1374,10 +1384,11 @@ version = "0.1.0"
dependencies = [
"anyhow",
"collections",
- "derive_more 0.99.19",
"gpui",
- "parking_lot",
"rodio",
+ "schemars",
+ "serde",
+ "settings",
"util",
"workspace-hack",
]
@@ -3060,7 +3071,7 @@ dependencies = [
"cocoa 0.26.0",
"collections",
"credentials_provider",
- "derive_more 0.99.19",
+ "derive_more",
"feature_flags",
"fs",
"futures 0.3.31",
@@ -3492,7 +3503,7 @@ name = "command_palette_hooks"
version = "0.1.0"
dependencies = [
"collections",
- "derive_more 0.99.19",
+ "derive_more",
"gpui",
"workspace-hack",
]
@@ -4043,6 +4054,7 @@ dependencies = [
name = "crashes"
version = "0.1.0"
dependencies = [
+ "bincode",
"crash-handler",
"log",
"mach2 0.5.0",
@@ -4052,6 +4064,7 @@ dependencies = [
"serde",
"serde_json",
"smol",
+ "system_specs",
"workspace-hack",
]
@@ -4653,27 +4666,6 @@ dependencies = [
"syn 2.0.101",
]
-[[package]]
-name = "derive_more"
-version = "2.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
-dependencies = [
- "derive_more-impl",
-]
-
-[[package]]
-name = "derive_more-impl"
-version = "2.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.101",
- "unicode-xid",
-]
-
[[package]]
name = "derive_refineable"
version = "0.1.0"
@@ -4694,7 +4686,6 @@ dependencies = [
"component",
"ctor",
"editor",
- "futures 0.3.31",
"gpui",
"indoc",
"language",
@@ -5731,14 +5722,10 @@ dependencies = [
name = "feedback"
version = "0.1.0"
dependencies = [
- "client",
"editor",
"gpui",
- "human_bytes",
"menu",
- "release_channel",
- "serde",
- "sysinfo",
+ "system_specs",
"ui",
"urlencoding",
"util",
@@ -6414,7 +6401,7 @@ dependencies = [
"askpass",
"async-trait",
"collections",
- "derive_more 0.99.19",
+ "derive_more",
"futures 0.3.31",
"git2",
"gpui",
@@ -7444,7 +7431,7 @@ dependencies = [
"core-video",
"cosmic-text",
"ctor",
- "derive_more 0.99.19",
+ "derive_more",
"embed-resource",
"env_logger 0.11.8",
"etagere",
@@ -7532,6 +7519,7 @@ dependencies = [
name = "gpui_tokio"
version = "0.1.0"
dependencies = [
+ "anyhow",
"gpui",
"tokio",
"util",
@@ -7968,7 +7956,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"bytes 1.10.1",
- "derive_more 0.99.19",
+ "derive_more",
"futures 0.3.31",
"http 1.3.1",
"http-body 1.0.1",
@@ -8480,6 +8468,7 @@ dependencies = [
"theme",
"ui",
"util",
+ "util_macros",
"workspace",
"workspace-hack",
"zed_actions",
@@ -9616,6 +9605,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
+ "audio",
"collections",
"core-foundation 0.10.0",
"core-video",
@@ -9638,6 +9628,7 @@ dependencies = [
"scap",
"serde",
"serde_json",
+ "settings",
"sha2",
"simplelog",
"smallvec",
@@ -11624,6 +11615,12 @@ dependencies = [
"hmac",
]
+[[package]]
+name = "pciid-parser"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0008e816fcdaf229cdd540e9b6ca2dc4a10d65c31624abb546c6420a02846e61"
+
[[package]]
name = "pem"
version = "3.0.5"
@@ -13524,6 +13521,7 @@ dependencies = [
"smol",
"sysinfo",
"telemetry_events",
+ "thiserror 2.0.12",
"toml 0.8.20",
"unindent",
"util",
@@ -14363,12 +14361,10 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe8c9d1c68d67dd9f97ecbc6f932b60eb289c5dbddd8aa1405484a8fd2fcd984"
dependencies = [
- "chrono",
"dyn-clone",
"indexmap",
"ref-cast",
"schemars_derive",
- "semver",
"serde",
"serde_json",
]
@@ -16144,6 +16140,21 @@ dependencies = [
"winx",
]
+[[package]]
+name = "system_specs"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "client",
+ "gpui",
+ "human_bytes",
+ "pciid-parser",
+ "release_channel",
+ "serde",
+ "sysinfo",
+ "workspace-hack",
+]
+
[[package]]
name = "tab_switcher"
version = "0.1.0"
@@ -16437,7 +16448,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"collections",
- "derive_more 0.99.19",
+ "derive_more",
"fs",
"futures 0.3.31",
"gpui",
@@ -19779,7 +19790,6 @@ dependencies = [
"any_vec",
"anyhow",
"async-recursion",
- "bincode",
"call",
"client",
"clock",
@@ -19798,6 +19808,7 @@ dependencies = [
"node_runtime",
"parking_lot",
"postage",
+ "pretty_assertions",
"project",
"remote",
"schemars",
@@ -19953,7 +19964,6 @@ dependencies = [
"rustix 1.0.7",
"rustls 0.23.26",
"rustls-webpki 0.103.1",
- "schemars",
"scopeguard",
"sea-orm",
"sea-query-binder",
@@ -20387,8 +20397,9 @@ dependencies = [
[[package]]
name = "zed"
-version = "0.201.0"
+version = "0.202.0"
dependencies = [
+ "acp_tools",
"activity_indicator",
"agent",
"agent_servers",
@@ -20404,6 +20415,7 @@ dependencies = [
"auto_update",
"auto_update_ui",
"backtrace",
+ "bincode",
"breadcrumbs",
"call",
"channel",
@@ -20502,6 +20514,7 @@ dependencies = [
"supermaven",
"svg_preview",
"sysinfo",
+ "system_specs",
"tab_switcher",
"task",
"tasks_ui",
diff --git a/Cargo.toml b/Cargo.toml
index dc14c8ebd9..6ec243a9b9 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,7 @@
[workspace]
resolver = "2"
members = [
+ "crates/acp_tools",
"crates/acp_thread",
"crates/action_log",
"crates/activity_indicator",
@@ -155,6 +156,7 @@ members = [
"crates/streaming_diff",
"crates/sum_tree",
"crates/supermaven",
+ "crates/system_specs",
"crates/supermaven_api",
"crates/svg_preview",
"crates/tab_switcher",
@@ -226,6 +228,7 @@ edition = "2024"
# Workspace member crates
#
+acp_tools = { path = "crates/acp_tools" }
acp_thread = { path = "crates/acp_thread" }
action_log = { path = "crates/action_log" }
agent = { path = "crates/agent" }
@@ -381,6 +384,7 @@ streaming_diff = { path = "crates/streaming_diff" }
sum_tree = { path = "crates/sum_tree" }
supermaven = { path = "crates/supermaven" }
supermaven_api = { path = "crates/supermaven_api" }
+system_specs = { path = "crates/system_specs" }
tab_switcher = { path = "crates/tab_switcher" }
task = { path = "crates/task" }
tasks_ui = { path = "crates/tasks_ui" }
@@ -422,8 +426,7 @@ zlog_settings = { path = "crates/zlog_settings" }
# External crates
#
-agentic-coding-protocol = "0.0.10"
-agent-client-protocol = "0.0.26"
+agent-client-protocol = "0.0.31"
aho-corasick = "1.1"
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
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-types = { version = "1.3.0", features = ["http-body-1-x"] }
base64 = "0.22"
+bincode = "1.2.1"
bitflags = "2.6.0"
blade-graphics = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
@@ -493,6 +497,7 @@ handlebars = "4.3"
heck = "0.5"
heed = { version = "0.21.0", features = ["read-txn-no-tls"] }
hex = "0.4.3"
+human_bytes = "0.4.1"
html5ever = "0.27.0"
http = "1.1"
http-body = "1.0"
@@ -532,6 +537,7 @@ palette = { version = "0.7.5", default-features = false, features = ["std"] }
parking_lot = "0.12.1"
partial-json-fixer = "0.5.3"
parse_int = "0.9"
+pciid-parser = "0.8.0"
pathdiff = "0.2"
pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
@@ -802,60 +808,32 @@ unexpected_cfgs = { level = "allow" }
dbg_macro = "deny"
todo = "deny"
-# 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"
+# This is not a style lint, see https://github.com/rust-lang/rust-clippy/pull/15454
+# Remove when the lint gets promoted to `suspicious`.
+declare_interior_mutable_const = "deny"
-# These are all of the rules that currently have violations in the Zed
-# codebase.
-#
-# We'll want to drive this list down by either:
-# 1. fixing violations of the rule and begin enforcing it
-# 2. deciding we want to allow the rule permanently, at which point
-# we should codify that separately above.
-#
-# This list shouldn't be added to; it should only get shorter.
-# =============================================================================
+redundant_clone = "deny"
-# There are a bunch of rules currently failing in the `style` group, so
-# allow all of those, for now.
+# We currently do not restrict any style rules
+# as it slows down shipping code to Zed.
+#
+# Running ./script/clippy can take several minutes, and so it's
+# common to skip that step and let CI do it. Any unexpected failures
+# (which also take minutes to discover) thus require switching back
+# to an old branch, manual fixing, and re-pushing.
+#
+# In the future we could improve this by either making sure
+# Zed can surface clippy errors in diagnostics (in addition to the
+# rust-analyzer errors), or by having CI fix style nits automatically.
style = { level = "allow", priority = -1 }
-# 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:
type_complexity = "allow"
-# We often return trait objects from `new` functions.
-new_ret_no_self = { level = "allow" }
-# We have a few `next` functions that differ in lifetimes
-# compared to Iterator::next. Yet, clippy complains about those.
-should_implement_trait = { level = "allow" }
let_underscore_future = "allow"
-# It doesn't make sense to implement `Default` unilaterally.
-new_without_default = "allow"
+
+# 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
# 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.
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]
ignored = [
"bindgen",
diff --git a/Procfile.web b/Procfile.web
new file mode 100644
index 0000000000..8140555144
--- /dev/null
+++ b/Procfile.web
@@ -0,0 +1,2 @@
+postgrest_llm: postgrest crates/collab/postgrest_llm.conf
+website: cd ../zed.dev; npm run dev -- --port=3000
diff --git a/assets/icons/attach.svg b/assets/icons/attach.svg
new file mode 100644
index 0000000000..f923a3c7c8
--- /dev/null
+++ b/assets/icons/attach.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/copy.svg b/assets/icons/copy.svg
index bca13f8d56..aba193930b 100644
--- a/assets/icons/copy.svg
+++ b/assets/icons/copy.svg
@@ -1 +1,4 @@
-
+
diff --git a/assets/icons/menu_alt.svg b/assets/icons/menu_alt.svg
index 87add13216..b9cc19e22f 100644
--- a/assets/icons/menu_alt.svg
+++ b/assets/icons/menu_alt.svg
@@ -1,3 +1,3 @@
diff --git a/assets/icons/pencil_unavailable.svg b/assets/icons/pencil_unavailable.svg
new file mode 100644
index 0000000000..4241d766ac
--- /dev/null
+++ b/assets/icons/pencil_unavailable.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/icons/tool_think.svg b/assets/icons/tool_think.svg
index efd5908a90..773f5e7fa7 100644
--- a/assets/icons/tool_think.svg
+++ b/assets/icons/tool_think.svg
@@ -1,3 +1,3 @@
diff --git a/assets/icons/zed_agent.svg b/assets/icons/zed_agent.svg
index b6e120a0b6..0c80e22c51 100644
--- a/assets/icons/zed_agent.svg
+++ b/assets/icons/zed_agent.svg
@@ -1,27 +1,27 @@
diff --git a/assets/icons/zed_assistant.svg b/assets/icons/zed_assistant.svg
index 470eb0fede..812277a100 100644
--- a/assets/icons/zed_assistant.svg
+++ b/assets/icons/zed_assistant.svg
@@ -1,5 +1,5 @@
diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json
index b4efa70572..e84f4834af 100644
--- a/assets/keymaps/default-linux.json
+++ b/assets/keymaps/default-linux.json
@@ -16,7 +16,6 @@
"up": "menu::SelectPrevious",
"enter": "menu::Confirm",
"ctrl-enter": "menu::SecondaryConfirm",
- "ctrl-escape": "menu::Cancel",
"ctrl-c": "menu::Cancel",
"escape": "menu::Cancel",
"alt-shift-enter": "menu::Restart",
@@ -138,7 +137,7 @@
"find": "buffer_search::Deploy",
"ctrl-f": "buffer_search::Deploy",
"ctrl-h": "buffer_search::DeployReplace",
- "ctrl->": "assistant::QuoteSelection",
+ "ctrl->": "agent::QuoteSelection",
"ctrl-<": "assistant::InsertIntoEditor",
"ctrl-alt-e": "editor::SelectEnclosingSymbol",
"ctrl-shift-backspace": "editor::GoToPreviousChange",
@@ -241,7 +240,7 @@
"ctrl-shift-i": "agent::ToggleOptionsMenu",
"ctrl-alt-shift-n": "agent::ToggleNewThreadMenu",
"shift-alt-escape": "agent::ExpandMessageEditor",
- "ctrl->": "assistant::QuoteSelection",
+ "ctrl->": "agent::QuoteSelection",
"ctrl-alt-e": "agent::RemoveAllContext",
"ctrl-shift-e": "project_panel::ToggleFocus",
"ctrl-shift-enter": "agent::ContinueThread",
@@ -856,7 +855,7 @@
"ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }],
"ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }],
"alt-ctrl-r": "project_panel::RevealInFileManager",
- "ctrl-shift-enter": "project_panel::OpenWithSystem",
+ "ctrl-shift-enter": "workspace::OpenWithSystem",
"alt-d": "project_panel::CompareMarkedFiles",
"shift-find": "project_panel::NewSearchInDirectory",
"ctrl-alt-shift-f": "project_panel::NewSearchInDirectory",
@@ -1195,9 +1194,16 @@
"ctrl-1": "onboarding::ActivateBasicsPage",
"ctrl-2": "onboarding::ActivateEditingPage",
"ctrl-3": "onboarding::ActivateAISetupPage",
- "ctrl-escape": "onboarding::Finish",
- "alt-tab": "onboarding::SignIn",
+ "ctrl-enter": "onboarding::Finish",
+ "alt-shift-l": "onboarding::SignIn",
"alt-shift-a": "onboarding::OpenAccount"
}
+ },
+ {
+ "context": "InvalidBuffer",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-shift-enter": "workspace::OpenWithSystem"
+ }
}
]
diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json
index ad2ab2ba89..e72f4174ff 100644
--- a/assets/keymaps/default-macos.json
+++ b/assets/keymaps/default-macos.json
@@ -162,7 +162,7 @@
"cmd-alt-f": "buffer_search::DeployReplace",
"cmd-alt-l": ["buffer_search::Deploy", { "selection_search_enabled": true }],
"cmd-e": ["buffer_search::Deploy", { "focus": false }],
- "cmd->": "assistant::QuoteSelection",
+ "cmd->": "agent::QuoteSelection",
"cmd-<": "assistant::InsertIntoEditor",
"cmd-alt-e": "editor::SelectEnclosingSymbol",
"alt-enter": "editor::OpenSelectionsInMultibuffer"
@@ -281,7 +281,7 @@
"cmd-shift-i": "agent::ToggleOptionsMenu",
"cmd-alt-shift-n": "agent::ToggleNewThreadMenu",
"shift-alt-escape": "agent::ExpandMessageEditor",
- "cmd->": "assistant::QuoteSelection",
+ "cmd->": "agent::QuoteSelection",
"cmd-alt-e": "agent::RemoveAllContext",
"cmd-shift-e": "project_panel::ToggleFocus",
"cmd-ctrl-b": "agent::ToggleBurnMode",
@@ -915,7 +915,7 @@
"cmd-backspace": ["project_panel::Trash", { "skip_prompt": true }],
"cmd-delete": ["project_panel::Delete", { "skip_prompt": false }],
"alt-cmd-r": "project_panel::RevealInFileManager",
- "ctrl-shift-enter": "project_panel::OpenWithSystem",
+ "ctrl-shift-enter": "workspace::OpenWithSystem",
"alt-d": "project_panel::CompareMarkedFiles",
"cmd-alt-backspace": ["project_panel::Delete", { "skip_prompt": false }],
"cmd-alt-shift-f": "project_panel::NewSearchInDirectory",
@@ -1301,5 +1301,12 @@
"alt-tab": "onboarding::SignIn",
"alt-shift-a": "onboarding::OpenAccount"
}
+ },
+ {
+ "context": "InvalidBuffer",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-shift-enter": "workspace::OpenWithSystem"
+ }
}
]
diff --git a/assets/keymaps/linux/cursor.json b/assets/keymaps/linux/cursor.json
index 1c381b0cf0..2e27158e11 100644
--- a/assets/keymaps/linux/cursor.json
+++ b/assets/keymaps/linux/cursor.json
@@ -17,8 +17,8 @@
"bindings": {
"ctrl-i": "agent::ToggleFocus",
"ctrl-shift-i": "agent::ToggleFocus",
- "ctrl-shift-l": "assistant::QuoteSelection", // In cursor uses "Ask" mode
- "ctrl-l": "assistant::QuoteSelection", // In cursor uses "Agent" mode
+ "ctrl-shift-l": "agent::QuoteSelection", // In cursor uses "Ask" mode
+ "ctrl-l": "agent::QuoteSelection", // In cursor uses "Agent" mode
"ctrl-k": "assistant::InlineAssist",
"ctrl-shift-k": "assistant::InsertIntoEditor"
}
diff --git a/assets/keymaps/macos/cursor.json b/assets/keymaps/macos/cursor.json
index fdf9c437cf..1d723bd75b 100644
--- a/assets/keymaps/macos/cursor.json
+++ b/assets/keymaps/macos/cursor.json
@@ -17,8 +17,8 @@
"bindings": {
"cmd-i": "agent::ToggleFocus",
"cmd-shift-i": "agent::ToggleFocus",
- "cmd-shift-l": "assistant::QuoteSelection", // In cursor uses "Ask" mode
- "cmd-l": "assistant::QuoteSelection", // In cursor uses "Agent" mode
+ "cmd-shift-l": "agent::QuoteSelection", // In cursor uses "Ask" mode
+ "cmd-l": "agent::QuoteSelection", // In cursor uses "Agent" mode
"cmd-k": "assistant::InlineAssist",
"cmd-shift-k": "assistant::InsertIntoEditor"
}
diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json
index be6d34a134..62e50b3c8c 100644
--- a/assets/keymaps/vim.json
+++ b/assets/keymaps/vim.json
@@ -819,7 +819,7 @@
"v": "project_panel::OpenPermanent",
"p": "project_panel::Open",
"x": "project_panel::RevealInFileManager",
- "s": "project_panel::OpenWithSystem",
+ "s": "workspace::OpenWithSystem",
"z d": "project_panel::CompareMarkedFiles",
"] c": "project_panel::SelectNextGitEntry",
"[ c": "project_panel::SelectPrevGitEntry",
diff --git a/assets/settings/default.json b/assets/settings/default.json
index c290baf003..f0b9e11e57 100644
--- a/assets/settings/default.json
+++ b/assets/settings/default.json
@@ -162,6 +162,12 @@
// 2. Always quit the application
// "on_last_window_closed": "quit_app",
"on_last_window_closed": "platform_default",
+ // Whether to show padding for zoomed panels.
+ // When enabled, zoomed center panels (e.g. code editor) will have padding all around,
+ // while zoomed bottom/left/right panels will have padding to the top/right/left (respectively).
+ //
+ // Default: true
+ "zoomed_padding": true,
// Whether to use the system provided dialogs for Open and Save As.
// When set to false, Zed will use the built-in keyboard-first pickers.
"use_system_path_prompts": true,
@@ -1133,11 +1139,6 @@
// The minimum severity of the diagnostics to show inline.
// Inherits editor's diagnostics' max severity settings when `null`.
"max_severity": null
- },
- "cargo": {
- // When enabled, Zed disables rust-analyzer's check on save and starts to query
- // Cargo diagnostics separately.
- "fetch_cargo_diagnostics": false
}
},
// Files or globs of files that will be excluded by Zed entirely. They will be skipped during file
@@ -1503,6 +1504,11 @@
//
// Default: fallback
"words": "fallback",
+ // Minimum number of characters required to automatically trigger word-based completions.
+ // Before that value, it's still possible to trigger the words-based completion manually with the corresponding editor command.
+ //
+ // Default: 3
+ "words_min_length": 3,
// Whether to fetch LSP completions or not.
//
// Default: true
@@ -1629,6 +1635,9 @@
"allowed": true
}
},
+ "Kotlin": {
+ "language_servers": ["kotlin-language-server", "!kotlin-lsp", "..."]
+ },
"LaTeX": {
"formatter": "language_server",
"language_servers": ["texlab", "..."],
@@ -1642,9 +1651,6 @@
"use_on_type_format": false,
"allow_rewrap": "anywhere",
"soft_wrap": "editor_width",
- "completions": {
- "words": "disabled"
- },
"prettier": {
"allowed": true
}
@@ -1658,9 +1664,6 @@
}
},
"Plain Text": {
- "completions": {
- "words": "disabled"
- },
"allow_rewrap": "anywhere"
},
"Python": {
diff --git a/assets/themes/ayu/ayu.json b/assets/themes/ayu/ayu.json
index f9f8720729..0ffbb9f61e 100644
--- a/assets/themes/ayu/ayu.json
+++ b/assets/themes/ayu/ayu.json
@@ -93,7 +93,7 @@
"terminal.ansi.bright_cyan": "#4c806fff",
"terminal.ansi.dim_cyan": "#cbf2e4ff",
"terminal.ansi.white": "#bfbdb6ff",
- "terminal.ansi.bright_white": "#bfbdb6ff",
+ "terminal.ansi.bright_white": "#fafafaff",
"terminal.ansi.dim_white": "#787876ff",
"link_text.hover": "#5ac1feff",
"conflict": "#feb454ff",
@@ -479,7 +479,7 @@
"terminal.ansi.bright_cyan": "#ace0cbff",
"terminal.ansi.dim_cyan": "#2a5f4aff",
"terminal.ansi.white": "#fcfcfcff",
- "terminal.ansi.bright_white": "#fcfcfcff",
+ "terminal.ansi.bright_white": "#ffffffff",
"terminal.ansi.dim_white": "#bcbec0ff",
"link_text.hover": "#3b9ee5ff",
"conflict": "#f1ad49ff",
@@ -865,7 +865,7 @@
"terminal.ansi.bright_cyan": "#4c806fff",
"terminal.ansi.dim_cyan": "#cbf2e4ff",
"terminal.ansi.white": "#cccac2ff",
- "terminal.ansi.bright_white": "#cccac2ff",
+ "terminal.ansi.bright_white": "#fafafaff",
"terminal.ansi.dim_white": "#898a8aff",
"link_text.hover": "#72cffeff",
"conflict": "#fecf72ff",
diff --git a/assets/themes/gruvbox/gruvbox.json b/assets/themes/gruvbox/gruvbox.json
index 459825c733..f0f0358b76 100644
--- a/assets/themes/gruvbox/gruvbox.json
+++ b/assets/themes/gruvbox/gruvbox.json
@@ -94,7 +94,7 @@
"terminal.ansi.bright_cyan": "#45603eff",
"terminal.ansi.dim_cyan": "#c7dfbdff",
"terminal.ansi.white": "#fbf1c7ff",
- "terminal.ansi.bright_white": "#fbf1c7ff",
+ "terminal.ansi.bright_white": "#ffffffff",
"terminal.ansi.dim_white": "#b0a189ff",
"link_text.hover": "#83a598ff",
"version_control.added": "#b7bb26ff",
@@ -494,7 +494,7 @@
"terminal.ansi.bright_cyan": "#45603eff",
"terminal.ansi.dim_cyan": "#c7dfbdff",
"terminal.ansi.white": "#fbf1c7ff",
- "terminal.ansi.bright_white": "#fbf1c7ff",
+ "terminal.ansi.bright_white": "#ffffffff",
"terminal.ansi.dim_white": "#b0a189ff",
"link_text.hover": "#83a598ff",
"version_control.added": "#b7bb26ff",
@@ -894,7 +894,7 @@
"terminal.ansi.bright_cyan": "#45603eff",
"terminal.ansi.dim_cyan": "#c7dfbdff",
"terminal.ansi.white": "#fbf1c7ff",
- "terminal.ansi.bright_white": "#fbf1c7ff",
+ "terminal.ansi.bright_white": "#ffffffff",
"terminal.ansi.dim_white": "#b0a189ff",
"link_text.hover": "#83a598ff",
"version_control.added": "#b7bb26ff",
@@ -1294,7 +1294,7 @@
"terminal.ansi.bright_cyan": "#9fbca8ff",
"terminal.ansi.dim_cyan": "#253e2eff",
"terminal.ansi.white": "#fbf1c7ff",
- "terminal.ansi.bright_white": "#fbf1c7ff",
+ "terminal.ansi.bright_white": "#ffffffff",
"terminal.ansi.dim_white": "#b0a189ff",
"link_text.hover": "#0b6678ff",
"version_control.added": "#797410ff",
@@ -1694,7 +1694,7 @@
"terminal.ansi.bright_cyan": "#9fbca8ff",
"terminal.ansi.dim_cyan": "#253e2eff",
"terminal.ansi.white": "#f9f5d7ff",
- "terminal.ansi.bright_white": "#f9f5d7ff",
+ "terminal.ansi.bright_white": "#ffffffff",
"terminal.ansi.dim_white": "#b0a189ff",
"link_text.hover": "#0b6678ff",
"version_control.added": "#797410ff",
@@ -2094,7 +2094,7 @@
"terminal.ansi.bright_cyan": "#9fbca8ff",
"terminal.ansi.dim_cyan": "#253e2eff",
"terminal.ansi.white": "#f2e5bcff",
- "terminal.ansi.bright_white": "#f2e5bcff",
+ "terminal.ansi.bright_white": "#ffffffff",
"terminal.ansi.dim_white": "#b0a189ff",
"link_text.hover": "#0b6678ff",
"version_control.added": "#797410ff",
diff --git a/assets/themes/one/one.json b/assets/themes/one/one.json
index 23ebbcc67e..33f6d3c622 100644
--- a/assets/themes/one/one.json
+++ b/assets/themes/one/one.json
@@ -93,7 +93,7 @@
"terminal.ansi.bright_cyan": "#3a565bff",
"terminal.ansi.dim_cyan": "#b9d9dfff",
"terminal.ansi.white": "#dce0e5ff",
- "terminal.ansi.bright_white": "#dce0e5ff",
+ "terminal.ansi.bright_white": "#fafafaff",
"terminal.ansi.dim_white": "#575d65ff",
"link_text.hover": "#74ade8ff",
"version_control.added": "#27a657ff",
@@ -468,7 +468,7 @@
"terminal.bright_foreground": "#242529ff",
"terminal.dim_foreground": "#fafafaff",
"terminal.ansi.black": "#242529ff",
- "terminal.ansi.bright_black": "#242529ff",
+ "terminal.ansi.bright_black": "#747579ff",
"terminal.ansi.dim_black": "#97979aff",
"terminal.ansi.red": "#d36151ff",
"terminal.ansi.bright_red": "#f0b0a4ff",
@@ -489,7 +489,7 @@
"terminal.ansi.bright_cyan": "#a3bedaff",
"terminal.ansi.dim_cyan": "#254058ff",
"terminal.ansi.white": "#fafafaff",
- "terminal.ansi.bright_white": "#fafafaff",
+ "terminal.ansi.bright_white": "#ffffffff",
"terminal.ansi.dim_white": "#aaaaaaff",
"link_text.hover": "#5c78e2ff",
"version_control.added": "#27a657ff",
diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs
index 5d3b35d018..4ded647a74 100644
--- a/crates/acp_thread/src/acp_thread.rs
+++ b/crates/acp_thread/src/acp_thread.rs
@@ -183,16 +183,15 @@ impl ToolCall {
language_registry: Arc,
cx: &mut App,
) -> Self {
+ let title = if let Some((first_line, _)) = tool_call.title.split_once("\n") {
+ first_line.to_owned() + "…"
+ } else {
+ tool_call.title
+ };
Self {
id: tool_call.id,
- label: cx.new(|cx| {
- Markdown::new(
- tool_call.title.into(),
- Some(language_registry.clone()),
- None,
- cx,
- )
- }),
+ label: cx
+ .new(|cx| Markdown::new(title.into(), Some(language_registry.clone()), None, cx)),
kind: tool_call.kind,
content: tool_call
.content
@@ -233,15 +232,30 @@ impl ToolCall {
if let Some(title) = title {
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 {
- self.content = content
- .into_iter()
- .map(|chunk| ToolCallContent::from_acp(chunk, language_registry.clone(), cx))
- .collect();
+ let new_content_len = content.len();
+ let mut content = content.into_iter();
+
+ // 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 {
@@ -301,11 +315,9 @@ impl ToolCall {
) -> Option {
let buffer = project
.update(cx, |project, cx| {
- if let Some(path) = project.project_path_for_absolute_path(&location.path, cx) {
- Some(project.open_buffer(path, cx))
- } else {
- None
- }
+ project
+ .project_path_for_absolute_path(&location.path, cx)
+ .map(|path| project.open_buffer(path, cx))
})
.ok()??;
let buffer = buffer.await.log_err()?;
@@ -471,7 +483,7 @@ impl ContentBlock {
fn block_string_contents(&self, block: acp::ContentBlock) -> String {
match block {
- acp::ContentBlock::Text(text_content) => text_content.text.clone(),
+ acp::ContentBlock::Text(text_content) => text_content.text,
acp::ContentBlock::ResourceLink(resource_link) => {
Self::resource_link_md(&resource_link.uri)
}
@@ -500,7 +512,7 @@ impl ContentBlock {
"`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 {
ContentBlock::Empty => "",
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,
+ 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 {
match self {
Self::ContentBlock(content) => content.to_markdown(cx).to_string(),
@@ -674,6 +708,37 @@ pub struct TokenUsage {
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)]
pub struct RetryStatus {
pub last_error: SharedString,
@@ -694,6 +759,8 @@ pub struct AcpThread {
connection: Rc,
session_id: acp::SessionId,
token_usage: Option,
+ prompt_capabilities: acp::PromptCapabilities,
+ _observe_prompt_capabilities: Task>,
}
#[derive(Debug)]
@@ -708,11 +775,12 @@ pub enum AcpThreadEvent {
Stopped,
Error,
LoadError(LoadError),
+ PromptCapabilitiesUpdated,
}
impl EventEmitter for AcpThread {}
-#[derive(PartialEq, Eq)]
+#[derive(PartialEq, Eq, Debug)]
pub enum ThreadStatus {
Idle,
WaitingForToolConfirmation,
@@ -759,7 +827,20 @@ impl AcpThread {
project: Entity,
action_log: Entity,
session_id: acp::SessionId,
+ mut prompt_capabilities_rx: watch::Receiver,
+ cx: &mut Context,
) -> 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 {
action_log,
shared_buffers: Default::default(),
@@ -771,9 +852,15 @@ impl AcpThread {
connection,
session_id,
token_usage: None,
+ prompt_capabilities,
+ _observe_prompt_capabilities: task,
}
}
+ pub fn prompt_capabilities(&self) -> acp::PromptCapabilities {
+ self.prompt_capabilities
+ }
+
pub fn connection(&self) -> &Rc {
&self.connection
}
@@ -958,10 +1045,19 @@ impl AcpThread {
cx.emit(AcpThreadEvent::NewEntry);
}
- pub fn update_title(&mut self, title: SharedString, cx: &mut Context) -> Result<()> {
- self.title = title;
- cx.emit(AcpThreadEvent::TitleUpdated);
- Ok(())
+ pub fn can_set_title(&mut self, cx: &mut Context) -> bool {
+ self.connection.set_title(&self.session_id, cx).is_some()
+ }
+
+ pub fn set_title(&mut self, title: SharedString, cx: &mut Context) -> Task> {
+ 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, cx: &mut Context) {
@@ -989,7 +1085,7 @@ impl AcpThread {
let location_updated = update.fields.locations.is_some();
current_call.update_fields(update.fields, languages, cx);
if location_updated {
- self.resolve_locations(update.id.clone(), cx);
+ self.resolve_locations(update.id, cx);
}
}
ToolCallUpdate::UpdateDiff(update) => {
@@ -1264,11 +1360,7 @@ impl AcpThread {
};
let git_store = self.project.read(cx).git_store().clone();
- let message_id = if self
- .connection
- .session_editor(&self.session_id, cx)
- .is_some()
- {
+ let message_id = if self.connection.truncate(&self.session_id, cx).is_some() {
Some(UserMessageId::new())
} else {
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) -> BoxFuture<'static, Result<()>> {
self.run_turn(cx, async move |this, cx| {
this.update(cx, |this, cx| {
@@ -1352,7 +1448,7 @@ impl AcpThread {
let canceled = matches!(
result,
Ok(Ok(acp::PromptResponse {
- stop_reason: acp::StopReason::Canceled
+ stop_reason: acp::StopReason::Cancelled
}))
);
@@ -1365,6 +1461,17 @@ impl AcpThread {
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);
Ok(())
}
@@ -1403,7 +1510,7 @@ impl AcpThread {
/// Rewinds this thread to before the entry at `index`, removing it and all
/// subsequent entries while reverting any changes made from that point.
pub fn rewind(&mut self, id: UserMessageId, cx: &mut Context) -> Task> {
- 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")));
};
let Some(message) = self.user_message(&id) else {
@@ -1423,8 +1530,7 @@ impl AcpThread {
.await?;
}
- cx.update(|cx| session_editor.truncate(id.clone(), cx))?
- .await?;
+ cx.update(|cx| truncate.run(id.clone(), cx))?.await?;
this.update(cx, |this, cx| {
if let Some((ix, _)) = this.user_message_mut(&id) {
let range = ix..this.entries.len();
@@ -2340,6 +2446,92 @@ mod tests {
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(
thread: &Entity,
cx: &mut TestAppContext,
@@ -2432,13 +2624,19 @@ mod tests {
.into(),
);
let action_log = cx.new(|_| ActionLog::new(project.clone()));
- let thread = cx.new(|_cx| {
+ let thread = cx.new(|cx| {
AcpThread::new(
"Test",
self.clone(),
project,
action_log,
session_id.clone(),
+ watch::Receiver::constant(acp::PromptCapabilities {
+ image: true,
+ audio: true,
+ embedded_context: true,
+ }),
+ cx,
)
});
self.sessions.lock().insert(session_id, thread.downgrade());
@@ -2485,11 +2683,11 @@ mod tests {
.detach();
}
- fn session_editor(
+ fn truncate(
&self,
session_id: &acp::SessionId,
- _cx: &mut App,
- ) -> Option> {
+ _cx: &App,
+ ) -> Option> {
Some(Rc::new(FakeAgentSessionEditor {
_session_id: session_id.clone(),
}))
@@ -2504,8 +2702,8 @@ mod tests {
_session_id: acp::SessionId,
}
- impl AgentSessionEditor for FakeAgentSessionEditor {
- fn truncate(&self, _message_id: UserMessageId, _cx: &mut App) -> Task> {
+ impl AgentSessionTruncate for FakeAgentSessionEditor {
+ fn run(&self, _message_id: UserMessageId, _cx: &mut App) -> Task> {
Task::ready(Ok(()))
}
}
diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs
index 8cae975ce5..af229b7545 100644
--- a/crates/acp_thread/src/connection.rs
+++ b/crates/acp_thread/src/connection.rs
@@ -41,18 +41,26 @@ pub trait AgentConnection {
fn resume(
&self,
_session_id: &acp::SessionId,
- _cx: &mut App,
+ _cx: &App,
) -> Option> {
None
}
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App);
- fn session_editor(
+ fn truncate(
&self,
_session_id: &acp::SessionId,
- _cx: &mut App,
- ) -> Option> {
+ _cx: &App,
+ ) -> Option> {
+ None
+ }
+
+ fn set_title(
+ &self,
+ _session_id: &acp::SessionId,
+ _cx: &App,
+ ) -> Option> {
None
}
@@ -64,6 +72,10 @@ pub trait AgentConnection {
None
}
+ fn telemetry(&self) -> Option> {
+ None
+ }
+
fn into_any(self: Rc) -> Rc;
}
@@ -73,14 +85,31 @@ impl dyn AgentConnection {
}
}
-pub trait AgentSessionEditor {
- fn truncate(&self, message_id: UserMessageId, cx: &mut App) -> Task>;
+pub trait AgentSessionTruncate {
+ fn run(&self, message_id: UserMessageId, cx: &mut App) -> Task>;
}
pub trait AgentSessionResume {
fn run(&self, cx: &mut App) -> Task>;
}
+pub trait AgentSessionSetTitle {
+ fn run(&self, title: SharedString, cx: &mut App) -> Task>;
+}
+
+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>;
+}
+
#[derive(Debug)]
pub struct AuthRequired {
pub description: Option,
@@ -298,13 +327,19 @@ mod test_support {
) -> Task>> {
let session_id = acp::SessionId(self.sessions.lock().len().to_string().into());
let action_log = cx.new(|_| ActionLog::new(project.clone()));
- let thread = cx.new(|_cx| {
+ let thread = cx.new(|cx| {
AcpThread::new(
"Test",
self.clone(),
project,
action_log,
session_id.clone(),
+ watch::Receiver::constant(acp::PromptCapabilities {
+ image: true,
+ audio: true,
+ embedded_context: true,
+ }),
+ cx,
)
});
self.sessions.lock().insert(
@@ -393,15 +428,15 @@ mod test_support {
.response_tx
.take()
{
- end_turn_tx.send(acp::StopReason::Canceled).unwrap();
+ end_turn_tx.send(acp::StopReason::Cancelled).unwrap();
}
}
- fn session_editor(
+ fn truncate(
&self,
_session_id: &agent_client_protocol::SessionId,
- _cx: &mut App,
- ) -> Option> {
+ _cx: &App,
+ ) -> Option> {
Some(Rc::new(StubAgentSessionEditor))
}
@@ -412,8 +447,8 @@ mod test_support {
struct StubAgentSessionEditor;
- impl AgentSessionEditor for StubAgentSessionEditor {
- fn truncate(&self, _: UserMessageId, _: &mut App) -> Task> {
+ impl AgentSessionTruncate for StubAgentSessionEditor {
+ fn run(&self, _: UserMessageId, _: &mut App) -> Task> {
Task::ready(Ok(()))
}
}
diff --git a/crates/acp_thread/src/diff.rs b/crates/acp_thread/src/diff.rs
index 4b779931c5..0fec6809e0 100644
--- a/crates/acp_thread/src/diff.rs
+++ b/crates/acp_thread/src/diff.rs
@@ -28,57 +28,46 @@ impl Diff {
cx: &mut Context,
) -> Self {
let multibuffer = cx.new(|_cx| MultiBuffer::without_headers(Capability::ReadOnly));
-
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 new_buffer_snapshot = new_buffer.read(cx).text_snapshot();
- let buffer_diff = cx.new(|cx| BufferDiff::new(&new_buffer_snapshot, cx));
-
+ let base_text = old_text.clone().unwrap_or(String::new()).into();
let task = cx.spawn({
let multibuffer = multibuffer.clone();
let path = path.clone();
+ let buffer = new_buffer.clone();
async move |_, cx| {
let language = language_registry
.language_for_file_path(&path)
.await
.log_err();
- 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| {
- buffer.set_language(language, cx);
- buffer.snapshot()
- })?;
-
- buffer_diff
- .update(cx, |diff, cx| {
- diff.set_base_text(
- old_buffer_snapshot,
- Some(language_registry),
- new_buffer_snapshot,
- cx,
- )
- })?
- .await?;
+ let diff = build_buffer_diff(
+ old_text.unwrap_or("".into()).into(),
+ &buffer,
+ Some(language_registry.clone()),
+ cx,
+ )
+ .await?;
multibuffer
.update(cx, |multibuffer, cx| {
let hunk_ranges = {
- let buffer = new_buffer.read(cx);
- let diff = buffer_diff.read(cx);
+ let buffer = buffer.read(cx);
+ let diff = diff.read(cx);
diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer, cx)
.map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer))
.collect::>()
};
multibuffer.set_excerpts_for_path(
- PathKey::for_buffer(&new_buffer, cx),
- new_buffer.clone(),
+ PathKey::for_buffer(&buffer, cx),
+ buffer.clone(),
hunk_ranges,
editor::DEFAULT_MULTIBUFFER_CONTEXT,
cx,
);
- multibuffer.add_diff(buffer_diff, cx);
+ multibuffer.add_diff(diff, cx);
})
.log_err();
@@ -89,23 +78,26 @@ impl Diff {
Self::Finalized(FinalizedDiff {
multibuffer,
path,
+ base_text,
+ new_buffer,
_update_diff: task,
})
}
pub fn new(buffer: Entity, cx: &mut Context) -> Self {
- let buffer_snapshot = buffer.read(cx).snapshot();
- let base_text = buffer_snapshot.text();
- let language_registry = buffer.read(cx).language_registry();
- let text_snapshot = buffer.read(cx).text_snapshot();
+ let buffer_text_snapshot = buffer.read(cx).text_snapshot();
+ let base_text_snapshot = buffer.read(cx).snapshot();
+ let base_text = base_text_snapshot.text();
+ debug_assert_eq!(buffer_text_snapshot.text(), base_text);
let buffer_diff = cx.new(|cx| {
- let mut diff = BufferDiff::new(&text_snapshot, cx);
- let _ = diff.set_base_text(
- buffer_snapshot.clone(),
- language_registry,
- text_snapshot,
- cx,
- );
+ let mut diff = BufferDiff::new_unchanged(&buffer_text_snapshot, base_text_snapshot);
+ let snapshot = diff.snapshot(cx);
+ let secondary_diff = cx.new(|cx| {
+ let mut diff = BufferDiff::new(&buffer_text_snapshot, cx);
+ diff.set_snapshot(snapshot, &buffer_text_snapshot, cx);
+ diff
+ });
+ diff.set_secondary_diff(secondary_diff);
diff
});
@@ -123,7 +115,7 @@ impl Diff {
diff.update(cx);
}
}),
- buffer,
+ new_buffer: buffer,
diff: buffer_diff,
revealed_ranges: Vec::new(),
update_diff: Task::ready(Ok(())),
@@ -158,9 +150,9 @@ impl Diff {
.map(|buffer| buffer.read(cx).text())
.join("\n");
let path = match self {
- Diff::Pending(PendingDiff { buffer, .. }) => {
- buffer.read(cx).file().map(|file| file.path().as_ref())
- }
+ Diff::Pending(PendingDiff {
+ new_buffer: buffer, ..
+ }) => buffer.read(cx).file().map(|file| file.path().as_ref()),
Diff::Finalized(FinalizedDiff { path, .. }) => Some(path.as_path()),
};
format!(
@@ -173,12 +165,33 @@ impl Diff {
pub fn has_revealed_range(&self, cx: &App) -> bool {
self.multibuffer().read(cx).excerpt_paths().next().is_some()
}
+
+ pub fn needs_update(&self, old_text: &str, new_text: &str, cx: &App) -> bool {
+ match self {
+ Diff::Pending(PendingDiff {
+ base_text,
+ new_buffer,
+ ..
+ }) => {
+ base_text.as_str() != old_text
+ || !new_buffer.read(cx).as_rope().chunks().equals_str(new_text)
+ }
+ Diff::Finalized(FinalizedDiff {
+ base_text,
+ new_buffer,
+ ..
+ }) => {
+ base_text.as_str() != old_text
+ || !new_buffer.read(cx).as_rope().chunks().equals_str(new_text)
+ }
+ }
+ }
}
pub struct PendingDiff {
multibuffer: Entity,
base_text: Arc,
- buffer: Entity,
+ new_buffer: Entity,
diff: Entity,
revealed_ranges: Vec>,
_subscription: Subscription,
@@ -187,7 +200,7 @@ pub struct PendingDiff {
impl PendingDiff {
pub fn update(&mut self, cx: &mut Context) {
- let buffer = self.buffer.clone();
+ let buffer = self.new_buffer.clone();
let buffer_diff = self.diff.clone();
let base_text = self.base_text.clone();
self.update_diff = cx.spawn(async move |diff, cx| {
@@ -204,7 +217,10 @@ impl PendingDiff {
)
.await?;
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| {
if let Diff::Pending(diff) = diff {
@@ -222,10 +238,10 @@ impl PendingDiff {
fn finalize(&self, cx: &mut Context) -> FinalizedDiff {
let ranges = self.excerpt_ranges(cx);
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
- .buffer
+ .new_buffer
.read(cx)
.file()
.map(|file| file.path().as_ref())
@@ -234,12 +250,12 @@ impl PendingDiff {
// Replace the buffer in the multibuffer with the snapshot
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(
0,
cx.entity_id().as_non_zero_u64().into(),
- self.buffer.read(cx).line_ending(),
- self.buffer.read(cx).as_rope().clone(),
+ self.new_buffer.read(cx).line_ending(),
+ self.new_buffer.read(cx).as_rope().clone(),
);
let mut buffer = Buffer::build(buffer, None, Capability::ReadWrite);
buffer.set_language(language, cx);
@@ -248,7 +264,6 @@ impl PendingDiff {
let buffer_diff = cx.spawn({
let buffer = buffer.clone();
- let language_registry = language_registry.clone();
async move |_this, cx| {
build_buffer_diff(base_text, &buffer, language_registry, cx).await
}
@@ -276,7 +291,9 @@ impl PendingDiff {
FinalizedDiff {
path,
+ base_text: self.base_text.clone(),
multibuffer: self.multibuffer.clone(),
+ new_buffer: self.new_buffer.clone(),
_update_diff: update_diff,
}
}
@@ -285,8 +302,8 @@ impl PendingDiff {
let ranges = self.excerpt_ranges(cx);
self.multibuffer.update(cx, |multibuffer, cx| {
multibuffer.set_excerpts_for_path(
- PathKey::for_buffer(&self.buffer, cx),
- self.buffer.clone(),
+ PathKey::for_buffer(&self.new_buffer, cx),
+ self.new_buffer.clone(),
ranges,
editor::DEFAULT_MULTIBUFFER_CONTEXT,
cx,
@@ -298,7 +315,7 @@ impl PendingDiff {
}
fn excerpt_ranges(&self, cx: &App) -> Vec> {
- let buffer = self.buffer.read(cx);
+ let buffer = self.new_buffer.read(cx);
let diff = self.diff.read(cx);
let mut ranges = diff
.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer, cx)
@@ -332,6 +349,8 @@ impl PendingDiff {
pub struct FinalizedDiff {
path: PathBuf,
+ base_text: Arc,
+ new_buffer: Entity,
multibuffer: Entity,
_update_diff: Task>,
}
@@ -385,3 +404,21 @@ async fn build_buffer_diff(
diff
})
}
+
+#[cfg(test)]
+mod tests {
+ use gpui::{AppContext as _, TestAppContext};
+ use language::Buffer;
+
+ use crate::Diff;
+
+ #[gpui::test]
+ async fn test_pending_diff(cx: &mut TestAppContext) {
+ let buffer = cx.new(|cx| Buffer::local("hello!", cx));
+ let _diff = cx.new(|cx| Diff::new(buffer.clone(), cx));
+ buffer.update(cx, |buffer, cx| {
+ buffer.set_text("HELLO!", cx);
+ });
+ cx.run_until_parked();
+ }
+}
diff --git a/crates/acp_thread/src/mention.rs b/crates/acp_thread/src/mention.rs
index a1e713cffa..6fa0887e22 100644
--- a/crates/acp_thread/src/mention.rs
+++ b/crates/acp_thread/src/mention.rs
@@ -5,7 +5,7 @@ use prompt_store::{PromptId, UserPromptId};
use serde::{Deserialize, Serialize};
use std::{
fmt,
- ops::Range,
+ ops::RangeInclusive,
path::{Path, PathBuf},
str::FromStr,
};
@@ -17,13 +17,14 @@ pub enum MentionUri {
File {
abs_path: PathBuf,
},
+ PastedImage,
Directory {
abs_path: PathBuf,
},
Symbol {
- path: PathBuf,
+ abs_path: PathBuf,
name: String,
- line_range: Range,
+ line_range: RangeInclusive,
},
Thread {
id: acp::SessionId,
@@ -38,8 +39,9 @@ pub enum MentionUri {
name: String,
},
Selection {
- path: PathBuf,
- line_range: Range,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ abs_path: Option,
+ line_range: RangeInclusive,
},
Fetch {
url: Url,
@@ -48,36 +50,44 @@ pub enum MentionUri {
impl MentionUri {
pub fn parse(input: &str) -> Result {
+ fn parse_line_range(fragment: &str) -> Result> {
+ 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::()
+ .context("Parsing line range start")?
+ .checked_sub(1)
+ .context("Line numbers should be 1-based")?
+ ..=end
+ .parse::()
+ .context("Parsing line range end")?
+ .checked_sub(1)
+ .context("Line numbers should be 1-based")?;
+ Ok(range)
+ }
+
let url = url::Url::parse(input)?;
let path = url.path();
match url.scheme() {
"file" => {
let path = url.to_file_path().ok().context("Extracting file path")?;
if let Some(fragment) = url.fragment() {
- let 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::()
- .context("Parsing line range start")?
- .checked_sub(1)
- .context("Line numbers should be 1-based")?
- ..end
- .parse::()
- .context("Parsing line range end")?
- .checked_sub(1)
- .context("Line numbers should be 1-based")?;
+ let line_range = parse_line_range(fragment)?;
if let Some(name) = single_query_param(&url, "symbol")? {
Ok(Self::Symbol {
name,
- path,
+ abs_path: path,
line_range,
})
} else {
- Ok(Self::Selection { path, line_range })
+ Ok(Self::Selection {
+ abs_path: Some(path),
+ line_range,
+ })
}
} else if input.ends_with("/") {
Ok(Self::Directory { abs_path: path })
@@ -105,6 +115,17 @@ impl MentionUri {
id: rule_id.into(),
name,
})
+ } else if path.starts_with("/agent/pasted-image") {
+ Ok(Self::PastedImage)
+ } else if path.starts_with("/agent/untitled-buffer") {
+ let fragment = url
+ .fragment()
+ .context("Missing fragment for untitled buffer selection")?;
+ let line_range = parse_line_range(fragment)?;
+ Ok(Self::Selection {
+ abs_path: None,
+ line_range,
+ })
} else {
bail!("invalid zed url: {:?}", input);
}
@@ -121,13 +142,16 @@ impl MentionUri {
.unwrap_or_default()
.to_string_lossy()
.into_owned(),
+ MentionUri::PastedImage => "Image".to_string(),
MentionUri::Symbol { name, .. } => name.clone(),
MentionUri::Thread { name, .. } => name.clone(),
MentionUri::TextThread { name, .. } => name.clone(),
MentionUri::Rule { name, .. } => name.clone(),
MentionUri::Selection {
- path, line_range, ..
- } => selection_name(path, line_range),
+ abs_path: path,
+ line_range,
+ ..
+ } => selection_name(path.as_deref(), line_range),
MentionUri::Fetch { url } => url.to_string(),
}
}
@@ -137,6 +161,7 @@ impl MentionUri {
MentionUri::File { abs_path } => {
FileIcons::get_icon(abs_path, cx).unwrap_or_else(|| IconName::File.path().into())
}
+ MentionUri::PastedImage => IconName::Image.path().into(),
MentionUri::Directory { .. } => FileIcons::get_folder_icon(false, cx)
.unwrap_or_else(|| IconName::Folder.path().into()),
MentionUri::Symbol { .. } => IconName::Code.path().into(),
@@ -157,29 +182,40 @@ impl MentionUri {
MentionUri::File { abs_path } => {
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 } => {
Url::from_directory_path(abs_path).expect("mention path should be absolute")
}
MentionUri::Symbol {
- path,
+ abs_path,
name,
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.set_fragment(Some(&format!(
"L{}:{}",
- line_range.start + 1,
- line_range.end + 1
+ line_range.start() + 1,
+ line_range.end() + 1
)));
url
}
- MentionUri::Selection { path, line_range } => {
- let mut url = Url::from_file_path(path).expect("mention path should be absolute");
+ MentionUri::Selection {
+ abs_path: path,
+ line_range,
+ } => {
+ let mut url = if let Some(path) = path {
+ Url::from_file_path(path).expect("mention path should be absolute")
+ } else {
+ let mut url = Url::parse("zed:///").unwrap();
+ url.set_path("/agent/untitled-buffer");
+ url
+ };
url.set_fragment(Some(&format!(
"L{}:{}",
- line_range.start + 1,
- line_range.end + 1
+ line_range.start() + 1,
+ line_range.end() + 1
)));
url
}
@@ -191,7 +227,10 @@ impl MentionUri {
}
MentionUri::TextThread { path, name } => {
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
}
@@ -237,12 +276,14 @@ fn single_query_param(url: &Url, name: &'static str) -> Result