diff --git a/Cargo.lock b/Cargo.lock
index 70b8f630f7..37853f82ac 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.28"
+version = "0.0.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c887e795097665ab95119580534e7cc1335b59e1a7fec296943e534b970f4ed"
+checksum = "289eb34ee17213dadcca47eedadd386a5e7678094095414e475965d1bcca2860"
dependencies = [
"anyhow",
+ "async-broadcast",
"futures 0.3.31",
"log",
"parking_lot",
@@ -244,6 +265,7 @@ dependencies = [
"terminal",
"text",
"theme",
+ "thiserror 2.0.12",
"tree-sitter-rust",
"ui",
"unindent",
@@ -263,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",
@@ -284,6 +309,7 @@ dependencies = [
"paths",
"project",
"rand 0.8.5",
+ "reqwest_client",
"schemars",
"semver",
"serde",
@@ -377,6 +403,7 @@ dependencies = [
"parking_lot",
"paths",
"picker",
+ "postage",
"pretty_assertions",
"project",
"prompt_store",
@@ -416,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"
@@ -849,7 +858,7 @@ dependencies = [
"anyhow",
"async-trait",
"collections",
- "derive_more 0.99.19",
+ "derive_more",
"extension",
"futures 0.3.31",
"gpui",
@@ -912,7 +921,7 @@ dependencies = [
"clock",
"collections",
"ctor",
- "derive_more 0.99.19",
+ "derive_more",
"gpui",
"icons",
"indoc",
@@ -949,7 +958,7 @@ dependencies = [
"cloud_llm_client",
"collections",
"component",
- "derive_more 0.99.19",
+ "derive_more",
"diffy",
"editor",
"feature_flags",
@@ -1375,7 +1384,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"collections",
- "derive_more 0.99.19",
+ "derive_more",
"gpui",
"parking_lot",
"rodio",
@@ -3061,7 +3070,7 @@ dependencies = [
"cocoa 0.26.0",
"collections",
"credentials_provider",
- "derive_more 0.99.19",
+ "derive_more",
"feature_flags",
"fs",
"futures 0.3.31",
@@ -3493,7 +3502,7 @@ name = "command_palette_hooks"
version = "0.1.0"
dependencies = [
"collections",
- "derive_more 0.99.19",
+ "derive_more",
"gpui",
"workspace-hack",
]
@@ -4654,27 +4663,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"
@@ -6415,7 +6403,7 @@ dependencies = [
"askpass",
"async-trait",
"collections",
- "derive_more 0.99.19",
+ "derive_more",
"futures 0.3.31",
"git2",
"gpui",
@@ -7445,7 +7433,7 @@ dependencies = [
"core-video",
"cosmic-text",
"ctor",
- "derive_more 0.99.19",
+ "derive_more",
"embed-resource",
"env_logger 0.11.8",
"etagere",
@@ -7533,6 +7521,7 @@ dependencies = [
name = "gpui_tokio"
version = "0.1.0"
dependencies = [
+ "anyhow",
"gpui",
"tokio",
"util",
@@ -7969,7 +7958,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",
@@ -14364,12 +14353,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",
]
@@ -16438,7 +16425,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"collections",
- "derive_more 0.99.19",
+ "derive_more",
"fs",
"futures 0.3.31",
"gpui",
@@ -19953,7 +19940,6 @@ dependencies = [
"rustix 1.0.7",
"rustls 0.23.26",
"rustls-webpki 0.103.1",
- "schemars",
"scopeguard",
"sea-orm",
"sea-query-binder",
@@ -20387,8 +20373,9 @@ dependencies = [
[[package]]
name = "zed"
-version = "0.201.0"
+version = "0.201.4"
dependencies = [
+ "acp_tools",
"activity_indicator",
"agent",
"agent_servers",
diff --git a/Cargo.toml b/Cargo.toml
index 436d4a7f5c..6cd6dec791 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",
@@ -226,6 +227,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" }
@@ -422,8 +424,7 @@ zlog_settings = { path = "crates/zlog_settings" }
# External crates
#
-agentic-coding-protocol = "0.0.10"
-agent-client-protocol = "0.0.28"
+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"
@@ -802,147 +803,26 @@ 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"
-
-redundant_clone = "warn"
-declare_interior_mutable_const = "deny"
-
-# These are all of the rules that currently have violations in the Zed
-# codebase.
+# We currently do not restrict any style rules
+# as it slows down shipping code to Zed.
#
-# 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.
+# 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.
#
-# This list shouldn't be added to; it should only get shorter.
-# =============================================================================
-
-# There are a bunch of rules currently failing in the `style` group, so
-# allow all of those, for now.
+# 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.
-# Progress is being tracked in #36577
-blocks_in_conditions = "warn"
-bool_assert_comparison = "warn"
-borrow_interior_mutable_const = "warn"
-box_default = "warn"
-builtin_type_shadow = "warn"
-bytes_nth = "warn"
-chars_next_cmp = "warn"
-cmp_null = "warn"
-collapsible_else_if = "warn"
-collapsible_if = "warn"
-comparison_to_empty = "warn"
-default_instead_of_iter_empty = "warn"
-disallowed_macros = "warn"
-disallowed_methods = "warn"
-disallowed_names = "warn"
-disallowed_types = "warn"
-doc_lazy_continuation = "warn"
-doc_overindented_list_items = "warn"
-duplicate_underscore_argument = "warn"
-err_expect = "warn"
-fn_to_numeric_cast = "warn"
-fn_to_numeric_cast_with_truncation = "warn"
-for_kv_map = "warn"
-implicit_saturating_add = "warn"
-implicit_saturating_sub = "warn"
-inconsistent_digit_grouping = "warn"
-infallible_destructuring_match = "warn"
-inherent_to_string = "warn"
-init_numbered_fields = "warn"
-into_iter_on_ref = "warn"
-io_other_error = "warn"
-items_after_test_module = "warn"
-iter_cloned_collect = "warn"
-iter_next_slice = "warn"
-iter_nth = "warn"
-iter_nth_zero = "warn"
-iter_skip_next = "warn"
-just_underscores_and_digits = "warn"
-len_zero = "warn"
-let_and_return = "warn"
-main_recursion = "warn"
-manual_bits = "warn"
-manual_dangling_ptr = "warn"
-manual_is_ascii_check = "warn"
-manual_is_finite = "warn"
-manual_is_infinite = "warn"
-manual_map = "warn"
-manual_next_back = "warn"
-manual_non_exhaustive = "warn"
-manual_ok_or = "warn"
-manual_pattern_char_comparison = "warn"
-manual_rotate = "warn"
-manual_slice_fill = "warn"
-manual_while_let_some = "warn"
-map_clone = "warn"
-map_collect_result_unit = "warn"
-match_like_matches_macro = "warn"
-match_overlapping_arm = "warn"
-mem_replace_option_with_none = "warn"
-mem_replace_option_with_some = "warn"
-missing_enforced_import_renames = "warn"
-missing_safety_doc = "warn"
-mixed_attributes_style = "warn"
-mixed_case_hex_literals = "warn"
-module_inception = "warn"
-must_use_unit = "warn"
-mut_mutex_lock = "warn"
-needless_borrow = "warn"
-needless_doctest_main = "warn"
-needless_else = "warn"
-needless_parens_on_range_literals = "warn"
-needless_pub_self = "warn"
-needless_return = "warn"
-needless_return_with_question_mark = "warn"
-non_minimal_cfg = "warn"
-ok_expect = "warn"
-owned_cow = "warn"
-print_literal = "warn"
-print_with_newline = "warn"
-println_empty_string = "warn"
-ptr_eq = "warn"
-question_mark = "warn"
-redundant_closure = "warn"
-redundant_field_names = "warn"
-redundant_pattern_matching = "warn"
-redundant_static_lifetimes = "warn"
-result_map_or_into_option = "warn"
-self_named_constructors = "warn"
-single_match = "warn"
-tabs_in_doc_comments = "warn"
-to_digit_is_some = "warn"
-toplevel_ref_arg = "warn"
-unnecessary_fold = "warn"
-unnecessary_map_or = "warn"
-unnecessary_mut_passed = "warn"
-unnecessary_owned_empty_strings = "warn"
-unneeded_struct_pattern = "warn"
-unsafe_removed_from_name = "warn"
-unused_unit = "warn"
-unusual_byte_groupings = "warn"
-while_let_on_iterator = "warn"
-write_literal = "warn"
-write_with_newline = "warn"
-writeln_empty_string = "warn"
-wrong_self_convention = "warn"
-zero_ptr = "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.
@@ -951,10 +831,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/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/terminal_ghost.svg b/assets/icons/terminal_ghost.svg
new file mode 100644
index 0000000000..7d0d0e068e
--- /dev/null
+++ b/assets/icons/terminal_ghost.svg
@@ -0,0 +1,4 @@
+
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/images/acp_grid.svg b/assets/images/acp_grid.svg
new file mode 100644
index 0000000000..8ebff8e1bc
--- /dev/null
+++ b/assets/images/acp_grid.svg
@@ -0,0 +1,1257 @@
+
diff --git a/assets/images/acp_logo.svg b/assets/images/acp_logo.svg
new file mode 100644
index 0000000000..efaa46707b
--- /dev/null
+++ b/assets/images/acp_logo.svg
@@ -0,0 +1 @@
+
diff --git a/assets/images/acp_logo_serif.svg b/assets/images/acp_logo_serif.svg
new file mode 100644
index 0000000000..6bc359cf82
--- /dev/null
+++ b/assets/images/acp_logo_serif.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json
index b4efa70572..955e68f5a9 100644
--- a/assets/keymaps/default-linux.json
+++ b/assets/keymaps/default-linux.json
@@ -138,7 +138,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 +241,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",
diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json
index ad2ab2ba89..8b18299a91 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",
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/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 9833e1957c..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 {
@@ -498,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(),
@@ -551,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(),
@@ -723,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)]
@@ -737,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,
@@ -788,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(),
@@ -800,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
}
@@ -987,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) {
@@ -1293,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
@@ -1335,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| {
@@ -1381,7 +1448,7 @@ impl AcpThread {
let canceled = matches!(
result,
Ok(Ok(acp::PromptResponse {
- stop_reason: acp::StopReason::Canceled
+ stop_reason: acp::StopReason::Cancelled
}))
);
@@ -1443,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 {
@@ -1463,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();
@@ -2558,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());
@@ -2598,14 +2670,6 @@ mod tests {
}
}
- fn prompt_capabilities(&self) -> acp::PromptCapabilities {
- acp::PromptCapabilities {
- image: true,
- audio: true,
- embedded_context: true,
- }
- }
-
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
let sessions = self.sessions.lock();
let thread = sessions.get(session_id).unwrap().clone();
@@ -2619,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(),
}))
@@ -2638,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 791b161417..af229b7545 100644
--- a/crates/acp_thread/src/connection.rs
+++ b/crates/acp_thread/src/connection.rs
@@ -38,23 +38,29 @@ pub trait AgentConnection {
cx: &mut App,
) -> Task>;
- fn prompt_capabilities(&self) -> acp::PromptCapabilities;
-
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
}
@@ -79,14 +85,18 @@ 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;
@@ -317,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(
@@ -336,14 +352,6 @@ mod test_support {
Task::ready(Ok(thread))
}
- fn prompt_capabilities(&self) -> acp::PromptCapabilities {
- acp::PromptCapabilities {
- image: true,
- audio: true,
- embedded_context: true,
- }
- }
-
fn authenticate(
&self,
_method_id: acp::AuthMethodId,
@@ -420,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))
}
@@ -439,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 70367e340a..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();
+ 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);
@@ -275,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,
}
}
@@ -284,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,
@@ -297,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)
@@ -331,6 +349,8 @@ impl PendingDiff {
pub struct FinalizedDiff {
path: PathBuf,
+ base_text: Arc,
+ new_buffer: Entity,
multibuffer: Entity,
_update_diff: Task>,
}
@@ -384,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