From 58e2b7ecdd9170cf3c23cf04da903201a739f52f Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Wed, 22 Apr 2026 17:02:57 +0200 Subject: [PATCH] acp: Use new Rust SDK (#52997) Testing out Niko's new SDK design Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Release Notes: - N/A --- Cargo.lock | 392 ++-- Cargo.toml | 2 +- crates/acp_thread/src/acp_thread.rs | 2 +- crates/acp_thread/src/connection.rs | 4 +- crates/acp_thread/src/mention.rs | 2 +- crates/acp_thread/src/terminal.rs | 2 +- crates/acp_tools/Cargo.toml | 5 + crates/acp_tools/src/acp_tools.rs | 414 +++- crates/agent/src/agent.rs | 2 +- crates/agent/src/db.rs | 2 +- crates/agent/src/native_agent_server.rs | 2 +- crates/agent/src/tests/mod.rs | 4 +- crates/agent/src/thread.rs | 4 +- crates/agent/src/thread_store.rs | 2 +- .../src/tools/context_server_registry.rs | 6 +- crates/agent/src/tools/copy_path_tool.rs | 7 +- .../agent/src/tools/create_directory_tool.rs | 7 +- crates/agent/src/tools/delete_path_tool.rs | 7 +- crates/agent/src/tools/diagnostics_tool.rs | 2 +- crates/agent/src/tools/edit_file_tool.rs | 6 +- crates/agent/src/tools/fetch_tool.rs | 2 +- crates/agent/src/tools/find_path_tool.rs | 2 +- crates/agent/src/tools/grep_tool.rs | 2 +- crates/agent/src/tools/list_directory_tool.rs | 7 +- crates/agent/src/tools/move_path_tool.rs | 7 +- crates/agent/src/tools/now_tool.rs | 2 +- crates/agent/src/tools/open_tool.rs | 6 +- crates/agent/src/tools/read_file_tool.rs | 9 +- .../src/tools/restore_file_from_disk_tool.rs | 2 +- crates/agent/src/tools/save_file_tool.rs | 2 +- crates/agent/src/tools/spawn_agent_tool.rs | 2 +- .../src/tools/streaming_edit_file_tool.rs | 2 +- crates/agent/src/tools/terminal_tool.rs | 2 +- crates/agent/src/tools/update_plan_tool.rs | 2 +- crates/agent/src/tools/web_search_tool.rs | 2 +- crates/agent_servers/Cargo.toml | 5 +- crates/agent_servers/src/acp.rs | 1678 +++++++++++------ crates/agent_servers/src/agent_servers.rs | 21 +- crates/agent_servers/src/custom.rs | 2 +- crates/agent_servers/src/e2e_tests.rs | 4 +- crates/agent_settings/src/agent_settings.rs | 6 +- crates/agent_ui/src/agent_panel.rs | 2 +- crates/agent_ui/src/agent_ui.rs | 6 +- crates/agent_ui/src/completion_provider.rs | 2 +- crates/agent_ui/src/config_options.rs | 2 +- crates/agent_ui/src/conversation_view.rs | 23 +- .../src/conversation_view/thread_view.rs | 10 +- crates/agent_ui/src/entry_view_state.rs | 10 +- crates/agent_ui/src/mention_set.rs | 2 +- crates/agent_ui/src/message_editor.rs | 4 +- crates/agent_ui/src/mode_selector.rs | 2 +- crates/agent_ui/src/model_selector.rs | 13 +- crates/agent_ui/src/test_support.rs | 2 +- crates/agent_ui/src/thread_import.rs | 2 +- crates/agent_ui/src/thread_metadata_store.rs | 5 +- crates/agent_ui/src/threads_archive_view.rs | 2 +- crates/agent_ui/src/ui/mention_crease.rs | 2 +- crates/eval_cli/src/main.rs | 2 +- crates/sidebar/src/sidebar.rs | 2 +- crates/sidebar/src/thread_switcher.rs | 2 +- crates/zed/src/main.rs | 4 +- crates/zed/src/visual_test_runner.rs | 2 +- 62 files changed, 1853 insertions(+), 889 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 37bc8d7795e1bbad53da25b355320c9eee79b8fa..a35068e268d2586d9032ac182cbc41028a7e988a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -55,11 +55,13 @@ dependencies = [ "collections", "gpui", "language", + "log", "markdown", "project", "serde", "serde_json", "settings", + "smol", "theme_settings", "ui", "util", @@ -190,7 +192,7 @@ dependencies = [ "regex", "reqwest_client", "rust-embed", - "schemars", + "schemars 1.0.4", "serde", "serde_json", "settings", @@ -220,33 +222,53 @@ dependencies = [ [[package]] name = "agent-client-protocol" -version = "0.10.2" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c56a59cf6315e99f874d2c1f96c69d2da5ffe0087d211297fc4a41f849770a2" +checksum = "2af62fb84df2af0f933d8f5fd78b843fa5eb0ec5a48fa1b528c41951d0bbe36c" dependencies = [ + "agent-client-protocol-derive", "agent-client-protocol-schema", "anyhow", - "async-broadcast", - "async-trait", - "derive_more", "futures 0.3.32", - "log", + "futures-concurrency", + "jsonrpcmsg", + "rmcp", + "rustc-hash 2.1.1", + "schemars 1.0.4", "serde", "serde_json", + "thiserror 2.0.17", + "tokio", + "tokio-util", + "tracing", + "uuid", +] + +[[package]] +name = "agent-client-protocol-derive" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce42c2d3c048c12897eef2e577dfff1e3355c632c9f1625cc953b9df48b44631" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] name = "agent-client-protocol-schema" -version = "0.11.2" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0497b9a95a404e35799904835c57c6f8c69b9d08ccfd3cb5b7d746425cd6789" +checksum = "49bae57dad1c28a362fbdcf7bab0583316a02b45a70792109fced55780a3b63c" dependencies = [ "anyhow", "derive_more", - "schemars", + "schemars 1.0.4", "serde", "serde_json", + "serde_with", "strum 0.28.0", + "tracing", ] [[package]] @@ -258,8 +280,6 @@ dependencies = [ "action_log", "agent-client-protocol", "anyhow", - "async-pipe", - "async-trait", "chrono", "client", "collections", @@ -311,7 +331,7 @@ dependencies = [ "paths", "project", "regex", - "schemars", + "schemars 1.0.4", "serde", "serde_json", "serde_json_lenient", @@ -391,7 +411,7 @@ dependencies = [ "reqwest_client", "rope", "rules_library", - "schemars", + "schemars 1.0.4", "search", "semver", "serde", @@ -656,7 +676,7 @@ dependencies = [ "http_client", "language_model_core", "log", - "schemars", + "schemars 1.0.4", "serde", "serde_json", "strum 0.27.2", @@ -1304,7 +1324,7 @@ dependencies = [ "log", "num-rational", "num-traits", - "pastey", + "pastey 0.1.1", "rayon", "thiserror 2.0.17", "v_frame", @@ -1932,7 +1952,7 @@ dependencies = [ "aws-sdk-bedrockruntime", "aws-smithy-types", "futures 0.3.32", - "schemars", + "schemars 1.0.4", "serde", "serde_json", "strum 0.27.2", @@ -2144,7 +2164,7 @@ version = "3.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89ec27229c38ed0eb3c0feee3d2c1d6a4379ae44f418a29a658890e062d8f365" dependencies = [ - "darling 0.21.3", + "darling 0.23.0", "ident_case", "prettyplease", "proc-macro2", @@ -2672,7 +2692,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eadd868a2ce9ca38de7eeafdcec9c7065ef89b42b32f0839278d55f35c54d1ff" dependencies = [ "heck 0.4.1", - "indexmap", + "indexmap 2.11.4", "log", "proc-macro2", "quote", @@ -3292,7 +3312,7 @@ dependencies = [ name = "collections" version = "0.1.0" dependencies = [ - "indexmap", + "indexmap 2.11.4", "rustc-hash 2.1.1", ] @@ -3548,7 +3568,7 @@ dependencies = [ "parking_lot", "postage", "rand 0.9.3", - "schemars", + "schemars 1.0.4", "serde", "serde_json", "settings", @@ -4377,7 +4397,7 @@ checksum = "d74b6bcf49ebbd91f1b1875b706ea46545032a14003b5557b7dfa4bbeba6766e" dependencies = [ "cc", "codespan-reporting", - "indexmap", + "indexmap 2.11.4", "proc-macro2", "quote", "scratch", @@ -4392,7 +4412,7 @@ checksum = "94ca2ad69673c4b35585edfa379617ac364bccd0ba0adf319811ba3a74ffa48a" dependencies = [ "clap", "codespan-reporting", - "indexmap", + "indexmap 2.11.4", "proc-macro2", "quote", "syn 2.0.117", @@ -4410,7 +4430,7 @@ version = "1.0.187" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a8ebf0b6138325af3ec73324cb3a48b64d57721f17291b151206782e61f66cd" dependencies = [ - "indexmap", + "indexmap 2.11.4", "proc-macro2", "quote", "syn 2.0.117", @@ -4439,7 +4459,7 @@ dependencies = [ "parking_lot", "paths", "proto", - "schemars", + "schemars 1.0.4", "serde", "serde_json", "settings", @@ -4456,7 +4476,7 @@ name = "dap-types" version = "0.0.1" source = "git+https://github.com/zed-industries/dap-types?rev=1b461b310481d01e02b2603c16d7144b926339f8#1b461b310481d01e02b2603c16d7144b926339f8" dependencies = [ - "schemars", + "schemars 1.0.4", "serde", "serde_json", ] @@ -4507,6 +4527,16 @@ dependencies = [ "darling_macro 0.21.3", ] +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + [[package]] name = "darling_core" version = "0.20.11" @@ -4535,6 +4565,19 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + [[package]] name = "darling_macro" version = "0.20.11" @@ -4557,6 +4600,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", + "quote", + "syn 2.0.117", +] + [[package]] name = "dashmap" version = "6.1.0" @@ -4686,7 +4740,7 @@ dependencies = [ "pretty_assertions", "project", "rpc", - "schemars", + "schemars 1.0.4", "serde", "serde_json", "serde_json_lenient", @@ -4726,7 +4780,7 @@ dependencies = [ "anyhow", "futures 0.3.32", "http_client", - "schemars", + "schemars 1.0.4", "serde", "serde_json", ] @@ -5005,7 +5059,7 @@ dependencies = [ "jsonschema", "mdbook", "regex", - "schemars", + "schemars 1.0.4", "serde", "serde_json", "settings", @@ -5441,7 +5495,7 @@ dependencies = [ "release_channel", "rope", "rpc", - "schemars", + "schemars 1.0.4", "semver", "serde", "serde_json", @@ -6185,7 +6239,7 @@ dependencies = [ "fs", "gpui", "inventory", - "schemars", + "schemars 1.0.4", "serde_json", "settings", ] @@ -7138,7 +7192,7 @@ dependencies = [ "derive_more", "derive_setters", "gh-workflow-macros", - "indexmap", + "indexmap 2.11.4", "merge", "serde", "serde_json", @@ -7183,7 +7237,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" dependencies = [ "fallible-iterator", - "indexmap", + "indexmap 2.11.4", "stable_deref_trait", ] @@ -7220,7 +7274,7 @@ dependencies = [ "rand 0.9.3", "regex", "rope", - "schemars", + "schemars 1.0.4", "serde", "serde_json", "smallvec", @@ -7342,7 +7396,7 @@ dependencies = [ "rand 0.9.3", "remote", "remote_connection", - "schemars", + "schemars 1.0.4", "serde", "serde_json", "settings", @@ -7528,7 +7582,7 @@ dependencies = [ "http_client", "language_model_core", "log", - "schemars", + "schemars 1.0.4", "serde", "serde_json", "strum 0.27.2", @@ -7633,7 +7687,7 @@ dependencies = [ "reqwest_client", "resvg", "scheduler", - "schemars", + "schemars 1.0.4", "seahash", "serde", "serde_json", @@ -7778,7 +7832,7 @@ version = "0.1.0" dependencies = [ "derive_more", "gpui_util", - "schemars", + "schemars 1.0.4", "serde", ] @@ -7933,7 +7987,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap", + "indexmap 2.11.4", "slab", "tokio", "tokio-util", @@ -7952,7 +8006,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.3.1", - "indexmap", + "indexmap 2.11.4", "slab", "tokio", "tokio-util", @@ -8779,6 +8833,17 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.11.4" @@ -9189,7 +9254,7 @@ dependencies = [ "parking_lot", "paths", "project", - "schemars", + "schemars 1.0.4", "serde", "serde_json", "settings", @@ -9199,6 +9264,16 @@ dependencies = [ "util", ] +[[package]] +name = "jsonrpcmsg" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d833a15225c779251e13929203518c2ff26e2fe0f322d584b213f4f4dad37bd" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "jsonschema" version = "0.37.4" @@ -9451,7 +9526,7 @@ dependencies = [ "lsp", "parking_lot", "regex", - "schemars", + "schemars 1.0.4", "serde", "serde_json", "toml 0.8.23", @@ -9514,7 +9589,7 @@ dependencies = [ "gpui_shared_string", "http_client", "partial-json-fixer", - "schemars", + "schemars 1.0.4", "serde", "serde_json", "smol", @@ -9566,7 +9641,7 @@ dependencies = [ "opencode", "pretty_assertions", "release_channel", - "schemars", + "schemars 1.0.4", "serde", "serde_json", "settings", @@ -9593,7 +9668,7 @@ dependencies = [ "http_client", "language_model", "open_ai", - "schemars", + "schemars 1.0.4", "semver", "serde", "serde_json", @@ -10112,7 +10187,7 @@ dependencies = [ "anyhow", "futures 0.3.32", "http_client", - "schemars", + "schemars 1.0.4", "serde", "serde_json", ] @@ -10189,7 +10264,7 @@ dependencies = [ "parking_lot", "postage", "release_channel", - "schemars", + "schemars 1.0.4", "semver", "serde", "serde_json", @@ -10771,7 +10846,7 @@ dependencies = [ "anyhow", "futures 0.3.32", "http_client", - "schemars", + "schemars 1.0.4", "serde", "serde_json", "strum 0.27.2", @@ -10868,7 +10943,7 @@ dependencies = [ "half", "hashbrown 0.16.1", "hexf-parse", - "indexmap", + "indexmap 2.11.4", "libm", "log", "num-traits", @@ -10892,7 +10967,7 @@ dependencies = [ "half", "hashbrown 0.16.1", "hexf-parse", - "indexmap", + "indexmap 2.11.4", "libm", "log", "num-traits", @@ -11590,7 +11665,7 @@ checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" dependencies = [ "crc32fast", "hashbrown 0.15.5", - "indexmap", + "indexmap 2.11.4", "memchr", ] @@ -11643,7 +11718,7 @@ dependencies = [ "anyhow", "futures 0.3.32", "http_client", - "schemars", + "schemars 1.0.4", "serde", "serde_json", "settings", @@ -11667,7 +11742,7 @@ dependencies = [ "notifications", "picker", "project", - "schemars", + "schemars 1.0.4", "serde", "settings", "telemetry", @@ -11757,7 +11832,7 @@ dependencies = [ "log", "pretty_assertions", "rand 0.9.3", - "schemars", + "schemars 1.0.4", "serde", "serde_json", "strum 0.27.2", @@ -11775,7 +11850,7 @@ dependencies = [ "gpui", "picker", "project", - "schemars", + "schemars 1.0.4", "serde", "serde_json", "settings", @@ -11794,7 +11869,7 @@ dependencies = [ "futures 0.3.32", "http_client", "language_model_core", - "schemars", + "schemars 1.0.4", "serde", "serde_json", "settings", @@ -11810,7 +11885,7 @@ dependencies = [ "futures 0.3.32", "google_ai", "http_client", - "schemars", + "schemars 1.0.4", "serde", "serde_json", "strum 0.27.2", @@ -12127,6 +12202,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" +[[package]] +name = "pastey" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b867cad97c0791bbd3aaa6472142568c6c9e8f71937e98379f584cfb0cf35bec" + [[package]] name = "pathdiff" version = "0.2.3" @@ -12747,7 +12828,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset 0.4.2", - "indexmap", + "indexmap 2.11.4", ] [[package]] @@ -12861,7 +12942,7 @@ dependencies = [ "editor", "gpui", "menu", - "schemars", + "schemars 1.0.4", "serde", "settings", "theme", @@ -12987,7 +13068,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" dependencies = [ "base64 0.22.1", - "indexmap", + "indexmap 2.11.4", "quick-xml 0.38.3", "serde", "time", @@ -13369,7 +13450,7 @@ dependencies = [ "gpui", "http_client", "image", - "indexmap", + "indexmap 2.11.4", "itertools 0.14.0", "language", "log", @@ -13388,7 +13469,7 @@ dependencies = [ "release_channel", "remote", "rpc", - "schemars", + "schemars 1.0.4", "semver", "serde", "serde_json", @@ -13467,7 +13548,7 @@ dependencies = [ "project", "rayon", "remote_connection", - "schemars", + "schemars 1.0.4", "search", "serde", "serde_json", @@ -14468,7 +14549,7 @@ dependencies = [ "prost 0.9.0", "release_channel", "rpc", - "schemars", + "schemars 1.0.4", "semver", "serde", "serde_json", @@ -14824,6 +14905,41 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "rmcp" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2231b2c085b371c01bc90c0e6c1cab8834711b6394533375bdbf870b0166d419" +dependencies = [ + "async-trait", + "base64 0.22.1", + "chrono", + "futures 0.3.32", + "pastey 0.2.1", + "pin-project-lite", + "rmcp-macros", + "schemars 1.0.4", + "serde", + "serde_json", + "thiserror 2.0.17", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "rmcp-macros" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36ea0e100fadf81be85d7ff70f86cd805c7572601d4ab2946207f36540854b43" +dependencies = [ + "darling 0.23.0", + "proc-macro2", + "quote", + "serde_json", + "syn 2.0.117", +] + [[package]] name = "rmp" version = "0.8.14" @@ -15385,7 +15501,7 @@ dependencies = [ "anyhow", "clap", "env_logger 0.11.8", - "schemars", + "schemars 1.0.4", "serde", "serde_json", "settings", @@ -15393,14 +15509,27 @@ dependencies = [ "theme_settings", ] +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "schemars" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" dependencies = [ + "chrono", "dyn-clone", - "indexmap", + "indexmap 2.11.4", "ref-cast", "schemars_derive", "serde", @@ -15772,7 +15901,7 @@ version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ - "indexmap", + "indexmap 2.11.4", "itoa", "memchr", "ryu", @@ -15786,7 +15915,7 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e033097bf0d2b59a62b42c18ebbb797503839b26afdda2c4e1415cb6c813540" dependencies = [ - "indexmap", + "indexmap 2.11.4", "itoa", "memchr", "ryu", @@ -15845,13 +15974,44 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.11.4", + "schemars 0.9.0", + "schemars 1.0.4", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" +dependencies = [ + "darling 0.23.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "serde_yaml" version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap", + "indexmap 2.11.4", "itoa", "ryu", "serde", @@ -15898,7 +16058,7 @@ dependencies = [ "pretty_assertions", "release_channel", "rust-embed", - "schemars", + "schemars 1.0.4", "serde", "serde_json", "serde_json_lenient", @@ -15921,7 +16081,7 @@ dependencies = [ "gpui", "language_model_core", "log", - "schemars", + "schemars 1.0.4", "serde", "serde_json", "serde_json_lenient", @@ -16009,7 +16169,7 @@ dependencies = [ "regex", "release_channel", "rodio", - "schemars", + "schemars 1.0.4", "search", "serde", "serde_json", @@ -16399,7 +16559,7 @@ dependencies = [ "indoc", "parking_lot", "paths", - "schemars", + "schemars 1.0.4", "serde", "serde_json", "serde_json_lenient", @@ -16570,7 +16730,7 @@ dependencies = [ "futures-util", "hashbrown 0.15.5", "hashlink 0.10.0", - "indexmap", + "indexmap 2.11.4", "log", "memchr", "once_cell", @@ -17428,7 +17588,7 @@ dependencies = [ "menu", "picker", "project", - "schemars", + "schemars 1.0.4", "serde", "serde_json", "settings", @@ -17501,7 +17661,7 @@ dependencies = [ "parking_lot", "pretty_assertions", "proto", - "schemars", + "schemars 1.0.4", "serde", "serde_json", "serde_json_lenient", @@ -17605,7 +17765,7 @@ dependencies = [ "rand 0.9.3", "regex", "release_channel", - "schemars", + "schemars 1.0.4", "serde", "settings", "smol", @@ -17654,7 +17814,7 @@ dependencies = [ "release_channel", "remote", "rpc", - "schemars", + "schemars 1.0.4", "semver", "serde", "serde_json", @@ -17701,7 +17861,7 @@ dependencies = [ "palette", "parking_lot", "refineable", - "schemars", + "schemars 1.0.4", "serde", "serde_json", "serde_json_lenient", @@ -17731,7 +17891,7 @@ dependencies = [ "clap", "collections", "gpui", - "indexmap", + "indexmap 2.11.4", "log", "palette", "serde", @@ -17778,7 +17938,7 @@ dependencies = [ "log", "palette", "refineable", - "schemars", + "schemars 1.0.4", "serde", "serde_json", "serde_json_lenient", @@ -18001,7 +18161,7 @@ dependencies = [ "remote", "remote_connection", "rpc", - "schemars", + "schemars 1.0.4", "semver", "serde", "settings", @@ -18189,7 +18349,7 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" dependencies = [ - "indexmap", + "indexmap 2.11.4", "serde_core", "serde_spanned 1.0.3", "toml_datetime 0.7.3", @@ -18222,7 +18382,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap", + "indexmap 2.11.4", "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.11", @@ -18236,7 +18396,7 @@ version = "0.23.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" dependencies = [ - "indexmap", + "indexmap 2.11.4", "toml_datetime 0.7.3", "toml_parser", "winnow", @@ -18919,7 +19079,7 @@ dependencies = [ "icons", "itertools 0.14.0", "menu", - "schemars", + "schemars 1.0.4", "serde", "smallvec", "strum 0.27.2", @@ -19181,7 +19341,7 @@ dependencies = [ "rand 0.9.3", "regex", "rust-embed", - "schemars", + "schemars 1.0.4", "serde", "serde_json", "serde_json_lenient", @@ -19293,7 +19453,7 @@ name = "vercel" version = "0.1.0" dependencies = [ "anyhow", - "schemars", + "schemars 1.0.4", "serde", "strum 0.27.2", ] @@ -19344,7 +19504,7 @@ dependencies = [ "project_panel", "regex", "release_channel", - "schemars", + "schemars 1.0.4", "search", "semver", "serde", @@ -19628,7 +19788,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fd83062c17b9f4985d438603cde0a5e8c5c8198201a6937f778b607924c7da2" dependencies = [ "anyhow", - "indexmap", + "indexmap 2.11.4", "serde", "serde_derive", "serde_json", @@ -19646,7 +19806,7 @@ dependencies = [ "anyhow", "auditable-serde", "flate2", - "indexmap", + "indexmap 2.11.4", "serde", "serde_derive", "serde_json", @@ -19663,7 +19823,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", - "indexmap", + "indexmap 2.11.4", "wasm-encoder 0.244.0", "wasmparser 0.244.0", ] @@ -19700,7 +19860,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84e5df6dba6c0d7fafc63a450f1738451ed7a0b52295d83e868218fa286bf708" dependencies = [ "bitflags 2.10.0", - "indexmap", + "indexmap 2.11.4", "semver", ] @@ -19712,7 +19872,7 @@ checksum = "d06bfa36ab3ac2be0dee563380147a5b81ba10dd8885d7fbbc9eb574be67d185" dependencies = [ "bitflags 2.10.0", "hashbrown 0.15.5", - "indexmap", + "indexmap 2.11.4", "semver", "serde", ] @@ -19725,7 +19885,7 @@ checksum = "0f51cad774fb3c9461ab9bccc9c62dfb7388397b5deda31bf40e8108ccd678b2" dependencies = [ "bitflags 2.10.0", "hashbrown 0.15.5", - "indexmap", + "indexmap 2.11.4", "semver", ] @@ -19737,7 +19897,7 @@ checksum = "a9b1e81f3eb254cf7404a82cee6926a4a3ccc5aad80cc3d43608a070c67aa1d7" dependencies = [ "bitflags 2.10.0", "hashbrown 0.15.5", - "indexmap", + "indexmap 2.11.4", "semver", "serde", ] @@ -19750,7 +19910,7 @@ checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags 2.10.0", "hashbrown 0.15.5", - "indexmap", + "indexmap 2.11.4", "semver", ] @@ -19780,7 +19940,7 @@ dependencies = [ "cfg-if", "encoding_rs", "hashbrown 0.15.5", - "indexmap", + "indexmap 2.11.4", "libc", "log", "mach2 0.4.3", @@ -19837,7 +19997,7 @@ dependencies = [ "cranelift-bitset", "cranelift-entity", "gimli", - "indexmap", + "indexmap 2.11.4", "log", "object", "postcard", @@ -20023,7 +20183,7 @@ dependencies = [ "anyhow", "bitflags 2.10.0", "heck 0.5.0", - "indexmap", + "indexmap 2.11.4", "wit-parser 0.236.1", ] @@ -20360,7 +20520,7 @@ dependencies = [ "cfg_aliases 0.2.1", "document-features", "hashbrown 0.16.1", - "indexmap", + "indexmap 2.11.4", "log", "naga 29.0.0 (git+https://github.com/zed-industries/wgpu.git?branch=v29)", "once_cell", @@ -21507,7 +21667,7 @@ checksum = "d8a39a15d1ae2077688213611209849cad40e9e5cccf6e61951a425850677ff3" dependencies = [ "anyhow", "heck 0.4.1", - "indexmap", + "indexmap 2.11.4", "wasm-metadata 0.201.0", "wit-bindgen-core 0.22.0", "wit-component 0.201.0", @@ -21521,7 +21681,7 @@ checksum = "9d0809dc5ba19e2e98661bf32fc0addc5a3ca5bf3a6a7083aa6ba484085ff3ce" dependencies = [ "anyhow", "heck 0.5.0", - "indexmap", + "indexmap 2.11.4", "prettyplease", "syn 2.0.117", "wasm-metadata 0.227.1", @@ -21537,7 +21697,7 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck 0.5.0", - "indexmap", + "indexmap 2.11.4", "prettyplease", "syn 2.0.117", "wasm-metadata 0.244.0", @@ -21597,7 +21757,7 @@ checksum = "421c0c848a0660a8c22e2fd217929a0191f14476b68962afd2af89fd22e39825" dependencies = [ "anyhow", "bitflags 2.10.0", - "indexmap", + "indexmap 2.11.4", "log", "serde", "serde_derive", @@ -21616,7 +21776,7 @@ checksum = "635c3adc595422cbf2341a17fb73a319669cc8d33deed3a48368a841df86b676" dependencies = [ "anyhow", "bitflags 2.10.0", - "indexmap", + "indexmap 2.11.4", "log", "serde", "serde_derive", @@ -21635,7 +21795,7 @@ checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", "bitflags 2.10.0", - "indexmap", + "indexmap 2.11.4", "log", "serde", "serde_derive", @@ -21654,7 +21814,7 @@ checksum = "196d3ecfc4b759a8573bf86a9b3f8996b304b3732e4c7de81655f875f6efdca6" dependencies = [ "anyhow", "id-arena", - "indexmap", + "indexmap 2.11.4", "log", "semver", "serde", @@ -21672,7 +21832,7 @@ checksum = "ddf445ed5157046e4baf56f9138c124a0824d4d1657e7204d71886ad8ce2fc11" dependencies = [ "anyhow", "id-arena", - "indexmap", + "indexmap 2.11.4", "log", "semver", "serde", @@ -21690,7 +21850,7 @@ checksum = "16e4833a20cd6e85d6abfea0e63a399472d6f88c6262957c17f546879a80ba15" dependencies = [ "anyhow", "id-arena", - "indexmap", + "indexmap 2.11.4", "log", "semver", "serde", @@ -21708,7 +21868,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", - "indexmap", + "indexmap 2.11.4", "log", "semver", "serde", @@ -21760,7 +21920,7 @@ dependencies = [ "pretty_assertions", "project", "remote", - "schemars", + "schemars 1.0.4", "serde", "serde_json", "session", @@ -21901,7 +22061,7 @@ name = "x_ai" version = "0.1.0" dependencies = [ "anyhow", - "schemars", + "schemars 1.0.4", "serde", "strum 0.27.2", ] @@ -22008,7 +22168,7 @@ dependencies = [ "clap", "compliance", "gh-workflow", - "indexmap", + "indexmap 2.11.4", "indoc", "itertools 0.14.0", "regex", @@ -22484,7 +22644,7 @@ name = "zed_actions" version = "0.1.0" dependencies = [ "gpui", - "schemars", + "schemars 1.0.4", "serde", "util", "uuid", @@ -22735,7 +22895,7 @@ dependencies = [ "crc32fast", "crossbeam-utils", "displaydoc", - "indexmap", + "indexmap 2.11.4", "num_enum", "thiserror 1.0.69", ] diff --git a/Cargo.toml b/Cargo.toml index 448a4dd25c3b67f6f012194563cba996d3dc06bd..27f612605ffe2177298fe9c021e349faf37842b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -496,7 +496,7 @@ ztracing_macro = { path = "crates/ztracing_macro" } # External crates # -agent-client-protocol = { version = "=0.10.2", features = ["unstable"] } +agent-client-protocol = { version = "=0.11.1", features = ["unstable"] } aho-corasick = "1.1" alacritty_terminal = { git = "https://github.com/zed-industries/alacritty", rev = "9d9640d4" } any_vec = "0.14" diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 279ec6bf66802af72a68fe5202ea9684287217f2..cf4693beba7d4257e0fd828bb6ca61a78651fce4 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -3,7 +3,7 @@ mod diff; mod mention; mod terminal; use action_log::{ActionLog, ActionLogTelemetry}; -use agent_client_protocol::{self as acp}; +use agent_client_protocol::schema as acp; use anyhow::{Context as _, Result, anyhow}; use collections::HashSet; pub use connection::*; diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index 58e66a7685d524a20a284cdc227df2c5b5b4e59b..4bbf13bdb5ddcf8f14688b1e335594697c4e09c9 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -1,5 +1,5 @@ use crate::AcpThread; -use agent_client_protocol::{self as acp}; +use agent_client_protocol::schema as acp; use anyhow::Result; use chrono::{DateTime, Utc}; use collections::{HashMap, IndexMap}; @@ -954,7 +954,7 @@ mod test_support { fn truncate( &self, - _session_id: &agent_client_protocol::SessionId, + _session_id: &acp::SessionId, _cx: &App, ) -> Option> { Some(Rc::new(StubAgentSessionEditor)) diff --git a/crates/acp_thread/src/mention.rs b/crates/acp_thread/src/mention.rs index 28038ecbc04c59d1c5107872210056f11b413141..ac7b2d23cb796634fc61022411bb583808f697ef 100644 --- a/crates/acp_thread/src/mention.rs +++ b/crates/acp_thread/src/mention.rs @@ -1,4 +1,4 @@ -use agent_client_protocol as acp; +use agent_client_protocol::schema as acp; use anyhow::{Context as _, Result, bail}; use file_icons::FileIcons; use prompt_store::{PromptId, UserPromptId}; diff --git a/crates/acp_thread/src/terminal.rs b/crates/acp_thread/src/terminal.rs index fceb816f7f1471af1e5e2fb87f82bf66978c3df7..2fe769cb737b716acf495e561df826f6afa16d94 100644 --- a/crates/acp_thread/src/terminal.rs +++ b/crates/acp_thread/src/terminal.rs @@ -1,4 +1,4 @@ -use agent_client_protocol as acp; +use agent_client_protocol::schema as acp; use anyhow::Result; use futures::{FutureExt as _, future::Shared}; use gpui::{App, AppContext, AsyncApp, Context, Entity, Task}; diff --git a/crates/acp_tools/Cargo.toml b/crates/acp_tools/Cargo.toml index 8f14b1f93b32c6df521ea13ebf3f0f73e7ed755c..2d7162b9dec538f1596d00114886ab998dc25952 100644 --- a/crates/acp_tools/Cargo.toml +++ b/crates/acp_tools/Cargo.toml @@ -13,15 +13,20 @@ workspace = true path = "src/acp_tools.rs" doctest = false +[features] +test-support = ["workspace/test-support"] + [dependencies] agent-client-protocol.workspace = true collections.workspace = true gpui.workspace = true language.workspace= true +log.workspace = true markdown.workspace = true project.workspace = true serde.workspace = true serde_json.workspace = true +smol.workspace = true settings.workspace = true theme_settings.workspace = true ui.workspace = true diff --git a/crates/acp_tools/src/acp_tools.rs b/crates/acp_tools/src/acp_tools.rs index ae8a39c8df4f73ae8be6b748694dbde5d2a0c102..ea6de9f7d606bef5c317b1063fc4e839b26ff690 100644 --- a/crates/acp_tools/src/acp_tools.rs +++ b/crates/acp_tools/src/acp_tools.rs @@ -1,12 +1,13 @@ use std::{ - cell::RefCell, - collections::HashSet, + collections::{HashSet, VecDeque}, fmt::Display, - rc::{Rc, Weak}, - sync::Arc, + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, }; -use agent_client_protocol as acp; +use agent_client_protocol::schema as acp; use collections::HashMap; use gpui::{ App, Empty, Entity, EventEmitter, FocusHandle, Focusable, Global, ListAlignment, ListState, @@ -23,6 +24,111 @@ use workspace::{ Item, ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, }; +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum StreamMessageDirection { + Incoming, + Outgoing, + /// Lines captured from the agent's stderr. These are not part of the + /// JSON-RPC protocol, but agents often emit useful diagnostics there. + Stderr, +} + +#[derive(Clone)] +pub enum StreamMessageContent { + Request { + id: acp::RequestId, + method: Arc, + params: Option, + }, + Response { + id: acp::RequestId, + result: Result, acp::Error>, + }, + Notification { + method: Arc, + params: Option, + }, + /// A raw stderr line from the agent process. + Stderr { line: Arc }, +} + +#[derive(Clone)] +pub struct StreamMessage { + pub direction: StreamMessageDirection, + pub message: StreamMessageContent, +} + +impl StreamMessage { + /// Build a `StreamMessage` from a raw line captured off the transport. + /// + /// For `Stderr`, the line is wrapped as-is (no JSON parsing). For + /// `Incoming`/`Outgoing`, the line is parsed as JSON-RPC; returns `None` + /// if it doesn't look like a valid JSON-RPC message. + pub fn from_raw_line(direction: StreamMessageDirection, line: &str) -> Option { + if direction == StreamMessageDirection::Stderr { + return Some(StreamMessage { + direction, + message: StreamMessageContent::Stderr { + line: Arc::from(line), + }, + }); + } + + let value: serde_json::Value = serde_json::from_str(line).ok()?; + let obj = value.as_object()?; + + let parsed_id = obj + .get("id") + .map(|raw| serde_json::from_value::(raw.clone())); + + let message = if let Some(method) = obj.get("method").and_then(|m| m.as_str()) { + match parsed_id { + Some(Ok(id)) => StreamMessageContent::Request { + id, + method: method.into(), + params: obj.get("params").cloned(), + }, + Some(Err(err)) => { + log::warn!("Skipping JSON-RPC message with unparsable id: {err}"); + return None; + } + None => StreamMessageContent::Notification { + method: method.into(), + params: obj.get("params").cloned(), + }, + } + } else if let Some(parsed_id) = parsed_id { + let id = match parsed_id { + Ok(id) => id, + Err(err) => { + log::warn!("Skipping JSON-RPC response with unparsable id: {err}"); + return None; + } + }; + if let Some(error) = obj.get("error") { + let acp_err = + serde_json::from_value::(error.clone()).unwrap_or_else(|err| { + log::warn!("Failed to deserialize ACP error: {err}"); + acp::Error::internal_error().data(error.to_string()) + }); + StreamMessageContent::Response { + id, + result: Err(acp_err), + } + } else { + StreamMessageContent::Response { + id, + result: Ok(obj.get("result").cloned()), + } + } + } else { + return None; + }; + + Some(StreamMessage { direction, message }) + } +} + actions!(dev, [OpenAcpLogs]); pub fn init(cx: &mut App) { @@ -42,14 +148,87 @@ struct GlobalAcpConnectionRegistry(Entity); impl Global for GlobalAcpConnectionRegistry {} -#[derive(Default)] -pub struct AcpConnectionRegistry { - active_connection: RefCell>, +/// A raw line captured from the transport (or from stderr), tagged with +/// direction. Deserialization into [`StreamMessage`] happens on the +/// registry's foreground task so the ring buffer can be replayed to late +/// subscribers. +struct RawStreamLine { + direction: StreamMessageDirection, + line: Arc, } -struct ActiveConnection { - agent_id: AgentId, - connection: Weak, +/// Handle to an ACP connection's log tap. Passed back by +/// [`AcpConnectionRegistry::set_active_connection`] so that the connection +/// can publish transport and stderr lines without knowing anything about +/// the logs panel's channel. +/// +/// The tap carries a shared `enabled` flag that the registry flips on when +/// the first observer subscribes. Until then, `emit_*` methods are +/// effectively free: they check an atomic and return. This keeps the +/// logs panel's memory footprint opt-in — if no one ever opens it, the +/// transport never allocates a line or pushes a channel item. +#[derive(Clone)] +pub struct AcpLogTap { + enabled: Arc, + sender: smol::channel::Sender, +} + +impl AcpLogTap { + fn is_enabled(&self) -> bool { + self.enabled.load(Ordering::Relaxed) + } + + fn enable(&self) { + self.enabled.store(true, Ordering::Relaxed); + } + + fn emit(&self, direction: StreamMessageDirection, line: &str) { + if !self.is_enabled() { + return; + } + self.sender + .try_send(RawStreamLine { + direction, + line: Arc::from(line), + }) + .log_err(); + } + + /// Record a line read from the agent's stdout. + pub fn emit_incoming(&self, line: &str) { + self.emit(StreamMessageDirection::Incoming, line); + } + + /// Record a line written to the agent's stdin. + pub fn emit_outgoing(&self, line: &str) { + self.emit(StreamMessageDirection::Outgoing, line); + } + + /// Record a line read from the agent's stderr. + pub fn emit_stderr(&self, line: &str) { + self.emit(StreamMessageDirection::Stderr, line); + } +} + +/// Maximum number of messages retained in the registry's backlog. +/// +/// Mirrors `MAX_STORED_LOG_ENTRIES` in the LSP log store, so that opening the +/// ACP logs panel after a session has been running for a while still shows +/// meaningful history. +const MAX_BACKLOG_MESSAGES: usize = 2000; + +#[derive(Default)] +pub struct AcpConnectionRegistry { + active_agent_id: Option, + generation: u64, + /// Bounded ring buffer of every message observed on the current connection. + /// When a new connection is set, this is cleared. + backlog: VecDeque, + subscribers: Vec>, + /// The tap handed to the currently active connection, so the registry + /// can flip its `enabled` flag the first time someone subscribes. + active_tap: Option, + _broadcast_task: Option>, } impl AcpConnectionRegistry { @@ -63,17 +242,94 @@ impl AcpConnectionRegistry { } } + /// Register a new active connection and return an [`AcpLogTap`] that + /// the connection should hand to its transport + stderr readers. + /// + /// The tap starts out disabled: transport lines are dropped cheaply + /// until someone subscribes via [`Self::subscribe`], at which point + /// the tap is flipped on and subsequent lines are broadcast to all + /// current and future subscribers. pub fn set_active_connection( - &self, + &mut self, agent_id: AgentId, - connection: &Rc, cx: &mut Context, - ) { - self.active_connection.replace(Some(ActiveConnection { - agent_id, - connection: Rc::downgrade(connection), + ) -> AcpLogTap { + let (sender, raw_rx) = smol::channel::unbounded::(); + let tap = AcpLogTap { + enabled: Arc::new(AtomicBool::new(false)), + sender, + }; + + self.active_agent_id = Some(agent_id); + self.generation += 1; + self.backlog.clear(); + self.subscribers.clear(); + self.active_tap = Some(tap.clone()); + + self._broadcast_task = Some(cx.spawn(async move |this, cx| { + while let Ok(raw) = raw_rx.recv().await { + this.update(cx, |this, _cx| { + let Some(message) = StreamMessage::from_raw_line(raw.direction, &raw.line) + else { + return; + }; + + if this.backlog.len() == MAX_BACKLOG_MESSAGES { + this.backlog.pop_front(); + } + this.backlog.push_back(message.clone()); + + this.subscribers.retain(|sender| !sender.is_closed()); + for sender in &this.subscribers { + sender.try_send(message.clone()).log_err(); + } + }) + .log_err(); + } + + // The transport closed — clear state so observers (e.g. the ACP + // logs tab) can transition back to the disconnected state. + this.update(cx, |this, cx| { + this.active_agent_id = None; + this.subscribers.clear(); + this.active_tap = None; + cx.notify(); + }) + .log_err(); })); + cx.notify(); + tap + } + + /// Clear the retained message history for the current connection and force + /// watchers to resubscribe so their local correlation state is reset too. + pub fn clear_messages(&mut self, cx: &mut Context) { + self.backlog.clear(); + self.generation += 1; + self.subscribers.clear(); + cx.notify(); + } + + /// Subscribe to messages on the current connection. + /// + /// Returns the existing backlog (already-observed messages) together with + /// a receiver for new messages. The caller is responsible for flushing the + /// backlog into its local state before draining the receiver, so that no + /// messages are dropped between the snapshot and live subscription. + /// + /// The first subscription enables the connection's log tap; prior + /// messages are therefore not available. This is intentional: the tap + /// is opt-in so that the default case (no one ever opens the ACP logs + /// panel) performs zero per-message bookkeeping. + pub fn subscribe(&mut self) -> (Vec, smol::channel::Receiver) { + if let Some(tap) = &self.active_tap { + tap.enable(); + } + let backlog = self.backlog.iter().cloned().collect(); + let (sender, receiver) = smol::channel::unbounded(); + self.subscribers.push(sender); + (backlog, receiver) } } @@ -88,9 +344,9 @@ struct AcpTools { struct WatchedConnection { agent_id: AgentId, + generation: u64, messages: Vec, list_state: ListState, - connection: Weak, incoming_request_methods: HashMap>, outgoing_request_methods: HashMap>, _task: Task<()>, @@ -118,44 +374,54 @@ impl AcpTools { } fn update_connection(&mut self, cx: &mut Context) { - let active_connection = self.connection_registry.read(cx).active_connection.borrow(); - let Some(active_connection) = active_connection.as_ref() else { + let (generation, agent_id) = { + let registry = self.connection_registry.read(cx); + (registry.generation, registry.active_agent_id.clone()) + }; + + let Some(agent_id) = agent_id else { + self.watched_connection = None; + self.expanded.clear(); return; }; - if let Some(watched_connection) = self.watched_connection.as_ref() { - if Weak::ptr_eq( - &watched_connection.connection, - &active_connection.connection, - ) { + if let Some(watched) = self.watched_connection.as_ref() { + if watched.generation == generation { return; } } - if let Some(connection) = active_connection.connection.upgrade() { - let mut receiver = connection.subscribe(); - let task = cx.spawn(async move |this, cx| { - while let Ok(message) = receiver.recv().await { - this.update(cx, |this, cx| { - this.push_stream_message(message, cx); - }) - .ok(); - } - }); + self.expanded.clear(); - self.watched_connection = Some(WatchedConnection { - agent_id: active_connection.agent_id.clone(), - messages: vec![], - list_state: ListState::new(0, ListAlignment::Bottom, px(2048.)), - connection: active_connection.connection.clone(), - incoming_request_methods: HashMap::default(), - outgoing_request_methods: HashMap::default(), - _task: task, - }); + let (backlog, messages_rx) = self + .connection_registry + .update(cx, |registry, _cx| registry.subscribe()); + + let task = cx.spawn(async move |this, cx| { + while let Ok(message) = messages_rx.recv().await { + this.update(cx, |this, cx| { + this.push_stream_message(message, cx); + }) + .log_err(); + } + }); + + self.watched_connection = Some(WatchedConnection { + agent_id, + generation, + messages: vec![], + list_state: ListState::new(0, ListAlignment::Bottom, px(2048.)), + incoming_request_methods: HashMap::default(), + outgoing_request_methods: HashMap::default(), + _task: task, + }); + + for message in backlog { + self.push_stream_message(message, cx); } } - fn push_stream_message(&mut self, stream_message: acp::StreamMessage, cx: &mut Context) { + fn push_stream_message(&mut self, stream_message: StreamMessage, cx: &mut Context) { let Some(connection) = self.watched_connection.as_mut() else { return; }; @@ -163,27 +429,22 @@ impl AcpTools { let index = connection.messages.len(); let (request_id, method, message_type, params) = match stream_message.message { - acp::StreamMessageContent::Request { id, method, params } => { + StreamMessageContent::Request { id, method, params } => { let method_map = match stream_message.direction { - acp::StreamMessageDirection::Incoming => { - &mut connection.incoming_request_methods - } - acp::StreamMessageDirection::Outgoing => { - &mut connection.outgoing_request_methods - } + StreamMessageDirection::Incoming => &mut connection.incoming_request_methods, + StreamMessageDirection::Outgoing => &mut connection.outgoing_request_methods, + // Stderr lines never carry request/response correlation. + StreamMessageDirection::Stderr => return, }; method_map.insert(id.clone(), method.clone()); (Some(id), method.into(), MessageType::Request, Ok(params)) } - acp::StreamMessageContent::Response { id, result } => { + StreamMessageContent::Response { id, result } => { let method_map = match stream_message.direction { - acp::StreamMessageDirection::Incoming => { - &mut connection.outgoing_request_methods - } - acp::StreamMessageDirection::Outgoing => { - &mut connection.incoming_request_methods - } + StreamMessageDirection::Incoming => &mut connection.outgoing_request_methods, + StreamMessageDirection::Outgoing => &mut connection.incoming_request_methods, + StreamMessageDirection::Stderr => return, }; if let Some(method) = method_map.remove(&id) { @@ -197,9 +458,20 @@ impl AcpTools { ) } } - acp::StreamMessageContent::Notification { method, params } => { + StreamMessageContent::Notification { method, params } => { (None, method.into(), MessageType::Notification, Ok(params)) } + StreamMessageContent::Stderr { line } => { + // Stderr is rendered as plain text inline with JSON-RPC traffic, + // using `stderr` as the pseudo-method name so it shows up in the + // header the same way real methods do. + ( + None, + "stderr".into(), + MessageType::Stderr, + Ok(Some(serde_json::Value::String(line.to_string()))), + ) + } }; let message = WatchedConnectionMessage { @@ -243,8 +515,9 @@ impl AcpTools { }; Some(serde_json::json!({ "_direction": match message.direction { - acp::StreamMessageDirection::Incoming => "incoming", - acp::StreamMessageDirection::Outgoing => "outgoing", + StreamMessageDirection::Incoming => "incoming", + StreamMessageDirection::Outgoing => "outgoing", + StreamMessageDirection::Stderr => "stderr", }, "_type": message.message_type.to_string().to_lowercase(), "id": message.request_id, @@ -261,6 +534,8 @@ impl AcpTools { if let Some(connection) = self.watched_connection.as_mut() { connection.messages.clear(); connection.list_state.reset(0); + connection.incoming_request_methods.clear(); + connection.outgoing_request_methods.clear(); self.expanded.clear(); cx.notify(); } @@ -326,12 +601,15 @@ impl AcpTools { cx.notify() })) .child(match message.direction { - acp::StreamMessageDirection::Incoming => Icon::new(IconName::ArrowDown) + StreamMessageDirection::Incoming => Icon::new(IconName::ArrowDown) .color(Color::Error) .size(IconSize::Small), - acp::StreamMessageDirection::Outgoing => Icon::new(IconName::ArrowUp) + StreamMessageDirection::Outgoing => Icon::new(IconName::ArrowUp) .color(Color::Success) .size(IconSize::Small), + StreamMessageDirection::Stderr => Icon::new(IconName::Warning) + .color(Color::Warning) + .size(IconSize::Small), }) .child( Label::new(message.name.clone()) @@ -403,7 +681,7 @@ impl AcpTools { struct WatchedConnectionMessage { name: SharedString, request_id: Option, - direction: acp::StreamMessageDirection, + direction: StreamMessageDirection, message_type: MessageType, params: Result, acp::Error>, collapsed_params_md: Option>, @@ -463,6 +741,7 @@ enum MessageType { Request, Response, Notification, + Stderr, } impl Display for MessageType { @@ -471,6 +750,7 @@ impl Display for MessageType { MessageType::Request => write!(f, "Request"), MessageType::Response => write!(f, "Response"), MessageType::Notification => write!(f, "Notification"), + MessageType::Stderr => write!(f, "Stderr"), } } } @@ -561,6 +841,7 @@ impl Render for AcpToolsToolbarItemView { }; let acp_tools = acp_tools.clone(); + let connection_registry = acp_tools.read(cx).connection_registry.clone(); let has_messages = acp_tools .read(cx) .watched_connection @@ -585,6 +866,9 @@ impl Render for AcpToolsToolbarItemView { .tooltip(Tooltip::text("Clear Messages")) .disabled(!has_messages) .on_click(cx.listener(move |_this, _, _window, cx| { + connection_registry.update(cx, |registry, cx| { + registry.clear_messages(cx); + }); acp_tools.update(cx, |acp_tools, cx| { acp_tools.clear_messages(cx); }); diff --git a/crates/agent/src/agent.rs b/crates/agent/src/agent.rs index 02c3b3a11f988f5b9c018d1d8cabe94ba8fb52a8..9eb0f84b6fb3155200bcc529e4ded4945381211f 100644 --- a/crates/agent/src/agent.rs +++ b/crates/agent/src/agent.rs @@ -28,7 +28,7 @@ use acp_thread::{ AcpThread, AgentModelSelector, AgentSessionInfo, AgentSessionList, AgentSessionListRequest, AgentSessionListResponse, TokenUsageRatio, UserMessageId, }; -use agent_client_protocol as acp; +use agent_client_protocol::schema as acp; use anyhow::{Context as _, Result, anyhow}; use chrono::{DateTime, Utc}; use collections::{HashMap, HashSet, IndexMap}; diff --git a/crates/agent/src/db.rs b/crates/agent/src/db.rs index bde07a040869bf11a1b95bf433bf6af1e2d0a932..0ed03ed51703b0141fad5de4154e4f3c1cdf23cc 100644 --- a/crates/agent/src/db.rs +++ b/crates/agent/src/db.rs @@ -1,6 +1,6 @@ use crate::{AgentMessage, AgentMessageContent, UserMessage, UserMessageContent}; use acp_thread::UserMessageId; -use agent_client_protocol as acp; +use agent_client_protocol::schema as acp; use agent_settings::AgentProfileId; use anyhow::{Result, anyhow}; use chrono::{DateTime, Utc}; diff --git a/crates/agent/src/native_agent_server.rs b/crates/agent/src/native_agent_server.rs index e11e823caee7ad3b8968372f1b089f053a5fe721..b79cd67b598bfa2f5abec3c0287f587f14d64e29 100644 --- a/crates/agent/src/native_agent_server.rs +++ b/crates/agent/src/native_agent_server.rs @@ -1,6 +1,6 @@ use std::{any::Any, rc::Rc, sync::Arc}; -use agent_client_protocol as acp; +use agent_client_protocol::schema as acp; use agent_servers::{AgentServer, AgentServerDelegate}; use agent_settings::{AgentSettings, language_model_to_selection}; use anyhow::Result; diff --git a/crates/agent/src/tests/mod.rs b/crates/agent/src/tests/mod.rs index 16952e178aff86a2af4e04dfb58b05c2391b3b5c..a6419b52a0ee776dba0c7ef9ec65bd17f828a795 100644 --- a/crates/agent/src/tests/mod.rs +++ b/crates/agent/src/tests/mod.rs @@ -3,7 +3,7 @@ use acp_thread::{ AgentConnection, AgentModelGroupName, AgentModelList, PermissionOptions, ThreadStatus, UserMessageId, }; -use agent_client_protocol::{self as acp}; +use agent_client_protocol::schema as acp; use agent_settings::AgentProfileId; use anyhow::Result; use client::{Client, RefreshLlmTokenListener, UserStore}; @@ -5402,7 +5402,7 @@ async fn test_max_subagent_depth_prevents_tool_registration(cx: &mut TestAppCont cx, ); thread.set_subagent_context(SubagentContext { - parent_thread_id: agent_client_protocol::SessionId::new("parent-id"), + parent_thread_id: acp::SessionId::new("parent-id"), depth: MAX_SUBAGENT_DEPTH - 1, }); thread diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 432c8c74a143e13d19eaaf4462136e4db57843f3..da5602050de1096d4aea2d84de9f7f15eef4dbb3 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -12,7 +12,7 @@ use feature_flags::{ FeatureFlagAppExt as _, StreamingEditFileToolFeatureFlag, UpdatePlanToolFeatureFlag, }; -use agent_client_protocol as acp; +use agent_client_protocol::schema as acp; use agent_settings::{ AgentProfileId, AgentSettings, SUMMARIZE_THREAD_DETAILED_PROMPT, SUMMARIZE_THREAD_PROMPT, }; @@ -3445,7 +3445,7 @@ where T::description() } - fn kind(&self) -> agent_client_protocol::ToolKind { + fn kind(&self) -> acp::ToolKind { T::kind() } diff --git a/crates/agent/src/thread_store.rs b/crates/agent/src/thread_store.rs index e62ff78871c65311627aab8f6a6e3c00481a0c2b..f1367457d327d6f873f4c5cc7439759122e6be71 100644 --- a/crates/agent/src/thread_store.rs +++ b/crates/agent/src/thread_store.rs @@ -1,5 +1,5 @@ use crate::{DbThread, DbThreadMetadata, ThreadsDatabase}; -use agent_client_protocol as acp; +use agent_client_protocol::schema as acp; use anyhow::{Result, anyhow}; use gpui::{App, Context, Entity, Global, Task, prelude::*}; use util::path_list::PathList; diff --git a/crates/agent/src/tools/context_server_registry.rs b/crates/agent/src/tools/context_server_registry.rs index df4cc313036b55e8842a9c46567256afb92ed944..65b5df8abfe1c0686fbfde4f0fe8ce27489b0113 100644 --- a/crates/agent/src/tools/context_server_registry.rs +++ b/crates/agent/src/tools/context_server_registry.rs @@ -1,5 +1,5 @@ use crate::{AgentToolOutput, AnyAgentTool, ToolCallEventStream, ToolInput}; -use agent_client_protocol::ToolKind; +use agent_client_protocol::schema as acp; use anyhow::Result; use collections::{BTreeMap, HashMap}; use context_server::{ContextServerId, client::NotificationSubscription}; @@ -304,8 +304,8 @@ impl AnyAgentTool for ContextServerTool { self.tool.description.clone().unwrap_or_default().into() } - fn kind(&self) -> ToolKind { - ToolKind::Other + fn kind(&self) -> acp::ToolKind { + acp::ToolKind::Other } fn initial_title(&self, _input: serde_json::Value, _cx: &mut App) -> SharedString { diff --git a/crates/agent/src/tools/copy_path_tool.rs b/crates/agent/src/tools/copy_path_tool.rs index 06600f64874851c8d703513ea006d7f0327a0952..063742cef8b888a5d8716afa87787ddfc3c1f9b0 100644 --- a/crates/agent/src/tools/copy_path_tool.rs +++ b/crates/agent/src/tools/copy_path_tool.rs @@ -5,7 +5,7 @@ use super::tool_permissions::{ use crate::{ AgentTool, ToolCallEventStream, ToolInput, ToolPermissionDecision, decide_permission_for_paths, }; -use agent_client_protocol::ToolKind; +use agent_client_protocol::schema as acp; use agent_settings::AgentSettings; use futures::FutureExt as _; use gpui::{App, Entity, Task}; @@ -61,8 +61,8 @@ impl AgentTool for CopyPathTool { const NAME: &'static str = "copy_path"; - fn kind() -> ToolKind { - ToolKind::Move + fn kind() -> acp::ToolKind { + acp::ToolKind::Move } fn initial_title( @@ -198,7 +198,6 @@ impl AgentTool for CopyPathTool { #[cfg(test)] mod tests { use super::*; - use agent_client_protocol as acp; use fs::Fs as _; use gpui::TestAppContext; use project::{FakeFs, Project}; diff --git a/crates/agent/src/tools/create_directory_tool.rs b/crates/agent/src/tools/create_directory_tool.rs index 60bb44e39ee5ab76168d909c08889cbbbc63f9f4..0e5261d0715907e084e89cd57fb1547431aef785 100644 --- a/crates/agent/src/tools/create_directory_tool.rs +++ b/crates/agent/src/tools/create_directory_tool.rs @@ -2,7 +2,7 @@ use super::tool_permissions::{ SensitiveSettingsKind, authorize_symlink_access, canonicalize_worktree_roots, detect_symlink_escape, sensitive_settings_kind, }; -use agent_client_protocol::ToolKind; +use agent_client_protocol::schema as acp; use agent_settings::AgentSettings; use futures::FutureExt as _; use gpui::{App, Entity, SharedString, Task}; @@ -52,8 +52,8 @@ impl AgentTool for CreateDirectoryTool { const NAME: &'static str = "create_directory"; - fn kind() -> ToolKind { - ToolKind::Read + fn kind() -> acp::ToolKind { + acp::ToolKind::Read } fn initial_title( @@ -169,7 +169,6 @@ impl AgentTool for CreateDirectoryTool { #[cfg(test)] mod tests { use super::*; - use agent_client_protocol as acp; use fs::Fs as _; use gpui::TestAppContext; use project::{FakeFs, Project}; diff --git a/crates/agent/src/tools/delete_path_tool.rs b/crates/agent/src/tools/delete_path_tool.rs index 21b4674425d9169e7740dd35c929302814006684..d790896425885e568019588a6a522412f2ec7fc0 100644 --- a/crates/agent/src/tools/delete_path_tool.rs +++ b/crates/agent/src/tools/delete_path_tool.rs @@ -6,7 +6,7 @@ use crate::{ AgentTool, ToolCallEventStream, ToolInput, ToolPermissionDecision, decide_permission_for_path, }; use action_log::ActionLog; -use agent_client_protocol::ToolKind; +use agent_client_protocol::schema as acp; use agent_settings::AgentSettings; use futures::{FutureExt as _, SinkExt, StreamExt, channel::mpsc}; use gpui::{App, AppContext, Entity, SharedString, Task}; @@ -55,8 +55,8 @@ impl AgentTool for DeletePathTool { const NAME: &'static str = "delete_path"; - fn kind() -> ToolKind { - ToolKind::Delete + fn kind() -> acp::ToolKind { + acp::ToolKind::Delete } fn initial_title( @@ -228,7 +228,6 @@ impl AgentTool for DeletePathTool { #[cfg(test)] mod tests { use super::*; - use agent_client_protocol as acp; use fs::Fs as _; use gpui::TestAppContext; use project::{FakeFs, Project}; diff --git a/crates/agent/src/tools/diagnostics_tool.rs b/crates/agent/src/tools/diagnostics_tool.rs index 5889f66c2edbe06055678b19474447e0f23e2b0f..a59f61ae97a187fd36bc4c170d2e770262d74e9d 100644 --- a/crates/agent/src/tools/diagnostics_tool.rs +++ b/crates/agent/src/tools/diagnostics_tool.rs @@ -1,5 +1,5 @@ use crate::{AgentTool, ToolCallEventStream, ToolInput}; -use agent_client_protocol as acp; +use agent_client_protocol::schema as acp; use anyhow::Result; use futures::FutureExt as _; use gpui::{App, Entity, Task}; diff --git a/crates/agent/src/tools/edit_file_tool.rs b/crates/agent/src/tools/edit_file_tool.rs index 9bcf164096b99675febd3d7ae1bde8341f7c5ff8..85c17c58e8f2543097a3881cb93e840ee09834a9 100644 --- a/crates/agent/src/tools/edit_file_tool.rs +++ b/crates/agent/src/tools/edit_file_tool.rs @@ -6,7 +6,7 @@ use crate::{ edit_agent::{EditAgent, EditAgentOutputEvent, EditFormat}, }; use acp_thread::Diff; -use agent_client_protocol::{self as acp, ToolCallLocation, ToolCallUpdateFields}; +use agent_client_protocol::schema as acp; use anyhow::{Context as _, Result}; use collections::HashSet; use futures::{FutureExt as _, StreamExt as _}; @@ -260,7 +260,7 @@ impl AgentTool for EditFileTool { let abs_path = project.read(cx).absolute_path(&project_path, cx); if let Some(abs_path) = abs_path.clone() { event_stream.update_fields( - ToolCallUpdateFields::new() + acp::ToolCallUpdateFields::new() .locations(vec![acp::ToolCallLocation::new(abs_path)]), ); } @@ -409,7 +409,7 @@ impl AgentTool for EditFileTool { range.start.to_point(&buffer.snapshot()).row })); if let Some(abs_path) = abs_path.clone() { - event_stream.update_fields(ToolCallUpdateFields::new().locations(vec![ToolCallLocation::new(abs_path).line(line)])); + event_stream.update_fields(acp::ToolCallUpdateFields::new().locations(vec![acp::ToolCallLocation::new(abs_path).line(line)])); } emitted_location = true; } diff --git a/crates/agent/src/tools/fetch_tool.rs b/crates/agent/src/tools/fetch_tool.rs index 75880801595ad0604c9f3a1fac58bd916809a8ba..8723a6d9882df2c27cfcf8f53f10fbfac2cae0f4 100644 --- a/crates/agent/src/tools/fetch_tool.rs +++ b/crates/agent/src/tools/fetch_tool.rs @@ -2,7 +2,7 @@ use std::rc::Rc; use std::sync::Arc; use std::{borrow::Cow, cell::RefCell}; -use agent_client_protocol as acp; +use agent_client_protocol::schema as acp; use agent_settings::AgentSettings; use anyhow::{Context as _, Result, bail}; use futures::{AsyncReadExt as _, FutureExt as _}; diff --git a/crates/agent/src/tools/find_path_tool.rs b/crates/agent/src/tools/find_path_tool.rs index 9c65461503225171bcda482d58871a94743481e3..66d127e756ca838014e11f924c0d56545360c758 100644 --- a/crates/agent/src/tools/find_path_tool.rs +++ b/crates/agent/src/tools/find_path_tool.rs @@ -1,5 +1,5 @@ use crate::{AgentTool, ToolCallEventStream, ToolInput}; -use agent_client_protocol as acp; +use agent_client_protocol::schema as acp; use anyhow::{Result, anyhow}; use futures::FutureExt as _; use gpui::{App, AppContext, Entity, SharedString, Task}; diff --git a/crates/agent/src/tools/grep_tool.rs b/crates/agent/src/tools/grep_tool.rs index fbfdc18585b822361effb6fd770e678b3e434a17..a56c793bb856b7a531631837288ac0c171c1a394 100644 --- a/crates/agent/src/tools/grep_tool.rs +++ b/crates/agent/src/tools/grep_tool.rs @@ -1,5 +1,5 @@ use crate::{AgentTool, ToolCallEventStream, ToolInput}; -use agent_client_protocol as acp; +use agent_client_protocol::schema as acp; use anyhow::Result; use futures::{FutureExt as _, StreamExt}; use gpui::{App, Entity, SharedString, Task}; diff --git a/crates/agent/src/tools/list_directory_tool.rs b/crates/agent/src/tools/list_directory_tool.rs index c88492bba40ee4fdfa928f153e49a302ad60be8b..8431648b64a8a082cf27bd7bc216935169f586f8 100644 --- a/crates/agent/src/tools/list_directory_tool.rs +++ b/crates/agent/src/tools/list_directory_tool.rs @@ -3,7 +3,7 @@ use super::tool_permissions::{ resolve_project_path, }; use crate::{AgentTool, ToolCallEventStream, ToolInput}; -use agent_client_protocol::ToolKind; +use agent_client_protocol::schema as acp; use anyhow::{Context as _, Result, anyhow}; use gpui::{App, Entity, SharedString, Task}; use project::{Project, ProjectPath, WorktreeSettings}; @@ -127,8 +127,8 @@ impl AgentTool for ListDirectoryTool { const NAME: &'static str = "list_directory"; - fn kind() -> ToolKind { - ToolKind::Read + fn kind() -> acp::ToolKind { + acp::ToolKind::Read } fn initial_title( @@ -267,7 +267,6 @@ impl AgentTool for ListDirectoryTool { #[cfg(test)] mod tests { use super::*; - use agent_client_protocol as acp; use fs::Fs as _; use gpui::{TestAppContext, UpdateGlobal}; use indoc::indoc; diff --git a/crates/agent/src/tools/move_path_tool.rs b/crates/agent/src/tools/move_path_tool.rs index eaea204d84d96ab841f2e075a42a1a42b827374d..4a8aad8455019e776b77e326d5438cedd4518c70 100644 --- a/crates/agent/src/tools/move_path_tool.rs +++ b/crates/agent/src/tools/move_path_tool.rs @@ -5,7 +5,7 @@ use super::tool_permissions::{ use crate::{ AgentTool, ToolCallEventStream, ToolInput, ToolPermissionDecision, decide_permission_for_paths, }; -use agent_client_protocol::ToolKind; +use agent_client_protocol::schema as acp; use agent_settings::AgentSettings; use futures::FutureExt as _; use gpui::{App, Entity, SharedString, Task}; @@ -62,8 +62,8 @@ impl AgentTool for MovePathTool { const NAME: &'static str = "move_path"; - fn kind() -> ToolKind { - ToolKind::Move + fn kind() -> acp::ToolKind { + acp::ToolKind::Move } fn initial_title( @@ -205,7 +205,6 @@ impl AgentTool for MovePathTool { #[cfg(test)] mod tests { use super::*; - use agent_client_protocol as acp; use fs::Fs as _; use gpui::TestAppContext; use project::{FakeFs, Project}; diff --git a/crates/agent/src/tools/now_tool.rs b/crates/agent/src/tools/now_tool.rs index fe1cafe5881d14c9700813f742e1f2df0aa1203e..04aba44ff3a1f47ec72606a8eb9763f1ff2cbc14 100644 --- a/crates/agent/src/tools/now_tool.rs +++ b/crates/agent/src/tools/now_tool.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use agent_client_protocol as acp; +use agent_client_protocol::schema as acp; use chrono::{Local, Utc}; use gpui::{App, SharedString, Task}; use schemars::JsonSchema; diff --git a/crates/agent/src/tools/open_tool.rs b/crates/agent/src/tools/open_tool.rs index 344a513d10c2d62e4247dd3e47bcdf428586d6f0..dc72c758e36b049c9d9a865d5710e79f5c27befb 100644 --- a/crates/agent/src/tools/open_tool.rs +++ b/crates/agent/src/tools/open_tool.rs @@ -3,7 +3,7 @@ use super::tool_permissions::{ resolve_project_path, }; use crate::{AgentTool, ToolInput}; -use agent_client_protocol::ToolKind; +use agent_client_protocol::schema as acp; use futures::FutureExt as _; use gpui::{App, AppContext as _, Entity, SharedString, Task}; use project::Project; @@ -43,8 +43,8 @@ impl AgentTool for OpenTool { const NAME: &'static str = "open"; - fn kind() -> ToolKind { - ToolKind::Execute + fn kind() -> acp::ToolKind { + acp::ToolKind::Execute } fn initial_title( diff --git a/crates/agent/src/tools/read_file_tool.rs b/crates/agent/src/tools/read_file_tool.rs index 9b013f111e7eaa981652d8868dfcf3c098d9dc7e..4fa27114c8e2ea77e93afdc3dd9f0c710907a15c 100644 --- a/crates/agent/src/tools/read_file_tool.rs +++ b/crates/agent/src/tools/read_file_tool.rs @@ -1,5 +1,5 @@ use action_log::ActionLog; -use agent_client_protocol::{self as acp, ToolCallUpdateFields}; +use agent_client_protocol::schema as acp; use anyhow::{Context as _, Result, anyhow}; use futures::FutureExt as _; use gpui::{App, Entity, SharedString, Task}; @@ -200,7 +200,7 @@ impl AgentTool for ReadFileTool { let file_path = input.path.clone(); cx.update(|_cx| { - event_stream.update_fields(ToolCallUpdateFields::new().locations(vec![ + event_stream.update_fields(acp::ToolCallUpdateFields::new().locations(vec![ acp::ToolCallLocation::new(&abs_path) .line(input.start_line.map(|line| line.saturating_sub(1))), ])); @@ -228,7 +228,7 @@ impl AgentTool for ReadFileTool { .context("processing image") .map_err(tool_content_err)?; - event_stream.update_fields(ToolCallUpdateFields::new().content(vec![ + event_stream.update_fields(acp::ToolCallUpdateFields::new().content(vec![ acp::ToolCallContent::Content(acp::Content::new(acp::ContentBlock::Image( acp::ImageContent::new(language_model_image.source.clone(), "image/png"), ))), @@ -333,7 +333,7 @@ impl AgentTool for ReadFileTool { text, } .to_string(); - event_stream.update_fields(ToolCallUpdateFields::new().content(vec![ + event_stream.update_fields(acp::ToolCallUpdateFields::new().content(vec![ acp::ToolCallContent::Content(acp::Content::new(markdown)), ])); } @@ -347,7 +347,6 @@ impl AgentTool for ReadFileTool { #[cfg(test)] mod test { use super::*; - use agent_client_protocol as acp; use fs::Fs as _; use gpui::{AppContext, TestAppContext, UpdateGlobal as _}; use project::{FakeFs, Project}; diff --git a/crates/agent/src/tools/restore_file_from_disk_tool.rs b/crates/agent/src/tools/restore_file_from_disk_tool.rs index b808a966cf983c92a5e93c19599ff5333ed70860..6953e234e9574c1745e4fd204849584d74b72a88 100644 --- a/crates/agent/src/tools/restore_file_from_disk_tool.rs +++ b/crates/agent/src/tools/restore_file_from_disk_tool.rs @@ -3,7 +3,7 @@ use super::tool_permissions::{ canonicalize_worktree_roots, path_has_symlink_escape, resolve_project_path, sensitive_settings_kind, }; -use agent_client_protocol as acp; +use agent_client_protocol::schema as acp; use agent_settings::AgentSettings; use collections::FxHashSet; use futures::FutureExt as _; diff --git a/crates/agent/src/tools/save_file_tool.rs b/crates/agent/src/tools/save_file_tool.rs index 0cf9666a415f8174e9036ebadf8368589294c885..904e9ba8642f1b6cd15d6fa7d285d2969c11d81b 100644 --- a/crates/agent/src/tools/save_file_tool.rs +++ b/crates/agent/src/tools/save_file_tool.rs @@ -1,4 +1,4 @@ -use agent_client_protocol as acp; +use agent_client_protocol::schema as acp; use agent_settings::AgentSettings; use collections::FxHashSet; use futures::FutureExt as _; diff --git a/crates/agent/src/tools/spawn_agent_tool.rs b/crates/agent/src/tools/spawn_agent_tool.rs index 27afbbdc3ea05ddbfea689d1bb1a18c53b42198b..cdb36126f5763d22e8538b12b6b03610695fb982 100644 --- a/crates/agent/src/tools/spawn_agent_tool.rs +++ b/crates/agent/src/tools/spawn_agent_tool.rs @@ -1,5 +1,5 @@ use acp_thread::{SUBAGENT_SESSION_INFO_META_KEY, SubagentSessionInfo}; -use agent_client_protocol as acp; +use agent_client_protocol::schema as acp; use anyhow::Result; use gpui::{App, SharedString, Task}; use language_model::LanguageModelToolResultContent; diff --git a/crates/agent/src/tools/streaming_edit_file_tool.rs b/crates/agent/src/tools/streaming_edit_file_tool.rs index c988fede454ff6e8b4dc327c81132224a8b87a49..5f6d51ee2bb5c19e4eb4548f9edf4ec1ce014529 100644 --- a/crates/agent/src/tools/streaming_edit_file_tool.rs +++ b/crates/agent/src/tools/streaming_edit_file_tool.rs @@ -12,7 +12,7 @@ use crate::{ }; use acp_thread::Diff; use action_log::ActionLog; -use agent_client_protocol::{self as acp, ToolCallLocation, ToolCallUpdateFields}; +use agent_client_protocol::schema::{self as acp, ToolCallLocation, ToolCallUpdateFields}; use anyhow::Result; use collections::HashSet; use futures::FutureExt as _; diff --git a/crates/agent/src/tools/terminal_tool.rs b/crates/agent/src/tools/terminal_tool.rs index f36bd0fe3d3fb00931a7dc272d76eb042f6570f6..33560f2cf7cc7db6ae0abf6831c1c8050c901308 100644 --- a/crates/agent/src/tools/terminal_tool.rs +++ b/crates/agent/src/tools/terminal_tool.rs @@ -1,4 +1,4 @@ -use agent_client_protocol as acp; +use agent_client_protocol::schema as acp; use agent_settings::AgentSettings; use anyhow::Result; use futures::FutureExt as _; diff --git a/crates/agent/src/tools/update_plan_tool.rs b/crates/agent/src/tools/update_plan_tool.rs index 8d45f8aad42a8cb10b3164212e1cde2b0104bdc2..39e88590b1872ad959f82a432ab3eebb58759980 100644 --- a/crates/agent/src/tools/update_plan_tool.rs +++ b/crates/agent/src/tools/update_plan_tool.rs @@ -1,5 +1,5 @@ use crate::{AgentTool, ToolCallEventStream, ToolInput}; -use agent_client_protocol as acp; +use agent_client_protocol::schema as acp; use gpui::{App, SharedString, Task}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; diff --git a/crates/agent/src/tools/web_search_tool.rs b/crates/agent/src/tools/web_search_tool.rs index 75d7689fd7c8e22a4daf45f96f5517f7888977a4..3b4b5a4563ca67eaacc19ad327f27416f0c15ed5 100644 --- a/crates/agent/src/tools/web_search_tool.rs +++ b/crates/agent/src/tools/web_search_tool.rs @@ -4,7 +4,7 @@ use crate::{ AgentTool, ToolCallEventStream, ToolInput, ToolPermissionDecision, decide_permission_from_settings, }; -use agent_client_protocol as acp; +use agent_client_protocol::schema as acp; use agent_settings::AgentSettings; use anyhow::Result; use cloud_llm_client::WebSearchResponse; diff --git a/crates/agent_servers/Cargo.toml b/crates/agent_servers/Cargo.toml index 0b547e8a0af797c0d13819869c4cac4eb7f046fb..c8970ec57a905010e41730d562b060dfd6f33baa 100644 --- a/crates/agent_servers/Cargo.toml +++ b/crates/agent_servers/Cargo.toml @@ -6,7 +6,7 @@ publish.workspace = true license = "GPL-3.0-or-later" [features] -test-support = ["acp_thread/test-support", "gpui/test-support", "project/test-support", "dep:async-pipe", "dep:env_logger", "client/test-support", "dep:gpui_tokio", "reqwest_client/test-support"] +test-support = ["acp_tools/test-support", "acp_thread/test-support", "gpui/test-support", "project/test-support", "dep:env_logger", "client/test-support", "dep:gpui_tokio", "reqwest_client/test-support"] e2e = [] [lints] @@ -22,8 +22,6 @@ acp_thread.workspace = true action_log.workspace = true agent-client-protocol.workspace = true anyhow.workspace = true -async-pipe = { workspace = true, optional = true } -async-trait.workspace = true chrono.workspace = true client.workspace = true collections.workspace = true @@ -67,7 +65,6 @@ fs.workspace = true indoc.workspace = true acp_thread = { workspace = true, features = ["test-support"] } -async-pipe.workspace = true gpui = { workspace = true, features = ["test-support"] } gpui_tokio.workspace = true project = { workspace = true, features = ["test-support"] } diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs index aba5a1b55c566365e7637fbc58d414e9c2825eba..28ec60e404314a7c2ff974d3f47c8030b017c29c 100644 --- a/crates/agent_servers/src/acp.rs +++ b/crates/agent_servers/src/acp.rs @@ -4,14 +4,17 @@ use acp_thread::{ }; use acp_tools::AcpConnectionRegistry; use action_log::ActionLog; -use agent_client_protocol::{self as acp, Agent as _, ErrorCode}; +use agent_client_protocol::schema::{self as acp, ErrorCode}; +use agent_client_protocol::{ + Agent, Client, ConnectionTo, JsonRpcResponse, Lines, Responder, SentRequest, +}; use anyhow::anyhow; use collections::HashMap; use feature_flags::{AcpBetaFeatureFlag, FeatureFlagAppExt as _}; -use futures::AsyncBufReadExt as _; -use futures::FutureExt as _; +use futures::channel::mpsc; use futures::future::Shared; use futures::io::BufReader; +use futures::{AsyncBufReadExt as _, Future, FutureExt as _, StreamExt as _}; use project::agent_server_store::{AgentServerCommand, AgentServerStore}; use project::{AgentId, Project}; use remote::remote_client::Interactive; @@ -19,6 +22,7 @@ use serde::Deserialize; use std::path::PathBuf; use std::process::Stdio; use std::rc::Rc; +use std::sync::Arc; use std::{any::Any, cell::RefCell}; use task::{Shell, ShellBuilder, SpawnInTerminal}; use thiserror::Error; @@ -26,8 +30,6 @@ use util::ResultExt as _; use util::path_list::PathList; use util::process::Child; -use std::sync::Arc; - use anyhow::{Context as _, Result}; use gpui::{App, AppContext as _, AsyncApp, Entity, SharedString, Task, WeakEntity}; @@ -39,14 +41,195 @@ use crate::GEMINI_ID; pub const GEMINI_TERMINAL_AUTH_METHOD_ID: &str = "spawn-gemini-cli"; +/// Awaits the response to an ACP request from a GPUI foreground task. +/// +/// The ACP SDK offers two ways to consume a [`SentRequest`]: +/// - [`SentRequest::block_task`]: linear `.await` inside a spawned task. +/// - [`SentRequest::on_receiving_result`]: a callback invoked when the +/// response arrives, with the guarantee that no other inbound messages +/// are processed while the callback runs. This is the recommended form +/// inside SDK handler callbacks, where [`block_task`] would deadlock. +/// +/// We use `on_receiving_result` with a oneshot bridge here (rather than +/// [`block_task`]) so that our handler-side code paths can share a single +/// request-awaiting helper. The SDK callback itself is trivial (one channel +/// send) so the extra ordering guarantee it imposes on the dispatch loop is +/// negligible. +fn into_foreground_future( + sent: SentRequest, +) -> impl Future> { + let (tx, rx) = futures::channel::oneshot::channel(); + let spawn_result = sent.on_receiving_result(async move |result| { + tx.send(result).ok(); + Ok(()) + }); + async move { + spawn_result?; + rx.await.map_err(|_| { + acp::Error::internal_error() + .data("response channel cancelled — connection may have dropped") + })? + } +} + #[derive(Debug, Error)] #[error("Unsupported version")] pub struct UnsupportedVersion; +/// Helper for flattening the nested `Result` shapes that come out of +/// `entity.update(cx, |_, cx| fallible_op(cx))` into a single `Result`. +/// +/// `anyhow::Error` values get converted via `acp::Error::from`, which +/// downcasts an `acp::Error` back out of `anyhow` when present, so typed +/// errors like auth-required survive the trip. +trait FlattenAcpResult { + fn flatten_acp(self) -> Result; +} + +impl FlattenAcpResult for Result, anyhow::Error> { + fn flatten_acp(self) -> Result { + match self { + Ok(Ok(value)) => Ok(value), + Ok(Err(err)) => Err(err.into()), + Err(err) => Err(err.into()), + } + } +} + +impl FlattenAcpResult for Result, anyhow::Error> { + fn flatten_acp(self) -> Result { + match self { + Ok(Ok(value)) => Ok(value), + Ok(Err(err)) => Err(err), + Err(err) => Err(err.into()), + } + } +} + +/// Holds state needed by foreground work dispatched from background handler closures. +struct ClientContext { + sessions: Rc>>, + session_list: Rc>>>, +} + +fn dispatch_queue_closed_error() -> acp::Error { + acp::Error::internal_error().data("ACP foreground dispatch queue closed") +} + +/// Work items sent from `Send` handler closures to the `!Send` foreground thread. +trait ForegroundWorkItem: Send { + fn run(self: Box, cx: &mut AsyncApp, ctx: &ClientContext); + fn reject(self: Box); +} + +type ForegroundWork = Box; + +struct RequestForegroundWork +where + Req: Send + 'static, + Res: JsonRpcResponse + Send + 'static, +{ + request: Req, + responder: Responder, + handler: fn(Req, Responder, &mut AsyncApp, &ClientContext), +} + +impl ForegroundWorkItem for RequestForegroundWork +where + Req: Send + 'static, + Res: JsonRpcResponse + Send + 'static, +{ + fn run(self: Box, cx: &mut AsyncApp, ctx: &ClientContext) { + let Self { + request, + responder, + handler, + } = *self; + handler(request, responder, cx, ctx); + } + + fn reject(self: Box) { + let Self { responder, .. } = *self; + log::error!("ACP foreground dispatch queue closed while handling inbound request"); + responder + .respond_with_error(dispatch_queue_closed_error()) + .log_err(); + } +} + +struct NotificationForegroundWork +where + Notif: Send + 'static, +{ + notification: Notif, + connection: ConnectionTo, + handler: fn(Notif, &mut AsyncApp, &ClientContext), +} + +impl ForegroundWorkItem for NotificationForegroundWork +where + Notif: Send + 'static, +{ + fn run(self: Box, cx: &mut AsyncApp, ctx: &ClientContext) { + let Self { + notification, + handler, + .. + } = *self; + handler(notification, cx, ctx); + } + + fn reject(self: Box) { + let Self { connection, .. } = *self; + log::error!("ACP foreground dispatch queue closed while handling inbound notification"); + connection + .send_error_notification(dispatch_queue_closed_error()) + .log_err(); + } +} + +fn enqueue_request( + dispatch_tx: &mpsc::UnboundedSender, + request: Req, + responder: Responder, + handler: fn(Req, Responder, &mut AsyncApp, &ClientContext), +) where + Req: Send + 'static, + Res: JsonRpcResponse + Send + 'static, +{ + let work: ForegroundWork = Box::new(RequestForegroundWork { + request, + responder, + handler, + }); + if let Err(err) = dispatch_tx.unbounded_send(work) { + err.into_inner().reject(); + } +} + +fn enqueue_notification( + dispatch_tx: &mpsc::UnboundedSender, + notification: Notif, + connection: ConnectionTo, + handler: fn(Notif, &mut AsyncApp, &ClientContext), +) where + Notif: Send + 'static, +{ + let work: ForegroundWork = Box::new(NotificationForegroundWork { + notification, + connection, + handler, + }); + if let Err(err) = dispatch_tx.unbounded_send(work) { + err.into_inner().reject(); + } +} + pub struct AcpConnection { id: AgentId, telemetry_id: SharedString, - connection: Rc, + connection: ConnectionTo, sessions: Rc>>, pending_sessions: Rc>>, auth_methods: Vec, @@ -57,7 +240,8 @@ pub struct AcpConnection { default_config_options: HashMap, child: Option, session_list: Option>, - _io_task: Task>, + _io_task: Task<()>, + _dispatch_task: Task<()>, _wait_task: Task>, _stderr_task: Task>, } @@ -101,13 +285,13 @@ pub struct AcpSession { } pub struct AcpSessionList { - connection: Rc, + connection: ConnectionTo, updates_tx: smol::channel::Sender, updates_rx: smol::channel::Receiver, } impl AcpSessionList { - fn new(connection: Rc) -> Self { + fn new(connection: ConnectionTo) -> Self { let (tx, rx) = smol::channel::unbounded(); Self { connection, @@ -140,7 +324,9 @@ impl AgentSessionList for AcpSessionList { let acp_request = acp::ListSessionsRequest::new() .cwd(request.cwd) .cursor(request.cursor); - let response = conn.list_sessions(acp_request).await?; + let response = into_foreground_future(conn.send_request(acp_request)) + .await + .map_err(map_acp_error)?; Ok(AgentSessionListResponse { sessions: response .sessions @@ -206,6 +392,97 @@ pub async fn connect( const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::ProtocolVersion::V1; +/// Build a `Client` connection over `transport` with Zed's full +/// agent→client handler set wired up. +/// +/// All incoming requests and notifications are forwarded to the foreground +/// dispatch queue via `dispatch_tx`, where they are handled by the +/// `handle_*` functions on a GPUI context. The returned future drives the +/// connection and completes when the transport closes; callers are expected +/// to spawn it on a background executor and hold the task for the lifetime +/// of the connection. The `connection_tx` oneshot receives the +/// `ConnectionTo` handle as soon as the builder runs its `main_fn`. +fn connect_client_future( + name: &'static str, + transport: impl agent_client_protocol::ConnectTo + 'static, + dispatch_tx: mpsc::UnboundedSender, + connection_tx: futures::channel::oneshot::Sender>, +) -> impl Future> { + // Each handler forwards its inputs onto the foreground dispatch queue. + // The SDK requires the closure to be `Send`, so we move a clone of + // `dispatch_tx` into each one. + macro_rules! on_request { + ($handler:ident) => {{ + let dispatch_tx = dispatch_tx.clone(); + async move |req, responder, _connection| { + enqueue_request(&dispatch_tx, req, responder, $handler); + Ok(()) + } + }}; + } + macro_rules! on_notification { + ($handler:ident) => {{ + let dispatch_tx = dispatch_tx.clone(); + async move |notif, connection| { + enqueue_notification(&dispatch_tx, notif, connection, $handler); + Ok(()) + } + }}; + } + + Client + .builder() + .name(name) + // --- Request handlers (agent→client) --- + .on_receive_request( + on_request!(handle_request_permission), + agent_client_protocol::on_receive_request!(), + ) + .on_receive_request( + on_request!(handle_write_text_file), + agent_client_protocol::on_receive_request!(), + ) + .on_receive_request( + on_request!(handle_read_text_file), + agent_client_protocol::on_receive_request!(), + ) + .on_receive_request( + on_request!(handle_create_terminal), + agent_client_protocol::on_receive_request!(), + ) + .on_receive_request( + on_request!(handle_kill_terminal), + agent_client_protocol::on_receive_request!(), + ) + .on_receive_request( + on_request!(handle_release_terminal), + agent_client_protocol::on_receive_request!(), + ) + .on_receive_request( + on_request!(handle_terminal_output), + agent_client_protocol::on_receive_request!(), + ) + .on_receive_request( + on_request!(handle_wait_for_terminal_exit), + agent_client_protocol::on_receive_request!(), + ) + // --- Notification handlers (agent→client) --- + .on_receive_notification( + on_notification!(handle_session_notification), + agent_client_protocol::on_receive_notification!(), + ) + .connect_with( + transport, + move |connection: ConnectionTo| async move { + if connection_tx.send(connection).is_err() { + log::error!("failed to send ACP connection handle — receiver was dropped"); + } + // Keep the connection alive until the transport closes. + futures::future::pending::>().await + }, + ) +} + impl AcpConnection { pub async fn stdio( agent_id: AgentId, @@ -283,30 +560,93 @@ impl AcpConnection { let client_session_list: Rc>>> = Rc::new(RefCell::new(None)); - let client = ClientDelegate { + // Set up the foreground dispatch channel for bridging Send handler + // closures to the !Send foreground thread. + let (dispatch_tx, dispatch_rx) = mpsc::unbounded::(); + + // Register this connection with the logs panel registry. The + // returned tap is opt-in: until someone subscribes to the ACP logs + // panel, `emit_*` calls below are ~free (atomic load + return). + let log_tap = cx.update(|cx| { + AcpConnectionRegistry::default_global(cx).update(cx, |registry, cx| { + registry.set_active_connection(agent_id.clone(), cx) + }) + }); + + let incoming_lines = futures::io::BufReader::new(stdout).lines(); + let tapped_incoming = incoming_lines.inspect({ + let log_tap = log_tap.clone(); + move |result| match result { + Ok(line) => log_tap.emit_incoming(line), + Err(err) => { + // I/O errors on the transport are fatal for the SDK, but + // without logging them the ACP logs panel shows no trace + // of why the connection died. + log::warn!("ACP transport read error: {err}"); + } + } + }); + + let tapped_outgoing = futures::sink::unfold( + (Box::pin(stdin), log_tap.clone()), + async move |(mut writer, log_tap), line: String| { + use futures::AsyncWriteExt; + log_tap.emit_outgoing(&line); + let mut bytes = line.into_bytes(); + bytes.push(b'\n'); + writer.write_all(&bytes).await?; + Ok::<_, std::io::Error>((writer, log_tap)) + }, + ); + + let transport = Lines::new(tapped_outgoing, tapped_incoming); + + // `connect_client_future` installs the production handler set and + // hands us back both the connection-future (to run on a background + // executor) and a oneshot receiver that produces the + // `ConnectionTo` once the transport handshake is ready. + let (connection_tx, connection_rx) = futures::channel::oneshot::channel(); + let connection_future = + connect_client_future("zed", transport, dispatch_tx.clone(), connection_tx); + let io_task = cx.background_spawn(async move { + if let Err(err) = connection_future.await { + log::error!("ACP connection error: {err}"); + } + }); + + let connection: ConnectionTo = connection_rx + .await + .context("Failed to receive ACP connection handle")?; + + // Set up the foreground dispatch loop to process work items from handlers. + let dispatch_context = ClientContext { sessions: sessions.clone(), session_list: client_session_list.clone(), - cx: cx.clone(), }; - let (connection, io_task) = acp::ClientSideConnection::new(client, stdin, stdout, { - let foreground_executor = cx.foreground_executor().clone(); - move |fut| { - foreground_executor.spawn(fut).detach(); + let dispatch_task = cx.spawn({ + let mut dispatch_rx = dispatch_rx; + async move |cx| { + while let Some(work) = dispatch_rx.next().await { + work.run(cx, &dispatch_context); + } } }); - let io_task = cx.background_spawn(io_task); - - let stderr_task = cx.background_spawn(async move { - let mut stderr = BufReader::new(stderr); - let mut line = String::new(); - while let Ok(n) = stderr.read_line(&mut line).await - && n > 0 - { - log::warn!("agent stderr: {}", line.trim()); - line.clear(); + let stderr_task = cx.background_spawn({ + let log_tap = log_tap.clone(); + async move { + let mut stderr = BufReader::new(stderr); + let mut line = String::new(); + while let Ok(n) = stderr.read_line(&mut line).await + && n > 0 + { + let trimmed = line.trim_end_matches(['\n', '\r']); + log::warn!("agent stderr: {trimmed}"); + log_tap.emit_stderr(trimmed); + line.clear(); + } + Ok(()) } - Ok(()) }); let wait_task = cx.spawn({ @@ -319,16 +659,8 @@ impl AcpConnection { } }); - let connection = Rc::new(connection); - - cx.update(|cx| { - AcpConnectionRegistry::default_global(cx).update(cx, |registry, cx| { - registry.set_active_connection(agent_id.clone(), &connection, cx) - }); - }); - - let response = connection - .initialize( + let response = into_foreground_future( + connection.send_request( acp::InitializeRequest::new(acp::ProtocolVersion::V1) .client_capabilities( acp::ClientCapabilities::new() @@ -337,7 +669,6 @@ impl AcpConnection { .write_text_file(true)) .terminal(true) .auth(acp::AuthCapabilities::new().terminal(true)) - // Experimental: Allow for rendering terminal output from the agents .meta(acp::Meta::from_iter([ ("terminal_output".into(), true.into()), ("terminal-auth".into(), true.into()), @@ -347,8 +678,9 @@ impl AcpConnection { acp::Implementation::new("zed", version) .title(release_channel.map(ToOwned::to_owned)), ), - ) - .await?; + ), + ) + .await?; if response.protocol_version < MINIMUM_SUPPORTED_VERSION { return Err(UnsupportedVersion.into()); @@ -407,6 +739,7 @@ impl AcpConnection { default_config_options, session_list, _io_task: io_task, + _dispatch_task: dispatch_task, _wait_task: wait_task, _stderr_task: stderr_task, child: Some(child), @@ -419,11 +752,12 @@ impl AcpConnection { #[cfg(any(test, feature = "test-support"))] fn new_for_test( - connection: Rc, + connection: ConnectionTo, sessions: Rc>>, agent_capabilities: acp::AgentCapabilities, agent_server_store: WeakEntity, - io_task: Task>, + io_task: Task<()>, + dispatch_task: Task<()>, _cx: &mut App, ) -> Self { Self { @@ -441,6 +775,7 @@ impl AcpConnection { child: None, session_list: None, _io_task: io_task, + _dispatch_task: dispatch_task, _wait_task: Task::ready(Ok(())), _stderr_task: Task::ready(Ok(())), } @@ -453,7 +788,7 @@ impl AcpConnection { work_dirs: PathList, title: Option, rpc_call: impl FnOnce( - Rc, + ConnectionTo, acp::SessionId, PathBuf, ) @@ -647,14 +982,15 @@ impl AcpConnection { let config_opts = config_options.clone(); let conn = self.connection.clone(); async move |_| { - let result = conn - .set_session_config_option(acp::SetSessionConfigOptionRequest::new( + let result = into_foreground_future(conn.send_request( + acp::SetSessionConfigOptionRequest::new( session_id, config_id_clone.clone(), default_value_id, - )) - .await - .log_err(); + ), + )) + .await + .log_err(); if result.is_none() { if let Some(initial) = initial_value { @@ -781,17 +1117,23 @@ impl AgentConnection for AcpConnection { let mcp_servers = mcp_servers_for_project(&project, cx); cx.spawn(async move |cx| { - let response = self.connection - .new_session(acp::NewSessionRequest::new(cwd.clone()).mcp_servers(mcp_servers)) - .await - .map_err(map_acp_error)?; + let response = into_foreground_future( + self.connection + .send_request(acp::NewSessionRequest::new(cwd.clone()).mcp_servers(mcp_servers)), + ) + .await + .map_err(map_acp_error)?; - let (modes, models, config_options) = config_state(response.modes, response.models, response.config_options); + let (modes, models, config_options) = + config_state(response.modes, response.models, response.config_options); if let Some(default_mode) = self.default_mode.clone() { if let Some(modes) = modes.as_ref() { let mut modes_ref = modes.borrow_mut(); - let has_mode = modes_ref.available_modes.iter().any(|mode| mode.id == default_mode); + let has_mode = modes_ref + .available_modes + .iter() + .any(|mode| mode.id == default_mode); if has_mode { let initial_mode_id = modes_ref.current_mode_id.clone(); @@ -802,14 +1144,21 @@ impl AgentConnection for AcpConnection { let modes = modes.clone(); let conn = self.connection.clone(); async move |_| { - let result = conn.set_session_mode(acp::SetSessionModeRequest::new(session_id, default_mode)) - .await.log_err(); + let result = into_foreground_future( + conn.send_request(acp::SetSessionModeRequest::new( + session_id, + default_mode, + )), + ) + .await + .log_err(); if result.is_none() { modes.borrow_mut().current_mode_id = initial_mode_id; } } - }).detach(); + }) + .detach(); modes_ref.current_mode_id = default_mode; } else { @@ -830,7 +1179,10 @@ impl AgentConnection for AcpConnection { if let Some(default_model) = self.default_model.clone() { if let Some(models) = models.as_ref() { let mut models_ref = models.borrow_mut(); - let has_model = models_ref.available_models.iter().any(|model| model.model_id == default_model); + let has_model = models_ref + .available_models + .iter() + .any(|model| model.model_id == default_model); if has_model { let initial_model_id = models_ref.current_model_id.clone(); @@ -841,14 +1193,21 @@ impl AgentConnection for AcpConnection { let models = models.clone(); let conn = self.connection.clone(); async move |_| { - let result = conn.set_session_model(acp::SetSessionModelRequest::new(session_id, default_model)) - .await.log_err(); + let result = into_foreground_future( + conn.send_request(acp::SetSessionModelRequest::new( + session_id, + default_model, + )), + ) + .await + .log_err(); if result.is_none() { models.borrow_mut().current_model_id = initial_model_id; } } - }).detach(); + }) + .detach(); models_ref.current_model_id = default_model; } else { @@ -881,7 +1240,9 @@ impl AgentConnection for AcpConnection { action_log, response.session_id.clone(), // ACP doesn't currently support per-session prompt capabilities or changing capabilities dynamically. - watch::Receiver::constant(self.agent_capabilities.prompt_capabilities.clone()), + watch::Receiver::constant( + self.agent_capabilities.prompt_capabilities.clone(), + ), cx, ) }); @@ -935,12 +1296,14 @@ impl AgentConnection for AcpConnection { title, move |connection, session_id, cwd| { Box::pin(async move { - let response = connection - .load_session( - acp::LoadSessionRequest::new(session_id, cwd).mcp_servers(mcp_servers), - ) - .await - .map_err(map_acp_error)?; + let response = into_foreground_future( + connection.send_request( + acp::LoadSessionRequest::new(session_id.clone(), cwd) + .mcp_servers(mcp_servers), + ), + ) + .await + .map_err(map_acp_error)?; Ok(SessionConfigResponse { modes: response.modes, models: response.models, @@ -979,13 +1342,14 @@ impl AgentConnection for AcpConnection { title, move |connection, session_id, cwd| { Box::pin(async move { - let response = connection - .resume_session( - acp::ResumeSessionRequest::new(session_id, cwd) + let response = into_foreground_future( + connection.send_request( + acp::ResumeSessionRequest::new(session_id.clone(), cwd) .mcp_servers(mcp_servers), - ) - .await - .map_err(map_acp_error)?; + ), + ) + .await + .map_err(map_acp_error)?; Ok(SessionConfigResponse { modes: response.modes, models: response.models, @@ -1034,8 +1398,10 @@ impl AgentConnection for AcpConnection { let conn = self.connection.clone(); let session_id = session_id.clone(); return cx.foreground_executor().spawn(async move { - conn.close_session(acp::CloseSessionRequest::new(session_id)) - .await?; + into_foreground_future( + conn.send_request(acp::CloseSessionRequest::new(session_id)), + ) + .await?; Ok(()) }); } @@ -1059,8 +1425,10 @@ impl AgentConnection for AcpConnection { let conn = self.connection.clone(); let session_id = session_id.clone(); cx.foreground_executor().spawn(async move { - conn.close_session(acp::CloseSessionRequest::new(session_id)) - .await?; + into_foreground_future( + conn.send_request(acp::CloseSessionRequest::new(session_id.clone())), + ) + .await?; Ok(()) }) } @@ -1109,7 +1477,7 @@ impl AgentConnection for AcpConnection { fn authenticate(&self, method_id: acp::AuthMethodId, cx: &mut App) -> Task> { let conn = self.connection.clone(); cx.foreground_executor().spawn(async move { - conn.authenticate(acp::AuthenticateRequest::new(method_id)) + into_foreground_future(conn.send_request(acp::AuthenticateRequest::new(method_id))) .await?; Ok(()) }) @@ -1125,7 +1493,7 @@ impl AgentConnection for AcpConnection { let sessions = self.sessions.clone(); let session_id = params.session_id.clone(); cx.foreground_executor().spawn(async move { - let result = conn.prompt(params).await; + let result = into_foreground_future(conn.send_request(params)).await; let mut suppress_abort_err = false; @@ -1176,15 +1544,12 @@ impl AgentConnection for AcpConnection { }) } - fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { + fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) { if let Some(session) = self.sessions.borrow_mut().get_mut(session_id) { session.suppress_abort_err = true; } - let conn = self.connection.clone(); let params = acp::CancelNotification::new(session_id.clone()); - cx.foreground_executor() - .spawn(async move { conn.cancel(params).await }) - .detach(); + self.connection.send_notification(params).log_err(); } fn session_modes( @@ -1544,73 +1909,6 @@ pub mod test_support { } } - struct FakeAcpAgent { - load_session_count: Arc, - close_session_count: Arc, - fail_next_prompt: Arc, - } - - #[async_trait::async_trait(?Send)] - impl acp::Agent for FakeAcpAgent { - async fn initialize( - &self, - args: acp::InitializeRequest, - ) -> acp::Result { - Ok( - acp::InitializeResponse::new(args.protocol_version).agent_capabilities( - acp::AgentCapabilities::default() - .load_session(true) - .session_capabilities( - acp::SessionCapabilities::default() - .close(acp::SessionCloseCapabilities::new()), - ), - ), - ) - } - - async fn authenticate( - &self, - _: acp::AuthenticateRequest, - ) -> acp::Result { - Ok(Default::default()) - } - - async fn new_session( - &self, - _: acp::NewSessionRequest, - ) -> acp::Result { - Ok(acp::NewSessionResponse::new(acp::SessionId::new("unused"))) - } - - async fn prompt(&self, _: acp::PromptRequest) -> acp::Result { - if self.fail_next_prompt.swap(false, Ordering::SeqCst) { - Err(acp::ErrorCode::InternalError.into()) - } else { - Ok(acp::PromptResponse::new(acp::StopReason::EndTurn)) - } - } - - async fn cancel(&self, _: acp::CancelNotification) -> acp::Result<()> { - Ok(()) - } - - async fn load_session( - &self, - _: acp::LoadSessionRequest, - ) -> acp::Result { - self.load_session_count.fetch_add(1, Ordering::SeqCst); - Ok(acp::LoadSessionResponse::new()) - } - - async fn close_session( - &self, - _: acp::CloseSessionRequest, - ) -> acp::Result { - self.close_session_count.fetch_add(1, Ordering::SeqCst); - Ok(acp::CloseSessionResponse::new()) - } - } - async fn build_fake_acp_connection( project: Entity, load_session_count: Arc, @@ -1618,63 +1916,135 @@ pub mod test_support { fail_next_prompt: Arc, cx: &mut AsyncApp, ) -> Result { - let (c2a_writer, c2a_reader) = async_pipe::pipe(); - let (a2c_writer, a2c_reader) = async_pipe::pipe(); + let (client_transport, agent_transport) = agent_client_protocol::Channel::duplex(); let sessions: Rc>> = Rc::new(RefCell::new(HashMap::default())); - let session_list_container: Rc>>> = + let client_session_list: Rc>>> = Rc::new(RefCell::new(None)); - let foreground = cx.foreground_executor().clone(); - - let client_delegate = ClientDelegate { - sessions: sessions.clone(), - session_list: session_list_container, - cx: cx.clone(), - }; + let agent_future = Agent + .builder() + .name("fake-agent") + .on_receive_request( + async move |req: acp::InitializeRequest, responder, _cx| { + responder.respond( + acp::InitializeResponse::new(req.protocol_version).agent_capabilities( + acp::AgentCapabilities::default() + .load_session(true) + .session_capabilities( + acp::SessionCapabilities::default() + .close(acp::SessionCloseCapabilities::new()), + ), + ), + ) + }, + agent_client_protocol::on_receive_request!(), + ) + .on_receive_request( + async move |_req: acp::AuthenticateRequest, responder, _cx| { + responder.respond(Default::default()) + }, + agent_client_protocol::on_receive_request!(), + ) + .on_receive_request( + async move |_req: acp::NewSessionRequest, responder, _cx| { + responder.respond(acp::NewSessionResponse::new(acp::SessionId::new("unused"))) + }, + agent_client_protocol::on_receive_request!(), + ) + .on_receive_request( + { + let fail_next_prompt = fail_next_prompt.clone(); + async move |_req: acp::PromptRequest, responder, _cx| { + if fail_next_prompt.swap(false, Ordering::SeqCst) { + responder.respond_with_error(acp::ErrorCode::InternalError.into()) + } else { + responder.respond(acp::PromptResponse::new(acp::StopReason::EndTurn)) + } + } + }, + agent_client_protocol::on_receive_request!(), + ) + .on_receive_request( + { + let load_session_count = load_session_count.clone(); + async move |_req: acp::LoadSessionRequest, responder, _cx| { + load_session_count.fetch_add(1, Ordering::SeqCst); + responder.respond(acp::LoadSessionResponse::new()) + } + }, + agent_client_protocol::on_receive_request!(), + ) + .on_receive_request( + { + let close_session_count = close_session_count.clone(); + async move |_req: acp::CloseSessionRequest, responder, _cx| { + close_session_count.fetch_add(1, Ordering::SeqCst); + responder.respond(acp::CloseSessionResponse::new()) + } + }, + agent_client_protocol::on_receive_request!(), + ) + .on_receive_notification( + async move |_notif: acp::CancelNotification, _cx| Ok(()), + agent_client_protocol::on_receive_notification!(), + ) + .connect_to(agent_transport); - let (client_conn, client_io_task) = - acp::ClientSideConnection::new(client_delegate, c2a_writer, a2c_reader, { - let foreground = foreground.clone(); - move |fut| { - foreground.spawn(fut).detach(); - } - }); + let agent_io_task = cx.background_spawn(agent_future); - let fake_agent = FakeAcpAgent { - load_session_count: load_session_count.clone(), - close_session_count: close_session_count.clone(), - fail_next_prompt, - }; + // Wire the production handler set into the fake client so inbound + // requests/notifications from the fake agent are dispatched the + // same way the real `stdio` path does. + let (dispatch_tx, dispatch_rx) = mpsc::unbounded::(); - let (_, agent_io_task) = - acp::AgentSideConnection::new(fake_agent, a2c_writer, c2a_reader, { - let foreground = foreground.clone(); - move |fut| { - foreground.spawn(fut).detach(); - } - }); + let (connection_tx, connection_rx) = futures::channel::oneshot::channel(); + let client_future = connect_client_future( + "zed-test", + client_transport, + dispatch_tx.clone(), + connection_tx, + ); + let client_io_task = cx.background_spawn(async move { + client_future.await.ok(); + }); - let client_io_task = cx.background_spawn(client_io_task); - let agent_io_task = cx.background_spawn(agent_io_task); + let client_conn: ConnectionTo = connection_rx + .await + .context("failed to receive fake ACP connection handle")?; - let response = client_conn - .initialize(acp::InitializeRequest::new(acp::ProtocolVersion::V1)) - .await?; + let response = into_foreground_future( + client_conn.send_request(acp::InitializeRequest::new(acp::ProtocolVersion::V1)), + ) + .await?; let agent_capabilities = response.agent_capabilities; + let dispatch_context = ClientContext { + sessions: sessions.clone(), + session_list: client_session_list.clone(), + }; + let dispatch_task = cx.spawn({ + let mut dispatch_rx = dispatch_rx; + async move |cx| { + while let Some(work) = dispatch_rx.next().await { + work.run(cx, &dispatch_context); + } + } + }); + let agent_server_store = project.read_with(cx, |project, _| project.agent_server_store().downgrade()); let connection = cx.update(|cx| { AcpConnection::new_for_test( - Rc::new(client_conn), + client_conn, sessions, agent_capabilities, agent_server_store, client_io_task, + dispatch_task, cx, ) }); @@ -1846,100 +2216,6 @@ mod tests { assert_eq!(task.label, "Login"); } - struct FakeAcpAgent { - load_session_count: Arc, - close_session_count: Arc, - load_session_updates: Rc>>, - /// When `Some`, `load_session` will await a message on this receiver - /// before returning its response, allowing tests to interleave other - /// work (e.g. `close_session`) with an in-flight load. - load_session_gate: Rc>>>, - client: Rc>>>, - } - - #[async_trait::async_trait(?Send)] - impl acp::Agent for FakeAcpAgent { - async fn initialize( - &self, - args: acp::InitializeRequest, - ) -> acp::Result { - Ok( - acp::InitializeResponse::new(args.protocol_version).agent_capabilities( - acp::AgentCapabilities::default() - .load_session(true) - .session_capabilities( - acp::SessionCapabilities::default() - .close(acp::SessionCloseCapabilities::new()), - ), - ), - ) - } - - async fn authenticate( - &self, - _: acp::AuthenticateRequest, - ) -> acp::Result { - Ok(Default::default()) - } - - async fn new_session( - &self, - _: acp::NewSessionRequest, - ) -> acp::Result { - Ok(acp::NewSessionResponse::new(acp::SessionId::new("unused"))) - } - - async fn prompt(&self, _: acp::PromptRequest) -> acp::Result { - Ok(acp::PromptResponse::new(acp::StopReason::EndTurn)) - } - - async fn cancel(&self, _: acp::CancelNotification) -> acp::Result<()> { - Ok(()) - } - - async fn load_session( - &self, - args: acp::LoadSessionRequest, - ) -> acp::Result { - self.load_session_count.fetch_add(1, Ordering::SeqCst); - - // Simulate spec-compliant history replay: send notifications to the - // client before responding to the load request. - let updates = std::mem::take(&mut *self.load_session_updates.borrow_mut()); - if !updates.is_empty() { - let client = self - .client - .borrow() - .clone() - .expect("client should be set before load_session is called"); - for update in updates { - use acp::Client as _; - client - .session_notification(acp::SessionNotification::new( - args.session_id.clone(), - update, - )) - .await?; - } - } - - let gate = self.load_session_gate.borrow_mut().take(); - if let Some(gate) = gate { - gate.recv().await.ok(); - } - - Ok(acp::LoadSessionResponse::new()) - } - - async fn close_session( - &self, - _: acp::CloseSessionRequest, - ) -> acp::Result { - self.close_session_count.fetch_add(1, Ordering::SeqCst); - Ok(acp::CloseSessionResponse::new()) - } - } - async fn connect_fake_agent( cx: &mut gpui::TestAppContext, ) -> ( @@ -1947,8 +2223,8 @@ mod tests { Entity, Arc, Arc, - Rc>>, - Rc>>>, + Arc>>, + Arc>>>, Task>, ) { cx.update(|cx| { @@ -1962,74 +2238,170 @@ mod tests { let load_count = Arc::new(AtomicUsize::new(0)); let close_count = Arc::new(AtomicUsize::new(0)); - let load_session_updates: Rc>> = - Rc::new(RefCell::new(Vec::new())); - let load_session_gate: Rc>>> = - Rc::new(RefCell::new(None)); - let agent_client: Rc>>> = - Rc::new(RefCell::new(None)); + let load_session_updates: Arc>> = + Arc::new(std::sync::Mutex::new(Vec::new())); + let load_session_gate: Arc>>> = + Arc::new(std::sync::Mutex::new(None)); - let (c2a_writer, c2a_reader) = async_pipe::pipe(); - let (a2c_writer, a2c_reader) = async_pipe::pipe(); + let (client_transport, agent_transport) = agent_client_protocol::Channel::duplex(); let sessions: Rc>> = Rc::new(RefCell::new(HashMap::default())); - let session_list_container: Rc>>> = + let client_session_list: Rc>>> = Rc::new(RefCell::new(None)); - let foreground = cx.foreground_executor().clone(); + // Build the fake agent side. It handles the requests issued by + // `AcpConnection` during the test and tracks load/close counts. + let agent_future = Agent + .builder() + .name("fake-agent") + .on_receive_request( + async move |req: acp::InitializeRequest, responder, _cx| { + responder.respond( + acp::InitializeResponse::new(req.protocol_version).agent_capabilities( + acp::AgentCapabilities::default() + .load_session(true) + .session_capabilities( + acp::SessionCapabilities::default() + .close(acp::SessionCloseCapabilities::new()), + ), + ), + ) + }, + agent_client_protocol::on_receive_request!(), + ) + .on_receive_request( + async move |_req: acp::AuthenticateRequest, responder, _cx| { + responder.respond(Default::default()) + }, + agent_client_protocol::on_receive_request!(), + ) + .on_receive_request( + async move |_req: acp::NewSessionRequest, responder, _cx| { + responder.respond(acp::NewSessionResponse::new(acp::SessionId::new("unused"))) + }, + agent_client_protocol::on_receive_request!(), + ) + .on_receive_request( + async move |_req: acp::PromptRequest, responder, _cx| { + responder.respond(acp::PromptResponse::new(acp::StopReason::EndTurn)) + }, + agent_client_protocol::on_receive_request!(), + ) + .on_receive_request( + { + let load_count = load_count.clone(); + let load_session_updates = load_session_updates.clone(); + let load_session_gate = load_session_gate.clone(); + async move |req: acp::LoadSessionRequest, responder, cx| { + load_count.fetch_add(1, Ordering::SeqCst); + + // Simulate spec-compliant history replay: send + // notifications to the client before responding to the + // load request. + let updates = std::mem::take( + &mut *load_session_updates + .lock() + .expect("load_session_updates mutex poisoned"), + ); + for update in updates { + cx.send_notification(acp::SessionNotification::new( + req.session_id.clone(), + update, + ))?; + } - let client_delegate = ClientDelegate { - sessions: sessions.clone(), - session_list: session_list_container, - cx: cx.to_async(), - }; + // If a gate was installed, park on it before responding + // so tests can interleave other work (e.g. + // `close_session`) with an in-flight load. + let gate = load_session_gate + .lock() + .expect("load_session_gate mutex poisoned") + .take(); + if let Some(gate) = gate { + gate.recv().await.ok(); + } - let (client_conn, client_io_task) = - acp::ClientSideConnection::new(client_delegate, c2a_writer, a2c_reader, { - let foreground = foreground.clone(); - move |fut| { - foreground.spawn(fut).detach(); - } - }); + responder.respond(acp::LoadSessionResponse::new()) + } + }, + agent_client_protocol::on_receive_request!(), + ) + .on_receive_request( + { + let close_count = close_count.clone(); + async move |_req: acp::CloseSessionRequest, responder, _cx| { + close_count.fetch_add(1, Ordering::SeqCst); + responder.respond(acp::CloseSessionResponse::new()) + } + }, + agent_client_protocol::on_receive_request!(), + ) + .on_receive_notification( + async move |_notif: acp::CancelNotification, _cx| Ok(()), + agent_client_protocol::on_receive_notification!(), + ) + .connect_to(agent_transport); - let fake_agent = FakeAcpAgent { - load_session_count: load_count.clone(), - close_session_count: close_count.clone(), - load_session_updates: load_session_updates.clone(), - load_session_gate: load_session_gate.clone(), - client: agent_client.clone(), - }; + let agent_io_task = cx.background_spawn(agent_future); - let (agent_conn, agent_io_task) = - acp::AgentSideConnection::new(fake_agent, a2c_writer, c2a_reader, { - let foreground = foreground.clone(); - move |fut| { - foreground.spawn(fut).detach(); - } - }); - *agent_client.borrow_mut() = Some(Rc::new(agent_conn)); + // Wire the production handler set into the fake client so inbound + // requests/notifications from the fake agent reach the same + // dispatcher that the real `stdio` path uses. + let (dispatch_tx, dispatch_rx) = mpsc::unbounded::(); - let client_io_task = cx.background_spawn(client_io_task); - let agent_io_task = cx.background_spawn(agent_io_task); + let (connection_tx, connection_rx) = futures::channel::oneshot::channel(); + let client_future = connect_client_future( + "zed-test", + client_transport, + dispatch_tx.clone(), + connection_tx, + ); + let client_io_task = cx.background_spawn(async move { + client_future.await.ok(); + }); - let response = client_conn - .initialize(acp::InitializeRequest::new(acp::ProtocolVersion::V1)) + let client_conn: ConnectionTo = connection_rx .await - .expect("failed to initialize ACP connection"); + .expect("failed to receive ACP connection handle"); + + let response = into_foreground_future( + client_conn.send_request(acp::InitializeRequest::new(acp::ProtocolVersion::V1)), + ) + .await + .expect("failed to initialize ACP connection"); let agent_capabilities = response.agent_capabilities; + let dispatch_context = ClientContext { + sessions: sessions.clone(), + session_list: client_session_list.clone(), + }; + // `TestAppContext::spawn` hands out an `AsyncApp` by value, whereas the + // production path uses `Context::spawn` which hands out `&mut AsyncApp`. + // Bind the value-form to a local and take `&mut` of it to reuse the + // same dispatch loop shape. + let dispatch_task = cx.spawn({ + let mut dispatch_rx = dispatch_rx; + move |cx| async move { + let mut cx = cx; + while let Some(work) = dispatch_rx.next().await { + work.run(&mut cx, &dispatch_context); + } + } + }); + let agent_server_store = project.read_with(cx, |project, _| project.agent_server_store().downgrade()); let connection = cx.update(|cx| { AcpConnection::new_for_test( - Rc::new(client_conn), + client_conn, sessions, agent_capabilities, agent_server_store, client_io_task, + dispatch_task, cx, ) }); @@ -2155,7 +2527,9 @@ mod tests { // Queue up some history updates that the fake agent will stream to // the client during the `load_session` call, before responding. - *load_session_updates.borrow_mut() = vec![ + *load_session_updates + .lock() + .expect("load_session_updates mutex poisoned") = vec![ acp::SessionUpdate::UserMessageChunk(acp::ContentChunk::new(acp::ContentBlock::Text( acp::TextContent::new(String::from("hello agent")), ))), @@ -2222,7 +2596,9 @@ mod tests { // before sending its response. We'll close the session while the // load is parked. let (gate_tx, gate_rx) = smol::channel::bounded::<()>(1); - *load_session_gate.borrow_mut() = Some(gate_rx); + *load_session_gate + .lock() + .expect("load_session_gate mutex poisoned") = Some(gate_rx); let session_id = acp::SessionId::new("session-close-during-load"); let work_dirs = util::path_list::PathList::new(&[std::path::Path::new("/a")]); @@ -2315,7 +2691,9 @@ mod tests { ) = connect_fake_agent(cx).await; let (gate_tx, gate_rx) = smol::channel::bounded::<()>(1); - *load_session_gate.borrow_mut() = Some(gate_rx); + *load_session_gate + .lock() + .expect("load_session_gate mutex poisoned") = Some(gate_rx); let session_id = acp::SessionId::new("session-concurrent-close"); let work_dirs = util::path_list::PathList::new(&[std::path::Path::new("/a")]); @@ -2469,7 +2847,7 @@ fn config_state( struct AcpSessionModes { session_id: acp::SessionId, - connection: Rc, + connection: ConnectionTo, state: Rc>, } @@ -2493,9 +2871,10 @@ impl acp_thread::AgentSessionModes for AcpSessionModes { }; let state = self.state.clone(); cx.foreground_executor().spawn(async move { - let result = connection - .set_session_mode(acp::SetSessionModeRequest::new(session_id, mode_id)) - .await; + let result = into_foreground_future( + connection.send_request(acp::SetSessionModeRequest::new(session_id, mode_id)), + ) + .await; if result.is_err() { state.borrow_mut().current_mode_id = old_mode_id; @@ -2510,14 +2889,14 @@ impl acp_thread::AgentSessionModes for AcpSessionModes { struct AcpModelSelector { session_id: acp::SessionId, - connection: Rc, + connection: ConnectionTo, state: Rc>, } impl AcpModelSelector { fn new( session_id: acp::SessionId, - connection: Rc, + connection: ConnectionTo, state: Rc>, ) -> Self { Self { @@ -2552,9 +2931,10 @@ impl acp_thread::AgentModelSelector for AcpModelSelector { }; let state = self.state.clone(); cx.foreground_executor().spawn(async move { - let result = connection - .set_session_model(acp::SetSessionModelRequest::new(session_id, model_id)) - .await; + let result = into_foreground_future( + connection.send_request(acp::SetSessionModelRequest::new(session_id, model_id)), + ) + .await; if result.is_err() { state.borrow_mut().current_model_id = old_model_id; @@ -2582,7 +2962,7 @@ impl acp_thread::AgentModelSelector for AcpModelSelector { struct AcpSessionConfigOptions { session_id: acp::SessionId, - connection: Rc, + connection: ConnectionTo, state: Rc>>, watch_tx: Rc>>, watch_rx: watch::Receiver<()>, @@ -2606,11 +2986,10 @@ impl acp_thread::AgentSessionConfigOptions for AcpSessionConfigOptions { let watch_tx = self.watch_tx.clone(); cx.foreground_executor().spawn(async move { - let response = connection - .set_session_config_option(acp::SetSessionConfigOptionRequest::new( - session_id, config_id, value, - )) - .await?; + let response = into_foreground_future(connection.send_request( + acp::SetSessionConfigOptionRequest::new(session_id, config_id, value), + )) + .await?; *state.borrow_mut() = response.config_options.clone(); watch_tx.borrow_mut().send(()).ok(); @@ -2623,133 +3002,204 @@ impl acp_thread::AgentSessionConfigOptions for AcpSessionConfigOptions { } } -struct ClientDelegate { - sessions: Rc>>, - session_list: Rc>>>, - cx: AsyncApp, +// --------------------------------------------------------------------------- +// Handler functions dispatched from background handler closures to the +// foreground thread via the ForegroundWork channel. +// --------------------------------------------------------------------------- + +fn session_thread( + ctx: &ClientContext, + session_id: &acp::SessionId, +) -> Result, acp::Error> { + let sessions = ctx.sessions.borrow(); + sessions + .get(session_id) + .map(|session| session.thread.clone()) + .ok_or_else(|| acp::Error::internal_error().data(format!("unknown session: {session_id}"))) } -#[async_trait::async_trait(?Send)] -impl acp::Client for ClientDelegate { - async fn request_permission( - &self, - arguments: acp::RequestPermissionRequest, - ) -> Result { - let thread; - { - let sessions_ref = self.sessions.borrow(); - let session = sessions_ref - .get(&arguments.session_id) - .context("Failed to get session")?; - thread = session.thread.clone(); - } - - let cx = &mut self.cx.clone(); - - let task = thread.update(cx, |thread, cx| { - thread.request_tool_call_authorization( - arguments.tool_call, - acp_thread::PermissionOptions::Flat(arguments.options), - cx, - ) - })??; - - let outcome = task.await; +fn respond_err(responder: Responder, err: acp::Error) { + // Log the actual error we're returning — otherwise agents that hit an + // error path (e.g. unknown session) would see only the generic internal + // error returned over the wire with no trace of why on the client side. + log::warn!( + "Responding to ACP request `{method}` with error: {err:?}", + method = responder.method() + ); + responder.respond_with_error(err).log_err(); +} - Ok(acp::RequestPermissionResponse::new(outcome.into())) - } +fn handle_request_permission( + args: acp::RequestPermissionRequest, + responder: Responder, + cx: &mut AsyncApp, + ctx: &ClientContext, +) { + let thread = match session_thread(ctx, &args.session_id) { + Ok(t) => t, + Err(e) => return respond_err(responder, e), + }; - async fn write_text_file( - &self, - arguments: acp::WriteTextFileRequest, - ) -> Result { - let cx = &mut self.cx.clone(); - let task = self - .session_thread(&arguments.session_id)? - .update(cx, |thread, cx| { - thread.write_text_file(arguments.path, arguments.content, cx) - })?; + cx.spawn(async move |cx| { + let result: Result<_, acp::Error> = async { + let task = thread + .update(cx, |thread, cx| { + thread.request_tool_call_authorization( + args.tool_call, + acp_thread::PermissionOptions::Flat(args.options), + cx, + ) + }) + .flatten_acp()?; + Ok(task.await) + } + .await; - task.await?; + match result { + Ok(outcome) => { + responder + .respond(acp::RequestPermissionResponse::new(outcome.into())) + .log_err(); + } + Err(e) => respond_err(responder, e), + } + }) + .detach(); +} - Ok(Default::default()) - } +fn handle_write_text_file( + args: acp::WriteTextFileRequest, + responder: Responder, + cx: &mut AsyncApp, + ctx: &ClientContext, +) { + let thread = match session_thread(ctx, &args.session_id) { + Ok(t) => t, + Err(e) => return respond_err(responder, e), + }; - async fn read_text_file( - &self, - arguments: acp::ReadTextFileRequest, - ) -> Result { - let task = self.session_thread(&arguments.session_id)?.update( - &mut self.cx.clone(), - |thread, cx| { - thread.read_text_file(arguments.path, arguments.line, arguments.limit, false, cx) - }, - )?; + cx.spawn(async move |cx| { + let result: Result<_, acp::Error> = async { + thread + .update(cx, |thread, cx| { + thread.write_text_file(args.path, args.content, cx) + }) + .map_err(acp::Error::from)? + .await?; + Ok(()) + } + .await; - let content = task.await?; + match result { + Ok(()) => { + responder + .respond(acp::WriteTextFileResponse::default()) + .log_err(); + } + Err(e) => respond_err(responder, e), + } + }) + .detach(); +} - Ok(acp::ReadTextFileResponse::new(content)) - } +fn handle_read_text_file( + args: acp::ReadTextFileRequest, + responder: Responder, + cx: &mut AsyncApp, + ctx: &ClientContext, +) { + let thread = match session_thread(ctx, &args.session_id) { + Ok(t) => t, + Err(e) => return respond_err(responder, e), + }; - async fn session_notification( - &self, - notification: acp::SessionNotification, - ) -> Result<(), acp::Error> { - let (thread, session_modes, session_config_options) = { - let sessions = self.sessions.borrow(); - let session = sessions - .get(¬ification.session_id) - .context("Failed to get session")?; - ( - session.thread.clone(), - session.session_modes.clone(), - session.config_options.clone(), - ) - }; + cx.spawn(async move |cx| { + let result: Result<_, acp::Error> = async { + thread + .update(cx, |thread, cx| { + thread.read_text_file(args.path, args.line, args.limit, false, cx) + }) + .map_err(acp::Error::from)? + .await + } + .await; - if let acp::SessionUpdate::CurrentModeUpdate(acp::CurrentModeUpdate { - current_mode_id, - .. - }) = ¬ification.update - { - if let Some(session_modes) = &session_modes { - session_modes.borrow_mut().current_mode_id = current_mode_id.clone(); + match result { + Ok(content) => { + responder + .respond(acp::ReadTextFileResponse::new(content)) + .log_err(); } + Err(e) => respond_err(responder, e), } + }) + .detach(); +} - if let acp::SessionUpdate::ConfigOptionUpdate(acp::ConfigOptionUpdate { - config_options, - .. - }) = ¬ification.update - { - if let Some(opts) = &session_config_options { - *opts.config_options.borrow_mut() = config_options.clone(); - opts.tx.borrow_mut().send(()).ok(); - } +fn handle_session_notification( + notification: acp::SessionNotification, + cx: &mut AsyncApp, + ctx: &ClientContext, +) { + // Extract everything we need from the session while briefly borrowing. + let (thread, session_modes, config_opts_data) = { + let sessions = ctx.sessions.borrow(); + let Some(session) = sessions.get(¬ification.session_id) else { + log::warn!( + "Received session notification for unknown session: {:?}", + notification.session_id + ); + return; + }; + ( + session.thread.clone(), + session.session_modes.clone(), + session + .config_options + .as_ref() + .map(|opts| (opts.config_options.clone(), opts.tx.clone())), + ) + }; + // Borrow is dropped here. + + // Apply mode/config/session_list updates without holding the borrow. + if let acp::SessionUpdate::CurrentModeUpdate(acp::CurrentModeUpdate { + current_mode_id, .. + }) = ¬ification.update + { + if let Some(session_modes) = &session_modes { + session_modes.borrow_mut().current_mode_id = current_mode_id.clone(); } + } - if let acp::SessionUpdate::SessionInfoUpdate(info_update) = ¬ification.update - && let Some(session_list) = self.session_list.borrow().as_ref() - { - session_list.send_info_update(notification.session_id.clone(), info_update.clone()); + if let acp::SessionUpdate::ConfigOptionUpdate(acp::ConfigOptionUpdate { + config_options, .. + }) = ¬ification.update + { + if let Some((config_opts_cell, tx_cell)) = &config_opts_data { + *config_opts_cell.borrow_mut() = config_options.clone(); + tx_cell.borrow_mut().send(()).ok(); } + } - // Clone so we can inspect meta both before and after handing off to the thread - let update_clone = notification.update.clone(); + if let acp::SessionUpdate::SessionInfoUpdate(info_update) = ¬ification.update + && let Some(session_list) = ctx.session_list.borrow().as_ref() + { + session_list.send_info_update(notification.session_id.clone(), info_update.clone()); + } - // Pre-handle: if a ToolCall carries terminal_info, create/register a display-only terminal. - if let acp::SessionUpdate::ToolCall(tc) = &update_clone { - if let Some(meta) = &tc.meta { - if let Some(terminal_info) = meta.get("terminal_info") { - if let Some(id_str) = terminal_info.get("terminal_id").and_then(|v| v.as_str()) - { - let terminal_id = acp::TerminalId::new(id_str); - let cwd = terminal_info - .get("cwd") - .and_then(|v| v.as_str().map(PathBuf::from)); + // Pre-handle: if a ToolCall carries terminal_info, create/register a display-only terminal. + if let acp::SessionUpdate::ToolCall(tc) = ¬ification.update { + if let Some(meta) = &tc.meta { + if let Some(terminal_info) = meta.get("terminal_info") { + if let Some(id_str) = terminal_info.get("terminal_id").and_then(|v| v.as_str()) { + let terminal_id = acp::TerminalId::new(id_str); + let cwd = terminal_info + .get("cwd") + .and_then(|v| v.as_str().map(PathBuf::from)); - // Create a minimal display-only lower-level terminal and register it. - let _ = thread.update(&mut self.cx.clone(), |thread, cx| { + thread + .update(cx, |thread, cx| { let builder = TerminalBuilder::new_display_only( CursorShape::default(), AlternateScroll::On, @@ -2770,53 +3220,64 @@ impl acp::Client for ClientDelegate { cx, ); anyhow::Ok(()) - }); - } + }) + .log_err(); } } } + } - // Forward the update to the acp_thread as usual. - thread.update(&mut self.cx.clone(), |thread, cx| { + // Forward the update to the acp_thread as usual. + if let Err(err) = thread + .update(cx, |thread, cx| { thread.handle_session_update(notification.update.clone(), cx) - })??; - - // Post-handle: stream terminal output/exit if present on ToolCallUpdate meta. - if let acp::SessionUpdate::ToolCallUpdate(tcu) = &update_clone { - if let Some(meta) = &tcu.meta { - if let Some(term_out) = meta.get("terminal_output") { - if let Some(id_str) = term_out.get("terminal_id").and_then(|v| v.as_str()) { - let terminal_id = acp::TerminalId::new(id_str); - if let Some(s) = term_out.get("data").and_then(|v| v.as_str()) { - let data = s.as_bytes().to_vec(); - let _ = thread.update(&mut self.cx.clone(), |thread, cx| { + }) + .flatten_acp() + { + log::error!( + "Failed to handle session update for {:?}: {err:?}", + notification.session_id + ); + } + + // Post-handle: stream terminal output/exit if present on ToolCallUpdate meta. + if let acp::SessionUpdate::ToolCallUpdate(tcu) = ¬ification.update { + if let Some(meta) = &tcu.meta { + if let Some(term_out) = meta.get("terminal_output") { + if let Some(id_str) = term_out.get("terminal_id").and_then(|v| v.as_str()) { + let terminal_id = acp::TerminalId::new(id_str); + if let Some(s) = term_out.get("data").and_then(|v| v.as_str()) { + let data = s.as_bytes().to_vec(); + thread + .update(cx, |thread, cx| { thread.on_terminal_provider_event( TerminalProviderEvent::Output { terminal_id, data }, cx, ); - }); - } + }) + .log_err(); } } + } - // terminal_exit - if let Some(term_exit) = meta.get("terminal_exit") { - if let Some(id_str) = term_exit.get("terminal_id").and_then(|v| v.as_str()) { - let terminal_id = acp::TerminalId::new(id_str); - let status = acp::TerminalExitStatus::new() - .exit_code( - term_exit - .get("exit_code") - .and_then(|v| v.as_u64()) - .map(|i| i as u32), - ) - .signal( - term_exit - .get("signal") - .and_then(|v| v.as_str().map(|s| s.to_string())), - ); + if let Some(term_exit) = meta.get("terminal_exit") { + if let Some(id_str) = term_exit.get("terminal_id").and_then(|v| v.as_str()) { + let terminal_id = acp::TerminalId::new(id_str); + let status = acp::TerminalExitStatus::new() + .exit_code( + term_exit + .get("exit_code") + .and_then(|v| v.as_u64()) + .map(|i| i as u32), + ) + .signal( + term_exit + .get("signal") + .and_then(|v| v.as_str().map(|s| s.to_string())), + ); - let _ = thread.update(&mut self.cx.clone(), |thread, cx| { + thread + .update(cx, |thread, cx| { thread.on_terminal_provider_event( TerminalProviderEvent::Exit { terminal_id, @@ -2824,118 +3285,183 @@ impl acp::Client for ClientDelegate { }, cx, ); - }); - } + }) + .log_err(); } } } - - Ok(()) } +} - async fn create_terminal( - &self, - args: acp::CreateTerminalRequest, - ) -> Result { - let thread = self.session_thread(&args.session_id)?; - let project = thread.read_with(&self.cx, |thread, _cx| thread.project().clone())?; - - let terminal_entity = acp_thread::create_terminal_entity( - args.command.clone(), - &args.args, - args.env - .into_iter() - .map(|env| (env.name, env.value)) - .collect(), - args.cwd.clone(), - &project, - &mut self.cx.clone(), - ) - .await?; +fn handle_create_terminal( + args: acp::CreateTerminalRequest, + responder: Responder, + cx: &mut AsyncApp, + ctx: &ClientContext, +) { + let thread = match session_thread(ctx, &args.session_id) { + Ok(t) => t, + Err(e) => return respond_err(responder, e), + }; + let project = match thread + .read_with(cx, |thread, _cx| thread.project().clone()) + .map_err(acp::Error::from) + { + Ok(p) => p, + Err(e) => return respond_err(responder, e), + }; - // Register with renderer - let terminal_entity = thread.update(&mut self.cx.clone(), |thread, cx| { - thread.register_terminal_created( - acp::TerminalId::new(uuid::Uuid::new_v4().to_string()), - format!("{} {}", args.command, args.args.join(" ")), + cx.spawn(async move |cx| { + let result: Result<_, acp::Error> = async { + let terminal_entity = acp_thread::create_terminal_entity( + args.command.clone(), + &args.args, + args.env + .into_iter() + .map(|env| (env.name, env.value)) + .collect(), args.cwd.clone(), - args.output_byte_limit, - terminal_entity, + &project, cx, ) - })?; - let terminal_id = terminal_entity.read_with(&self.cx, |terminal, _| terminal.id().clone()); - Ok(acp::CreateTerminalResponse::new(terminal_id)) - } + .await?; - async fn kill_terminal( - &self, - args: acp::KillTerminalRequest, - ) -> Result { - self.session_thread(&args.session_id)? - .update(&mut self.cx.clone(), |thread, cx| { - thread.kill_terminal(args.terminal_id, cx) - })??; + let terminal_entity = thread.update(cx, |thread, cx| { + thread.register_terminal_created( + acp::TerminalId::new(uuid::Uuid::new_v4().to_string()), + format!("{} {}", args.command, args.args.join(" ")), + args.cwd.clone(), + args.output_byte_limit, + terminal_entity, + cx, + ) + })?; + let terminal_id = terminal_entity.read_with(cx, |terminal, _| terminal.id().clone()); + Ok(terminal_id) + } + .await; - Ok(Default::default()) - } + match result { + Ok(terminal_id) => { + responder + .respond(acp::CreateTerminalResponse::new(terminal_id)) + .log_err(); + } + Err(e) => respond_err(responder, e), + } + }) + .detach(); +} - async fn ext_method(&self, _args: acp::ExtRequest) -> Result { - Err(acp::Error::method_not_found()) - } +fn handle_kill_terminal( + args: acp::KillTerminalRequest, + responder: Responder, + cx: &mut AsyncApp, + ctx: &ClientContext, +) { + let thread = match session_thread(ctx, &args.session_id) { + Ok(t) => t, + Err(e) => return respond_err(responder, e), + }; - async fn ext_notification(&self, _args: acp::ExtNotification) -> Result<(), acp::Error> { - Err(acp::Error::method_not_found()) + match thread + .update(cx, |thread, cx| thread.kill_terminal(args.terminal_id, cx)) + .flatten_acp() + { + Ok(()) => { + responder + .respond(acp::KillTerminalResponse::default()) + .log_err(); + } + Err(e) => respond_err(responder, e), } +} - async fn release_terminal( - &self, - args: acp::ReleaseTerminalRequest, - ) -> Result { - self.session_thread(&args.session_id)? - .update(&mut self.cx.clone(), |thread, cx| { - thread.release_terminal(args.terminal_id, cx) - })??; +fn handle_release_terminal( + args: acp::ReleaseTerminalRequest, + responder: Responder, + cx: &mut AsyncApp, + ctx: &ClientContext, +) { + let thread = match session_thread(ctx, &args.session_id) { + Ok(t) => t, + Err(e) => return respond_err(responder, e), + }; - Ok(Default::default()) + match thread + .update(cx, |thread, cx| { + thread.release_terminal(args.terminal_id, cx) + }) + .flatten_acp() + { + Ok(()) => { + responder + .respond(acp::ReleaseTerminalResponse::default()) + .log_err(); + } + Err(e) => respond_err(responder, e), } +} - async fn terminal_output( - &self, - args: acp::TerminalOutputRequest, - ) -> Result { - self.session_thread(&args.session_id)? - .read_with(&mut self.cx.clone(), |thread, cx| { - let out = thread - .terminal(args.terminal_id)? - .read(cx) - .current_output(cx); +fn handle_terminal_output( + args: acp::TerminalOutputRequest, + responder: Responder, + cx: &mut AsyncApp, + ctx: &ClientContext, +) { + let thread = match session_thread(ctx, &args.session_id) { + Ok(t) => t, + Err(e) => return respond_err(responder, e), + }; - Ok(out) - })? + match thread + .read_with(cx, |thread, cx| -> anyhow::Result<_> { + let out = thread + .terminal(args.terminal_id)? + .read(cx) + .current_output(cx); + Ok(out) + }) + .flatten_acp() + { + Ok(output) => { + responder.respond(output).log_err(); + } + Err(e) => respond_err(responder, e), } +} - async fn wait_for_terminal_exit( - &self, - args: acp::WaitForTerminalExitRequest, - ) -> Result { - let exit_status = self - .session_thread(&args.session_id)? - .update(&mut self.cx.clone(), |thread, cx| { - anyhow::Ok(thread.terminal(args.terminal_id)?.read(cx).wait_for_exit()) - })?? - .await; +fn handle_wait_for_terminal_exit( + args: acp::WaitForTerminalExitRequest, + responder: Responder, + cx: &mut AsyncApp, + ctx: &ClientContext, +) { + let thread = match session_thread(ctx, &args.session_id) { + Ok(t) => t, + Err(e) => return respond_err(responder, e), + }; - Ok(acp::WaitForTerminalExitResponse::new(exit_status)) - } -} + cx.spawn(async move |cx| { + let result: Result<_, acp::Error> = async { + let exit_status = thread + .update(cx, |thread, cx| { + anyhow::Ok(thread.terminal(args.terminal_id)?.read(cx).wait_for_exit()) + }) + .flatten_acp()? + .await; + Ok(exit_status) + } + .await; -impl ClientDelegate { - fn session_thread(&self, session_id: &acp::SessionId) -> Result> { - let sessions = self.sessions.borrow(); - sessions - .get(session_id) - .context("Failed to get session") - .map(|session| session.thread.clone()) - } + match result { + Ok(exit_status) => { + responder + .respond(acp::WaitForTerminalExitResponse::new(exit_status)) + .log_err(); + } + Err(e) => respond_err(responder, e), + } + }) + .detach(); } diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs index f609a5f50aef3af9a0a27482e0022ac0cee8d501..9c1d36bf9a7ff77772819fadbe6129634be7d0f9 100644 --- a/crates/agent_servers/src/agent_servers.rs +++ b/crates/agent_servers/src/agent_servers.rs @@ -12,6 +12,7 @@ use http_client::read_no_proxy_from_env; use project::{AgentId, Project, agent_server_store::AgentServerStore}; use acp_thread::AgentConnection; +use agent_client_protocol::schema as acp_schema; use anyhow::Result; use gpui::{App, AppContext, Entity, Task}; use settings::SettingsStore; @@ -52,31 +53,31 @@ pub trait AgentServer: Send { fn into_any(self: Rc) -> Rc; - fn default_mode(&self, _cx: &App) -> Option { + fn default_mode(&self, _cx: &App) -> Option { None } fn set_default_mode( &self, - _mode_id: Option, + _mode_id: Option, _fs: Arc, _cx: &mut App, ) { } - fn default_model(&self, _cx: &App) -> Option { + fn default_model(&self, _cx: &App) -> Option { None } fn set_default_model( &self, - _model_id: Option, + _model_id: Option, _fs: Arc, _cx: &mut App, ) { } - fn favorite_model_ids(&self, _cx: &mut App) -> HashSet { + fn favorite_model_ids(&self, _cx: &mut App) -> HashSet { HashSet::default() } @@ -95,16 +96,16 @@ pub trait AgentServer: Send { fn favorite_config_option_value_ids( &self, - _config_id: &agent_client_protocol::SessionConfigId, + _config_id: &acp_schema::SessionConfigId, _cx: &mut App, - ) -> HashSet { + ) -> HashSet { HashSet::default() } fn toggle_favorite_config_option_value( &self, - _config_id: agent_client_protocol::SessionConfigId, - _value_id: agent_client_protocol::SessionConfigValueId, + _config_id: acp_schema::SessionConfigId, + _value_id: acp_schema::SessionConfigValueId, _should_be_favorite: bool, _fs: Arc, _cx: &App, @@ -113,7 +114,7 @@ pub trait AgentServer: Send { fn toggle_favorite_model( &self, - _model_id: agent_client_protocol::ModelId, + _model_id: acp_schema::ModelId, _should_be_favorite: bool, _fs: Arc, _cx: &App, diff --git a/crates/agent_servers/src/custom.rs b/crates/agent_servers/src/custom.rs index 151ddcefcfb0b839199c21d826a4c9f6836f876b..b3574f6e81a5a1770adcc4aba574e67af58e81be 100644 --- a/crates/agent_servers/src/custom.rs +++ b/crates/agent_servers/src/custom.rs @@ -1,6 +1,6 @@ use crate::{AgentServer, AgentServerDelegate, load_proxy_env}; use acp_thread::AgentConnection; -use agent_client_protocol as acp; +use agent_client_protocol::schema as acp; use anyhow::{Context as _, Result}; use collections::HashSet; use fs::Fs; diff --git a/crates/agent_servers/src/e2e_tests.rs b/crates/agent_servers/src/e2e_tests.rs index aa29a0c230c13949b15f2b39a245ae41ead4884d..aa9cdb2cc1bd9a09f8905c568aed4ce041cf5570 100644 --- a/crates/agent_servers/src/e2e_tests.rs +++ b/crates/agent_servers/src/e2e_tests.rs @@ -1,6 +1,6 @@ use crate::{AgentServer, AgentServerDelegate}; use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus}; -use agent_client_protocol as acp; +use agent_client_protocol::schema as acp; use client::RefreshLlmTokenListener; use futures::{FutureExt, StreamExt, channel::mpsc, select}; use gpui::AppContext; @@ -379,7 +379,7 @@ macro_rules! common_e2e_tests { async fn tool_call_with_permission(cx: &mut ::gpui::TestAppContext) { $crate::e2e_tests::test_tool_call_with_permission( $server, - ::agent_client_protocol::PermissionOptionId::new($allow_option_id), + ::agent_client_protocol::schema::PermissionOptionId::new($allow_option_id), cx, ) .await; diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index e3a52555e70dba1a825889209f2ec7b9c1f689e3..5dd939c4ad1d5d313ca03b0893bd3d5fc798364a 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/crates/agent_settings/src/agent_settings.rs @@ -3,7 +3,7 @@ mod agent_profile; use std::path::{Component, Path}; use std::sync::{Arc, LazyLock}; -use agent_client_protocol::ModelId; +use agent_client_protocol::schema as acp; use collections::{HashSet, IndexMap}; use fs::Fs; use futures::channel::oneshot; @@ -204,10 +204,10 @@ impl AgentSettings { self.message_editor_min_lines * 2 } - pub fn favorite_model_ids(&self) -> HashSet { + pub fn favorite_model_ids(&self) -> HashSet { self.favorite_models .iter() - .map(|sel| ModelId::new(format!("{}/{}", sel.provider.0, sel.model))) + .map(|sel| acp::ModelId::new(format!("{}/{}", sel.provider.0, sel.model))) .collect() } } diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 19d85807380e298b7304186ce1d7b6fa03b25661..b363d6a42ae37115b685350773485e819f37bf77 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -10,7 +10,7 @@ use std::{ use acp_thread::{AcpThread, AcpThreadEvent, MentionUri, ThreadStatus}; use agent::{ContextServerRegistry, SharedThread, ThreadStore}; -use agent_client_protocol as acp; +use agent_client_protocol::schema as acp; use agent_servers::AgentServer; use collections::HashSet; use db::kvp::{Dismissable, KeyValueStore}; diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 4786800644d6cabc8b33767d39bd751c016e54c9..d7a8adf80ec9532ecef3ddbf20a1f52003f2d9da 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -38,7 +38,7 @@ use std::rc::Rc; use std::sync::Arc; use ::ui::IconName; -use agent_client_protocol as acp; +use agent_client_protocol::schema as acp; use agent_settings::{AgentProfileId, AgentSettings}; use command_palette_hooks::CommandPaletteFilter; use feature_flags::FeatureFlagAppExt as _; @@ -255,7 +255,7 @@ pub struct NewExternalAgentThread { #[action(namespace = agent)] #[serde(deny_unknown_fields)] pub struct NewNativeAgentThreadFromSummary { - from_session_id: agent_client_protocol::SessionId, + from_session_id: acp::SessionId, } #[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] @@ -339,7 +339,7 @@ pub enum AgentInitialContent { title: Option, }, ContentBlock { - blocks: Vec, + blocks: Vec, auto_submit: bool, }, FromExternalSource(ExternalSourcePrompt), diff --git a/crates/agent_ui/src/completion_provider.rs b/crates/agent_ui/src/completion_provider.rs index 308494409bd9bf4a2cf4346dcd56d06ad13f20be..59a6cb4c924add6a477ccc9f1a574cc5fc1d69aa 100644 --- a/crates/agent_ui/src/completion_provider.rs +++ b/crates/agent_ui/src/completion_provider.rs @@ -7,7 +7,7 @@ use std::sync::atomic::AtomicBool; use crate::DEFAULT_THREAD_TITLE; use crate::thread_metadata_store::{ThreadMetadata, ThreadMetadataStore}; use acp_thread::MentionUri; -use agent_client_protocol as acp; +use agent_client_protocol::schema as acp; use anyhow::Result; use editor::{CompletionProvider, Editor, code_context_menus::COMPLETION_MENU_MAX_WIDTH}; use futures::FutureExt as _; diff --git a/crates/agent_ui/src/config_options.rs b/crates/agent_ui/src/config_options.rs index 58f9606d80dcce51cb5722d47c745322187f02c4..c1f9a09c22ff280f74fa3c2990891e72f04aa5dc 100644 --- a/crates/agent_ui/src/config_options.rs +++ b/crates/agent_ui/src/config_options.rs @@ -1,7 +1,7 @@ use std::{cmp::Reverse, rc::Rc, sync::Arc}; use acp_thread::AgentSessionConfigOptions; -use agent_client_protocol as acp; +use agent_client_protocol::schema as acp; use agent_servers::AgentServer; use collections::HashSet; diff --git a/crates/agent_ui/src/conversation_view.rs b/crates/agent_ui/src/conversation_view.rs index 4b3d77b53b2ba3337451b1e75abc62cc217c54c7..6577496965a2b259b929e49e87a9f7a3d8053916 100644 --- a/crates/agent_ui/src/conversation_view.rs +++ b/crates/agent_ui/src/conversation_view.rs @@ -10,7 +10,7 @@ use action_log::{ActionLog, ActionLogTelemetry, DiffStats}; use agent::{ NativeAgentServer, NativeAgentSessionList, NoModelConfiguredError, SharedThread, ThreadStore, }; -use agent_client_protocol as acp; +use agent_client_protocol::schema as acp; #[cfg(test)] use agent_servers::AgentServerDelegate; use agent_servers::{AgentServer, GEMINI_TERMINAL_AUTH_METHOD_ID}; @@ -2852,7 +2852,6 @@ pub(crate) mod tests { use acp_thread::StubAgentConnection; use action_log::ActionLog; use agent::{AgentTool, EditFileTool, FetchTool, TerminalTool, ToolPermissionContext}; - use agent_client_protocol::SessionId; use agent_servers::FakeAcpAgentServer; use editor::MultiBufferOffset; use fs::FakeFs; @@ -3034,7 +3033,7 @@ pub(crate) mod tests { Rc::new(StubAgentServer::new(ResumeOnlyAgentConnection)), connection_store, Agent::Custom { id: "Test".into() }, - Some(SessionId::new("resume-session")), + Some(acp::SessionId::new("resume-session")), None, None, None, @@ -3081,7 +3080,7 @@ pub(crate) mod tests { self, project, "RestoredAvailableCommandsConnection", - SessionId::new("new-session"), + acp::SessionId::new("new-session"), cx, ); Task::ready(Ok(thread)) @@ -3171,7 +3170,7 @@ pub(crate) mod tests { Rc::new(StubAgentServer::new(RestoredAvailableCommandsConnection)), connection_store, Agent::Custom { id: "Test".into() }, - Some(SessionId::new("restored-session")), + Some(acp::SessionId::new("restored-session")), None, None, None, @@ -3253,7 +3252,7 @@ pub(crate) mod tests { Rc::new(StubAgentServer::new(connection)), connection_store, Agent::Custom { id: "Test".into() }, - Some(SessionId::new("session-1")), + Some(acp::SessionId::new("session-1")), None, Some(PathList::new(&[PathBuf::from("/project/subdir")])), None, @@ -3360,7 +3359,7 @@ pub(crate) mod tests { cx.update(|_window, cx| cx.new(|cx| AgentConnectionStore::new(project.clone(), cx))); // Simulate a previous run that persisted metadata for this session. - let resume_session_id = SessionId::new("persistent-session"); + let resume_session_id = acp::SessionId::new("persistent-session"); let stored_title: SharedString = "Persistent chat".into(); cx.update(|_window, cx| { ThreadMetadataStore::global(cx).update(cx, |store, cx| { @@ -4320,7 +4319,7 @@ pub(crate) mod tests { connection: Rc, project: Entity, name: &'static str, - session_id: SessionId, + session_id: acp::SessionId, cx: &mut App, ) -> Entity { let action_log = cx.new(|_| ActionLog::new(project.clone())); @@ -4366,7 +4365,7 @@ pub(crate) mod tests { self, project, "ResumeOnlyAgentConnection", - SessionId::new("new-session"), + acp::SessionId::new("new-session"), cx, ); Task::ready(Ok(thread)) @@ -4546,7 +4545,7 @@ pub(crate) mod tests { self, project, action_log, - SessionId::new("test"), + acp::SessionId::new("test"), watch::Receiver::constant( acp::PromptCapabilities::new() .image(true) @@ -4626,7 +4625,7 @@ pub(crate) mod tests { self.clone(), project, action_log, - SessionId::new("new-session"), + acp::SessionId::new("new-session"), watch::Receiver::constant( acp::PromptCapabilities::new() .image(true) @@ -7252,7 +7251,7 @@ pub(crate) mod tests { self, project, action_log, - SessionId::new("close-capable-session"), + acp::SessionId::new("close-capable-session"), watch::Receiver::constant( acp::PromptCapabilities::new() .image(true) diff --git a/crates/agent_ui/src/conversation_view/thread_view.rs b/crates/agent_ui/src/conversation_view/thread_view.rs index fc4e4d50c6226317eb1c48d6a26f1cf1ba683b3e..6f134850c1a114c891d857caeae4401550e343ec 100644 --- a/crates/agent_ui/src/conversation_view/thread_view.rs +++ b/crates/agent_ui/src/conversation_view/thread_view.rs @@ -2,6 +2,7 @@ use crate::{ DEFAULT_THREAD_TITLE, SelectPermissionGranularity, agent_configuration::configure_context_server_modal::default_markdown_style, }; +use agent_client_protocol::schema as acp; use std::cell::RefCell; use acp_thread::{ContentBlock, PlanEntry}; @@ -289,8 +290,8 @@ pub struct ThreadView { pub session_capabilities: SharedSessionCapabilities, /// Tracks which tool calls have their content/output expanded. /// Used for showing/hiding tool call results, terminal output, etc. - pub expanded_tool_calls: HashSet, - pub expanded_tool_call_raw_inputs: HashSet, + pub expanded_tool_calls: HashSet, + pub expanded_tool_call_raw_inputs: HashSet, pub expanded_thinking_blocks: HashSet<(usize, usize)>, auto_expanded_thinking_block: Option<(usize, usize)>, user_toggled_thinking_blocks: HashSet<(usize, usize)>, @@ -306,12 +307,11 @@ pub struct ThreadView { pub queued_message_editor_subscriptions: Vec, pub last_synced_queue_length: usize, pub turn_fields: TurnFields, - pub discarded_partial_edits: HashSet, + pub discarded_partial_edits: HashSet, pub is_loading_contents: bool, pub new_server_version_available: Option, pub resumed_without_history: bool, - pub(crate) permission_selections: - HashMap, + pub(crate) permission_selections: HashMap, pub resume_thread_metadata: Option, pub _cancel_task: Option>, _save_task: Option>, diff --git a/crates/agent_ui/src/entry_view_state.rs b/crates/agent_ui/src/entry_view_state.rs index b1476ea93e073609b7ed4435de177ede28047ed9..853672142fb84376f74fa297273502908c801d3c 100644 --- a/crates/agent_ui/src/entry_view_state.rs +++ b/crates/agent_ui/src/entry_view_state.rs @@ -2,7 +2,7 @@ use std::ops::Range; use acp_thread::{AcpThread, AgentThreadEntry}; use agent::ThreadStore; -use agent_client_protocol::ToolCallId; +use agent_client_protocol::schema as acp; use collections::HashMap; use editor::{Editor, EditorEvent, EditorMode, MinimapVisibility, SizingBehavior}; use gpui::{ @@ -283,9 +283,9 @@ pub struct EntryViewEvent { } pub enum ViewEvent { - NewDiff(ToolCallId), - NewTerminal(ToolCallId), - TerminalMovedToBackground(ToolCallId), + NewDiff(acp::ToolCallId), + NewTerminal(acp::ToolCallId), + TerminalMovedToBackground(acp::ToolCallId), MessageEditorEvent(Entity, MessageEditorEvent), OpenDiffLocation { path: String, @@ -482,7 +482,7 @@ mod tests { use std::sync::Arc; use acp_thread::{AgentConnection, StubAgentConnection}; - use agent_client_protocol as acp; + use agent_client_protocol::schema as acp; use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind}; use editor::RowInfo; use fs::FakeFs; diff --git a/crates/agent_ui/src/mention_set.rs b/crates/agent_ui/src/mention_set.rs index 880257e3f942bf71d1d51b1e661d911474aa786b..c09ce09223c58c57e515d181f11738f63fffbf5c 100644 --- a/crates/agent_ui/src/mention_set.rs +++ b/crates/agent_ui/src/mention_set.rs @@ -1,7 +1,7 @@ use crate::diagnostics::{DiagnosticsOptions, codeblock_fence_for_path, collect_diagnostics}; use acp_thread::{MentionUri, selection_name}; use agent::{ThreadStore, outline}; -use agent_client_protocol as acp; +use agent_client_protocol::schema as acp; use agent_servers::{AgentServer, AgentServerDelegate}; use anyhow::{Context as _, Result, anyhow}; use collections::{HashMap, HashSet}; diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index 3bd40fd2537ff881b1779ecd2d32c4fb029885f2..0f213cb9f1e365fd16cde514b2db1044e55dbb59 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -10,7 +10,7 @@ use crate::{ }; use acp_thread::MentionUri; use agent::ThreadStore; -use agent_client_protocol as acp; +use agent_client_protocol::schema as acp; use anyhow::{Result, anyhow}; use editor::{ Addon, AnchorRangeExt, ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, @@ -1907,7 +1907,7 @@ mod tests { use acp_thread::MentionUri; use agent::{ThreadStore, outline}; - use agent_client_protocol as acp; + use agent_client_protocol::schema as acp; use base64::Engine as _; use editor::{ AnchorRangeExt as _, Editor, EditorMode, MultiBufferOffset, SelectionEffects, diff --git a/crates/agent_ui/src/mode_selector.rs b/crates/agent_ui/src/mode_selector.rs index b82d5ff99bfb4045edce98a91b35c3a683a26d30..9e4464517c2d4cd97f8de45287c5f535f859b396 100644 --- a/crates/agent_ui/src/mode_selector.rs +++ b/crates/agent_ui/src/mode_selector.rs @@ -1,5 +1,5 @@ use acp_thread::AgentSessionModes; -use agent_client_protocol as acp; +use agent_client_protocol::schema as acp; use agent_servers::AgentServer; use fs::Fs; diff --git a/crates/agent_ui/src/model_selector.rs b/crates/agent_ui/src/model_selector.rs index 89290bd9973216f04cdd1d70e442cf04a47b97f2..e1cf7307394571dbaad0b726f84728407e49321a 100644 --- a/crates/agent_ui/src/model_selector.rs +++ b/crates/agent_ui/src/model_selector.rs @@ -1,7 +1,7 @@ use std::{cmp::Reverse, rc::Rc, sync::Arc}; use acp_thread::{AgentModelIcon, AgentModelInfo, AgentModelList, AgentModelSelector}; -use agent_client_protocol::ModelId; +use agent_client_protocol::schema as acp; use agent_servers::AgentServer; use anyhow::Result; @@ -57,7 +57,7 @@ pub struct ModelPickerDelegate { selected_index: usize, selected_description: Option<(usize, SharedString, bool)>, selected_model: Option, - favorites: HashSet, + favorites: HashSet, _refresh_models_task: Task<()>, _settings_subscription: Subscription, focus_handle: FocusHandle, @@ -424,7 +424,7 @@ impl PickerDelegate for ModelPickerDelegate { fn info_list_to_picker_entries( model_list: AgentModelList, - favorites: &HashSet, + favorites: &HashSet, ) -> Vec { let mut entries = Vec::new(); @@ -530,7 +530,6 @@ async fn fuzzy_search( #[cfg(test)] mod tests { - use agent_client_protocol as acp; use gpui::TestAppContext; use super::*; @@ -592,10 +591,10 @@ mod tests { } } - fn create_favorites(models: Vec<&str>) -> HashSet { + fn create_favorites(models: Vec<&str>) -> HashSet { models .into_iter() - .map(|m| ModelId::new(m.to_string())) + .map(|m| acp::ModelId::new(m.to_string())) .collect() } @@ -791,7 +790,7 @@ mod tests { #[gpui::test] fn test_favorites_count_returns_correct_count(_cx: &mut TestAppContext) { - let empty_favorites: HashSet = HashSet::default(); + let empty_favorites: HashSet = HashSet::default(); assert_eq!(empty_favorites.len(), 0); let one_favorite = create_favorites(vec!["model-a"]); diff --git a/crates/agent_ui/src/test_support.rs b/crates/agent_ui/src/test_support.rs index a141121dda14320c8d1f8039ee2e4a192121e092..1f409ebfc7436059a8a6d2571572ca1c55aa9a7d 100644 --- a/crates/agent_ui/src/test_support.rs +++ b/crates/agent_ui/src/test_support.rs @@ -1,5 +1,5 @@ use acp_thread::{AgentConnection, StubAgentConnection}; -use agent_client_protocol as acp; +use agent_client_protocol::schema as acp; use agent_servers::{AgentServer, AgentServerDelegate}; use gpui::{Entity, Task, TestAppContext, VisualTestContext}; use project::AgentId; diff --git a/crates/agent_ui/src/thread_import.rs b/crates/agent_ui/src/thread_import.rs index 6044746bdeb2b768a66d31fc4803439d14bbd5fc..f5d6fa1a657d2fc6357b3124a202561746c472d2 100644 --- a/crates/agent_ui/src/thread_import.rs +++ b/crates/agent_ui/src/thread_import.rs @@ -1,6 +1,6 @@ use acp_thread::AgentSessionListRequest; use agent::ThreadStore; -use agent_client_protocol as acp; +use agent_client_protocol::schema as acp; use chrono::Utc; use collections::HashSet; use db::kvp::Dismissable; diff --git a/crates/agent_ui/src/thread_metadata_store.rs b/crates/agent_ui/src/thread_metadata_store.rs index 85a88b8444d9c1d07564018c73c5265f25ca5b95..21ac2af0997accf7c48f3a11e3ffdbd65f684d7c 100644 --- a/crates/agent_ui/src/thread_metadata_store.rs +++ b/crates/agent_ui/src/thread_metadata_store.rs @@ -4,7 +4,7 @@ use std::{ }; use agent::{ThreadStore, ZED_AGENT_ID}; -use agent_client_protocol as acp; +use agent_client_protocol::schema as acp; use anyhow::Context as _; use chrono::{DateTime, Utc}; use collections::{HashMap, HashSet}; @@ -1680,8 +1680,7 @@ mod tests { use acp_thread::StubAgentConnection; use action_log::ActionLog; use agent::DbThread; - use agent_client_protocol as acp; - + use agent_client_protocol::schema as acp; use gpui::{TestAppContext, VisualTestContext}; use project::FakeFs; use project::Project; diff --git a/crates/agent_ui/src/threads_archive_view.rs b/crates/agent_ui/src/threads_archive_view.rs index cb7d430854179507a55a202f1a235949d58c5a45..72b03692761742297e3deeb8ebca6ba2ac89be90 100644 --- a/crates/agent_ui/src/threads_archive_view.rs +++ b/crates/agent_ui/src/threads_archive_view.rs @@ -10,7 +10,7 @@ use crate::thread_metadata_store::{ use crate::{Agent, ArchiveSelectedThread, DEFAULT_THREAD_TITLE, RemoveSelectedThread}; use agent::ThreadStore; -use agent_client_protocol as acp; +use agent_client_protocol::schema as acp; use agent_settings::AgentSettings; use chrono::{DateTime, Datelike as _, Local, NaiveDate, TimeDelta, Utc}; use collections::HashMap; diff --git a/crates/agent_ui/src/ui/mention_crease.rs b/crates/agent_ui/src/ui/mention_crease.rs index 9fa245516f950b31201f41a45389e41837b07b56..e3059ab87247dd10dcc8675800b06e603f1db283 100644 --- a/crates/agent_ui/src/ui/mention_crease.rs +++ b/crates/agent_ui/src/ui/mention_crease.rs @@ -1,7 +1,7 @@ use std::{ops::RangeInclusive, path::PathBuf, time::Duration}; use acp_thread::MentionUri; -use agent_client_protocol as acp; +use agent_client_protocol::schema as acp; use editor::{Editor, SelectionEffects, scroll::Autoscroll}; use gpui::{ Animation, AnimationExt, AnyView, Context, IntoElement, WeakEntity, Window, pulsating_between, diff --git a/crates/eval_cli/src/main.rs b/crates/eval_cli/src/main.rs index f9ab1835f94327c72462ba7014bf7517d12ac55d..bb6cbc883e1b6d775d818b8c27f3faabdc924684 100644 --- a/crates/eval_cli/src/main.rs +++ b/crates/eval_cli/src/main.rs @@ -40,7 +40,7 @@ use std::time::{Duration, Instant}; use acp_thread::AgentConnection as _; use agent::{NativeAgent, NativeAgentConnection, Templates, ThreadStore}; -use agent_client_protocol as acp; +use agent_client_protocol::schema as acp; use anyhow::{Context, Result}; use clap::Parser; use feature_flags::FeatureFlagAppExt as _; diff --git a/crates/sidebar/src/sidebar.rs b/crates/sidebar/src/sidebar.rs index 3dda3625609ed61d509785913310d0fdffbc12d5..4e2be26ded552abcfe556b0756309c82ff37134f 100644 --- a/crates/sidebar/src/sidebar.rs +++ b/crates/sidebar/src/sidebar.rs @@ -2,7 +2,7 @@ mod thread_switcher; use acp_thread::ThreadStatus; use action_log::DiffStats; -use agent_client_protocol::{self as acp}; +use agent_client_protocol::schema as acp; use agent_settings::AgentSettings; use agent_ui::thread_metadata_store::{ ThreadMetadata, ThreadMetadataStore, WorktreePaths, worktree_info_from_thread_paths, diff --git a/crates/sidebar/src/thread_switcher.rs b/crates/sidebar/src/thread_switcher.rs index 218b32792a89578fc96a01866c3cb5c6361eaf8e..c74cdedc9fc9b9c34fe4f1c4aa4af9b3a020cf93 100644 --- a/crates/sidebar/src/thread_switcher.rs +++ b/crates/sidebar/src/thread_switcher.rs @@ -1,5 +1,5 @@ use action_log::DiffStats; -use agent_client_protocol as acp; +use agent_client_protocol::schema as acp; use agent_ui::thread_metadata_store::ThreadMetadata; use gpui::{ Action as _, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Modifiers, diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 15c426b3b0db0707e7ba928d98ee2a28e0af00e8..627d514f6c469ee954a773c1ff08a4351dba82c2 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -5,7 +5,7 @@ mod reliability; mod zed; use agent::{SharedThread, ThreadStore}; -use agent_client_protocol; +use agent_client_protocol::schema as acp; use agent_ui::AgentPanel; use anyhow::{Context as _, Result}; use clap::Parser; @@ -990,7 +990,7 @@ fn handle_open_request(request: OpenRequest, app_state: Arc, cx: &mut let shared_thread = SharedThread::from_bytes(&response.thread_data)?; let db_thread = shared_thread.to_db_thread(); - let session_id = agent_client_protocol::SessionId::new(session_id); + let session_id = acp::SessionId::new(session_id); let save_session_id = session_id.clone(); diff --git a/crates/zed/src/visual_test_runner.rs b/crates/zed/src/visual_test_runner.rs index 72c56c478a4a0f8ce1ee215fa520e055c6b27e16..8f85fcd3c86090cdaffccbc87dd7f8adce3df5b6 100644 --- a/crates/zed/src/visual_test_runner.rs +++ b/crates/zed/src/visual_test_runner.rs @@ -95,7 +95,7 @@ fn main() { #[cfg(target_os = "macos")] use { acp_thread::{AgentConnection, StubAgentConnection}, - agent_client_protocol as acp, + agent_client_protocol::schema as acp, agent_servers::{AgentServer, AgentServerDelegate}, anyhow::{Context as _, Result}, assets::Assets,