Merge branch 'zed-industries:main' into feature/debug_context

This commit is contained in:
Matt 2025-08-25 15:52:31 -05:00 committed by GitHub
commit 5aff16e389
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
181 changed files with 8013 additions and 5151 deletions

125
Cargo.lock generated
View file

@ -39,6 +39,26 @@ dependencies = [
"workspace-hack", "workspace-hack",
] ]
[[package]]
name = "acp_tools"
version = "0.1.0"
dependencies = [
"agent-client-protocol",
"collections",
"gpui",
"language",
"markdown",
"project",
"serde",
"serde_json",
"settings",
"theme",
"ui",
"util",
"workspace",
"workspace-hack",
]
[[package]] [[package]]
name = "action_log" name = "action_log"
version = "0.1.0" version = "0.1.0"
@ -171,11 +191,12 @@ dependencies = [
[[package]] [[package]]
name = "agent-client-protocol" name = "agent-client-protocol"
version = "0.0.30" version = "0.0.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f792e009ba59b137ee1db560bc37e567887ad4b5af6f32181d381fff690e2d4" checksum = "289eb34ee17213dadcca47eedadd386a5e7678094095414e475965d1bcca2860"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-broadcast",
"futures 0.3.31", "futures 0.3.31",
"log", "log",
"parking_lot", "parking_lot",
@ -264,10 +285,10 @@ name = "agent_servers"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"acp_thread", "acp_thread",
"acp_tools",
"action_log", "action_log",
"agent-client-protocol", "agent-client-protocol",
"agent_settings", "agent_settings",
"agentic-coding-protocol",
"anyhow", "anyhow",
"client", "client",
"collections", "collections",
@ -382,6 +403,7 @@ dependencies = [
"parking_lot", "parking_lot",
"paths", "paths",
"picker", "picker",
"postage",
"pretty_assertions", "pretty_assertions",
"project", "project",
"prompt_store", "prompt_store",
@ -421,24 +443,6 @@ dependencies = [
"zed_actions", "zed_actions",
] ]
[[package]]
name = "agentic-coding-protocol"
version = "0.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e6ae951b36fa2f8d9dd6e1af6da2fcaba13d7c866cf6a9e65deda9dc6c5fe4"
dependencies = [
"anyhow",
"chrono",
"derive_more 2.0.1",
"futures 0.3.31",
"log",
"parking_lot",
"schemars",
"semver",
"serde",
"serde_json",
]
[[package]] [[package]]
name = "ahash" name = "ahash"
version = "0.7.8" version = "0.7.8"
@ -854,7 +858,7 @@ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
"collections", "collections",
"derive_more 0.99.19", "derive_more",
"extension", "extension",
"futures 0.3.31", "futures 0.3.31",
"gpui", "gpui",
@ -917,7 +921,7 @@ dependencies = [
"clock", "clock",
"collections", "collections",
"ctor", "ctor",
"derive_more 0.99.19", "derive_more",
"gpui", "gpui",
"icons", "icons",
"indoc", "indoc",
@ -954,7 +958,7 @@ dependencies = [
"cloud_llm_client", "cloud_llm_client",
"collections", "collections",
"component", "component",
"derive_more 0.99.19", "derive_more",
"diffy", "diffy",
"editor", "editor",
"feature_flags", "feature_flags",
@ -3067,7 +3071,7 @@ dependencies = [
"cocoa 0.26.0", "cocoa 0.26.0",
"collections", "collections",
"credentials_provider", "credentials_provider",
"derive_more 0.99.19", "derive_more",
"feature_flags", "feature_flags",
"fs", "fs",
"futures 0.3.31", "futures 0.3.31",
@ -3499,7 +3503,7 @@ name = "command_palette_hooks"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"collections", "collections",
"derive_more 0.99.19", "derive_more",
"gpui", "gpui",
"workspace-hack", "workspace-hack",
] ]
@ -4050,6 +4054,7 @@ dependencies = [
name = "crashes" name = "crashes"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"bincode",
"crash-handler", "crash-handler",
"log", "log",
"mach2 0.5.0", "mach2 0.5.0",
@ -4059,6 +4064,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"smol", "smol",
"system_specs",
"workspace-hack", "workspace-hack",
] ]
@ -4660,27 +4666,6 @@ dependencies = [
"syn 2.0.101", "syn 2.0.101",
] ]
[[package]]
name = "derive_more"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
dependencies = [
"derive_more-impl",
]
[[package]]
name = "derive_more-impl"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"unicode-xid",
]
[[package]] [[package]]
name = "derive_refineable" name = "derive_refineable"
version = "0.1.0" version = "0.1.0"
@ -4701,7 +4686,6 @@ dependencies = [
"component", "component",
"ctor", "ctor",
"editor", "editor",
"futures 0.3.31",
"gpui", "gpui",
"indoc", "indoc",
"language", "language",
@ -5738,14 +5722,10 @@ dependencies = [
name = "feedback" name = "feedback"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"client",
"editor", "editor",
"gpui", "gpui",
"human_bytes",
"menu", "menu",
"release_channel", "system_specs",
"serde",
"sysinfo",
"ui", "ui",
"urlencoding", "urlencoding",
"util", "util",
@ -6421,7 +6401,7 @@ dependencies = [
"askpass", "askpass",
"async-trait", "async-trait",
"collections", "collections",
"derive_more 0.99.19", "derive_more",
"futures 0.3.31", "futures 0.3.31",
"git2", "git2",
"gpui", "gpui",
@ -7451,7 +7431,7 @@ dependencies = [
"core-video", "core-video",
"cosmic-text", "cosmic-text",
"ctor", "ctor",
"derive_more 0.99.19", "derive_more",
"embed-resource", "embed-resource",
"env_logger 0.11.8", "env_logger 0.11.8",
"etagere", "etagere",
@ -7976,7 +7956,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes 1.10.1", "bytes 1.10.1",
"derive_more 0.99.19", "derive_more",
"futures 0.3.31", "futures 0.3.31",
"http 1.3.1", "http 1.3.1",
"http-body 1.0.1", "http-body 1.0.1",
@ -8488,6 +8468,7 @@ dependencies = [
"theme", "theme",
"ui", "ui",
"util", "util",
"util_macros",
"workspace", "workspace",
"workspace-hack", "workspace-hack",
"zed_actions", "zed_actions",
@ -11634,6 +11615,12 @@ dependencies = [
"hmac", "hmac",
] ]
[[package]]
name = "pciid-parser"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0008e816fcdaf229cdd540e9b6ca2dc4a10d65c31624abb546c6420a02846e61"
[[package]] [[package]]
name = "pem" name = "pem"
version = "3.0.5" version = "3.0.5"
@ -13534,6 +13521,7 @@ dependencies = [
"smol", "smol",
"sysinfo", "sysinfo",
"telemetry_events", "telemetry_events",
"thiserror 2.0.12",
"toml 0.8.20", "toml 0.8.20",
"unindent", "unindent",
"util", "util",
@ -14373,12 +14361,10 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe8c9d1c68d67dd9f97ecbc6f932b60eb289c5dbddd8aa1405484a8fd2fcd984" checksum = "fe8c9d1c68d67dd9f97ecbc6f932b60eb289c5dbddd8aa1405484a8fd2fcd984"
dependencies = [ dependencies = [
"chrono",
"dyn-clone", "dyn-clone",
"indexmap", "indexmap",
"ref-cast", "ref-cast",
"schemars_derive", "schemars_derive",
"semver",
"serde", "serde",
"serde_json", "serde_json",
] ]
@ -16154,6 +16140,21 @@ dependencies = [
"winx", "winx",
] ]
[[package]]
name = "system_specs"
version = "0.1.0"
dependencies = [
"anyhow",
"client",
"gpui",
"human_bytes",
"pciid-parser",
"release_channel",
"serde",
"sysinfo",
"workspace-hack",
]
[[package]] [[package]]
name = "tab_switcher" name = "tab_switcher"
version = "0.1.0" version = "0.1.0"
@ -16447,7 +16448,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"collections", "collections",
"derive_more 0.99.19", "derive_more",
"fs", "fs",
"futures 0.3.31", "futures 0.3.31",
"gpui", "gpui",
@ -19788,7 +19789,6 @@ dependencies = [
"any_vec", "any_vec",
"anyhow", "anyhow",
"async-recursion", "async-recursion",
"bincode",
"call", "call",
"client", "client",
"clock", "clock",
@ -19807,6 +19807,7 @@ dependencies = [
"node_runtime", "node_runtime",
"parking_lot", "parking_lot",
"postage", "postage",
"pretty_assertions",
"project", "project",
"remote", "remote",
"schemars", "schemars",
@ -19962,7 +19963,6 @@ dependencies = [
"rustix 1.0.7", "rustix 1.0.7",
"rustls 0.23.26", "rustls 0.23.26",
"rustls-webpki 0.103.1", "rustls-webpki 0.103.1",
"schemars",
"scopeguard", "scopeguard",
"sea-orm", "sea-orm",
"sea-query-binder", "sea-query-binder",
@ -20398,6 +20398,7 @@ dependencies = [
name = "zed" name = "zed"
version = "0.202.0" version = "0.202.0"
dependencies = [ dependencies = [
"acp_tools",
"activity_indicator", "activity_indicator",
"agent", "agent",
"agent_servers", "agent_servers",
@ -20413,6 +20414,7 @@ dependencies = [
"auto_update", "auto_update",
"auto_update_ui", "auto_update_ui",
"backtrace", "backtrace",
"bincode",
"breadcrumbs", "breadcrumbs",
"call", "call",
"channel", "channel",
@ -20511,6 +20513,7 @@ dependencies = [
"supermaven", "supermaven",
"svg_preview", "svg_preview",
"sysinfo", "sysinfo",
"system_specs",
"tab_switcher", "tab_switcher",
"task", "task",
"tasks_ui", "tasks_ui",

View file

@ -1,6 +1,7 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
members = [ members = [
"crates/acp_tools",
"crates/acp_thread", "crates/acp_thread",
"crates/action_log", "crates/action_log",
"crates/activity_indicator", "crates/activity_indicator",
@ -155,6 +156,7 @@ members = [
"crates/streaming_diff", "crates/streaming_diff",
"crates/sum_tree", "crates/sum_tree",
"crates/supermaven", "crates/supermaven",
"crates/system_specs",
"crates/supermaven_api", "crates/supermaven_api",
"crates/svg_preview", "crates/svg_preview",
"crates/tab_switcher", "crates/tab_switcher",
@ -226,6 +228,7 @@ edition = "2024"
# Workspace member crates # Workspace member crates
# #
acp_tools = { path = "crates/acp_tools" }
acp_thread = { path = "crates/acp_thread" } acp_thread = { path = "crates/acp_thread" }
action_log = { path = "crates/action_log" } action_log = { path = "crates/action_log" }
agent = { path = "crates/agent" } agent = { path = "crates/agent" }
@ -381,6 +384,7 @@ streaming_diff = { path = "crates/streaming_diff" }
sum_tree = { path = "crates/sum_tree" } sum_tree = { path = "crates/sum_tree" }
supermaven = { path = "crates/supermaven" } supermaven = { path = "crates/supermaven" }
supermaven_api = { path = "crates/supermaven_api" } supermaven_api = { path = "crates/supermaven_api" }
system_specs = { path = "crates/system_specs" }
tab_switcher = { path = "crates/tab_switcher" } tab_switcher = { path = "crates/tab_switcher" }
task = { path = "crates/task" } task = { path = "crates/task" }
tasks_ui = { path = "crates/tasks_ui" } tasks_ui = { path = "crates/tasks_ui" }
@ -422,8 +426,7 @@ zlog_settings = { path = "crates/zlog_settings" }
# External crates # External crates
# #
agentic-coding-protocol = "0.0.10" agent-client-protocol = "0.0.31"
agent-client-protocol = "0.0.30"
aho-corasick = "1.1" aho-corasick = "1.1"
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" } alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
any_vec = "0.14" any_vec = "0.14"
@ -450,6 +453,7 @@ aws-sdk-bedrockruntime = { version = "1.80.0", features = [
aws-smithy-runtime-api = { version = "1.7.4", features = ["http-1x", "client"] } aws-smithy-runtime-api = { version = "1.7.4", features = ["http-1x", "client"] }
aws-smithy-types = { version = "1.3.0", features = ["http-body-1-x"] } aws-smithy-types = { version = "1.3.0", features = ["http-body-1-x"] }
base64 = "0.22" base64 = "0.22"
bincode = "1.2.1"
bitflags = "2.6.0" bitflags = "2.6.0"
blade-graphics = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" } blade-graphics = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" } blade-macros = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
@ -493,6 +497,7 @@ handlebars = "4.3"
heck = "0.5" heck = "0.5"
heed = { version = "0.21.0", features = ["read-txn-no-tls"] } heed = { version = "0.21.0", features = ["read-txn-no-tls"] }
hex = "0.4.3" hex = "0.4.3"
human_bytes = "0.4.1"
html5ever = "0.27.0" html5ever = "0.27.0"
http = "1.1" http = "1.1"
http-body = "1.0" http-body = "1.0"
@ -532,6 +537,7 @@ palette = { version = "0.7.5", default-features = false, features = ["std"] }
parking_lot = "0.12.1" parking_lot = "0.12.1"
partial-json-fixer = "0.5.3" partial-json-fixer = "0.5.3"
parse_int = "0.9" parse_int = "0.9"
pciid-parser = "0.8.0"
pathdiff = "0.2" pathdiff = "0.2"
pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" } pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" } pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }

2
Procfile.web Normal file
View file

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

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

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

After

Width:  |  Height:  |  Size: 1 KiB

View file

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

Before

Width:  |  Height:  |  Size: 515 B

After

Width:  |  Height:  |  Size: 802 B

Before After
Before After

View file

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

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

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

Before

Width:  |  Height:  |  Size: 768 B

After

Width:  |  Height:  |  Size: 762 B

Before After
Before After

View file

@ -16,7 +16,6 @@
"up": "menu::SelectPrevious", "up": "menu::SelectPrevious",
"enter": "menu::Confirm", "enter": "menu::Confirm",
"ctrl-enter": "menu::SecondaryConfirm", "ctrl-enter": "menu::SecondaryConfirm",
"ctrl-escape": "menu::Cancel",
"ctrl-c": "menu::Cancel", "ctrl-c": "menu::Cancel",
"escape": "menu::Cancel", "escape": "menu::Cancel",
"alt-shift-enter": "menu::Restart", "alt-shift-enter": "menu::Restart",
@ -856,7 +855,7 @@
"ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }], "ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }],
"ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }], "ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }],
"alt-ctrl-r": "project_panel::RevealInFileManager", "alt-ctrl-r": "project_panel::RevealInFileManager",
"ctrl-shift-enter": "project_panel::OpenWithSystem", "ctrl-shift-enter": "workspace::OpenWithSystem",
"alt-d": "project_panel::CompareMarkedFiles", "alt-d": "project_panel::CompareMarkedFiles",
"shift-find": "project_panel::NewSearchInDirectory", "shift-find": "project_panel::NewSearchInDirectory",
"ctrl-alt-shift-f": "project_panel::NewSearchInDirectory", "ctrl-alt-shift-f": "project_panel::NewSearchInDirectory",
@ -1195,9 +1194,16 @@
"ctrl-1": "onboarding::ActivateBasicsPage", "ctrl-1": "onboarding::ActivateBasicsPage",
"ctrl-2": "onboarding::ActivateEditingPage", "ctrl-2": "onboarding::ActivateEditingPage",
"ctrl-3": "onboarding::ActivateAISetupPage", "ctrl-3": "onboarding::ActivateAISetupPage",
"ctrl-escape": "onboarding::Finish", "ctrl-enter": "onboarding::Finish",
"alt-tab": "onboarding::SignIn", "alt-shift-l": "onboarding::SignIn",
"alt-shift-a": "onboarding::OpenAccount" "alt-shift-a": "onboarding::OpenAccount"
} }
},
{
"context": "InvalidBuffer",
"use_key_equivalents": true,
"bindings": {
"ctrl-shift-enter": "workspace::OpenWithSystem"
}
} }
] ]

View file

@ -915,7 +915,7 @@
"cmd-backspace": ["project_panel::Trash", { "skip_prompt": true }], "cmd-backspace": ["project_panel::Trash", { "skip_prompt": true }],
"cmd-delete": ["project_panel::Delete", { "skip_prompt": false }], "cmd-delete": ["project_panel::Delete", { "skip_prompt": false }],
"alt-cmd-r": "project_panel::RevealInFileManager", "alt-cmd-r": "project_panel::RevealInFileManager",
"ctrl-shift-enter": "project_panel::OpenWithSystem", "ctrl-shift-enter": "workspace::OpenWithSystem",
"alt-d": "project_panel::CompareMarkedFiles", "alt-d": "project_panel::CompareMarkedFiles",
"cmd-alt-backspace": ["project_panel::Delete", { "skip_prompt": false }], "cmd-alt-backspace": ["project_panel::Delete", { "skip_prompt": false }],
"cmd-alt-shift-f": "project_panel::NewSearchInDirectory", "cmd-alt-shift-f": "project_panel::NewSearchInDirectory",
@ -1301,5 +1301,12 @@
"alt-tab": "onboarding::SignIn", "alt-tab": "onboarding::SignIn",
"alt-shift-a": "onboarding::OpenAccount" "alt-shift-a": "onboarding::OpenAccount"
} }
},
{
"context": "InvalidBuffer",
"use_key_equivalents": true,
"bindings": {
"ctrl-shift-enter": "workspace::OpenWithSystem"
}
} }
] ]

View file

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

View file

@ -162,6 +162,12 @@
// 2. Always quit the application // 2. Always quit the application
// "on_last_window_closed": "quit_app", // "on_last_window_closed": "quit_app",
"on_last_window_closed": "platform_default", "on_last_window_closed": "platform_default",
// Whether to show padding for zoomed panels.
// When enabled, zoomed center panels (e.g. code editor) will have padding all around,
// while zoomed bottom/left/right panels will have padding to the top/right/left (respectively).
//
// Default: true
"zoomed_padding": true,
// Whether to use the system provided dialogs for Open and Save As. // Whether to use the system provided dialogs for Open and Save As.
// When set to false, Zed will use the built-in keyboard-first pickers. // When set to false, Zed will use the built-in keyboard-first pickers.
"use_system_path_prompts": true, "use_system_path_prompts": true,
@ -1133,11 +1139,6 @@
// The minimum severity of the diagnostics to show inline. // The minimum severity of the diagnostics to show inline.
// Inherits editor's diagnostics' max severity settings when `null`. // Inherits editor's diagnostics' max severity settings when `null`.
"max_severity": null "max_severity": null
},
"cargo": {
// When enabled, Zed disables rust-analyzer's check on save and starts to query
// Cargo diagnostics separately.
"fetch_cargo_diagnostics": false
} }
}, },
// Files or globs of files that will be excluded by Zed entirely. They will be skipped during file // Files or globs of files that will be excluded by Zed entirely. They will be skipped during file
@ -1503,6 +1504,11 @@
// //
// Default: fallback // Default: fallback
"words": "fallback", "words": "fallback",
// Minimum number of characters required to automatically trigger word-based completions.
// Before that value, it's still possible to trigger the words-based completion manually with the corresponding editor command.
//
// Default: 3
"words_min_length": 3,
// Whether to fetch LSP completions or not. // Whether to fetch LSP completions or not.
// //
// Default: true // Default: true
@ -1629,6 +1635,9 @@
"allowed": true "allowed": true
} }
}, },
"Kotlin": {
"language_servers": ["kotlin-language-server", "!kotlin-lsp", "..."]
},
"LaTeX": { "LaTeX": {
"formatter": "language_server", "formatter": "language_server",
"language_servers": ["texlab", "..."], "language_servers": ["texlab", "..."],
@ -1642,9 +1651,6 @@
"use_on_type_format": false, "use_on_type_format": false,
"allow_rewrap": "anywhere", "allow_rewrap": "anywhere",
"soft_wrap": "editor_width", "soft_wrap": "editor_width",
"completions": {
"words": "disabled"
},
"prettier": { "prettier": {
"allowed": true "allowed": true
} }
@ -1658,9 +1664,6 @@
} }
}, },
"Plain Text": { "Plain Text": {
"completions": {
"words": "disabled"
},
"allow_rewrap": "anywhere" "allow_rewrap": "anywhere"
}, },
"Python": { "Python": {

View file

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

View file

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

View file

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

View file

@ -183,16 +183,15 @@ impl ToolCall {
language_registry: Arc<LanguageRegistry>, language_registry: Arc<LanguageRegistry>,
cx: &mut App, cx: &mut App,
) -> Self { ) -> Self {
let title = if let Some((first_line, _)) = tool_call.title.split_once("\n") {
first_line.to_owned() + ""
} else {
tool_call.title
};
Self { Self {
id: tool_call.id, id: tool_call.id,
label: cx.new(|cx| { label: cx
Markdown::new( .new(|cx| Markdown::new(title.into(), Some(language_registry.clone()), None, cx)),
tool_call.title.into(),
Some(language_registry.clone()),
None,
cx,
)
}),
kind: tool_call.kind, kind: tool_call.kind,
content: tool_call content: tool_call
.content .content
@ -233,7 +232,11 @@ impl ToolCall {
if let Some(title) = title { if let Some(title) = title {
self.label.update(cx, |label, cx| { self.label.update(cx, |label, cx| {
label.replace(title, cx); if let Some((first_line, _)) = title.split_once("\n") {
label.replace(first_line.to_owned() + "", cx)
} else {
label.replace(title, cx);
}
}); });
} }
@ -509,7 +512,7 @@ impl ContentBlock {
"`Image`".into() "`Image`".into()
} }
fn to_markdown<'a>(&'a self, cx: &'a App) -> &'a str { pub fn to_markdown<'a>(&'a self, cx: &'a App) -> &'a str {
match self { match self {
ContentBlock::Empty => "", ContentBlock::Empty => "",
ContentBlock::Markdown { markdown } => markdown.read(cx).source(), ContentBlock::Markdown { markdown } => markdown.read(cx).source(),
@ -756,6 +759,8 @@ pub struct AcpThread {
connection: Rc<dyn AgentConnection>, connection: Rc<dyn AgentConnection>,
session_id: acp::SessionId, session_id: acp::SessionId,
token_usage: Option<TokenUsage>, token_usage: Option<TokenUsage>,
prompt_capabilities: acp::PromptCapabilities,
_observe_prompt_capabilities: Task<anyhow::Result<()>>,
} }
#[derive(Debug)] #[derive(Debug)]
@ -770,11 +775,12 @@ pub enum AcpThreadEvent {
Stopped, Stopped,
Error, Error,
LoadError(LoadError), LoadError(LoadError),
PromptCapabilitiesUpdated,
} }
impl EventEmitter<AcpThreadEvent> for AcpThread {} impl EventEmitter<AcpThreadEvent> for AcpThread {}
#[derive(PartialEq, Eq)] #[derive(PartialEq, Eq, Debug)]
pub enum ThreadStatus { pub enum ThreadStatus {
Idle, Idle,
WaitingForToolConfirmation, WaitingForToolConfirmation,
@ -821,7 +827,20 @@ impl AcpThread {
project: Entity<Project>, project: Entity<Project>,
action_log: Entity<ActionLog>, action_log: Entity<ActionLog>,
session_id: acp::SessionId, session_id: acp::SessionId,
mut prompt_capabilities_rx: watch::Receiver<acp::PromptCapabilities>,
cx: &mut Context<Self>,
) -> Self { ) -> Self {
let prompt_capabilities = *prompt_capabilities_rx.borrow();
let task = cx.spawn::<_, anyhow::Result<()>>(async move |this, cx| {
loop {
let caps = prompt_capabilities_rx.recv().await?;
this.update(cx, |this, cx| {
this.prompt_capabilities = caps;
cx.emit(AcpThreadEvent::PromptCapabilitiesUpdated);
})?;
}
});
Self { Self {
action_log, action_log,
shared_buffers: Default::default(), shared_buffers: Default::default(),
@ -833,9 +852,15 @@ impl AcpThread {
connection, connection,
session_id, session_id,
token_usage: None, token_usage: None,
prompt_capabilities,
_observe_prompt_capabilities: task,
} }
} }
pub fn prompt_capabilities(&self) -> acp::PromptCapabilities {
self.prompt_capabilities
}
pub fn connection(&self) -> &Rc<dyn AgentConnection> { pub fn connection(&self) -> &Rc<dyn AgentConnection> {
&self.connection &self.connection
} }
@ -1373,6 +1398,10 @@ impl AcpThread {
}) })
} }
pub fn can_resume(&self, cx: &App) -> bool {
self.connection.resume(&self.session_id, cx).is_some()
}
pub fn resume(&mut self, cx: &mut Context<Self>) -> BoxFuture<'static, Result<()>> { pub fn resume(&mut self, cx: &mut Context<Self>) -> BoxFuture<'static, Result<()>> {
self.run_turn(cx, async move |this, cx| { self.run_turn(cx, async move |this, cx| {
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
@ -2595,13 +2624,19 @@ mod tests {
.into(), .into(),
); );
let action_log = cx.new(|_| ActionLog::new(project.clone())); let action_log = cx.new(|_| ActionLog::new(project.clone()));
let thread = cx.new(|_cx| { let thread = cx.new(|cx| {
AcpThread::new( AcpThread::new(
"Test", "Test",
self.clone(), self.clone(),
project, project,
action_log, action_log,
session_id.clone(), session_id.clone(),
watch::Receiver::constant(acp::PromptCapabilities {
image: true,
audio: true,
embedded_context: true,
}),
cx,
) )
}); });
self.sessions.lock().insert(session_id, thread.downgrade()); self.sessions.lock().insert(session_id, thread.downgrade());
@ -2635,14 +2670,6 @@ mod tests {
} }
} }
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
acp::PromptCapabilities {
image: true,
audio: true,
embedded_context: true,
}
}
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
let sessions = self.sessions.lock(); let sessions = self.sessions.lock();
let thread = sessions.get(session_id).unwrap().clone(); let thread = sessions.get(session_id).unwrap().clone();
@ -2659,7 +2686,7 @@ mod tests {
fn truncate( fn truncate(
&self, &self,
session_id: &acp::SessionId, session_id: &acp::SessionId,
_cx: &mut App, _cx: &App,
) -> Option<Rc<dyn AgentSessionTruncate>> { ) -> Option<Rc<dyn AgentSessionTruncate>> {
Some(Rc::new(FakeAgentSessionEditor { Some(Rc::new(FakeAgentSessionEditor {
_session_id: session_id.clone(), _session_id: session_id.clone(),

View file

@ -38,12 +38,10 @@ pub trait AgentConnection {
cx: &mut App, cx: &mut App,
) -> Task<Result<acp::PromptResponse>>; ) -> Task<Result<acp::PromptResponse>>;
fn prompt_capabilities(&self) -> acp::PromptCapabilities;
fn resume( fn resume(
&self, &self,
_session_id: &acp::SessionId, _session_id: &acp::SessionId,
_cx: &mut App, _cx: &App,
) -> Option<Rc<dyn AgentSessionResume>> { ) -> Option<Rc<dyn AgentSessionResume>> {
None None
} }
@ -53,7 +51,7 @@ pub trait AgentConnection {
fn truncate( fn truncate(
&self, &self,
_session_id: &acp::SessionId, _session_id: &acp::SessionId,
_cx: &mut App, _cx: &App,
) -> Option<Rc<dyn AgentSessionTruncate>> { ) -> Option<Rc<dyn AgentSessionTruncate>> {
None None
} }
@ -61,7 +59,7 @@ pub trait AgentConnection {
fn set_title( fn set_title(
&self, &self,
_session_id: &acp::SessionId, _session_id: &acp::SessionId,
_cx: &mut App, _cx: &App,
) -> Option<Rc<dyn AgentSessionSetTitle>> { ) -> Option<Rc<dyn AgentSessionSetTitle>> {
None None
} }
@ -329,13 +327,19 @@ mod test_support {
) -> Task<gpui::Result<Entity<AcpThread>>> { ) -> Task<gpui::Result<Entity<AcpThread>>> {
let session_id = acp::SessionId(self.sessions.lock().len().to_string().into()); let session_id = acp::SessionId(self.sessions.lock().len().to_string().into());
let action_log = cx.new(|_| ActionLog::new(project.clone())); let action_log = cx.new(|_| ActionLog::new(project.clone()));
let thread = cx.new(|_cx| { let thread = cx.new(|cx| {
AcpThread::new( AcpThread::new(
"Test", "Test",
self.clone(), self.clone(),
project, project,
action_log, action_log,
session_id.clone(), session_id.clone(),
watch::Receiver::constant(acp::PromptCapabilities {
image: true,
audio: true,
embedded_context: true,
}),
cx,
) )
}); });
self.sessions.lock().insert( self.sessions.lock().insert(
@ -348,14 +352,6 @@ mod test_support {
Task::ready(Ok(thread)) Task::ready(Ok(thread))
} }
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
acp::PromptCapabilities {
image: true,
audio: true,
embedded_context: true,
}
}
fn authenticate( fn authenticate(
&self, &self,
_method_id: acp::AuthMethodId, _method_id: acp::AuthMethodId,
@ -439,7 +435,7 @@ mod test_support {
fn truncate( fn truncate(
&self, &self,
_session_id: &agent_client_protocol::SessionId, _session_id: &agent_client_protocol::SessionId,
_cx: &mut App, _cx: &App,
) -> Option<Rc<dyn AgentSessionTruncate>> { ) -> Option<Rc<dyn AgentSessionTruncate>> {
Some(Rc::new(StubAgentSessionEditor)) Some(Rc::new(StubAgentSessionEditor))
} }

View file

@ -85,27 +85,19 @@ impl Diff {
} }
pub fn new(buffer: Entity<Buffer>, cx: &mut Context<Self>) -> Self { pub fn new(buffer: Entity<Buffer>, cx: &mut Context<Self>) -> Self {
let buffer_snapshot = buffer.read(cx).snapshot(); let buffer_text_snapshot = buffer.read(cx).text_snapshot();
let base_text = buffer_snapshot.text(); let base_text_snapshot = buffer.read(cx).snapshot();
let language_registry = buffer.read(cx).language_registry(); let base_text = base_text_snapshot.text();
let text_snapshot = buffer.read(cx).text_snapshot(); debug_assert_eq!(buffer_text_snapshot.text(), base_text);
let buffer_diff = cx.new(|cx| { let buffer_diff = cx.new(|cx| {
let mut diff = BufferDiff::new(&text_snapshot, cx); let mut diff = BufferDiff::new_unchanged(&buffer_text_snapshot, base_text_snapshot);
let _ = diff.set_base_text(
buffer_snapshot.clone(),
language_registry,
text_snapshot,
cx,
);
let snapshot = diff.snapshot(cx); let snapshot = diff.snapshot(cx);
let secondary_diff = cx.new(|cx| { let secondary_diff = cx.new(|cx| {
let mut diff = BufferDiff::new(&buffer_snapshot, cx); let mut diff = BufferDiff::new(&buffer_text_snapshot, cx);
diff.set_snapshot(snapshot, &buffer_snapshot, cx); diff.set_snapshot(snapshot, &buffer_text_snapshot, cx);
diff diff
}); });
diff.set_secondary_diff(secondary_diff); diff.set_secondary_diff(secondary_diff);
diff diff
}); });
@ -412,3 +404,21 @@ async fn build_buffer_diff(
diff diff
}) })
} }
#[cfg(test)]
mod tests {
use gpui::{AppContext as _, TestAppContext};
use language::Buffer;
use crate::Diff;
#[gpui::test]
async fn test_pending_diff(cx: &mut TestAppContext) {
let buffer = cx.new(|cx| Buffer::local("hello!", cx));
let _diff = cx.new(|cx| Diff::new(buffer.clone(), cx));
buffer.update(cx, |buffer, cx| {
buffer.set_text("HELLO!", cx);
});
cx.run_until_parked();
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -664,7 +664,7 @@ impl Thread {
} }
pub fn get_or_init_configured_model(&mut self, cx: &App) -> Option<ConfiguredModel> { pub fn get_or_init_configured_model(&mut self, cx: &App) -> Option<ConfiguredModel> {
if self.configured_model.is_none() { if self.configured_model.is_none() || self.messages.is_empty() {
self.configured_model = LanguageModelRegistry::read_global(cx).default_model(); self.configured_model = LanguageModelRegistry::read_global(cx).default_model();
} }
self.configured_model.clone() self.configured_model.clone()
@ -2097,7 +2097,7 @@ impl Thread {
} }
pub fn summarize(&mut self, cx: &mut Context<Self>) { pub fn summarize(&mut self, cx: &mut Context<Self>) {
let Some(model) = LanguageModelRegistry::read_global(cx).thread_summary_model() else { let Some(model) = LanguageModelRegistry::read_global(cx).thread_summary_model(cx) else {
println!("No thread summary model"); println!("No thread summary model");
return; return;
}; };
@ -2416,7 +2416,7 @@ impl Thread {
} }
let Some(ConfiguredModel { model, provider }) = let Some(ConfiguredModel { model, provider }) =
LanguageModelRegistry::read_global(cx).thread_summary_model() LanguageModelRegistry::read_global(cx).thread_summary_model(cx)
else { else {
return; return;
}; };
@ -5410,13 +5410,10 @@ fn main() {{
}), }),
cx, cx,
); );
registry.set_thread_summary_model( registry.set_thread_summary_model(Some(ConfiguredModel {
Some(ConfiguredModel { provider,
provider, model: model.clone(),
model: model.clone(), }));
}),
cx,
);
}) })
}); });

View file

@ -893,8 +893,19 @@ impl ThreadsDatabase {
let needs_migration_from_heed = mdb_path.exists(); let needs_migration_from_heed = mdb_path.exists();
let connection = if *ZED_STATELESS || cfg!(any(feature = "test-support", test)) { let connection = if *ZED_STATELESS {
Connection::open_memory(Some("THREAD_FALLBACK_DB")) Connection::open_memory(Some("THREAD_FALLBACK_DB"))
} else if cfg!(any(feature = "test-support", test)) {
// rust stores the name of the test on the current thread.
// We use this to automatically create a database that will
// be shared within the test (for the test_retrieve_old_thread)
// but not with concurrent tests.
let thread = std::thread::current();
let test_name = thread.name();
Connection::open_memory(Some(&format!(
"THREAD_FALLBACK_{}",
test_name.unwrap_or_default()
)))
} else { } else {
Connection::open_file(&sqlite_path.to_string_lossy()) Connection::open_file(&sqlite_path.to_string_lossy())
}; };

View file

@ -180,7 +180,7 @@ impl NativeAgent {
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
cx: &mut AsyncApp, cx: &mut AsyncApp,
) -> Result<Entity<NativeAgent>> { ) -> Result<Entity<NativeAgent>> {
log::info!("Creating new NativeAgent"); log::debug!("Creating new NativeAgent");
let project_context = cx let project_context = cx
.update(|cx| Self::build_project_context(&project, prompt_store.as_ref(), cx))? .update(|cx| Self::build_project_context(&project, prompt_store.as_ref(), cx))?
@ -228,7 +228,7 @@ impl NativeAgent {
) -> Entity<AcpThread> { ) -> Entity<AcpThread> {
let connection = Rc::new(NativeAgentConnection(cx.entity())); let connection = Rc::new(NativeAgentConnection(cx.entity()));
let registry = LanguageModelRegistry::read_global(cx); let registry = LanguageModelRegistry::read_global(cx);
let summarization_model = registry.thread_summary_model().map(|c| c.model); let summarization_model = registry.thread_summary_model(cx).map(|c| c.model);
thread_handle.update(cx, |thread, cx| { thread_handle.update(cx, |thread, cx| {
thread.set_summarization_model(summarization_model, cx); thread.set_summarization_model(summarization_model, cx);
@ -240,13 +240,16 @@ impl NativeAgent {
let title = thread.title(); let title = thread.title();
let project = thread.project.clone(); let project = thread.project.clone();
let action_log = thread.action_log.clone(); let action_log = thread.action_log.clone();
let acp_thread = cx.new(|_cx| { let prompt_capabilities_rx = thread.prompt_capabilities_rx.clone();
let acp_thread = cx.new(|cx| {
acp_thread::AcpThread::new( acp_thread::AcpThread::new(
title, title,
connection, connection,
project.clone(), project.clone(),
action_log.clone(), action_log.clone(),
session_id.clone(), session_id.clone(),
prompt_capabilities_rx,
cx,
) )
}); });
let subscriptions = vec![ let subscriptions = vec![
@ -521,7 +524,7 @@ impl NativeAgent {
let registry = LanguageModelRegistry::read_global(cx); let registry = LanguageModelRegistry::read_global(cx);
let default_model = registry.default_model().map(|m| m.model); let default_model = registry.default_model().map(|m| m.model);
let summarization_model = registry.thread_summary_model().map(|m| m.model); let summarization_model = registry.thread_summary_model(cx).map(|m| m.model);
for session in self.sessions.values_mut() { for session in self.sessions.values_mut() {
session.thread.update(cx, |thread, cx| { session.thread.update(cx, |thread, cx| {
@ -756,7 +759,7 @@ impl NativeAgentConnection {
} }
} }
log::info!("Response stream completed"); log::debug!("Response stream completed");
anyhow::Ok(acp::PromptResponse { anyhow::Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn, stop_reason: acp::StopReason::EndTurn,
}) })
@ -781,7 +784,7 @@ impl AgentModelSelector for NativeAgentConnection {
model_id: acp_thread::AgentModelId, model_id: acp_thread::AgentModelId,
cx: &mut App, cx: &mut App,
) -> Task<Result<()>> { ) -> Task<Result<()>> {
log::info!("Setting model for session {}: {}", session_id, model_id); log::debug!("Setting model for session {}: {}", session_id, model_id);
let Some(thread) = self let Some(thread) = self
.0 .0
.read(cx) .read(cx)
@ -852,12 +855,11 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
cx: &mut App, cx: &mut App,
) -> Task<Result<Entity<acp_thread::AcpThread>>> { ) -> Task<Result<Entity<acp_thread::AcpThread>>> {
let agent = self.0.clone(); let agent = self.0.clone();
log::info!("Creating new thread for project at: {:?}", cwd); log::debug!("Creating new thread for project at: {:?}", cwd);
cx.spawn(async move |cx| { cx.spawn(async move |cx| {
log::debug!("Starting thread creation in async context"); log::debug!("Starting thread creation in async context");
let action_log = cx.new(|_cx| ActionLog::new(project.clone()))?;
// Create Thread // Create Thread
let thread = agent.update( let thread = agent.update(
cx, cx,
@ -878,7 +880,6 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
project.clone(), project.clone(),
agent.project_context.clone(), agent.project_context.clone(),
agent.context_server_registry.clone(), agent.context_server_registry.clone(),
action_log.clone(),
agent.templates.clone(), agent.templates.clone(),
default_model, default_model,
cx, cx,
@ -919,7 +920,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
.into_iter() .into_iter()
.map(Into::into) .map(Into::into)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
log::info!("Converted prompt to message: {} chars", content.len()); log::debug!("Converted prompt to message: {} chars", content.len());
log::debug!("Message id: {:?}", id); log::debug!("Message id: {:?}", id);
log::debug!("Message content: {:?}", content); log::debug!("Message content: {:?}", content);
@ -927,18 +928,10 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
}) })
} }
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
acp::PromptCapabilities {
image: true,
audio: false,
embedded_context: true,
}
}
fn resume( fn resume(
&self, &self,
session_id: &acp::SessionId, session_id: &acp::SessionId,
_cx: &mut App, _cx: &App,
) -> Option<Rc<dyn acp_thread::AgentSessionResume>> { ) -> Option<Rc<dyn acp_thread::AgentSessionResume>> {
Some(Rc::new(NativeAgentSessionResume { Some(Rc::new(NativeAgentSessionResume {
connection: self.clone(), connection: self.clone(),
@ -958,9 +951,9 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
fn truncate( fn truncate(
&self, &self,
session_id: &agent_client_protocol::SessionId, session_id: &agent_client_protocol::SessionId,
cx: &mut App, cx: &App,
) -> Option<Rc<dyn acp_thread::AgentSessionTruncate>> { ) -> Option<Rc<dyn acp_thread::AgentSessionTruncate>> {
self.0.update(cx, |agent, _cx| { self.0.read_with(cx, |agent, _cx| {
agent.sessions.get(session_id).map(|session| { agent.sessions.get(session_id).map(|session| {
Rc::new(NativeAgentSessionEditor { Rc::new(NativeAgentSessionEditor {
thread: session.thread.clone(), thread: session.thread.clone(),
@ -973,7 +966,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
fn set_title( fn set_title(
&self, &self,
session_id: &acp::SessionId, session_id: &acp::SessionId,
_cx: &mut App, _cx: &App,
) -> Option<Rc<dyn acp_thread::AgentSessionSetTitle>> { ) -> Option<Rc<dyn acp_thread::AgentSessionSetTitle>> {
Some(Rc::new(NativeAgentSessionSetTitle { Some(Rc::new(NativeAgentSessionSetTitle {
connection: self.clone(), connection: self.clone(),
@ -1408,10 +1401,9 @@ mod tests {
history: &Entity<HistoryStore>, history: &Entity<HistoryStore>,
cx: &mut TestAppContext, cx: &mut TestAppContext,
) -> Vec<(HistoryEntryId, String)> { ) -> Vec<(HistoryEntryId, String)> {
history.read_with(cx, |history, cx| { history.read_with(cx, |history, _| {
history history
.entries(cx) .entries()
.iter()
.map(|e| (e.id(), e.title().to_string())) .map(|e| (e.id(), e.title().to_string()))
.collect::<Vec<_>>() .collect::<Vec<_>>()
}) })

View file

@ -266,8 +266,19 @@ impl ThreadsDatabase {
} }
pub fn new(executor: BackgroundExecutor) -> Result<Self> { pub fn new(executor: BackgroundExecutor) -> Result<Self> {
let connection = if *ZED_STATELESS || cfg!(any(feature = "test-support", test)) { let connection = if *ZED_STATELESS {
Connection::open_memory(Some("THREAD_FALLBACK_DB")) Connection::open_memory(Some("THREAD_FALLBACK_DB"))
} else if cfg!(any(feature = "test-support", test)) {
// rust stores the name of the test on the current thread.
// We use this to automatically create a database that will
// be shared within the test (for the test_retrieve_old_thread)
// but not with concurrent tests.
let thread = std::thread::current();
let test_name = thread.name();
Connection::open_memory(Some(&format!(
"THREAD_FALLBACK_{}",
test_name.unwrap_or_default()
)))
} else { } else {
let threads_dir = paths::data_dir().join("threads"); let threads_dir = paths::data_dir().join("threads");
std::fs::create_dir_all(&threads_dir)?; std::fs::create_dir_all(&threads_dir)?;

View file

@ -86,6 +86,7 @@ enum SerializedRecentOpen {
pub struct HistoryStore { pub struct HistoryStore {
threads: Vec<DbThreadMetadata>, threads: Vec<DbThreadMetadata>,
entries: Vec<HistoryEntry>,
context_store: Entity<assistant_context::ContextStore>, context_store: Entity<assistant_context::ContextStore>,
recently_opened_entries: VecDeque<HistoryEntryId>, recently_opened_entries: VecDeque<HistoryEntryId>,
_subscriptions: Vec<gpui::Subscription>, _subscriptions: Vec<gpui::Subscription>,
@ -97,7 +98,7 @@ impl HistoryStore {
context_store: Entity<assistant_context::ContextStore>, context_store: Entity<assistant_context::ContextStore>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Self { ) -> Self {
let subscriptions = vec![cx.observe(&context_store, |_, _, cx| cx.notify())]; let subscriptions = vec![cx.observe(&context_store, |this, _, cx| this.update_entries(cx))];
cx.spawn(async move |this, cx| { cx.spawn(async move |this, cx| {
let entries = Self::load_recently_opened_entries(cx).await; let entries = Self::load_recently_opened_entries(cx).await;
@ -116,6 +117,7 @@ impl HistoryStore {
context_store, context_store,
recently_opened_entries: VecDeque::default(), recently_opened_entries: VecDeque::default(),
threads: Vec::default(), threads: Vec::default(),
entries: Vec::default(),
_subscriptions: subscriptions, _subscriptions: subscriptions,
_save_recently_opened_entries_task: Task::ready(()), _save_recently_opened_entries_task: Task::ready(()),
} }
@ -181,20 +183,18 @@ impl HistoryStore {
} }
} }
this.threads = threads; this.threads = threads;
cx.notify(); this.update_entries(cx);
}) })
}) })
.detach_and_log_err(cx); .detach_and_log_err(cx);
} }
pub fn entries(&self, cx: &App) -> Vec<HistoryEntry> { fn update_entries(&mut self, cx: &mut Context<Self>) {
let mut history_entries = Vec::new();
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() { if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
return history_entries; return;
} }
let mut history_entries = Vec::new();
history_entries.extend(self.threads.iter().cloned().map(HistoryEntry::AcpThread)); history_entries.extend(self.threads.iter().cloned().map(HistoryEntry::AcpThread));
history_entries.extend( history_entries.extend(
self.context_store self.context_store
@ -205,17 +205,12 @@ impl HistoryStore {
); );
history_entries.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.updated_at())); history_entries.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.updated_at()));
history_entries self.entries = history_entries;
cx.notify()
} }
pub fn is_empty(&self, cx: &App) -> bool { pub fn is_empty(&self, _cx: &App) -> bool {
self.threads.is_empty() self.entries.is_empty()
&& self
.context_store
.read(cx)
.unordered_contexts()
.next()
.is_none()
} }
pub fn recently_opened_entries(&self, cx: &App) -> Vec<HistoryEntry> { pub fn recently_opened_entries(&self, cx: &App) -> Vec<HistoryEntry> {
@ -356,7 +351,7 @@ impl HistoryStore {
self.save_recently_opened_entries(cx); self.save_recently_opened_entries(cx);
} }
pub fn recent_entries(&self, limit: usize, cx: &mut Context<Self>) -> Vec<HistoryEntry> { pub fn entries(&self) -> impl Iterator<Item = HistoryEntry> {
self.entries(cx).into_iter().take(limit).collect() self.entries.iter().cloned()
} }
} }

View file

@ -3,7 +3,7 @@ use std::{any::Any, path::Path, rc::Rc, sync::Arc};
use agent_servers::AgentServer; use agent_servers::AgentServer;
use anyhow::Result; use anyhow::Result;
use fs::Fs; use fs::Fs;
use gpui::{App, Entity, Task}; use gpui::{App, Entity, SharedString, Task};
use project::Project; use project::Project;
use prompt_store::PromptStore; use prompt_store::PromptStore;
@ -22,16 +22,20 @@ impl NativeAgentServer {
} }
impl AgentServer for NativeAgentServer { impl AgentServer for NativeAgentServer {
fn name(&self) -> &'static str { fn telemetry_id(&self) -> &'static str {
"Native Agent" "zed"
} }
fn empty_state_headline(&self) -> &'static str { fn name(&self) -> SharedString {
"Welcome to the Agent Panel" "Zed Agent".into()
} }
fn empty_state_message(&self) -> &'static str { fn empty_state_headline(&self) -> SharedString {
"" self.name()
}
fn empty_state_message(&self) -> SharedString {
"".into()
} }
fn logo(&self) -> ui::IconName { fn logo(&self) -> ui::IconName {
@ -44,7 +48,7 @@ impl AgentServer for NativeAgentServer {
project: &Entity<Project>, project: &Entity<Project>,
cx: &mut App, cx: &mut App,
) -> Task<Result<Rc<dyn acp_thread::AgentConnection>>> { ) -> Task<Result<Rc<dyn acp_thread::AgentConnection>>> {
log::info!( log::debug!(
"NativeAgentServer::connect called for path: {:?}", "NativeAgentServer::connect called for path: {:?}",
_root_dir _root_dir
); );
@ -63,7 +67,7 @@ impl AgentServer for NativeAgentServer {
// Create the connection wrapper // Create the connection wrapper
let connection = NativeAgentConnection(agent); let connection = NativeAgentConnection(agent);
log::info!("NativeAgentServer connection established successfully"); log::debug!("NativeAgentServer connection established successfully");
Ok(Rc::new(connection) as Rc<dyn acp_thread::AgentConnection>) Ok(Rc::new(connection) as Rc<dyn acp_thread::AgentConnection>)
}) })

View file

@ -1,30 +1,40 @@
use super::*; use super::*;
use acp_thread::{AgentConnection, AgentModelGroupName, AgentModelList, UserMessageId}; use acp_thread::{AgentConnection, AgentModelGroupName, AgentModelList, UserMessageId};
use action_log::ActionLog;
use agent_client_protocol::{self as acp}; use agent_client_protocol::{self as acp};
use agent_settings::AgentProfileId; use agent_settings::AgentProfileId;
use anyhow::Result; use anyhow::Result;
use client::{Client, UserStore}; use client::{Client, UserStore};
use cloud_llm_client::CompletionIntent;
use collections::IndexMap;
use context_server::{ContextServer, ContextServerCommand, ContextServerId};
use fs::{FakeFs, Fs}; use fs::{FakeFs, Fs};
use futures::{StreamExt, channel::mpsc::UnboundedReceiver}; use futures::{
StreamExt,
channel::{
mpsc::{self, UnboundedReceiver},
oneshot,
},
};
use gpui::{ use gpui::{
App, AppContext, Entity, Task, TestAppContext, UpdateGlobal, http_client::FakeHttpClient, App, AppContext, Entity, Task, TestAppContext, UpdateGlobal, http_client::FakeHttpClient,
}; };
use indoc::indoc; use indoc::indoc;
use language_model::{ use language_model::{
LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId,
LanguageModelProviderName, LanguageModelRegistry, LanguageModelRequestMessage, LanguageModelProviderName, LanguageModelRegistry, LanguageModelRequest,
LanguageModelToolResult, LanguageModelToolUse, MessageContent, Role, StopReason, LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelToolSchemaFormat,
fake_provider::FakeLanguageModel, LanguageModelToolUse, MessageContent, Role, StopReason, fake_provider::FakeLanguageModel,
}; };
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use project::Project; use project::{
Project, context_server_store::ContextServerStore, project_settings::ProjectSettings,
};
use prompt_store::ProjectContext; use prompt_store::ProjectContext;
use reqwest_client::ReqwestClient; use reqwest_client::ReqwestClient;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::json; use serde_json::json;
use settings::SettingsStore; use settings::{Settings, SettingsStore};
use std::{path::Path, rc::Rc, sync::Arc, time::Duration}; use std::{path::Path, rc::Rc, sync::Arc, time::Duration};
use util::path; use util::path;
@ -224,7 +234,7 @@ async fn test_prompt_caching(cx: &mut TestAppContext) {
let tool_use = LanguageModelToolUse { let tool_use = LanguageModelToolUse {
id: "tool_1".into(), id: "tool_1".into(),
name: EchoTool.name().into(), name: EchoTool::name().into(),
raw_input: json!({"text": "test"}).to_string(), raw_input: json!({"text": "test"}).to_string(),
input: json!({"text": "test"}), input: json!({"text": "test"}),
is_input_complete: true, is_input_complete: true,
@ -237,7 +247,7 @@ async fn test_prompt_caching(cx: &mut TestAppContext) {
let completion = fake_model.pending_completions().pop().unwrap(); let completion = fake_model.pending_completions().pop().unwrap();
let tool_result = LanguageModelToolResult { let tool_result = LanguageModelToolResult {
tool_use_id: "tool_1".into(), tool_use_id: "tool_1".into(),
tool_name: EchoTool.name().into(), tool_name: EchoTool::name().into(),
is_error: false, is_error: false,
content: "test".into(), content: "test".into(),
output: Some("test".into()), output: Some("test".into()),
@ -307,7 +317,7 @@ async fn test_basic_tool_calls(cx: &mut TestAppContext) {
// Test a tool calls that's likely to complete *after* streaming stops. // Test a tool calls that's likely to complete *after* streaming stops.
let events = thread let events = thread
.update(cx, |thread, cx| { .update(cx, |thread, cx| {
thread.remove_tool(&AgentTool::name(&EchoTool)); thread.remove_tool(&EchoTool::name());
thread.add_tool(DelayTool); thread.add_tool(DelayTool);
thread.send( thread.send(
UserMessageId::new(), UserMessageId::new(),
@ -411,7 +421,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
LanguageModelToolUse { LanguageModelToolUse {
id: "tool_id_1".into(), id: "tool_id_1".into(),
name: ToolRequiringPermission.name().into(), name: ToolRequiringPermission::name().into(),
raw_input: "{}".into(), raw_input: "{}".into(),
input: json!({}), input: json!({}),
is_input_complete: true, is_input_complete: true,
@ -420,7 +430,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
LanguageModelToolUse { LanguageModelToolUse {
id: "tool_id_2".into(), id: "tool_id_2".into(),
name: ToolRequiringPermission.name().into(), name: ToolRequiringPermission::name().into(),
raw_input: "{}".into(), raw_input: "{}".into(),
input: json!({}), input: json!({}),
is_input_complete: true, is_input_complete: true,
@ -451,14 +461,14 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
vec![ vec![
language_model::MessageContent::ToolResult(LanguageModelToolResult { language_model::MessageContent::ToolResult(LanguageModelToolResult {
tool_use_id: tool_call_auth_1.tool_call.id.0.to_string().into(), tool_use_id: tool_call_auth_1.tool_call.id.0.to_string().into(),
tool_name: ToolRequiringPermission.name().into(), tool_name: ToolRequiringPermission::name().into(),
is_error: false, is_error: false,
content: "Allowed".into(), content: "Allowed".into(),
output: Some("Allowed".into()) output: Some("Allowed".into())
}), }),
language_model::MessageContent::ToolResult(LanguageModelToolResult { language_model::MessageContent::ToolResult(LanguageModelToolResult {
tool_use_id: tool_call_auth_2.tool_call.id.0.to_string().into(), tool_use_id: tool_call_auth_2.tool_call.id.0.to_string().into(),
tool_name: ToolRequiringPermission.name().into(), tool_name: ToolRequiringPermission::name().into(),
is_error: true, is_error: true,
content: "Permission to run tool denied by user".into(), content: "Permission to run tool denied by user".into(),
output: None output: None
@ -470,7 +480,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
LanguageModelToolUse { LanguageModelToolUse {
id: "tool_id_3".into(), id: "tool_id_3".into(),
name: ToolRequiringPermission.name().into(), name: ToolRequiringPermission::name().into(),
raw_input: "{}".into(), raw_input: "{}".into(),
input: json!({}), input: json!({}),
is_input_complete: true, is_input_complete: true,
@ -492,7 +502,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
vec![language_model::MessageContent::ToolResult( vec![language_model::MessageContent::ToolResult(
LanguageModelToolResult { LanguageModelToolResult {
tool_use_id: tool_call_auth_3.tool_call.id.0.to_string().into(), tool_use_id: tool_call_auth_3.tool_call.id.0.to_string().into(),
tool_name: ToolRequiringPermission.name().into(), tool_name: ToolRequiringPermission::name().into(),
is_error: false, is_error: false,
content: "Allowed".into(), content: "Allowed".into(),
output: Some("Allowed".into()) output: Some("Allowed".into())
@ -504,7 +514,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
LanguageModelToolUse { LanguageModelToolUse {
id: "tool_id_4".into(), id: "tool_id_4".into(),
name: ToolRequiringPermission.name().into(), name: ToolRequiringPermission::name().into(),
raw_input: "{}".into(), raw_input: "{}".into(),
input: json!({}), input: json!({}),
is_input_complete: true, is_input_complete: true,
@ -519,7 +529,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
vec![language_model::MessageContent::ToolResult( vec![language_model::MessageContent::ToolResult(
LanguageModelToolResult { LanguageModelToolResult {
tool_use_id: "tool_id_4".into(), tool_use_id: "tool_id_4".into(),
tool_name: ToolRequiringPermission.name().into(), tool_name: ToolRequiringPermission::name().into(),
is_error: false, is_error: false,
content: "Allowed".into(), content: "Allowed".into(),
output: Some("Allowed".into()) output: Some("Allowed".into())
@ -571,7 +581,7 @@ async fn test_resume_after_tool_use_limit(cx: &mut TestAppContext) {
cx.run_until_parked(); cx.run_until_parked();
let tool_use = LanguageModelToolUse { let tool_use = LanguageModelToolUse {
id: "tool_id_1".into(), id: "tool_id_1".into(),
name: EchoTool.name().into(), name: EchoTool::name().into(),
raw_input: "{}".into(), raw_input: "{}".into(),
input: serde_json::to_value(&EchoToolInput { text: "def".into() }).unwrap(), input: serde_json::to_value(&EchoToolInput { text: "def".into() }).unwrap(),
is_input_complete: true, is_input_complete: true,
@ -584,7 +594,7 @@ async fn test_resume_after_tool_use_limit(cx: &mut TestAppContext) {
let completion = fake_model.pending_completions().pop().unwrap(); let completion = fake_model.pending_completions().pop().unwrap();
let tool_result = LanguageModelToolResult { let tool_result = LanguageModelToolResult {
tool_use_id: "tool_id_1".into(), tool_use_id: "tool_id_1".into(),
tool_name: EchoTool.name().into(), tool_name: EchoTool::name().into(),
is_error: false, is_error: false,
content: "def".into(), content: "def".into(),
output: Some("def".into()), output: Some("def".into()),
@ -664,15 +674,6 @@ async fn test_resume_after_tool_use_limit(cx: &mut TestAppContext) {
"} "}
) )
}); });
// Ensure we error if calling resume when tool use limit was *not* reached.
let error = thread
.update(cx, |thread, cx| thread.resume(cx))
.unwrap_err();
assert_eq!(
error.to_string(),
"can only resume after tool use limit is reached"
)
} }
#[gpui::test] #[gpui::test]
@ -690,14 +691,14 @@ async fn test_send_after_tool_use_limit(cx: &mut TestAppContext) {
let tool_use = LanguageModelToolUse { let tool_use = LanguageModelToolUse {
id: "tool_id_1".into(), id: "tool_id_1".into(),
name: EchoTool.name().into(), name: EchoTool::name().into(),
raw_input: "{}".into(), raw_input: "{}".into(),
input: serde_json::to_value(&EchoToolInput { text: "def".into() }).unwrap(), input: serde_json::to_value(&EchoToolInput { text: "def".into() }).unwrap(),
is_input_complete: true, is_input_complete: true,
}; };
let tool_result = LanguageModelToolResult { let tool_result = LanguageModelToolResult {
tool_use_id: "tool_id_1".into(), tool_use_id: "tool_id_1".into(),
tool_name: EchoTool.name().into(), tool_name: EchoTool::name().into(),
is_error: false, is_error: false,
content: "def".into(), content: "def".into(),
output: Some("def".into()), output: Some("def".into()),
@ -874,14 +875,14 @@ async fn test_profiles(cx: &mut TestAppContext) {
"test-1": { "test-1": {
"name": "Test Profile 1", "name": "Test Profile 1",
"tools": { "tools": {
EchoTool.name(): true, EchoTool::name(): true,
DelayTool.name(): true, DelayTool::name(): true,
} }
}, },
"test-2": { "test-2": {
"name": "Test Profile 2", "name": "Test Profile 2",
"tools": { "tools": {
InfiniteTool.name(): true, InfiniteTool::name(): true,
} }
} }
} }
@ -910,7 +911,7 @@ async fn test_profiles(cx: &mut TestAppContext) {
.iter() .iter()
.map(|tool| tool.name.clone()) .map(|tool| tool.name.clone())
.collect(); .collect();
assert_eq!(tool_names, vec![DelayTool.name(), EchoTool.name()]); assert_eq!(tool_names, vec![DelayTool::name(), EchoTool::name()]);
fake_model.end_last_completion_stream(); fake_model.end_last_completion_stream();
// Switch to test-2 profile, and verify that it has only the infinite tool. // Switch to test-2 profile, and verify that it has only the infinite tool.
@ -929,7 +930,335 @@ async fn test_profiles(cx: &mut TestAppContext) {
.iter() .iter()
.map(|tool| tool.name.clone()) .map(|tool| tool.name.clone())
.collect(); .collect();
assert_eq!(tool_names, vec![InfiniteTool.name()]); assert_eq!(tool_names, vec![InfiniteTool::name()]);
}
#[gpui::test]
async fn test_mcp_tools(cx: &mut TestAppContext) {
let ThreadTest {
model,
thread,
context_server_store,
fs,
..
} = setup(cx, TestModel::Fake).await;
let fake_model = model.as_fake();
// Override profiles and wait for settings to be loaded.
fs.insert_file(
paths::settings_file(),
json!({
"agent": {
"profiles": {
"test": {
"name": "Test Profile",
"enable_all_context_servers": true,
"tools": {
EchoTool::name(): true,
}
},
}
}
})
.to_string()
.into_bytes(),
)
.await;
cx.run_until_parked();
thread.update(cx, |thread, _| {
thread.set_profile(AgentProfileId("test".into()))
});
let mut mcp_tool_calls = setup_context_server(
"test_server",
vec![context_server::types::Tool {
name: "echo".into(),
description: None,
input_schema: serde_json::to_value(
EchoTool.input_schema(LanguageModelToolSchemaFormat::JsonSchema),
)
.unwrap(),
output_schema: None,
annotations: None,
}],
&context_server_store,
cx,
);
let events = thread.update(cx, |thread, cx| {
thread.send(UserMessageId::new(), ["Hey"], cx).unwrap()
});
cx.run_until_parked();
// Simulate the model calling the MCP tool.
let completion = fake_model.pending_completions().pop().unwrap();
assert_eq!(tool_names_for_completion(&completion), vec!["echo"]);
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
LanguageModelToolUse {
id: "tool_1".into(),
name: "echo".into(),
raw_input: json!({"text": "test"}).to_string(),
input: json!({"text": "test"}),
is_input_complete: true,
},
));
fake_model.end_last_completion_stream();
cx.run_until_parked();
let (tool_call_params, tool_call_response) = mcp_tool_calls.next().await.unwrap();
assert_eq!(tool_call_params.name, "echo");
assert_eq!(tool_call_params.arguments, Some(json!({"text": "test"})));
tool_call_response
.send(context_server::types::CallToolResponse {
content: vec![context_server::types::ToolResponseContent::Text {
text: "test".into(),
}],
is_error: None,
meta: None,
structured_content: None,
})
.unwrap();
cx.run_until_parked();
assert_eq!(tool_names_for_completion(&completion), vec!["echo"]);
fake_model.send_last_completion_stream_text_chunk("Done!");
fake_model.end_last_completion_stream();
events.collect::<Vec<_>>().await;
// Send again after adding the echo tool, ensuring the name collision is resolved.
let events = thread.update(cx, |thread, cx| {
thread.add_tool(EchoTool);
thread.send(UserMessageId::new(), ["Go"], cx).unwrap()
});
cx.run_until_parked();
let completion = fake_model.pending_completions().pop().unwrap();
assert_eq!(
tool_names_for_completion(&completion),
vec!["echo", "test_server_echo"]
);
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
LanguageModelToolUse {
id: "tool_2".into(),
name: "test_server_echo".into(),
raw_input: json!({"text": "mcp"}).to_string(),
input: json!({"text": "mcp"}),
is_input_complete: true,
},
));
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
LanguageModelToolUse {
id: "tool_3".into(),
name: "echo".into(),
raw_input: json!({"text": "native"}).to_string(),
input: json!({"text": "native"}),
is_input_complete: true,
},
));
fake_model.end_last_completion_stream();
cx.run_until_parked();
let (tool_call_params, tool_call_response) = mcp_tool_calls.next().await.unwrap();
assert_eq!(tool_call_params.name, "echo");
assert_eq!(tool_call_params.arguments, Some(json!({"text": "mcp"})));
tool_call_response
.send(context_server::types::CallToolResponse {
content: vec![context_server::types::ToolResponseContent::Text { text: "mcp".into() }],
is_error: None,
meta: None,
structured_content: None,
})
.unwrap();
cx.run_until_parked();
// Ensure the tool results were inserted with the correct names.
let completion = fake_model.pending_completions().pop().unwrap();
assert_eq!(
completion.messages.last().unwrap().content,
vec![
MessageContent::ToolResult(LanguageModelToolResult {
tool_use_id: "tool_3".into(),
tool_name: "echo".into(),
is_error: false,
content: "native".into(),
output: Some("native".into()),
},),
MessageContent::ToolResult(LanguageModelToolResult {
tool_use_id: "tool_2".into(),
tool_name: "test_server_echo".into(),
is_error: false,
content: "mcp".into(),
output: Some("mcp".into()),
},),
]
);
fake_model.end_last_completion_stream();
events.collect::<Vec<_>>().await;
}
#[gpui::test]
async fn test_mcp_tool_truncation(cx: &mut TestAppContext) {
let ThreadTest {
model,
thread,
context_server_store,
fs,
..
} = setup(cx, TestModel::Fake).await;
let fake_model = model.as_fake();
// Set up a profile with all tools enabled
fs.insert_file(
paths::settings_file(),
json!({
"agent": {
"profiles": {
"test": {
"name": "Test Profile",
"enable_all_context_servers": true,
"tools": {
EchoTool::name(): true,
DelayTool::name(): true,
WordListTool::name(): true,
ToolRequiringPermission::name(): true,
InfiniteTool::name(): true,
}
},
}
}
})
.to_string()
.into_bytes(),
)
.await;
cx.run_until_parked();
thread.update(cx, |thread, _| {
thread.set_profile(AgentProfileId("test".into()));
thread.add_tool(EchoTool);
thread.add_tool(DelayTool);
thread.add_tool(WordListTool);
thread.add_tool(ToolRequiringPermission);
thread.add_tool(InfiniteTool);
});
// Set up multiple context servers with some overlapping tool names
let _server1_calls = setup_context_server(
"xxx",
vec![
context_server::types::Tool {
name: "echo".into(), // Conflicts with native EchoTool
description: None,
input_schema: serde_json::to_value(
EchoTool.input_schema(LanguageModelToolSchemaFormat::JsonSchema),
)
.unwrap(),
output_schema: None,
annotations: None,
},
context_server::types::Tool {
name: "unique_tool_1".into(),
description: None,
input_schema: json!({"type": "object", "properties": {}}),
output_schema: None,
annotations: None,
},
],
&context_server_store,
cx,
);
let _server2_calls = setup_context_server(
"yyy",
vec![
context_server::types::Tool {
name: "echo".into(), // Also conflicts with native EchoTool
description: None,
input_schema: serde_json::to_value(
EchoTool.input_schema(LanguageModelToolSchemaFormat::JsonSchema),
)
.unwrap(),
output_schema: None,
annotations: None,
},
context_server::types::Tool {
name: "unique_tool_2".into(),
description: None,
input_schema: json!({"type": "object", "properties": {}}),
output_schema: None,
annotations: None,
},
context_server::types::Tool {
name: "a".repeat(MAX_TOOL_NAME_LENGTH - 2),
description: None,
input_schema: json!({"type": "object", "properties": {}}),
output_schema: None,
annotations: None,
},
context_server::types::Tool {
name: "b".repeat(MAX_TOOL_NAME_LENGTH - 1),
description: None,
input_schema: json!({"type": "object", "properties": {}}),
output_schema: None,
annotations: None,
},
],
&context_server_store,
cx,
);
let _server3_calls = setup_context_server(
"zzz",
vec![
context_server::types::Tool {
name: "a".repeat(MAX_TOOL_NAME_LENGTH - 2),
description: None,
input_schema: json!({"type": "object", "properties": {}}),
output_schema: None,
annotations: None,
},
context_server::types::Tool {
name: "b".repeat(MAX_TOOL_NAME_LENGTH - 1),
description: None,
input_schema: json!({"type": "object", "properties": {}}),
output_schema: None,
annotations: None,
},
context_server::types::Tool {
name: "c".repeat(MAX_TOOL_NAME_LENGTH + 1),
description: None,
input_schema: json!({"type": "object", "properties": {}}),
output_schema: None,
annotations: None,
},
],
&context_server_store,
cx,
);
thread
.update(cx, |thread, cx| {
thread.send(UserMessageId::new(), ["Go"], cx)
})
.unwrap();
cx.run_until_parked();
let completion = fake_model.pending_completions().pop().unwrap();
assert_eq!(
tool_names_for_completion(&completion),
vec![
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
"delay",
"echo",
"infinite",
"tool_requiring_permission",
"unique_tool_1",
"unique_tool_2",
"word_list",
"xxx_echo",
"y_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"yyy_echo",
"z_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
]
);
} }
#[gpui::test] #[gpui::test]
@ -1356,6 +1685,7 @@ async fn test_truncate_second_message(cx: &mut TestAppContext) {
} }
#[gpui::test] #[gpui::test]
#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows
async fn test_title_generation(cx: &mut TestAppContext) { async fn test_title_generation(cx: &mut TestAppContext) {
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
let fake_model = model.as_fake(); let fake_model = model.as_fake();
@ -1401,6 +1731,81 @@ async fn test_title_generation(cx: &mut TestAppContext) {
thread.read_with(cx, |thread, _| assert_eq!(thread.title(), "Hello world")); thread.read_with(cx, |thread, _| assert_eq!(thread.title(), "Hello world"));
} }
#[gpui::test]
async fn test_building_request_with_pending_tools(cx: &mut TestAppContext) {
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
let fake_model = model.as_fake();
let _events = thread
.update(cx, |thread, cx| {
thread.add_tool(ToolRequiringPermission);
thread.add_tool(EchoTool);
thread.send(UserMessageId::new(), ["Hey!"], cx)
})
.unwrap();
cx.run_until_parked();
let permission_tool_use = LanguageModelToolUse {
id: "tool_id_1".into(),
name: ToolRequiringPermission::name().into(),
raw_input: "{}".into(),
input: json!({}),
is_input_complete: true,
};
let echo_tool_use = LanguageModelToolUse {
id: "tool_id_2".into(),
name: EchoTool::name().into(),
raw_input: json!({"text": "test"}).to_string(),
input: json!({"text": "test"}),
is_input_complete: true,
};
fake_model.send_last_completion_stream_text_chunk("Hi!");
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
permission_tool_use,
));
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
echo_tool_use.clone(),
));
fake_model.end_last_completion_stream();
cx.run_until_parked();
// Ensure pending tools are skipped when building a request.
let request = thread
.read_with(cx, |thread, cx| {
thread.build_completion_request(CompletionIntent::EditFile, cx)
})
.unwrap();
assert_eq!(
request.messages[1..],
vec![
LanguageModelRequestMessage {
role: Role::User,
content: vec!["Hey!".into()],
cache: true
},
LanguageModelRequestMessage {
role: Role::Assistant,
content: vec![
MessageContent::Text("Hi!".into()),
MessageContent::ToolUse(echo_tool_use.clone())
],
cache: false
},
LanguageModelRequestMessage {
role: Role::User,
content: vec![MessageContent::ToolResult(LanguageModelToolResult {
tool_use_id: echo_tool_use.id.clone(),
tool_name: echo_tool_use.name,
is_error: false,
content: "test".into(),
output: Some("test".into())
})],
cache: false
},
],
);
}
#[gpui::test] #[gpui::test]
async fn test_agent_connection(cx: &mut TestAppContext) { async fn test_agent_connection(cx: &mut TestAppContext) {
cx.update(settings::init); cx.update(settings::init);
@ -1415,11 +1820,11 @@ async fn test_agent_connection(cx: &mut TestAppContext) {
let clock = Arc::new(clock::FakeSystemClock::new()); let clock = Arc::new(clock::FakeSystemClock::new());
let client = Client::new(clock, http_client, cx); let client = Client::new(clock, http_client, cx);
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
Project::init_settings(cx);
agent_settings::init(cx);
language_model::init(client.clone(), cx); language_model::init(client.clone(), cx);
language_models::init(user_store, client.clone(), cx); language_models::init(user_store, client.clone(), cx);
Project::init_settings(cx);
LanguageModelRegistry::test(cx); LanguageModelRegistry::test(cx);
agent_settings::init(cx);
}); });
cx.executor().forbid_parking(); cx.executor().forbid_parking();
@ -1552,7 +1957,7 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
LanguageModelToolUse { LanguageModelToolUse {
id: "1".into(), id: "1".into(),
name: ThinkingTool.name().into(), name: ThinkingTool::name().into(),
raw_input: input.to_string(), raw_input: input.to_string(),
input, input,
is_input_complete: false, is_input_complete: false,
@ -1693,6 +2098,7 @@ async fn test_send_retry_on_error(cx: &mut TestAppContext) {
.unwrap(); .unwrap();
cx.run_until_parked(); cx.run_until_parked();
fake_model.send_last_completion_stream_text_chunk("Hey,");
fake_model.send_last_completion_stream_error(LanguageModelCompletionError::ServerOverloaded { fake_model.send_last_completion_stream_error(LanguageModelCompletionError::ServerOverloaded {
provider: LanguageModelProviderName::new("Anthropic"), provider: LanguageModelProviderName::new("Anthropic"),
retry_after: Some(Duration::from_secs(3)), retry_after: Some(Duration::from_secs(3)),
@ -1702,8 +2108,9 @@ async fn test_send_retry_on_error(cx: &mut TestAppContext) {
cx.executor().advance_clock(Duration::from_secs(3)); cx.executor().advance_clock(Duration::from_secs(3));
cx.run_until_parked(); cx.run_until_parked();
fake_model.send_last_completion_stream_text_chunk("Hey!"); fake_model.send_last_completion_stream_text_chunk("there!");
fake_model.end_last_completion_stream(); fake_model.end_last_completion_stream();
cx.run_until_parked();
let mut retry_events = Vec::new(); let mut retry_events = Vec::new();
while let Some(Ok(event)) = events.next().await { while let Some(Ok(event)) = events.next().await {
@ -1731,12 +2138,94 @@ async fn test_send_retry_on_error(cx: &mut TestAppContext) {
## Assistant ## Assistant
Hey! Hey,
[resume]
## Assistant
there!
"} "}
) )
}); });
} }
#[gpui::test]
async fn test_send_retry_finishes_tool_calls_on_error(cx: &mut TestAppContext) {
let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await;
let fake_model = model.as_fake();
let events = thread
.update(cx, |thread, cx| {
thread.set_completion_mode(agent_settings::CompletionMode::Burn, cx);
thread.add_tool(EchoTool);
thread.send(UserMessageId::new(), ["Call the echo tool!"], cx)
})
.unwrap();
cx.run_until_parked();
let tool_use_1 = LanguageModelToolUse {
id: "tool_1".into(),
name: EchoTool::name().into(),
raw_input: json!({"text": "test"}).to_string(),
input: json!({"text": "test"}),
is_input_complete: true,
};
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
tool_use_1.clone(),
));
fake_model.send_last_completion_stream_error(LanguageModelCompletionError::ServerOverloaded {
provider: LanguageModelProviderName::new("Anthropic"),
retry_after: Some(Duration::from_secs(3)),
});
fake_model.end_last_completion_stream();
cx.executor().advance_clock(Duration::from_secs(3));
let completion = fake_model.pending_completions().pop().unwrap();
assert_eq!(
completion.messages[1..],
vec![
LanguageModelRequestMessage {
role: Role::User,
content: vec!["Call the echo tool!".into()],
cache: false
},
LanguageModelRequestMessage {
role: Role::Assistant,
content: vec![language_model::MessageContent::ToolUse(tool_use_1.clone())],
cache: false
},
LanguageModelRequestMessage {
role: Role::User,
content: vec![language_model::MessageContent::ToolResult(
LanguageModelToolResult {
tool_use_id: tool_use_1.id.clone(),
tool_name: tool_use_1.name.clone(),
is_error: false,
content: "test".into(),
output: Some("test".into())
}
)],
cache: true
},
]
);
fake_model.send_last_completion_stream_text_chunk("Done");
fake_model.end_last_completion_stream();
cx.run_until_parked();
events.collect::<Vec<_>>().await;
thread.read_with(cx, |thread, _cx| {
assert_eq!(
thread.last_message(),
Some(Message::Agent(AgentMessage {
content: vec![AgentMessageContent::Text("Done".into())],
tool_results: IndexMap::default()
}))
);
})
}
#[gpui::test] #[gpui::test]
async fn test_send_max_retries_exceeded(cx: &mut TestAppContext) { async fn test_send_max_retries_exceeded(cx: &mut TestAppContext) {
let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await; let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await;
@ -1807,6 +2296,7 @@ struct ThreadTest {
model: Arc<dyn LanguageModel>, model: Arc<dyn LanguageModel>,
thread: Entity<Thread>, thread: Entity<Thread>,
project_context: Entity<ProjectContext>, project_context: Entity<ProjectContext>,
context_server_store: Entity<ContextServerStore>,
fs: Arc<FakeFs>, fs: Arc<FakeFs>,
} }
@ -1840,11 +2330,12 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest {
"test-profile": { "test-profile": {
"name": "Test Profile", "name": "Test Profile",
"tools": { "tools": {
EchoTool.name(): true, EchoTool::name(): true,
DelayTool.name(): true, DelayTool::name(): true,
WordListTool.name(): true, WordListTool::name(): true,
ToolRequiringPermission.name(): true, ToolRequiringPermission::name(): true,
InfiniteTool.name(): true, InfiniteTool::name(): true,
ThinkingTool::name(): true,
} }
} }
} }
@ -1901,15 +2392,14 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest {
.await; .await;
let project_context = cx.new(|_cx| ProjectContext::default()); let project_context = cx.new(|_cx| ProjectContext::default());
let context_server_store = project.read_with(cx, |project, _| project.context_server_store());
let context_server_registry = let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); cx.new(|cx| ContextServerRegistry::new(context_server_store.clone(), cx));
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let thread = cx.new(|cx| { let thread = cx.new(|cx| {
Thread::new( Thread::new(
project, project,
project_context.clone(), project_context.clone(),
context_server_registry, context_server_registry,
action_log,
templates, templates,
Some(model.clone()), Some(model.clone()),
cx, cx,
@ -1919,6 +2409,7 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest {
model, model,
thread, thread,
project_context, project_context,
context_server_store,
fs, fs,
} }
} }
@ -1953,3 +2444,89 @@ fn watch_settings(fs: Arc<dyn Fs>, cx: &mut App) {
}) })
.detach(); .detach();
} }
fn tool_names_for_completion(completion: &LanguageModelRequest) -> Vec<String> {
completion
.tools
.iter()
.map(|tool| tool.name.clone())
.collect()
}
fn setup_context_server(
name: &'static str,
tools: Vec<context_server::types::Tool>,
context_server_store: &Entity<ContextServerStore>,
cx: &mut TestAppContext,
) -> mpsc::UnboundedReceiver<(
context_server::types::CallToolParams,
oneshot::Sender<context_server::types::CallToolResponse>,
)> {
cx.update(|cx| {
let mut settings = ProjectSettings::get_global(cx).clone();
settings.context_servers.insert(
name.into(),
project::project_settings::ContextServerSettings::Custom {
enabled: true,
command: ContextServerCommand {
path: "somebinary".into(),
args: Vec::new(),
env: None,
},
},
);
ProjectSettings::override_global(settings, cx);
});
let (mcp_tool_calls_tx, mcp_tool_calls_rx) = mpsc::unbounded();
let fake_transport = context_server::test::create_fake_transport(name, cx.executor())
.on_request::<context_server::types::requests::Initialize, _>(move |_params| async move {
context_server::types::InitializeResponse {
protocol_version: context_server::types::ProtocolVersion(
context_server::types::LATEST_PROTOCOL_VERSION.to_string(),
),
server_info: context_server::types::Implementation {
name: name.into(),
version: "1.0.0".to_string(),
},
capabilities: context_server::types::ServerCapabilities {
tools: Some(context_server::types::ToolsCapabilities {
list_changed: Some(true),
}),
..Default::default()
},
meta: None,
}
})
.on_request::<context_server::types::requests::ListTools, _>(move |_params| {
let tools = tools.clone();
async move {
context_server::types::ListToolsResponse {
tools,
next_cursor: None,
meta: None,
}
}
})
.on_request::<context_server::types::requests::CallTool, _>(move |params| {
let mcp_tool_calls_tx = mcp_tool_calls_tx.clone();
async move {
let (response_tx, response_rx) = oneshot::channel();
mcp_tool_calls_tx
.unbounded_send((params, response_tx))
.unwrap();
response_rx.await.unwrap()
}
});
context_server_store.update(cx, |store, cx| {
store.start_server(
Arc::new(ContextServer::new(
ContextServerId(name.into()),
Arc::new(fake_transport),
)),
cx,
);
});
cx.run_until_parked();
mcp_tool_calls_rx
}

View file

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

View file

@ -9,15 +9,15 @@ use action_log::ActionLog;
use agent::thread::{GitState, ProjectSnapshot, WorktreeSnapshot}; use agent::thread::{GitState, ProjectSnapshot, WorktreeSnapshot};
use agent_client_protocol as acp; use agent_client_protocol as acp;
use agent_settings::{ use agent_settings::{
AgentProfileId, AgentSettings, CompletionMode, SUMMARIZE_THREAD_DETAILED_PROMPT, AgentProfileId, AgentProfileSettings, AgentSettings, CompletionMode,
SUMMARIZE_THREAD_PROMPT, SUMMARIZE_THREAD_DETAILED_PROMPT, SUMMARIZE_THREAD_PROMPT,
}; };
use anyhow::{Context as _, Result, anyhow}; use anyhow::{Context as _, Result, anyhow};
use assistant_tool::adapt_schema_to_format; use assistant_tool::adapt_schema_to_format;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use client::{ModelRequestUsage, RequestUsage}; use client::{ModelRequestUsage, RequestUsage};
use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit}; use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit};
use collections::{HashMap, IndexMap}; use collections::{HashMap, HashSet, IndexMap};
use fs::Fs; use fs::Fs;
use futures::{ use futures::{
FutureExt, FutureExt,
@ -45,17 +45,19 @@ use schemars::{JsonSchema, Schema};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use settings::{Settings, update_settings_file}; use settings::{Settings, update_settings_file};
use smol::stream::StreamExt; use smol::stream::StreamExt;
use std::fmt::Write;
use std::{ use std::{
collections::BTreeMap, collections::BTreeMap,
ops::RangeInclusive,
path::Path, path::Path,
sync::Arc, sync::Arc,
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use std::{fmt::Write, ops::Range}; use util::{ResultExt, debug_panic, markdown::MarkdownCodeBlock};
use util::{ResultExt, markdown::MarkdownCodeBlock};
use uuid::Uuid; use uuid::Uuid;
const TOOL_CANCELED_MESSAGE: &str = "Tool canceled by user"; const TOOL_CANCELED_MESSAGE: &str = "Tool canceled by user";
pub const MAX_TOOL_NAME_LENGTH: usize = 64;
/// The ID of the user prompt that initiated a request. /// The ID of the user prompt that initiated a request.
/// ///
@ -121,7 +123,7 @@ impl Message {
match self { match self {
Message::User(message) => message.to_markdown(), Message::User(message) => message.to_markdown(),
Message::Agent(message) => message.to_markdown(), Message::Agent(message) => message.to_markdown(),
Message::Resume => "[resumed after tool use limit was reached]".into(), Message::Resume => "[resume]\n".into(),
} }
} }
@ -186,6 +188,7 @@ impl UserMessage {
const OPEN_FILES_TAG: &str = "<files>"; const OPEN_FILES_TAG: &str = "<files>";
const OPEN_DIRECTORIES_TAG: &str = "<directories>"; const OPEN_DIRECTORIES_TAG: &str = "<directories>";
const OPEN_SYMBOLS_TAG: &str = "<symbols>"; const OPEN_SYMBOLS_TAG: &str = "<symbols>";
const OPEN_SELECTIONS_TAG: &str = "<selections>";
const OPEN_THREADS_TAG: &str = "<threads>"; const OPEN_THREADS_TAG: &str = "<threads>";
const OPEN_FETCH_TAG: &str = "<fetched_urls>"; const OPEN_FETCH_TAG: &str = "<fetched_urls>";
const OPEN_RULES_TAG: &str = const OPEN_RULES_TAG: &str =
@ -194,6 +197,7 @@ impl UserMessage {
let mut file_context = OPEN_FILES_TAG.to_string(); let mut file_context = OPEN_FILES_TAG.to_string();
let mut directory_context = OPEN_DIRECTORIES_TAG.to_string(); let mut directory_context = OPEN_DIRECTORIES_TAG.to_string();
let mut symbol_context = OPEN_SYMBOLS_TAG.to_string(); let mut symbol_context = OPEN_SYMBOLS_TAG.to_string();
let mut selection_context = OPEN_SELECTIONS_TAG.to_string();
let mut thread_context = OPEN_THREADS_TAG.to_string(); let mut thread_context = OPEN_THREADS_TAG.to_string();
let mut fetch_context = OPEN_FETCH_TAG.to_string(); let mut fetch_context = OPEN_FETCH_TAG.to_string();
let mut rules_context = OPEN_RULES_TAG.to_string(); let mut rules_context = OPEN_RULES_TAG.to_string();
@ -210,7 +214,7 @@ impl UserMessage {
match uri { match uri {
MentionUri::File { abs_path } => { MentionUri::File { abs_path } => {
write!( write!(
&mut symbol_context, &mut file_context,
"\n{}", "\n{}",
MarkdownCodeBlock { MarkdownCodeBlock {
tag: &codeblock_tag(abs_path, None), tag: &codeblock_tag(abs_path, None),
@ -219,17 +223,19 @@ impl UserMessage {
) )
.ok(); .ok();
} }
MentionUri::PastedImage => {
debug_panic!("pasted image URI should not be used in mention content")
}
MentionUri::Directory { .. } => { MentionUri::Directory { .. } => {
write!(&mut directory_context, "\n{}\n", content).ok(); write!(&mut directory_context, "\n{}\n", content).ok();
} }
MentionUri::Symbol { MentionUri::Symbol {
path, line_range, .. abs_path: path,
} line_range,
| MentionUri::Selection { ..
path, line_range, ..
} => { } => {
write!( write!(
&mut rules_context, &mut symbol_context,
"\n{}", "\n{}",
MarkdownCodeBlock { MarkdownCodeBlock {
tag: &codeblock_tag(path, Some(line_range)), tag: &codeblock_tag(path, Some(line_range)),
@ -238,6 +244,24 @@ impl UserMessage {
) )
.ok(); .ok();
} }
MentionUri::Selection {
abs_path: path,
line_range,
..
} => {
write!(
&mut selection_context,
"\n{}",
MarkdownCodeBlock {
tag: &codeblock_tag(
path.as_deref().unwrap_or("Untitled".as_ref()),
Some(line_range)
),
text: content
}
)
.ok();
}
MentionUri::Thread { .. } => { MentionUri::Thread { .. } => {
write!(&mut thread_context, "\n{}\n", content).ok(); write!(&mut thread_context, "\n{}\n", content).ok();
} }
@ -290,6 +314,13 @@ impl UserMessage {
.push(language_model::MessageContent::Text(symbol_context)); .push(language_model::MessageContent::Text(symbol_context));
} }
if selection_context.len() > OPEN_SELECTIONS_TAG.len() {
selection_context.push_str("</selections>\n");
message
.content
.push(language_model::MessageContent::Text(selection_context));
}
if thread_context.len() > OPEN_THREADS_TAG.len() { if thread_context.len() > OPEN_THREADS_TAG.len() {
thread_context.push_str("</threads>\n"); thread_context.push_str("</threads>\n");
message message
@ -325,7 +356,7 @@ impl UserMessage {
} }
} }
fn codeblock_tag(full_path: &Path, line_range: Option<&Range<u32>>) -> String { fn codeblock_tag(full_path: &Path, line_range: Option<&RangeInclusive<u32>>) -> String {
let mut result = String::new(); let mut result = String::new();
if let Some(extension) = full_path.extension().and_then(|ext| ext.to_str()) { if let Some(extension) = full_path.extension().and_then(|ext| ext.to_str()) {
@ -335,10 +366,10 @@ fn codeblock_tag(full_path: &Path, line_range: Option<&Range<u32>>) -> String {
let _ = write!(result, "{}", full_path.display()); let _ = write!(result, "{}", full_path.display());
if let Some(range) = line_range { if let Some(range) = line_range {
if range.start == range.end { if range.start() == range.end() {
let _ = write!(result, ":{}", range.start + 1); let _ = write!(result, ":{}", range.start() + 1);
} else { } else {
let _ = write!(result, ":{}-{}", range.start + 1, range.end + 1); let _ = write!(result, ":{}-{}", range.start() + 1, range.end() + 1);
} }
} }
@ -417,24 +448,33 @@ impl AgentMessage {
cache: false, cache: false,
}; };
for chunk in &self.content { for chunk in &self.content {
let chunk = match chunk { match chunk {
AgentMessageContent::Text(text) => { AgentMessageContent::Text(text) => {
language_model::MessageContent::Text(text.clone()) assistant_message
.content
.push(language_model::MessageContent::Text(text.clone()));
} }
AgentMessageContent::Thinking { text, signature } => { AgentMessageContent::Thinking { text, signature } => {
language_model::MessageContent::Thinking { assistant_message
text: text.clone(), .content
signature: signature.clone(), .push(language_model::MessageContent::Thinking {
} text: text.clone(),
signature: signature.clone(),
});
} }
AgentMessageContent::RedactedThinking(value) => { AgentMessageContent::RedactedThinking(value) => {
language_model::MessageContent::RedactedThinking(value.clone()) assistant_message.content.push(
language_model::MessageContent::RedactedThinking(value.clone()),
);
} }
AgentMessageContent::ToolUse(value) => { AgentMessageContent::ToolUse(tool_use) => {
language_model::MessageContent::ToolUse(value.clone()) if self.tool_results.contains_key(&tool_use.id) {
assistant_message
.content
.push(language_model::MessageContent::ToolUse(tool_use.clone()));
}
} }
}; };
assistant_message.content.push(chunk);
} }
let mut user_message = LanguageModelRequestMessage { let mut user_message = LanguageModelRequestMessage {
@ -535,21 +575,34 @@ pub struct Thread {
templates: Arc<Templates>, templates: Arc<Templates>,
model: Option<Arc<dyn LanguageModel>>, model: Option<Arc<dyn LanguageModel>>,
summarization_model: Option<Arc<dyn LanguageModel>>, summarization_model: Option<Arc<dyn LanguageModel>>,
prompt_capabilities_tx: watch::Sender<acp::PromptCapabilities>,
pub(crate) prompt_capabilities_rx: watch::Receiver<acp::PromptCapabilities>,
pub(crate) project: Entity<Project>, pub(crate) project: Entity<Project>,
pub(crate) action_log: Entity<ActionLog>, pub(crate) action_log: Entity<ActionLog>,
} }
impl Thread { impl Thread {
fn prompt_capabilities(model: Option<&dyn LanguageModel>) -> acp::PromptCapabilities {
let image = model.map_or(true, |model| model.supports_images());
acp::PromptCapabilities {
image,
audio: false,
embedded_context: true,
}
}
pub fn new( pub fn new(
project: Entity<Project>, project: Entity<Project>,
project_context: Entity<ProjectContext>, project_context: Entity<ProjectContext>,
context_server_registry: Entity<ContextServerRegistry>, context_server_registry: Entity<ContextServerRegistry>,
action_log: Entity<ActionLog>,
templates: Arc<Templates>, templates: Arc<Templates>,
model: Option<Arc<dyn LanguageModel>>, model: Option<Arc<dyn LanguageModel>>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Self { ) -> Self {
let profile_id = AgentSettings::get_global(cx).default_profile.clone(); let profile_id = AgentSettings::get_global(cx).default_profile.clone();
let action_log = cx.new(|_cx| ActionLog::new(project.clone()));
let (prompt_capabilities_tx, prompt_capabilities_rx) =
watch::channel(Self::prompt_capabilities(model.as_deref()));
Self { Self {
id: acp::SessionId(uuid::Uuid::new_v4().to_string().into()), id: acp::SessionId(uuid::Uuid::new_v4().to_string().into()),
prompt_id: PromptId::new(), prompt_id: PromptId::new(),
@ -577,6 +630,8 @@ impl Thread {
templates, templates,
model, model,
summarization_model: None, summarization_model: None,
prompt_capabilities_tx,
prompt_capabilities_rx,
project, project,
action_log, action_log,
} }
@ -627,7 +682,20 @@ impl Thread {
stream: &ThreadEventStream, stream: &ThreadEventStream,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let Some(tool) = self.tools.get(tool_use.name.as_ref()) else { let tool = self.tools.get(tool_use.name.as_ref()).cloned().or_else(|| {
self.context_server_registry
.read(cx)
.servers()
.find_map(|(_, tools)| {
if let Some(tool) = tools.get(tool_use.name.as_ref()) {
Some(tool.clone())
} else {
None
}
})
});
let Some(tool) = tool else {
stream stream
.0 .0
.unbounded_send(Ok(ThreadEvent::ToolCall(acp::ToolCall { .unbounded_send(Ok(ThreadEvent::ToolCall(acp::ToolCall {
@ -697,6 +765,8 @@ impl Thread {
.or_else(|| registry.default_model()) .or_else(|| registry.default_model())
.map(|model| model.model) .map(|model| model.model)
}); });
let (prompt_capabilities_tx, prompt_capabilities_rx) =
watch::channel(Self::prompt_capabilities(model.as_deref()));
Self { Self {
id, id,
@ -726,6 +796,8 @@ impl Thread {
project, project,
action_log, action_log,
updated_at: db_thread.updated_at, updated_at: db_thread.updated_at,
prompt_capabilities_tx,
prompt_capabilities_rx,
} }
} }
@ -893,10 +965,12 @@ impl Thread {
pub fn set_model(&mut self, model: Arc<dyn LanguageModel>, cx: &mut Context<Self>) { pub fn set_model(&mut self, model: Arc<dyn LanguageModel>, cx: &mut Context<Self>) {
let old_usage = self.latest_token_usage(); let old_usage = self.latest_token_usage();
self.model = Some(model); self.model = Some(model);
let new_caps = Self::prompt_capabilities(self.model.as_deref());
let new_usage = self.latest_token_usage(); let new_usage = self.latest_token_usage();
if old_usage != new_usage { if old_usage != new_usage {
cx.emit(TokenUsageUpdated(new_usage)); cx.emit(TokenUsageUpdated(new_usage));
} }
self.prompt_capabilities_tx.send(new_caps).log_err();
cx.notify() cx.notify()
} }
@ -959,11 +1033,11 @@ impl Thread {
)); ));
self.add_tool(TerminalTool::new(self.project.clone(), cx)); self.add_tool(TerminalTool::new(self.project.clone(), cx));
self.add_tool(ThinkingTool); self.add_tool(ThinkingTool);
self.add_tool(WebSearchTool); // TODO: Enable this only if it's a zed model. self.add_tool(WebSearchTool);
} }
pub fn add_tool(&mut self, tool: impl AgentTool) { pub fn add_tool<T: AgentTool>(&mut self, tool: T) {
self.tools.insert(tool.name(), tool.erase()); self.tools.insert(T::name().into(), tool.erase());
} }
pub fn remove_tool(&mut self, name: &str) -> bool { pub fn remove_tool(&mut self, name: &str) -> bool {
@ -1032,15 +1106,10 @@ impl Thread {
&mut self, &mut self,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Result<mpsc::UnboundedReceiver<Result<ThreadEvent>>> { ) -> Result<mpsc::UnboundedReceiver<Result<ThreadEvent>>> {
anyhow::ensure!(
self.tool_use_limit_reached,
"can only resume after tool use limit is reached"
);
self.messages.push(Message::Resume); self.messages.push(Message::Resume);
cx.notify(); cx.notify();
log::info!("Total messages in thread: {}", self.messages.len()); log::debug!("Total messages in thread: {}", self.messages.len());
self.run_turn(cx) self.run_turn(cx)
} }
@ -1058,7 +1127,7 @@ impl Thread {
{ {
let model = self.model().context("No language model configured")?; let model = self.model().context("No language model configured")?;
log::info!("Thread::send called with model: {:?}", model.name()); log::info!("Thread::send called with model: {}", model.name().0);
self.advance_prompt_id(); self.advance_prompt_id();
let content = content.into_iter().map(Into::into).collect::<Vec<_>>(); let content = content.into_iter().map(Into::into).collect::<Vec<_>>();
@ -1068,7 +1137,7 @@ impl Thread {
.push(Message::User(UserMessage { id, content })); .push(Message::User(UserMessage { id, content }));
cx.notify(); cx.notify();
log::info!("Total messages in thread: {}", self.messages.len()); log::debug!("Total messages in thread: {}", self.messages.len());
self.run_turn(cx) self.run_turn(cx)
} }
@ -1079,6 +1148,10 @@ impl Thread {
self.cancel(cx); self.cancel(cx);
let model = self.model.clone().context("No language model configured")?; let model = self.model.clone().context("No language model configured")?;
let profile = AgentSettings::get_global(cx)
.profiles
.get(&self.profile_id)
.context("Profile not found")?;
let (events_tx, events_rx) = mpsc::unbounded::<Result<ThreadEvent>>(); let (events_tx, events_rx) = mpsc::unbounded::<Result<ThreadEvent>>();
let event_stream = ThreadEventStream(events_tx); let event_stream = ThreadEventStream(events_tx);
let message_ix = self.messages.len().saturating_sub(1); let message_ix = self.messages.len().saturating_sub(1);
@ -1086,45 +1159,16 @@ impl Thread {
self.summary = None; self.summary = None;
self.running_turn = Some(RunningTurn { self.running_turn = Some(RunningTurn {
event_stream: event_stream.clone(), event_stream: event_stream.clone(),
tools: self.enabled_tools(profile, &model, cx),
_task: cx.spawn(async move |this, cx| { _task: cx.spawn(async move |this, cx| {
log::info!("Starting agent turn execution"); log::debug!("Starting agent turn execution");
let turn_result: Result<()> = async { let turn_result = Self::run_turn_internal(&this, model, &event_stream, cx).await;
let mut intent = CompletionIntent::UserPrompt;
loop {
Self::stream_completion(&this, &model, intent, &event_stream, cx).await?;
let mut end_turn = true;
this.update(cx, |this, cx| {
// Generate title if needed.
if this.title.is_none() && this.pending_title_generation.is_none() {
this.generate_title(cx);
}
// End the turn if the model didn't use tools.
let message = this.pending_message.as_ref();
end_turn =
message.map_or(true, |message| message.tool_results.is_empty());
this.flush_pending_message(cx);
})?;
if this.read_with(cx, |this, _| this.tool_use_limit_reached)? {
log::info!("Tool use limit reached, completing turn");
return Err(language_model::ToolUseLimitReachedError.into());
} else if end_turn {
log::info!("No tool uses found, completing turn");
return Ok(());
} else {
intent = CompletionIntent::ToolResults;
}
}
}
.await;
_ = this.update(cx, |this, cx| this.flush_pending_message(cx)); _ = this.update(cx, |this, cx| this.flush_pending_message(cx));
match turn_result { match turn_result {
Ok(()) => { Ok(()) => {
log::info!("Turn execution completed"); log::debug!("Turn execution completed");
event_stream.send_stop(acp::StopReason::EndTurn); event_stream.send_stop(acp::StopReason::EndTurn);
} }
Err(error) => { Err(error) => {
@ -1150,20 +1194,18 @@ impl Thread {
Ok(events_rx) Ok(events_rx)
} }
async fn stream_completion( async fn run_turn_internal(
this: &WeakEntity<Self>, this: &WeakEntity<Self>,
model: &Arc<dyn LanguageModel>, model: Arc<dyn LanguageModel>,
completion_intent: CompletionIntent,
event_stream: &ThreadEventStream, event_stream: &ThreadEventStream,
cx: &mut AsyncApp, cx: &mut AsyncApp,
) -> Result<()> { ) -> Result<()> {
log::debug!("Stream completion started successfully"); let mut attempt = 0;
let request = this.update(cx, |this, cx| { let mut intent = CompletionIntent::UserPrompt;
this.build_completion_request(completion_intent, cx) loop {
})??; let request =
this.update(cx, |this, cx| this.build_completion_request(intent, cx))??;
let mut attempt = None;
'retry: loop {
telemetry::event!( telemetry::event!(
"Agent Thread Completion", "Agent Thread Completion",
thread_id = this.read_with(cx, |this, _| this.id.to_string())?, thread_id = this.read_with(cx, |this, _| this.id.to_string())?,
@ -1173,75 +1215,31 @@ impl Thread {
attempt attempt
); );
log::info!( log::debug!("Calling model.stream_completion, attempt {}", attempt);
"Calling model.stream_completion, attempt {}",
attempt.unwrap_or(0)
);
let mut events = model let mut events = model
.stream_completion(request.clone(), cx) .stream_completion(request, cx)
.await .await
.map_err(|error| anyhow!(error))?; .map_err(|error| anyhow!(error))?;
let mut tool_results = FuturesUnordered::new(); let mut tool_results = FuturesUnordered::new();
let mut error = None;
while let Some(event) = events.next().await { while let Some(event) = events.next().await {
log::trace!("Received completion event: {:?}", event);
match event { match event {
Ok(event) => { Ok(event) => {
log::trace!("Received completion event: {:?}", event);
tool_results.extend(this.update(cx, |this, cx| { tool_results.extend(this.update(cx, |this, cx| {
this.handle_streamed_completion_event(event, event_stream, cx) this.handle_completion_event(event, event_stream, cx)
})??); })??);
} }
Err(error) => { Err(err) => {
let completion_mode = error = Some(err);
this.read_with(cx, |thread, _cx| thread.completion_mode())?; break;
if completion_mode == CompletionMode::Normal {
return Err(anyhow!(error))?;
}
let Some(strategy) = Self::retry_strategy_for(&error) else {
return Err(anyhow!(error))?;
};
let max_attempts = match &strategy {
RetryStrategy::ExponentialBackoff { max_attempts, .. } => *max_attempts,
RetryStrategy::Fixed { max_attempts, .. } => *max_attempts,
};
let attempt = attempt.get_or_insert(0u8);
*attempt += 1;
let attempt = *attempt;
if attempt > max_attempts {
return Err(anyhow!(error))?;
}
let delay = match &strategy {
RetryStrategy::ExponentialBackoff { initial_delay, .. } => {
let delay_secs =
initial_delay.as_secs() * 2u64.pow((attempt - 1) as u32);
Duration::from_secs(delay_secs)
}
RetryStrategy::Fixed { delay, .. } => *delay,
};
log::debug!("Retry attempt {attempt} with delay {delay:?}");
event_stream.send_retry(acp_thread::RetryStatus {
last_error: error.to_string().into(),
attempt: attempt as usize,
max_attempts: max_attempts as usize,
started_at: Instant::now(),
duration: delay,
});
cx.background_executor().timer(delay).await;
continue 'retry;
} }
} }
} }
let end_turn = tool_results.is_empty();
while let Some(tool_result) = tool_results.next().await { while let Some(tool_result) = tool_results.next().await {
log::info!("Tool finished {:?}", tool_result); log::debug!("Tool finished {:?}", tool_result);
event_stream.update_tool_call_fields( event_stream.update_tool_call_fields(
&tool_result.tool_use_id, &tool_result.tool_use_id,
@ -1262,31 +1260,83 @@ impl Thread {
})?; })?;
} }
return Ok(()); this.update(cx, |this, cx| {
this.flush_pending_message(cx);
if this.title.is_none() && this.pending_title_generation.is_none() {
this.generate_title(cx);
}
})?;
if let Some(error) = error {
attempt += 1;
let retry =
this.update(cx, |this, _| this.handle_completion_error(error, attempt))??;
let timer = cx.background_executor().timer(retry.duration);
event_stream.send_retry(retry);
timer.await;
this.update(cx, |this, _cx| {
if let Some(Message::Agent(message)) = this.messages.last() {
if message.tool_results.is_empty() {
intent = CompletionIntent::UserPrompt;
this.messages.push(Message::Resume);
}
}
})?;
} else if this.read_with(cx, |this, _| this.tool_use_limit_reached)? {
return Err(language_model::ToolUseLimitReachedError.into());
} else if end_turn {
return Ok(());
} else {
intent = CompletionIntent::ToolResults;
attempt = 0;
}
} }
} }
pub fn build_system_message(&self, cx: &App) -> LanguageModelRequestMessage { fn handle_completion_error(
log::debug!("Building system message"); &mut self,
let prompt = SystemPromptTemplate { error: LanguageModelCompletionError,
project: self.project_context.read(cx), attempt: u8,
available_tools: self.tools.keys().cloned().collect(), ) -> Result<acp_thread::RetryStatus> {
if self.completion_mode == CompletionMode::Normal {
return Err(anyhow!(error));
} }
.render(&self.templates)
.context("failed to build system prompt") let Some(strategy) = Self::retry_strategy_for(&error) else {
.expect("Invalid template"); return Err(anyhow!(error));
log::debug!("System message built"); };
LanguageModelRequestMessage {
role: Role::System, let max_attempts = match &strategy {
content: vec![prompt.into()], RetryStrategy::ExponentialBackoff { max_attempts, .. } => *max_attempts,
cache: true, RetryStrategy::Fixed { max_attempts, .. } => *max_attempts,
};
if attempt > max_attempts {
return Err(anyhow!(error));
} }
let delay = match &strategy {
RetryStrategy::ExponentialBackoff { initial_delay, .. } => {
let delay_secs = initial_delay.as_secs() * 2u64.pow((attempt - 1) as u32);
Duration::from_secs(delay_secs)
}
RetryStrategy::Fixed { delay, .. } => *delay,
};
log::debug!("Retry attempt {attempt} with delay {delay:?}");
Ok(acp_thread::RetryStatus {
last_error: error.to_string().into(),
attempt: attempt as usize,
max_attempts: max_attempts as usize,
started_at: Instant::now(),
duration: delay,
})
} }
/// A helper method that's called on every streamed completion event. /// A helper method that's called on every streamed completion event.
/// Returns an optional tool result task, which the main agentic loop will /// Returns an optional tool result task, which the main agentic loop will
/// send back to the model when it resolves. /// send back to the model when it resolves.
fn handle_streamed_completion_event( fn handle_completion_event(
&mut self, &mut self,
event: LanguageModelCompletionEvent, event: LanguageModelCompletionEvent,
event_stream: &ThreadEventStream, event_stream: &ThreadEventStream,
@ -1417,7 +1467,7 @@ impl Thread {
) -> Option<Task<LanguageModelToolResult>> { ) -> Option<Task<LanguageModelToolResult>> {
cx.notify(); cx.notify();
let tool = self.tools.get(tool_use.name.as_ref()).cloned(); let tool = self.tool(tool_use.name.as_ref());
let mut title = SharedString::from(&tool_use.name); let mut title = SharedString::from(&tool_use.name);
let mut kind = acp::ToolKind::Other; let mut kind = acp::ToolKind::Other;
if let Some(tool) = tool.as_ref() { if let Some(tool) = tool.as_ref() {
@ -1481,7 +1531,7 @@ impl Thread {
}); });
let supports_images = self.model().is_some_and(|model| model.supports_images()); let supports_images = self.model().is_some_and(|model| model.supports_images());
let tool_result = tool.run(tool_use.input, tool_event_stream, cx); let tool_result = tool.run(tool_use.input, tool_event_stream, cx);
log::info!("Running tool {}", tool_use.name); log::debug!("Running tool {}", tool_use.name);
Some(cx.foreground_executor().spawn(async move { Some(cx.foreground_executor().spawn(async move {
let tool_result = tool_result.await.and_then(|output| { let tool_result = tool_result.await.and_then(|output| {
if let LanguageModelToolResultContent::Image(_) = &output.llm_output if let LanguageModelToolResultContent::Image(_) = &output.llm_output
@ -1593,7 +1643,7 @@ impl Thread {
summary.extend(lines.next()); summary.extend(lines.next());
} }
log::info!("Setting summary: {}", summary); log::debug!("Setting summary: {}", summary);
let summary = SharedString::from(summary); let summary = SharedString::from(summary);
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
@ -1610,7 +1660,7 @@ impl Thread {
return; return;
}; };
log::info!( log::debug!(
"Generating title with model: {:?}", "Generating title with model: {:?}",
self.summarization_model.as_ref().map(|model| model.name()) self.summarization_model.as_ref().map(|model| model.name())
); );
@ -1696,6 +1746,10 @@ impl Thread {
return; return;
}; };
if message.content.is_empty() {
return;
}
for content in &message.content { for content in &message.content {
let AgentMessageContent::ToolUse(tool_use) = content else { let AgentMessageContent::ToolUse(tool_use) = content else {
continue; continue;
@ -1724,34 +1778,32 @@ impl Thread {
pub(crate) fn build_completion_request( pub(crate) fn build_completion_request(
&self, &self,
completion_intent: CompletionIntent, completion_intent: CompletionIntent,
cx: &mut App, cx: &App,
) -> Result<LanguageModelRequest> { ) -> Result<LanguageModelRequest> {
let model = self.model().context("No language model configured")?; let model = self.model().context("No language model configured")?;
let tools = if let Some(turn) = self.running_turn.as_ref() {
turn.tools
.iter()
.filter_map(|(tool_name, tool)| {
log::trace!("Including tool: {}", tool_name);
Some(LanguageModelRequestTool {
name: tool_name.to_string(),
description: tool.description().to_string(),
input_schema: tool.input_schema(model.tool_input_format()).log_err()?,
})
})
.collect::<Vec<_>>()
} else {
Vec::new()
};
log::debug!("Building completion request"); log::debug!("Building completion request");
log::debug!("Completion intent: {:?}", completion_intent); log::debug!("Completion intent: {:?}", completion_intent);
log::debug!("Completion mode: {:?}", self.completion_mode); log::debug!("Completion mode: {:?}", self.completion_mode);
let messages = self.build_request_messages(cx); let messages = self.build_request_messages(cx);
log::info!("Request will include {} messages", messages.len()); log::debug!("Request will include {} messages", messages.len());
log::debug!("Request includes {} tools", tools.len());
let tools = if let Some(tools) = self.tools(cx).log_err() {
tools
.filter_map(|tool| {
let tool_name = tool.name().to_string();
log::trace!("Including tool: {}", tool_name);
Some(LanguageModelRequestTool {
name: tool_name,
description: tool.description().to_string(),
input_schema: tool.input_schema(model.tool_input_format()).log_err()?,
})
})
.collect()
} else {
Vec::new()
};
log::info!("Request includes {} tools", tools.len());
let request = LanguageModelRequest { let request = LanguageModelRequest {
thread_id: Some(self.id.to_string()), thread_id: Some(self.id.to_string()),
@ -1770,37 +1822,76 @@ impl Thread {
Ok(request) Ok(request)
} }
fn tools<'a>(&'a self, cx: &'a App) -> Result<impl Iterator<Item = &'a Arc<dyn AnyAgentTool>>> { fn enabled_tools(
let model = self.model().context("No language model configured")?; &self,
profile: &AgentProfileSettings,
model: &Arc<dyn LanguageModel>,
cx: &App,
) -> BTreeMap<SharedString, Arc<dyn AnyAgentTool>> {
fn truncate(tool_name: &SharedString) -> SharedString {
if tool_name.len() > MAX_TOOL_NAME_LENGTH {
let mut truncated = tool_name.to_string();
truncated.truncate(MAX_TOOL_NAME_LENGTH);
truncated.into()
} else {
tool_name.clone()
}
}
let profile = AgentSettings::get_global(cx) let mut tools = self
.profiles
.get(&self.profile_id)
.context("profile not found")?;
let provider_id = model.provider_id();
Ok(self
.tools .tools
.iter() .iter()
.filter(move |(_, tool)| tool.supported_provider(&provider_id))
.filter_map(|(tool_name, tool)| { .filter_map(|(tool_name, tool)| {
if profile.is_tool_enabled(tool_name) { if tool.supported_provider(&model.provider_id())
Some(tool) && profile.is_tool_enabled(tool_name)
{
Some((truncate(tool_name), tool.clone()))
} else { } else {
None None
} }
}) })
.chain(self.context_server_registry.read(cx).servers().flat_map( .collect::<BTreeMap<_, _>>();
|(server_id, tools)| {
tools.iter().filter_map(|(tool_name, tool)| { let mut context_server_tools = Vec::new();
if profile.is_context_server_tool_enabled(&server_id.0, tool_name) { let mut seen_tools = tools.keys().cloned().collect::<HashSet<_>>();
Some(tool) let mut duplicate_tool_names = HashSet::default();
} else { for (server_id, server_tools) in self.context_server_registry.read(cx).servers() {
None for (tool_name, tool) in server_tools {
} if profile.is_context_server_tool_enabled(&server_id.0, &tool_name) {
}) let tool_name = truncate(tool_name);
}, if !seen_tools.insert(tool_name.clone()) {
))) duplicate_tool_names.insert(tool_name.clone());
}
context_server_tools.push((server_id.clone(), tool_name, tool.clone()));
}
}
}
// When there are duplicate tool names, disambiguate by prefixing them
// with the server ID. In the rare case there isn't enough space for the
// disambiguated tool name, keep only the last tool with this name.
for (server_id, tool_name, tool) in context_server_tools {
if duplicate_tool_names.contains(&tool_name) {
let available = MAX_TOOL_NAME_LENGTH.saturating_sub(tool_name.len());
if available >= 2 {
let mut disambiguated = server_id.0.to_string();
disambiguated.truncate(available - 1);
disambiguated.push('_');
disambiguated.push_str(&tool_name);
tools.insert(disambiguated.into(), tool.clone());
} else {
tools.insert(tool_name, tool.clone());
}
} else {
tools.insert(tool_name, tool.clone());
}
}
tools
}
fn tool(&self, name: &str) -> Option<Arc<dyn AnyAgentTool>> {
self.running_turn.as_ref()?.tools.get(name).cloned()
} }
fn build_request_messages(&self, cx: &App) -> Vec<LanguageModelRequestMessage> { fn build_request_messages(&self, cx: &App) -> Vec<LanguageModelRequestMessage> {
@ -1808,21 +1899,29 @@ impl Thread {
"Building request messages from {} thread messages", "Building request messages from {} thread messages",
self.messages.len() self.messages.len()
); );
let mut messages = vec![self.build_system_message(cx)];
let system_prompt = SystemPromptTemplate {
project: self.project_context.read(cx),
available_tools: self.tools.keys().cloned().collect(),
}
.render(&self.templates)
.context("failed to build system prompt")
.expect("Invalid template");
let mut messages = vec![LanguageModelRequestMessage {
role: Role::System,
content: vec![system_prompt.into()],
cache: false,
}];
for message in &self.messages { for message in &self.messages {
messages.extend(message.to_request()); messages.extend(message.to_request());
} }
if let Some(message) = self.pending_message.as_ref() { if let Some(last_message) = messages.last_mut() {
messages.extend(message.to_request()); last_message.cache = true;
} }
if let Some(last_user_message) = messages if let Some(message) = self.pending_message.as_ref() {
.iter_mut() messages.extend(message.to_request());
.rev()
.find(|message| message.role == Role::User)
{
last_user_message.cache = true;
} }
messages messages
@ -1965,6 +2064,8 @@ struct RunningTurn {
/// The current event stream for the running turn. Used to report a final /// The current event stream for the running turn. Used to report a final
/// cancellation event if we cancel the turn. /// cancellation event if we cancel the turn.
event_stream: ThreadEventStream, event_stream: ThreadEventStream,
/// The tools that were enabled for this turn.
tools: BTreeMap<SharedString, Arc<dyn AnyAgentTool>>,
} }
impl RunningTurn { impl RunningTurn {
@ -1989,7 +2090,7 @@ where
type Input: for<'de> Deserialize<'de> + Serialize + JsonSchema; type Input: for<'de> Deserialize<'de> + Serialize + JsonSchema;
type Output: for<'de> Deserialize<'de> + Serialize + Into<LanguageModelToolResultContent>; type Output: for<'de> Deserialize<'de> + Serialize + Into<LanguageModelToolResultContent>;
fn name(&self) -> SharedString; fn name() -> &'static str;
fn description(&self) -> SharedString { fn description(&self) -> SharedString {
let schema = schemars::schema_for!(Self::Input); let schema = schemars::schema_for!(Self::Input);
@ -2001,7 +2102,7 @@ where
) )
} }
fn kind(&self) -> acp::ToolKind; fn kind() -> acp::ToolKind;
/// The initial tool title to display. Can be updated during the tool run. /// The initial tool title to display. Can be updated during the tool run.
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString; fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString;
@ -2077,7 +2178,7 @@ where
T: AgentTool, T: AgentTool,
{ {
fn name(&self) -> SharedString { fn name(&self) -> SharedString {
self.0.name() T::name().into()
} }
fn description(&self) -> SharedString { fn description(&self) -> SharedString {
@ -2085,7 +2186,7 @@ where
} }
fn kind(&self) -> agent_client_protocol::ToolKind { fn kind(&self) -> agent_client_protocol::ToolKind {
self.0.kind() T::kind()
} }
fn initial_title(&self, input: serde_json::Value) -> SharedString { fn initial_title(&self, input: serde_json::Value) -> SharedString {

View file

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

View file

@ -1,7 +1,7 @@
use crate::{AgentTool, ToolCallEventStream}; use crate::{AgentTool, ToolCallEventStream};
use agent_client_protocol::ToolKind; use agent_client_protocol::ToolKind;
use anyhow::{Context as _, Result, anyhow}; use anyhow::{Context as _, Result, anyhow};
use gpui::{App, AppContext, Entity, SharedString, Task}; use gpui::{App, AppContext, Entity, Task};
use project::Project; use project::Project;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -50,11 +50,11 @@ impl AgentTool for CopyPathTool {
type Input = CopyPathToolInput; type Input = CopyPathToolInput;
type Output = String; type Output = String;
fn name(&self) -> SharedString { fn name() -> &'static str {
"copy_path".into() "copy_path"
} }
fn kind(&self) -> ToolKind { fn kind() -> ToolKind {
ToolKind::Move ToolKind::Move
} }

View file

@ -41,11 +41,11 @@ impl AgentTool for CreateDirectoryTool {
type Input = CreateDirectoryToolInput; type Input = CreateDirectoryToolInput;
type Output = String; type Output = String;
fn name(&self) -> SharedString { fn name() -> &'static str {
"create_directory".into() "create_directory"
} }
fn kind(&self) -> ToolKind { fn kind() -> ToolKind {
ToolKind::Read ToolKind::Read
} }

View file

@ -44,11 +44,11 @@ impl AgentTool for DeletePathTool {
type Input = DeletePathToolInput; type Input = DeletePathToolInput;
type Output = String; type Output = String;
fn name(&self) -> SharedString { fn name() -> &'static str {
"delete_path".into() "delete_path"
} }
fn kind(&self) -> ToolKind { fn kind() -> ToolKind {
ToolKind::Delete ToolKind::Delete
} }

View file

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

View file

@ -186,11 +186,11 @@ impl AgentTool for EditFileTool {
type Input = EditFileToolInput; type Input = EditFileToolInput;
type Output = EditFileToolOutput; type Output = EditFileToolOutput;
fn name(&self) -> SharedString { fn name() -> &'static str {
"edit_file".into() "edit_file"
} }
fn kind(&self) -> acp::ToolKind { fn kind() -> acp::ToolKind {
acp::ToolKind::Edit acp::ToolKind::Edit
} }
@ -517,7 +517,6 @@ fn resolve_path(
mod tests { mod tests {
use super::*; use super::*;
use crate::{ContextServerRegistry, Templates}; use crate::{ContextServerRegistry, Templates};
use action_log::ActionLog;
use client::TelemetrySettings; use client::TelemetrySettings;
use fs::Fs; use fs::Fs;
use gpui::{TestAppContext, UpdateGlobal}; use gpui::{TestAppContext, UpdateGlobal};
@ -535,7 +534,6 @@ mod tests {
fs.insert_tree("/root", json!({})).await; fs.insert_tree("/root", json!({})).await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry = let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default()); let model = Arc::new(FakeLanguageModel::default());
@ -544,7 +542,6 @@ mod tests {
project, project,
cx.new(|_cx| ProjectContext::default()), cx.new(|_cx| ProjectContext::default()),
context_server_registry, context_server_registry,
action_log,
Templates::new(), Templates::new(),
Some(model), Some(model),
cx, cx,
@ -735,7 +732,6 @@ mod tests {
} }
}); });
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry = let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default()); let model = Arc::new(FakeLanguageModel::default());
@ -744,7 +740,6 @@ mod tests {
project, project,
cx.new(|_cx| ProjectContext::default()), cx.new(|_cx| ProjectContext::default()),
context_server_registry, context_server_registry,
action_log.clone(),
Templates::new(), Templates::new(),
Some(model.clone()), Some(model.clone()),
cx, cx,
@ -801,7 +796,9 @@ mod tests {
"Code should be formatted when format_on_save is enabled" "Code should be formatted when format_on_save is enabled"
); );
let stale_buffer_count = action_log.read_with(cx, |log, cx| log.stale_buffers(cx).count()); let stale_buffer_count = thread
.read_with(cx, |thread, _cx| thread.action_log.clone())
.read_with(cx, |log, cx| log.stale_buffers(cx).count());
assert_eq!( assert_eq!(
stale_buffer_count, 0, stale_buffer_count, 0,
@ -879,14 +876,12 @@ mod tests {
let context_server_registry = let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let model = Arc::new(FakeLanguageModel::default()); let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| { let thread = cx.new(|cx| {
Thread::new( Thread::new(
project, project,
cx.new(|_cx| ProjectContext::default()), cx.new(|_cx| ProjectContext::default()),
context_server_registry, context_server_registry,
action_log.clone(),
Templates::new(), Templates::new(),
Some(model.clone()), Some(model.clone()),
cx, cx,
@ -1008,14 +1003,12 @@ mod tests {
let context_server_registry = let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let model = Arc::new(FakeLanguageModel::default()); let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| { let thread = cx.new(|cx| {
Thread::new( Thread::new(
project, project,
cx.new(|_cx| ProjectContext::default()), cx.new(|_cx| ProjectContext::default()),
context_server_registry, context_server_registry,
action_log.clone(),
Templates::new(), Templates::new(),
Some(model.clone()), Some(model.clone()),
cx, cx,
@ -1146,14 +1139,12 @@ mod tests {
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
let context_server_registry = let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let model = Arc::new(FakeLanguageModel::default()); let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| { let thread = cx.new(|cx| {
Thread::new( Thread::new(
project, project,
cx.new(|_cx| ProjectContext::default()), cx.new(|_cx| ProjectContext::default()),
context_server_registry, context_server_registry,
action_log.clone(),
Templates::new(), Templates::new(),
Some(model.clone()), Some(model.clone()),
cx, cx,
@ -1254,7 +1245,6 @@ mod tests {
) )
.await; .await;
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry = let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default()); let model = Arc::new(FakeLanguageModel::default());
@ -1263,7 +1253,6 @@ mod tests {
project.clone(), project.clone(),
cx.new(|_cx| ProjectContext::default()), cx.new(|_cx| ProjectContext::default()),
context_server_registry.clone(), context_server_registry.clone(),
action_log.clone(),
Templates::new(), Templates::new(),
Some(model.clone()), Some(model.clone()),
cx, cx,
@ -1336,7 +1325,6 @@ mod tests {
.await; .await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry = let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default()); let model = Arc::new(FakeLanguageModel::default());
@ -1345,7 +1333,6 @@ mod tests {
project.clone(), project.clone(),
cx.new(|_cx| ProjectContext::default()), cx.new(|_cx| ProjectContext::default()),
context_server_registry.clone(), context_server_registry.clone(),
action_log.clone(),
Templates::new(), Templates::new(),
Some(model.clone()), Some(model.clone()),
cx, cx,
@ -1421,7 +1408,6 @@ mod tests {
.await; .await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry = let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default()); let model = Arc::new(FakeLanguageModel::default());
@ -1430,7 +1416,6 @@ mod tests {
project.clone(), project.clone(),
cx.new(|_cx| ProjectContext::default()), cx.new(|_cx| ProjectContext::default()),
context_server_registry.clone(), context_server_registry.clone(),
action_log.clone(),
Templates::new(), Templates::new(),
Some(model.clone()), Some(model.clone()),
cx, cx,
@ -1503,7 +1488,6 @@ mod tests {
let fs = project::FakeFs::new(cx.executor()); let fs = project::FakeFs::new(cx.executor());
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry = let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default()); let model = Arc::new(FakeLanguageModel::default());
@ -1512,7 +1496,6 @@ mod tests {
project.clone(), project.clone(),
cx.new(|_cx| ProjectContext::default()), cx.new(|_cx| ProjectContext::default()),
context_server_registry, context_server_registry,
action_log.clone(),
Templates::new(), Templates::new(),
Some(model.clone()), Some(model.clone()),
cx, cx,

View file

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

View file

@ -85,11 +85,11 @@ impl AgentTool for FindPathTool {
type Input = FindPathToolInput; type Input = FindPathToolInput;
type Output = FindPathToolOutput; type Output = FindPathToolOutput;
fn name(&self) -> SharedString { fn name() -> &'static str {
"find_path".into() "find_path"
} }
fn kind(&self) -> acp::ToolKind { fn kind() -> acp::ToolKind {
acp::ToolKind::Search acp::ToolKind::Search
} }
@ -165,16 +165,17 @@ fn search_paths(glob: &str, project: Entity<Project>, cx: &mut App) -> Task<Resu
.collect(); .collect();
cx.background_spawn(async move { cx.background_spawn(async move {
Ok(snapshots let mut results = Vec::new();
.iter() for snapshot in snapshots {
.flat_map(|snapshot| { for entry in snapshot.entries(false, 0) {
let root_name = PathBuf::from(snapshot.root_name()); let root_name = PathBuf::from(snapshot.root_name());
snapshot if path_matcher.is_match(root_name.join(&entry.path)) {
.entries(false, 0) results.push(snapshot.abs_path().join(entry.path.as_ref()));
.map(move |entry| root_name.join(&entry.path)) }
.filter(|path| path_matcher.is_match(&path)) }
}) }
.collect())
Ok(results)
}) })
} }
@ -215,8 +216,8 @@ mod test {
assert_eq!( assert_eq!(
matches, matches,
&[ &[
PathBuf::from("root/apple/banana/carrot"), PathBuf::from(path!("/root/apple/banana/carrot")),
PathBuf::from("root/apple/bandana/carbonara") PathBuf::from(path!("/root/apple/bandana/carbonara"))
] ]
); );
@ -227,8 +228,8 @@ mod test {
assert_eq!( assert_eq!(
matches, matches,
&[ &[
PathBuf::from("root/apple/banana/carrot"), PathBuf::from(path!("/root/apple/banana/carrot")),
PathBuf::from("root/apple/bandana/carbonara") PathBuf::from(path!("/root/apple/bandana/carbonara"))
] ]
); );
} }

View file

@ -67,11 +67,11 @@ impl AgentTool for GrepTool {
type Input = GrepToolInput; type Input = GrepToolInput;
type Output = String; type Output = String;
fn name(&self) -> SharedString { fn name() -> &'static str {
"grep".into() "grep"
} }
fn kind(&self) -> acp::ToolKind { fn kind() -> acp::ToolKind {
acp::ToolKind::Search acp::ToolKind::Search
} }

View file

@ -51,11 +51,11 @@ impl AgentTool for ListDirectoryTool {
type Input = ListDirectoryToolInput; type Input = ListDirectoryToolInput;
type Output = String; type Output = String;
fn name(&self) -> SharedString { fn name() -> &'static str {
"list_directory".into() "list_directory"
} }
fn kind(&self) -> ToolKind { fn kind() -> ToolKind {
ToolKind::Read ToolKind::Read
} }

View file

@ -52,11 +52,11 @@ impl AgentTool for MovePathTool {
type Input = MovePathToolInput; type Input = MovePathToolInput;
type Output = String; type Output = String;
fn name(&self) -> SharedString { fn name() -> &'static str {
"move_path".into() "move_path"
} }
fn kind(&self) -> ToolKind { fn kind() -> ToolKind {
ToolKind::Move ToolKind::Move
} }

View file

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

View file

@ -37,11 +37,11 @@ impl AgentTool for OpenTool {
type Input = OpenToolInput; type Input = OpenToolInput;
type Output = String; type Output = String;
fn name(&self) -> SharedString { fn name() -> &'static str {
"open".into() "open"
} }
fn kind(&self) -> ToolKind { fn kind() -> ToolKind {
ToolKind::Execute ToolKind::Execute
} }

View file

@ -10,7 +10,8 @@ use project::{AgentLocation, ImageItem, Project, WorktreeSettings, image_store};
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use settings::Settings; use settings::Settings;
use std::sync::Arc; use std::{path::Path, sync::Arc};
use util::markdown::MarkdownCodeBlock;
use crate::{AgentTool, ToolCallEventStream}; use crate::{AgentTool, ToolCallEventStream};
@ -59,36 +60,21 @@ impl AgentTool for ReadFileTool {
type Input = ReadFileToolInput; type Input = ReadFileToolInput;
type Output = LanguageModelToolResultContent; type Output = LanguageModelToolResultContent;
fn name(&self) -> SharedString { fn name() -> &'static str {
"read_file".into() "read_file"
} }
fn kind(&self) -> acp::ToolKind { fn kind() -> acp::ToolKind {
acp::ToolKind::Read acp::ToolKind::Read
} }
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString { fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
if let Ok(input) = input { input
let path = &input.path; .ok()
match (input.start_line, input.end_line) { .as_ref()
(Some(start), Some(end)) => { .and_then(|input| Path::new(&input.path).file_name())
format!( .map(|file_name| file_name.to_string_lossy().to_string().into())
"[Read file `{}` (lines {}-{})](@selection:{}:({}-{}))", .unwrap_or_default()
path, start, end, path, start, end
)
}
(Some(start), None) => {
format!(
"[Read file `{}` (from line {})](@selection:{}:({}-{}))",
path, start, path, start, start
)
}
_ => format!("[Read file `{}`](@file:{})", path, path),
}
.into()
} else {
"Read file".into()
}
} }
fn run( fn run(
@ -258,6 +244,19 @@ impl AgentTool for ReadFileTool {
}]), }]),
..Default::default() ..Default::default()
}); });
if let Ok(LanguageModelToolResultContent::Text(text)) = &result {
let markdown = MarkdownCodeBlock {
tag: &input.path,
text,
}
.to_string();
event_stream.update_fields(ToolCallUpdateFields {
content: Some(vec![acp::ToolCallContent::Content {
content: markdown.into(),
}]),
..Default::default()
})
}
} }
})?; })?;

View file

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

View file

@ -21,11 +21,11 @@ impl AgentTool for ThinkingTool {
type Input = ThinkingToolInput; type Input = ThinkingToolInput;
type Output = String; type Output = String;
fn name(&self) -> SharedString { fn name() -> &'static str {
"thinking".into() "thinking"
} }
fn kind(&self) -> acp::ToolKind { fn kind() -> acp::ToolKind {
acp::ToolKind::Think acp::ToolKind::Think
} }

View file

@ -40,11 +40,11 @@ impl AgentTool for WebSearchTool {
type Input = WebSearchToolInput; type Input = WebSearchToolInput;
type Output = WebSearchToolOutput; type Output = WebSearchToolOutput;
fn name(&self) -> SharedString { fn name() -> &'static str {
"web_search".into() "web_search"
} }
fn kind(&self) -> acp::ToolKind { fn kind() -> acp::ToolKind {
acp::ToolKind::Fetch acp::ToolKind::Fetch
} }

View file

@ -17,11 +17,11 @@ path = "src/agent_servers.rs"
doctest = false doctest = false
[dependencies] [dependencies]
acp_tools.workspace = true
acp_thread.workspace = true acp_thread.workspace = true
action_log.workspace = true action_log.workspace = true
agent-client-protocol.workspace = true agent-client-protocol.workspace = true
agent_settings.workspace = true agent_settings.workspace = true
agentic-coding-protocol.workspace = true
anyhow.workspace = true anyhow.workspace = true
client = { workspace = true, optional = true } client = { workspace = true, optional = true }
collections.workspace = true collections.workspace = true

View file

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

View file

@ -1,3 +1,4 @@
use acp_tools::AcpConnectionRegistry;
use action_log::ActionLog; use action_log::ActionLog;
use agent_client_protocol::{self as acp, Agent as _, ErrorCode}; use agent_client_protocol::{self as acp, Agent as _, ErrorCode};
use anyhow::anyhow; use anyhow::anyhow;
@ -101,6 +102,14 @@ impl AcpConnection {
}) })
.detach(); .detach();
let connection = Rc::new(connection);
cx.update(|cx| {
AcpConnectionRegistry::default_global(cx).update(cx, |registry, cx| {
registry.set_active_connection(server_name, &connection, cx)
});
})?;
let response = connection let response = connection
.initialize(acp::InitializeRequest { .initialize(acp::InitializeRequest {
protocol_version: acp::VERSION, protocol_version: acp::VERSION,
@ -119,7 +128,7 @@ impl AcpConnection {
Ok(Self { Ok(Self {
auth_methods: response.auth_methods, auth_methods: response.auth_methods,
connection: connection.into(), connection,
server_name, server_name,
sessions, sessions,
prompt_capabilities: response.agent_capabilities.prompt_capabilities, prompt_capabilities: response.agent_capabilities.prompt_capabilities,

View file

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

View file

@ -30,7 +30,7 @@ use futures::{
io::BufReader, io::BufReader,
select_biased, select_biased,
}; };
use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity}; use gpui::{App, AppContext, AsyncApp, Entity, SharedString, Task, WeakEntity};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use util::{ResultExt, debug_panic}; use util::{ResultExt, debug_panic};
@ -43,16 +43,20 @@ use acp_thread::{AcpThread, AgentConnection, AuthRequired, LoadError, MentionUri
pub struct ClaudeCode; pub struct ClaudeCode;
impl AgentServer for ClaudeCode { impl AgentServer for ClaudeCode {
fn name(&self) -> &'static str { fn telemetry_id(&self) -> &'static str {
"Welcome to Claude Code" "claude-code"
} }
fn empty_state_headline(&self) -> &'static str { fn name(&self) -> SharedString {
"Claude Code".into()
}
fn empty_state_headline(&self) -> SharedString {
self.name() self.name()
} }
fn empty_state_message(&self) -> &'static str { fn empty_state_message(&self) -> SharedString {
"How can I help you today?" "How can I help you today?".into()
} }
fn logo(&self) -> ui::IconName { fn logo(&self) -> ui::IconName {
@ -249,13 +253,19 @@ impl AgentConnection for ClaudeAgentConnection {
}); });
let action_log = cx.new(|_| ActionLog::new(project.clone()))?; let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
let thread = cx.new(|_cx| { let thread = cx.new(|cx| {
AcpThread::new( AcpThread::new(
"Claude Code", "Claude Code",
self.clone(), self.clone(),
project, project,
action_log, action_log,
session_id.clone(), session_id.clone(),
watch::Receiver::constant(acp::PromptCapabilities {
image: true,
audio: false,
embedded_context: true,
}),
cx,
) )
})?; })?;
@ -319,14 +329,6 @@ impl AgentConnection for ClaudeAgentConnection {
cx.foreground_executor().spawn(async move { end_rx.await? }) cx.foreground_executor().spawn(async move { end_rx.await? })
} }
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
acp::PromptCapabilities {
image: true,
audio: false,
embedded_context: true,
}
}
fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) { fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) {
let sessions = self.sessions.borrow(); let sessions = self.sessions.borrow();
let Some(session) = sessions.get(session_id) else { let Some(session) = sessions.get(session_id) else {

View file

@ -0,0 +1,63 @@
use crate::{AgentServerCommand, AgentServerSettings};
use acp_thread::AgentConnection;
use anyhow::Result;
use gpui::{App, Entity, SharedString, Task};
use project::Project;
use std::{path::Path, rc::Rc};
use ui::IconName;
/// A generic agent server implementation for custom user-defined agents
pub struct CustomAgentServer {
name: SharedString,
command: AgentServerCommand,
}
impl CustomAgentServer {
pub fn new(name: SharedString, settings: &AgentServerSettings) -> Self {
Self {
name,
command: settings.command.clone(),
}
}
}
impl crate::AgentServer for CustomAgentServer {
fn telemetry_id(&self) -> &'static str {
"custom"
}
fn name(&self) -> SharedString {
self.name.clone()
}
fn logo(&self) -> IconName {
IconName::Terminal
}
fn empty_state_headline(&self) -> SharedString {
"No conversations yet".into()
}
fn empty_state_message(&self) -> SharedString {
format!("Start a conversation with {}", self.name).into()
}
fn connect(
&self,
root_dir: &Path,
_project: &Entity<Project>,
cx: &mut App,
) -> Task<Result<Rc<dyn AgentConnection>>> {
let server_name = self.name();
let command = self.command.clone();
let root_dir = root_dir.to_path_buf();
cx.spawn(async move |mut cx| {
crate::acp::connect(server_name, command, &root_dir, &mut cx).await
})
}
fn into_any(self: Rc<Self>) -> Rc<dyn std::any::Any> {
self
}
}

View file

@ -1,17 +1,15 @@
use crate::AgentServer;
use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus};
use agent_client_protocol as acp;
use futures::{FutureExt, StreamExt, channel::mpsc, select};
use gpui::{AppContext, Entity, TestAppContext};
use indoc::indoc;
use project::{FakeFs, Project};
use std::{ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::Arc, sync::Arc,
time::Duration, time::Duration,
}; };
use crate::AgentServer;
use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus};
use agent_client_protocol as acp;
use futures::{FutureExt, StreamExt, channel::mpsc, select};
use gpui::{AppContext, Entity, TestAppContext};
use indoc::indoc;
use project::{FakeFs, Project};
use util::path; use util::path;
pub async fn test_basic<T, F>(server: F, cx: &mut TestAppContext) pub async fn test_basic<T, F>(server: F, cx: &mut TestAppContext)
@ -479,6 +477,7 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
gemini: Some(crate::AgentServerSettings { gemini: Some(crate::AgentServerSettings {
command: crate::gemini::tests::local_command(), command: crate::gemini::tests::local_command(),
}), }),
custom: collections::HashMap::default(),
}, },
cx, cx,
); );

View file

@ -4,11 +4,10 @@ use std::{any::Any, path::Path};
use crate::{AgentServer, AgentServerCommand}; use crate::{AgentServer, AgentServerCommand};
use acp_thread::{AgentConnection, LoadError}; use acp_thread::{AgentConnection, LoadError};
use anyhow::Result; use anyhow::Result;
use gpui::{Entity, Task}; use gpui::{App, Entity, SharedString, Task};
use language_models::provider::google::GoogleLanguageModelProvider; use language_models::provider::google::GoogleLanguageModelProvider;
use project::Project; use project::Project;
use settings::SettingsStore; use settings::SettingsStore;
use ui::App;
use crate::AllAgentServersSettings; use crate::AllAgentServersSettings;
@ -18,16 +17,20 @@ pub struct Gemini;
const ACP_ARG: &str = "--experimental-acp"; const ACP_ARG: &str = "--experimental-acp";
impl AgentServer for Gemini { impl AgentServer for Gemini {
fn name(&self) -> &'static str { fn telemetry_id(&self) -> &'static str {
"Gemini CLI" "gemini-cli"
} }
fn empty_state_headline(&self) -> &'static str { fn name(&self) -> SharedString {
"Welcome to Gemini CLI" "Gemini CLI".into()
} }
fn empty_state_message(&self) -> &'static str { fn empty_state_headline(&self) -> SharedString {
"Ask questions, edit files, run commands" self.name()
}
fn empty_state_message(&self) -> SharedString {
"Ask questions, edit files, run commands".into()
} }
fn logo(&self) -> ui::IconName { fn logo(&self) -> ui::IconName {
@ -54,7 +57,7 @@ impl AgentServer for Gemini {
return Err(LoadError::NotInstalled { return Err(LoadError::NotInstalled {
error_message: "Failed to find Gemini CLI binary".into(), error_message: "Failed to find Gemini CLI binary".into(),
install_message: "Install Gemini CLI".into(), install_message: "Install Gemini CLI".into(),
install_command: "npm install -g @google/gemini-cli@preview".into() install_command: Self::install_command().into(),
}.into()); }.into());
}; };
@ -89,7 +92,7 @@ impl AgentServer for Gemini {
current_version current_version
).into(), ).into(),
upgrade_message: "Upgrade Gemini CLI to latest".into(), upgrade_message: "Upgrade Gemini CLI to latest".into(),
upgrade_command: "npm install -g @google/gemini-cli@preview".into(), upgrade_command: Self::upgrade_command().into(),
}.into()) }.into())
} }
} }
@ -102,6 +105,20 @@ impl AgentServer for Gemini {
} }
} }
impl Gemini {
pub fn binary_name() -> &'static str {
"gemini"
}
pub fn install_command() -> &'static str {
"npm install -g @google/gemini-cli@preview"
}
pub fn upgrade_command() -> &'static str {
"npm install -g @google/gemini-cli@preview"
}
}
#[cfg(test)] #[cfg(test)]
pub(crate) mod tests { pub(crate) mod tests {
use super::*; use super::*;

View file

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

View file

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

View file

@ -247,9 +247,9 @@ impl ContextPickerCompletionProvider {
let abs_path = project.read(cx).absolute_path(&symbol.path, cx)?; let abs_path = project.read(cx).absolute_path(&symbol.path, cx)?;
let uri = MentionUri::Symbol { let uri = MentionUri::Symbol {
path: abs_path, abs_path,
name: symbol.name.clone(), name: symbol.name.clone(),
line_range: symbol.range.start.0.row..symbol.range.end.0.row, line_range: symbol.range.start.0.row..=symbol.range.end.0.row,
}; };
let new_text = format!("{} ", uri.as_link()); let new_text = format!("{} ", uri.as_link());
let new_text_len = new_text.len(); let new_text_len = new_text.len();
@ -805,7 +805,7 @@ pub(crate) fn search_threads(
history_store: &Entity<HistoryStore>, history_store: &Entity<HistoryStore>,
cx: &mut App, cx: &mut App,
) -> Task<Vec<HistoryEntry>> { ) -> Task<Vec<HistoryEntry>> {
let threads = history_store.read(cx).entries(cx); let threads = history_store.read(cx).entries().collect();
if query.is_empty() { if query.is_empty() {
return Task::ready(threads); return Task::ready(threads);
} }

File diff suppressed because it is too large Load diff

View file

@ -3,18 +3,18 @@ use crate::{AgentPanel, RemoveSelectedThread};
use agent2::{HistoryEntry, HistoryStore}; use agent2::{HistoryEntry, HistoryStore};
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta}; use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
use editor::{Editor, EditorEvent}; use editor::{Editor, EditorEvent};
use fuzzy::{StringMatch, StringMatchCandidate}; use fuzzy::StringMatchCandidate;
use gpui::{ use gpui::{
App, Empty, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Stateful, Task, App, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Stateful, Task,
UniformListScrollHandle, WeakEntity, Window, uniform_list, UniformListScrollHandle, WeakEntity, Window, uniform_list,
}; };
use std::{fmt::Display, ops::Range, sync::Arc}; use std::{fmt::Display, ops::Range};
use text::Bias;
use time::{OffsetDateTime, UtcOffset}; use time::{OffsetDateTime, UtcOffset};
use ui::{ use ui::{
HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Scrollbar, ScrollbarState, HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Scrollbar, ScrollbarState,
Tooltip, prelude::*, Tooltip, prelude::*,
}; };
use util::ResultExt;
pub struct AcpThreadHistory { pub struct AcpThreadHistory {
pub(crate) history_store: Entity<HistoryStore>, pub(crate) history_store: Entity<HistoryStore>,
@ -22,38 +22,38 @@ pub struct AcpThreadHistory {
selected_index: usize, selected_index: usize,
hovered_index: Option<usize>, hovered_index: Option<usize>,
search_editor: Entity<Editor>, search_editor: Entity<Editor>,
all_entries: Arc<Vec<HistoryEntry>>, search_query: SharedString,
// When the search is empty, we display date separators between history entries
// This vector contains an enum of either a separator or an actual entry visible_items: Vec<ListItemType>,
separated_items: Vec<ListItemType>,
// Maps entry indexes to list item indexes
separated_item_indexes: Vec<u32>,
_separated_items_task: Option<Task<()>>,
search_state: SearchState,
scrollbar_visibility: bool, scrollbar_visibility: bool,
scrollbar_state: ScrollbarState, scrollbar_state: ScrollbarState,
local_timezone: UtcOffset, local_timezone: UtcOffset,
_subscriptions: Vec<gpui::Subscription>,
}
enum SearchState { _update_task: Task<()>,
Empty, _subscriptions: Vec<gpui::Subscription>,
Searching {
query: SharedString,
_task: Task<()>,
},
Searched {
query: SharedString,
matches: Vec<StringMatch>,
},
} }
enum ListItemType { enum ListItemType {
BucketSeparator(TimeBucket), BucketSeparator(TimeBucket),
Entry { Entry {
index: usize, entry: HistoryEntry,
format: EntryTimeFormat, format: EntryTimeFormat,
}, },
SearchResult {
entry: HistoryEntry,
positions: Vec<usize>,
},
}
impl ListItemType {
fn history_entry(&self) -> Option<&HistoryEntry> {
match self {
ListItemType::Entry { entry, .. } => Some(entry),
ListItemType::SearchResult { entry, .. } => Some(entry),
_ => None,
}
}
} }
pub enum ThreadHistoryEvent { pub enum ThreadHistoryEvent {
@ -78,12 +78,15 @@ impl AcpThreadHistory {
cx.subscribe(&search_editor, |this, search_editor, event, cx| { cx.subscribe(&search_editor, |this, search_editor, event, cx| {
if let EditorEvent::BufferEdited = event { if let EditorEvent::BufferEdited = event {
let query = search_editor.read(cx).text(cx); let query = search_editor.read(cx).text(cx);
this.search(query.into(), cx); if this.search_query != query {
this.search_query = query.into();
this.update_visible_items(false, cx);
}
} }
}); });
let history_store_subscription = cx.observe(&history_store, |this, _, cx| { let history_store_subscription = cx.observe(&history_store, |this, _, cx| {
this.update_all_entries(cx); this.update_visible_items(true, cx);
}); });
let scroll_handle = UniformListScrollHandle::default(); let scroll_handle = UniformListScrollHandle::default();
@ -94,10 +97,7 @@ impl AcpThreadHistory {
scroll_handle, scroll_handle,
selected_index: 0, selected_index: 0,
hovered_index: None, hovered_index: None,
search_state: SearchState::Empty, visible_items: Default::default(),
all_entries: Default::default(),
separated_items: Default::default(),
separated_item_indexes: Default::default(),
search_editor, search_editor,
scrollbar_visibility: true, scrollbar_visibility: true,
scrollbar_state, scrollbar_state,
@ -105,29 +105,61 @@ impl AcpThreadHistory {
chrono::Local::now().offset().local_minus_utc(), chrono::Local::now().offset().local_minus_utc(),
) )
.unwrap(), .unwrap(),
search_query: SharedString::default(),
_subscriptions: vec![search_editor_subscription, history_store_subscription], _subscriptions: vec![search_editor_subscription, history_store_subscription],
_separated_items_task: None, _update_task: Task::ready(()),
}; };
this.update_all_entries(cx); this.update_visible_items(false, cx);
this this
} }
fn update_all_entries(&mut self, cx: &mut Context<Self>) { fn update_visible_items(&mut self, preserve_selected_item: bool, cx: &mut Context<Self>) {
let new_entries: Arc<Vec<HistoryEntry>> = self let entries = self
.history_store .history_store
.update(cx, |store, cx| store.entries(cx)) .update(cx, |store, _| store.entries().collect());
.into(); let new_list_items = if self.search_query.is_empty() {
self.add_list_separators(entries, cx)
} else {
self.filter_search_results(entries, cx)
};
let selected_history_entry = if preserve_selected_item {
self.selected_history_entry().cloned()
} else {
None
};
self._separated_items_task.take(); self._update_task = cx.spawn(async move |this, cx| {
let new_visible_items = new_list_items.await;
this.update(cx, |this, cx| {
let new_selected_index = if let Some(history_entry) = selected_history_entry {
let history_entry_id = history_entry.id();
new_visible_items
.iter()
.position(|visible_entry| {
visible_entry
.history_entry()
.is_some_and(|entry| entry.id() == history_entry_id)
})
.unwrap_or(0)
} else {
0
};
let mut items = Vec::with_capacity(new_entries.len() + 1); this.visible_items = new_visible_items;
let mut indexes = Vec::with_capacity(new_entries.len() + 1); this.set_selected_index(new_selected_index, Bias::Right, cx);
cx.notify();
})
.ok();
});
}
let bg_task = cx.background_spawn(async move { fn add_list_separators(&self, entries: Vec<HistoryEntry>, cx: &App) -> Task<Vec<ListItemType>> {
cx.background_spawn(async move {
let mut items = Vec::with_capacity(entries.len() + 1);
let mut bucket = None; let mut bucket = None;
let today = Local::now().naive_local().date(); let today = Local::now().naive_local().date();
for (index, entry) in new_entries.iter().enumerate() { for entry in entries.into_iter() {
let entry_date = entry let entry_date = entry
.updated_at() .updated_at()
.with_timezone(&Local) .with_timezone(&Local)
@ -140,75 +172,33 @@ impl AcpThreadHistory {
items.push(ListItemType::BucketSeparator(entry_bucket)); items.push(ListItemType::BucketSeparator(entry_bucket));
} }
indexes.push(items.len() as u32);
items.push(ListItemType::Entry { items.push(ListItemType::Entry {
index, entry,
format: entry_bucket.into(), format: entry_bucket.into(),
}); });
} }
(new_entries, items, indexes) items
}); })
let task = cx.spawn(async move |this, cx| {
let (new_entries, items, indexes) = bg_task.await;
this.update(cx, |this, cx| {
let previously_selected_entry =
this.all_entries.get(this.selected_index).map(|e| e.id());
this.all_entries = new_entries;
this.separated_items = items;
this.separated_item_indexes = indexes;
match &this.search_state {
SearchState::Empty => {
if this.selected_index >= this.all_entries.len() {
this.set_selected_entry_index(
this.all_entries.len().saturating_sub(1),
cx,
);
} else if let Some(prev_id) = previously_selected_entry
&& let Some(new_ix) = this
.all_entries
.iter()
.position(|probe| probe.id() == prev_id)
{
this.set_selected_entry_index(new_ix, cx);
}
}
SearchState::Searching { query, .. } | SearchState::Searched { query, .. } => {
this.search(query.clone(), cx);
}
}
cx.notify();
})
.log_err();
});
self._separated_items_task = Some(task);
} }
fn search(&mut self, query: SharedString, cx: &mut Context<Self>) { fn filter_search_results(
if query.is_empty() { &self,
self.search_state = SearchState::Empty; entries: Vec<HistoryEntry>,
cx.notify(); cx: &App,
return; ) -> Task<Vec<ListItemType>> {
} let query = self.search_query.clone();
cx.background_spawn({
let all_entries = self.all_entries.clone();
let fuzzy_search_task = cx.background_spawn({
let query = query.clone();
let executor = cx.background_executor().clone(); let executor = cx.background_executor().clone();
async move { async move {
let mut candidates = Vec::with_capacity(all_entries.len()); let mut candidates = Vec::with_capacity(entries.len());
for (idx, entry) in all_entries.iter().enumerate() { for (idx, entry) in entries.iter().enumerate() {
candidates.push(StringMatchCandidate::new(idx, entry.title())); candidates.push(StringMatchCandidate::new(idx, entry.title()));
} }
const MAX_MATCHES: usize = 100; const MAX_MATCHES: usize = 100;
fuzzy::match_strings( let matches = fuzzy::match_strings(
&candidates, &candidates,
&query, &query,
false, false,
@ -217,74 +207,61 @@ impl AcpThreadHistory {
&Default::default(), &Default::default(),
executor, executor,
) )
.await .await;
matches
.into_iter()
.map(|search_match| ListItemType::SearchResult {
entry: entries[search_match.candidate_id].clone(),
positions: search_match.positions,
})
.collect()
} }
}); })
let task = cx.spawn({
let query = query.clone();
async move |this, cx| {
let matches = fuzzy_search_task.await;
this.update(cx, |this, cx| {
let SearchState::Searching {
query: current_query,
_task,
} = &this.search_state
else {
return;
};
if &query == current_query {
this.search_state = SearchState::Searched {
query: query.clone(),
matches,
};
this.set_selected_entry_index(0, cx);
cx.notify();
};
})
.log_err();
}
});
self.search_state = SearchState::Searching { query, _task: task };
cx.notify();
}
fn matched_count(&self) -> usize {
match &self.search_state {
SearchState::Empty => self.all_entries.len(),
SearchState::Searching { .. } => 0,
SearchState::Searched { matches, .. } => matches.len(),
}
}
fn list_item_count(&self) -> usize {
match &self.search_state {
SearchState::Empty => self.separated_items.len(),
SearchState::Searching { .. } => 0,
SearchState::Searched { matches, .. } => matches.len(),
}
} }
fn search_produced_no_matches(&self) -> bool { fn search_produced_no_matches(&self) -> bool {
match &self.search_state { self.visible_items.is_empty() && !self.search_query.is_empty()
SearchState::Empty => false,
SearchState::Searching { .. } => false,
SearchState::Searched { matches, .. } => matches.is_empty(),
}
} }
fn get_match(&self, ix: usize) -> Option<&HistoryEntry> { fn selected_history_entry(&self) -> Option<&HistoryEntry> {
match &self.search_state { self.get_history_entry(self.selected_index)
SearchState::Empty => self.all_entries.get(ix), }
SearchState::Searching { .. } => None,
SearchState::Searched { matches, .. } => matches fn get_history_entry(&self, visible_items_ix: usize) -> Option<&HistoryEntry> {
.get(ix) self.visible_items.get(visible_items_ix)?.history_entry()
.and_then(|m| self.all_entries.get(m.candidate_id)), }
fn set_selected_index(&mut self, mut index: usize, bias: Bias, cx: &mut Context<Self>) {
if self.visible_items.len() == 0 {
self.selected_index = 0;
return;
} }
while matches!(
self.visible_items.get(index),
None | Some(ListItemType::BucketSeparator(..))
) {
index = match bias {
Bias::Left => {
if index == 0 {
self.visible_items.len() - 1
} else {
index - 1
}
}
Bias::Right => {
if index >= self.visible_items.len() - 1 {
0
} else {
index + 1
}
}
};
}
self.selected_index = index;
self.scroll_handle
.scroll_to_item(index, ScrollStrategy::Top);
cx.notify()
} }
pub fn select_previous( pub fn select_previous(
@ -293,13 +270,10 @@ impl AcpThreadHistory {
_window: &mut Window, _window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let count = self.matched_count(); if self.selected_index == 0 {
if count > 0 { self.set_selected_index(self.visible_items.len() - 1, Bias::Left, cx);
if self.selected_index == 0 { } else {
self.set_selected_entry_index(count - 1, cx); self.set_selected_index(self.selected_index - 1, Bias::Left, cx);
} else {
self.set_selected_entry_index(self.selected_index - 1, cx);
}
} }
} }
@ -309,13 +283,10 @@ impl AcpThreadHistory {
_window: &mut Window, _window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let count = self.matched_count(); if self.selected_index == self.visible_items.len() - 1 {
if count > 0 { self.set_selected_index(0, Bias::Right, cx);
if self.selected_index == count - 1 { } else {
self.set_selected_entry_index(0, cx); self.set_selected_index(self.selected_index + 1, Bias::Right, cx);
} else {
self.set_selected_entry_index(self.selected_index + 1, cx);
}
} }
} }
@ -325,35 +296,47 @@ impl AcpThreadHistory {
_window: &mut Window, _window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let count = self.matched_count(); self.set_selected_index(0, Bias::Right, cx);
if count > 0 {
self.set_selected_entry_index(0, cx);
}
} }
fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) { fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
let count = self.matched_count(); self.set_selected_index(self.visible_items.len() - 1, Bias::Left, cx);
if count > 0 {
self.set_selected_entry_index(count - 1, cx);
}
} }
fn set_selected_entry_index(&mut self, entry_index: usize, cx: &mut Context<Self>) { fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
self.selected_index = entry_index; self.confirm_entry(self.selected_index, cx);
}
let scroll_ix = match self.search_state { fn confirm_entry(&mut self, ix: usize, cx: &mut Context<Self>) {
SearchState::Empty | SearchState::Searching { .. } => self let Some(entry) = self.get_history_entry(ix) else {
.separated_item_indexes return;
.get(entry_index) };
.map(|ix| *ix as usize) cx.emit(ThreadHistoryEvent::Open(entry.clone()));
.unwrap_or(entry_index + 1), }
SearchState::Searched { .. } => entry_index,
fn remove_selected_thread(
&mut self,
_: &RemoveSelectedThread,
_window: &mut Window,
cx: &mut Context<Self>,
) {
self.remove_thread(self.selected_index, cx)
}
fn remove_thread(&mut self, visible_item_ix: usize, cx: &mut Context<Self>) {
let Some(entry) = self.get_history_entry(visible_item_ix) else {
return;
}; };
self.scroll_handle let task = match entry {
.scroll_to_item(scroll_ix, ScrollStrategy::Top); HistoryEntry::AcpThread(thread) => self
.history_store
cx.notify(); .update(cx, |this, cx| this.delete_thread(thread.id.clone(), cx)),
HistoryEntry::TextThread(context) => self.history_store.update(cx, |this, cx| {
this.delete_text_thread(context.path.clone(), cx)
}),
};
task.detach_and_log_err(cx);
} }
fn render_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> { fn render_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
@ -393,91 +376,33 @@ impl AcpThreadHistory {
) )
} }
fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) { fn render_list_items(
self.confirm_entry(self.selected_index, cx);
}
fn confirm_entry(&mut self, ix: usize, cx: &mut Context<Self>) {
let Some(entry) = self.get_match(ix) else {
return;
};
cx.emit(ThreadHistoryEvent::Open(entry.clone()));
}
fn remove_selected_thread(
&mut self,
_: &RemoveSelectedThread,
_window: &mut Window,
cx: &mut Context<Self>,
) {
self.remove_thread(self.selected_index, cx)
}
fn remove_thread(&mut self, ix: usize, cx: &mut Context<Self>) {
let Some(entry) = self.get_match(ix) else {
return;
};
let task = match entry {
HistoryEntry::AcpThread(thread) => self
.history_store
.update(cx, |this, cx| this.delete_thread(thread.id.clone(), cx)),
HistoryEntry::TextThread(context) => self.history_store.update(cx, |this, cx| {
this.delete_text_thread(context.path.clone(), cx)
}),
};
task.detach_and_log_err(cx);
}
fn list_items(
&mut self, &mut self,
range: Range<usize>, range: Range<usize>,
_window: &mut Window, _window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Vec<AnyElement> { ) -> Vec<AnyElement> {
match &self.search_state { self.visible_items
SearchState::Empty => self .get(range.clone())
.separated_items .into_iter()
.get(range) .flatten()
.iter() .enumerate()
.flat_map(|items| { .map(|(ix, item)| self.render_list_item(item, range.start + ix, cx))
items .collect()
.iter()
.map(|item| self.render_list_item(item, vec![], cx))
})
.collect(),
SearchState::Searched { matches, .. } => matches[range]
.iter()
.filter_map(|m| {
let entry = self.all_entries.get(m.candidate_id)?;
Some(self.render_history_entry(
entry,
EntryTimeFormat::DateAndTime,
m.candidate_id,
m.positions.clone(),
cx,
))
})
.collect(),
SearchState::Searching { .. } => {
vec![]
}
}
} }
fn render_list_item( fn render_list_item(&self, item: &ListItemType, ix: usize, cx: &Context<Self>) -> AnyElement {
&self,
item: &ListItemType,
highlight_positions: Vec<usize>,
cx: &Context<Self>,
) -> AnyElement {
match item { match item {
ListItemType::Entry { index, format } => match self.all_entries.get(*index) { ListItemType::Entry { entry, format } => self
Some(entry) => self .render_history_entry(entry, *format, ix, Vec::default(), cx)
.render_history_entry(entry, *format, *index, highlight_positions, cx) .into_any(),
.into_any(), ListItemType::SearchResult { entry, positions } => self.render_history_entry(
None => Empty.into_any_element(), entry,
}, EntryTimeFormat::DateAndTime,
ix,
positions.clone(),
cx,
),
ListItemType::BucketSeparator(bucket) => div() ListItemType::BucketSeparator(bucket) => div()
.px(DynamicSpacing::Base06.rems(cx)) .px(DynamicSpacing::Base06.rems(cx))
.pt_2() .pt_2()
@ -495,12 +420,12 @@ impl AcpThreadHistory {
&self, &self,
entry: &HistoryEntry, entry: &HistoryEntry,
format: EntryTimeFormat, format: EntryTimeFormat,
list_entry_ix: usize, ix: usize,
highlight_positions: Vec<usize>, highlight_positions: Vec<usize>,
cx: &Context<Self>, cx: &Context<Self>,
) -> AnyElement { ) -> AnyElement {
let selected = list_entry_ix == self.selected_index; let selected = ix == self.selected_index;
let hovered = Some(list_entry_ix) == self.hovered_index; let hovered = Some(ix) == self.hovered_index;
let timestamp = entry.updated_at().timestamp(); let timestamp = entry.updated_at().timestamp();
let thread_timestamp = format.format_timestamp(timestamp, self.local_timezone); let thread_timestamp = format.format_timestamp(timestamp, self.local_timezone);
@ -508,7 +433,7 @@ impl AcpThreadHistory {
.w_full() .w_full()
.pb_1() .pb_1()
.child( .child(
ListItem::new(list_entry_ix) ListItem::new(ix)
.rounded() .rounded()
.toggle_state(selected) .toggle_state(selected)
.spacing(ListItemSpacing::Sparse) .spacing(ListItemSpacing::Sparse)
@ -530,14 +455,14 @@ impl AcpThreadHistory {
) )
.on_hover(cx.listener(move |this, is_hovered, _window, cx| { .on_hover(cx.listener(move |this, is_hovered, _window, cx| {
if *is_hovered { if *is_hovered {
this.hovered_index = Some(list_entry_ix); this.hovered_index = Some(ix);
} else if this.hovered_index == Some(list_entry_ix) { } else if this.hovered_index == Some(ix) {
this.hovered_index = None; this.hovered_index = None;
} }
cx.notify(); cx.notify();
})) }))
.end_slot::<IconButton>(if hovered || selected { .end_slot::<IconButton>(if hovered {
Some( Some(
IconButton::new("delete", IconName::Trash) IconButton::new("delete", IconName::Trash)
.shape(IconButtonShape::Square) .shape(IconButtonShape::Square)
@ -546,16 +471,14 @@ impl AcpThreadHistory {
.tooltip(move |window, cx| { .tooltip(move |window, cx| {
Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx) Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx)
}) })
.on_click(cx.listener(move |this, _, _, cx| { .on_click(
this.remove_thread(list_entry_ix, cx) cx.listener(move |this, _, _, cx| this.remove_thread(ix, cx)),
})), ),
) )
} else { } else {
None None
}) })
.on_click( .on_click(cx.listener(move |this, _, _, cx| this.confirm_entry(ix, cx))),
cx.listener(move |this, _, _, cx| this.confirm_entry(list_entry_ix, cx)),
),
) )
.into_any_element() .into_any_element()
} }
@ -578,7 +501,7 @@ impl Render for AcpThreadHistory {
.on_action(cx.listener(Self::select_last)) .on_action(cx.listener(Self::select_last))
.on_action(cx.listener(Self::confirm)) .on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::remove_selected_thread)) .on_action(cx.listener(Self::remove_selected_thread))
.when(!self.all_entries.is_empty(), |parent| { .when(!self.history_store.read(cx).is_empty(cx), |parent| {
parent.child( parent.child(
h_flex() h_flex()
.h(px(41.)) // Match the toolbar perfectly .h(px(41.)) // Match the toolbar perfectly
@ -604,7 +527,7 @@ impl Render for AcpThreadHistory {
.overflow_hidden() .overflow_hidden()
.flex_grow(); .flex_grow();
if self.all_entries.is_empty() { if self.history_store.read(cx).is_empty(cx) {
view.justify_center() view.justify_center()
.child( .child(
h_flex().w_full().justify_center().child( h_flex().w_full().justify_center().child(
@ -623,9 +546,9 @@ impl Render for AcpThreadHistory {
.child( .child(
uniform_list( uniform_list(
"thread-history", "thread-history",
self.list_item_count(), self.visible_items.len(),
cx.processor(|this, range: Range<usize>, window, cx| { cx.processor(|this, range: Range<usize>, window, cx| {
this.list_items(range, window, cx) this.render_list_items(range, window, cx)
}), }),
) )
.p_1() .p_1()

File diff suppressed because it is too large Load diff

View file

@ -1595,11 +1595,6 @@ impl ActiveThread {
return; return;
}; };
if model.provider.must_accept_terms(cx) {
cx.notify();
return;
}
let edited_text = state.editor.read(cx).text(cx); let edited_text = state.editor.read(cx).text(cx);
let creases = state.editor.update(cx, extract_message_creases); let creases = state.editor.update(cx, extract_message_creases);

View file

@ -5,6 +5,7 @@ mod tool_picker;
use std::{sync::Arc, time::Duration}; use std::{sync::Arc, time::Duration};
use agent_servers::{AgentServerCommand, AllAgentServersSettings, Gemini};
use agent_settings::AgentSettings; use agent_settings::AgentSettings;
use assistant_tool::{ToolSource, ToolWorkingSet}; use assistant_tool::{ToolSource, ToolWorkingSet};
use cloud_llm_client::Plan; use cloud_llm_client::Plan;
@ -15,7 +16,7 @@ use extension_host::ExtensionStore;
use fs::Fs; use fs::Fs;
use gpui::{ use gpui::{
Action, Animation, AnimationExt as _, AnyView, App, Corner, Entity, EventEmitter, FocusHandle, Action, Animation, AnimationExt as _, AnyView, App, Corner, Entity, EventEmitter, FocusHandle,
Focusable, ScrollHandle, Subscription, Task, Transformation, WeakEntity, percentage, Focusable, Hsla, ScrollHandle, Subscription, Task, Transformation, WeakEntity, percentage,
}; };
use language::LanguageRegistry; use language::LanguageRegistry;
use language_model::{ use language_model::{
@ -23,10 +24,11 @@ use language_model::{
}; };
use notifications::status_toast::{StatusToast, ToastIcon}; use notifications::status_toast::{StatusToast, ToastIcon};
use project::{ use project::{
Project,
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore}, context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
project_settings::{ContextServerSettings, ProjectSettings}, project_settings::{ContextServerSettings, ProjectSettings},
}; };
use settings::{Settings, update_settings_file}; use settings::{Settings, SettingsStore, update_settings_file};
use ui::{ use ui::{
Chip, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu, Chip, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu,
Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip, prelude::*, Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip, prelude::*,
@ -39,7 +41,7 @@ pub(crate) use configure_context_server_modal::ConfigureContextServerModal;
pub(crate) use manage_profiles_modal::ManageProfilesModal; pub(crate) use manage_profiles_modal::ManageProfilesModal;
use crate::{ use crate::{
AddContextServer, AddContextServer, ExternalAgent, NewExternalAgentThread,
agent_configuration::add_llm_provider_modal::{AddLlmProviderModal, LlmCompatibleProvider}, agent_configuration::add_llm_provider_modal::{AddLlmProviderModal, LlmCompatibleProvider},
}; };
@ -47,6 +49,7 @@ pub struct AgentConfiguration {
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
language_registry: Arc<LanguageRegistry>, language_registry: Arc<LanguageRegistry>,
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
project: WeakEntity<Project>,
focus_handle: FocusHandle, focus_handle: FocusHandle,
configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>, configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>,
context_server_store: Entity<ContextServerStore>, context_server_store: Entity<ContextServerStore>,
@ -56,6 +59,8 @@ pub struct AgentConfiguration {
_registry_subscription: Subscription, _registry_subscription: Subscription,
scroll_handle: ScrollHandle, scroll_handle: ScrollHandle,
scrollbar_state: ScrollbarState, scrollbar_state: ScrollbarState,
gemini_is_installed: bool,
_check_for_gemini: Task<()>,
} }
impl AgentConfiguration { impl AgentConfiguration {
@ -65,6 +70,7 @@ impl AgentConfiguration {
tools: Entity<ToolWorkingSet>, tools: Entity<ToolWorkingSet>,
language_registry: Arc<LanguageRegistry>, language_registry: Arc<LanguageRegistry>,
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
project: WeakEntity<Project>,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Self { ) -> Self {
@ -89,33 +95,34 @@ impl AgentConfiguration {
cx.subscribe(&context_server_store, |_, _, _, cx| cx.notify()) cx.subscribe(&context_server_store, |_, _, _, cx| cx.notify())
.detach(); .detach();
cx.observe_global_in::<SettingsStore>(window, |this, _, cx| {
this.check_for_gemini(cx);
cx.notify();
})
.detach();
let scroll_handle = ScrollHandle::new(); let scroll_handle = ScrollHandle::new();
let scrollbar_state = ScrollbarState::new(scroll_handle.clone()); let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
let mut expanded_provider_configurations = HashMap::default();
if LanguageModelRegistry::read_global(cx)
.provider(&ZED_CLOUD_PROVIDER_ID)
.is_some_and(|cloud_provider| cloud_provider.must_accept_terms(cx))
{
expanded_provider_configurations.insert(ZED_CLOUD_PROVIDER_ID, true);
}
let mut this = Self { let mut this = Self {
fs, fs,
language_registry, language_registry,
workspace, workspace,
project,
focus_handle, focus_handle,
configuration_views_by_provider: HashMap::default(), configuration_views_by_provider: HashMap::default(),
context_server_store, context_server_store,
expanded_context_server_tools: HashMap::default(), expanded_context_server_tools: HashMap::default(),
expanded_provider_configurations, expanded_provider_configurations: HashMap::default(),
tools, tools,
_registry_subscription: registry_subscription, _registry_subscription: registry_subscription,
scroll_handle, scroll_handle,
scrollbar_state, scrollbar_state,
gemini_is_installed: false,
_check_for_gemini: Task::ready(()),
}; };
this.build_provider_configuration_views(window, cx); this.build_provider_configuration_views(window, cx);
this.check_for_gemini(cx);
this this
} }
@ -145,6 +152,34 @@ impl AgentConfiguration {
self.configuration_views_by_provider self.configuration_views_by_provider
.insert(provider.id(), configuration_view); .insert(provider.id(), configuration_view);
} }
fn check_for_gemini(&mut self, cx: &mut Context<Self>) {
let project = self.project.clone();
let settings = AllAgentServersSettings::get_global(cx).clone();
self._check_for_gemini = cx.spawn({
async move |this, cx| {
let Some(project) = project.upgrade() else {
return;
};
let gemini_is_installed = AgentServerCommand::resolve(
Gemini::binary_name(),
&[],
// TODO expose fallback path from the Gemini/CC types so we don't have to hardcode it again here
None,
settings.gemini,
&project,
cx,
)
.await
.is_some();
this.update(cx, |this, cx| {
this.gemini_is_installed = gemini_is_installed;
cx.notify();
})
.ok();
}
});
}
} }
impl Focusable for AgentConfiguration { impl Focusable for AgentConfiguration {
@ -219,7 +254,6 @@ impl AgentConfiguration {
.child( .child(
h_flex() h_flex()
.id(provider_id_string.clone()) .id(provider_id_string.clone())
.cursor_pointer()
.px_2() .px_2()
.py_0p5() .py_0p5()
.w_full() .w_full()
@ -239,10 +273,7 @@ impl AgentConfiguration {
h_flex() h_flex()
.w_full() .w_full()
.gap_1() .gap_1()
.child( .child(Label::new(provider_name.clone()))
Label::new(provider_name.clone())
.size(LabelSize::Large),
)
.map(|this| { .map(|this| {
if is_zed_provider && is_signed_in { if is_zed_provider && is_signed_in {
this.child( this.child(
@ -287,7 +318,7 @@ impl AgentConfiguration {
"Start New Thread", "Start New Thread",
) )
.icon_position(IconPosition::Start) .icon_position(IconPosition::Start)
.icon(IconName::Plus) .icon(IconName::Thread)
.icon_size(IconSize::Small) .icon_size(IconSize::Small)
.icon_color(Color::Muted) .icon_color(Color::Muted)
.label_size(LabelSize::Small) .label_size(LabelSize::Small)
@ -386,7 +417,7 @@ impl AgentConfiguration {
), ),
) )
.child( .child(
Label::new("Add at least one provider to use AI-powered features.") Label::new("Add at least one provider to use AI-powered features with Zed's native agent.")
.color(Color::Muted), .color(Color::Muted),
), ),
), ),
@ -527,6 +558,14 @@ impl AgentConfiguration {
} }
} }
fn card_item_bg_color(&self, cx: &mut Context<Self>) -> Hsla {
cx.theme().colors().background.opacity(0.25)
}
fn card_item_border_color(&self, cx: &mut Context<Self>) -> Hsla {
cx.theme().colors().border.opacity(0.6)
}
fn render_context_servers_section( fn render_context_servers_section(
&mut self, &mut self,
window: &mut Window, window: &mut Window,
@ -544,7 +583,12 @@ impl AgentConfiguration {
v_flex() v_flex()
.gap_0p5() .gap_0p5()
.child(Headline::new("Model Context Protocol (MCP) Servers")) .child(Headline::new("Model Context Protocol (MCP) Servers"))
.child(Label::new("Connect to context servers through the Model Context Protocol, either using Zed extensions or directly.").color(Color::Muted)), .child(
Label::new(
"All context servers connected through the Model Context Protocol.",
)
.color(Color::Muted),
),
) )
.children( .children(
context_server_ids.into_iter().map(|context_server_id| { context_server_ids.into_iter().map(|context_server_id| {
@ -554,7 +598,7 @@ impl AgentConfiguration {
.child( .child(
h_flex() h_flex()
.justify_between() .justify_between()
.gap_2() .gap_1p5()
.child( .child(
h_flex().w_full().child( h_flex().w_full().child(
Button::new("add-context-server", "Add Custom Server") Button::new("add-context-server", "Add Custom Server")
@ -645,8 +689,6 @@ impl AgentConfiguration {
.map_or([].as_slice(), |tools| tools.as_slice()); .map_or([].as_slice(), |tools| tools.as_slice());
let tool_count = tools.len(); let tool_count = tools.len();
let border_color = cx.theme().colors().border.opacity(0.6);
let (source_icon, source_tooltip) = if is_from_extension { let (source_icon, source_tooltip) = if is_from_extension {
( (
IconName::ZedMcpExtension, IconName::ZedMcpExtension,
@ -789,8 +831,8 @@ impl AgentConfiguration {
.id(item_id.clone()) .id(item_id.clone())
.border_1() .border_1()
.rounded_md() .rounded_md()
.border_color(border_color) .border_color(self.card_item_border_color(cx))
.bg(cx.theme().colors().background.opacity(0.2)) .bg(self.card_item_bg_color(cx))
.overflow_hidden() .overflow_hidden()
.child( .child(
h_flex() h_flex()
@ -798,7 +840,11 @@ impl AgentConfiguration {
.justify_between() .justify_between()
.when( .when(
error.is_some() || are_tools_expanded && tool_count >= 1, error.is_some() || are_tools_expanded && tool_count >= 1,
|element| element.border_b_1().border_color(border_color), |element| {
element
.border_b_1()
.border_color(self.card_item_border_color(cx))
},
) )
.child( .child(
h_flex() h_flex()
@ -980,6 +1026,166 @@ impl AgentConfiguration {
)) ))
}) })
} }
fn render_agent_servers_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
let settings = AllAgentServersSettings::get_global(cx).clone();
let user_defined_agents = settings
.custom
.iter()
.map(|(name, settings)| {
self.render_agent_server(
IconName::Ai,
name.clone(),
ExternalAgent::Custom {
name: name.clone(),
settings: settings.clone(),
},
None,
cx,
)
.into_any_element()
})
.collect::<Vec<_>>();
v_flex()
.border_b_1()
.border_color(cx.theme().colors().border)
.child(
v_flex()
.p(DynamicSpacing::Base16.rems(cx))
.pr(DynamicSpacing::Base20.rems(cx))
.gap_2()
.child(
v_flex()
.gap_0p5()
.child(Headline::new("External Agents"))
.child(
Label::new(
"Use the full power of Zed's UI with your favorite agent, connected via the Agent Client Protocol.",
)
.color(Color::Muted),
),
)
.child(self.render_agent_server(
IconName::AiGemini,
"Gemini CLI",
ExternalAgent::Gemini,
(!self.gemini_is_installed).then_some(Gemini::install_command().into()),
cx,
))
// TODO add CC
.children(user_defined_agents),
)
}
fn render_agent_server(
&self,
icon: IconName,
name: impl Into<SharedString>,
agent: ExternalAgent,
install_command: Option<SharedString>,
cx: &mut Context<Self>,
) -> impl IntoElement {
let name = name.into();
h_flex()
.p_1()
.pl_2()
.gap_1p5()
.justify_between()
.border_1()
.rounded_md()
.border_color(self.card_item_border_color(cx))
.bg(self.card_item_bg_color(cx))
.overflow_hidden()
.child(
h_flex()
.gap_1p5()
.child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
.child(Label::new(name.clone())),
)
.map(|this| {
if let Some(install_command) = install_command {
this.child(
Button::new(
SharedString::from(format!("install_external_agent-{name}")),
"Install Agent",
)
.label_size(LabelSize::Small)
.icon(IconName::Plus)
.icon_position(IconPosition::Start)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.tooltip(Tooltip::text(install_command.clone()))
.on_click(cx.listener(
move |this, _, window, cx| {
let Some(project) = this.project.upgrade() else {
return;
};
let Some(workspace) = this.workspace.upgrade() else {
return;
};
let cwd = project.read(cx).first_project_directory(cx);
let shell =
project.read(cx).terminal_settings(&cwd, cx).shell.clone();
let spawn_in_terminal = task::SpawnInTerminal {
id: task::TaskId(install_command.to_string()),
full_label: install_command.to_string(),
label: install_command.to_string(),
command: Some(install_command.to_string()),
args: Vec::new(),
command_label: install_command.to_string(),
cwd,
env: Default::default(),
use_new_terminal: true,
allow_concurrent_runs: true,
reveal: Default::default(),
reveal_target: Default::default(),
hide: Default::default(),
shell,
show_summary: true,
show_command: true,
show_rerun: false,
};
let task = workspace.update(cx, |workspace, cx| {
workspace.spawn_in_terminal(spawn_in_terminal, window, cx)
});
cx.spawn(async move |this, cx| {
task.await;
this.update(cx, |this, cx| {
this.check_for_gemini(cx);
})
.ok();
})
.detach();
},
)),
)
} else {
this.child(
h_flex().gap_1().child(
Button::new(
SharedString::from(format!("start_acp_thread-{name}")),
"Start New Thread",
)
.label_size(LabelSize::Small)
.icon(IconName::Thread)
.icon_position(IconPosition::Start)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.on_click(move |_, window, cx| {
window.dispatch_action(
NewExternalAgentThread {
agent: Some(agent.clone()),
}
.boxed_clone(),
cx,
);
}),
),
)
}
})
}
} }
impl Render for AgentConfiguration { impl Render for AgentConfiguration {
@ -999,6 +1205,7 @@ impl Render for AgentConfiguration {
.size_full() .size_full()
.overflow_y_scroll() .overflow_y_scroll()
.child(self.render_general_settings_section(cx)) .child(self.render_general_settings_section(cx))
.child(self.render_agent_servers_section(cx))
.child(self.render_context_servers_section(window, cx)) .child(self.render_context_servers_section(window, cx))
.child(self.render_provider_configuration_section(cx)), .child(self.render_provider_configuration_section(cx)),
) )

View file

@ -1529,6 +1529,7 @@ impl AgentDiff {
| AcpThreadEvent::TokenUsageUpdated | AcpThreadEvent::TokenUsageUpdated
| AcpThreadEvent::EntriesRemoved(_) | AcpThreadEvent::EntriesRemoved(_)
| AcpThreadEvent::ToolAuthorizationRequired | AcpThreadEvent::ToolAuthorizationRequired
| AcpThreadEvent::PromptCapabilitiesUpdated
| AcpThreadEvent::Retry(_) => {} | AcpThreadEvent::Retry(_) => {}
} }
} }

View file

@ -5,9 +5,12 @@ use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use acp_thread::AcpThread; use acp_thread::AcpThread;
use agent_servers::AgentServerSettings;
use agent2::{DbThreadMetadata, HistoryEntry}; use agent2::{DbThreadMetadata, HistoryEntry};
use db::kvp::{Dismissable, KEY_VALUE_STORE}; use db::kvp::{Dismissable, KEY_VALUE_STORE};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use zed_actions::OpenBrowser;
use zed_actions::agent::ReauthenticateAgent;
use crate::acp::{AcpThreadHistory, ThreadHistoryEvent}; use crate::acp::{AcpThreadHistory, ThreadHistoryEvent};
use crate::agent_diff::AgentDiffThread; use crate::agent_diff::AgentDiffThread;
@ -54,9 +57,7 @@ use gpui::{
Pixels, Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between, Pixels, Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between,
}; };
use language::LanguageRegistry; use language::LanguageRegistry;
use language_model::{ use language_model::{ConfigurationError, ConfiguredModel, LanguageModelRegistry};
ConfigurationError, ConfiguredModel, LanguageModelProviderTosView, LanguageModelRegistry,
};
use project::{DisableAiSettings, Project, ProjectPath, Worktree}; use project::{DisableAiSettings, Project, ProjectPath, Worktree};
use prompt_store::{PromptBuilder, PromptStore, UserPromptId}; use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
use rules_library::{RulesLibrary, open_rules_library}; use rules_library::{RulesLibrary, open_rules_library};
@ -130,7 +131,7 @@ pub fn init(cx: &mut App) {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) { if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx); workspace.focus_panel::<AgentPanel>(window, cx);
panel.update(cx, |panel, cx| { panel.update(cx, |panel, cx| {
panel.external_thread(action.agent, None, None, window, cx) panel.external_thread(action.agent.clone(), None, None, window, cx)
}); });
} }
}) })
@ -241,7 +242,8 @@ enum WhichFontSize {
None, None,
} }
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] // TODO unify this with ExternalAgent
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
pub enum AgentType { pub enum AgentType {
#[default] #[default]
Zed, Zed,
@ -249,23 +251,29 @@ pub enum AgentType {
Gemini, Gemini,
ClaudeCode, ClaudeCode,
NativeAgent, NativeAgent,
Custom {
name: SharedString,
settings: AgentServerSettings,
},
} }
impl AgentType { impl AgentType {
fn label(self) -> impl Into<SharedString> { fn label(&self) -> SharedString {
match self { match self {
Self::Zed | Self::TextThread => "Zed Agent", Self::Zed | Self::TextThread => "Zed Agent".into(),
Self::NativeAgent => "Agent 2", Self::NativeAgent => "Agent 2".into(),
Self::Gemini => "Gemini CLI", Self::Gemini => "Gemini CLI".into(),
Self::ClaudeCode => "Claude Code", Self::ClaudeCode => "Claude Code".into(),
Self::Custom { name, .. } => name.into(),
} }
} }
fn icon(self) -> Option<IconName> { fn icon(&self) -> Option<IconName> {
match self { match self {
Self::Zed | Self::NativeAgent | Self::TextThread => None, Self::Zed | Self::NativeAgent | Self::TextThread => None,
Self::Gemini => Some(IconName::AiGemini), Self::Gemini => Some(IconName::AiGemini),
Self::ClaudeCode => Some(IconName::AiClaude), Self::ClaudeCode => Some(IconName::AiClaude),
Self::Custom { .. } => Some(IconName::Terminal),
} }
} }
} }
@ -519,7 +527,7 @@ pub struct AgentPanel {
impl AgentPanel { impl AgentPanel {
fn serialize(&mut self, cx: &mut Context<Self>) { fn serialize(&mut self, cx: &mut Context<Self>) {
let width = self.width; let width = self.width;
let selected_agent = self.selected_agent; let selected_agent = self.selected_agent.clone();
self.pending_serialization = Some(cx.background_spawn(async move { self.pending_serialization = Some(cx.background_spawn(async move {
KEY_VALUE_STORE KEY_VALUE_STORE
.write_kvp( .write_kvp(
@ -609,7 +617,7 @@ impl AgentPanel {
panel.update(cx, |panel, cx| { panel.update(cx, |panel, cx| {
panel.width = serialized_panel.width.map(|w| w.round()); panel.width = serialized_panel.width.map(|w| w.round());
if let Some(selected_agent) = serialized_panel.selected_agent { if let Some(selected_agent) = serialized_panel.selected_agent {
panel.selected_agent = selected_agent; panel.selected_agent = selected_agent.clone();
panel.new_agent_thread(selected_agent, window, cx); panel.new_agent_thread(selected_agent, window, cx);
} }
cx.notify(); cx.notify();
@ -1019,6 +1027,8 @@ impl AgentPanel {
} }
fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
telemetry::event!("Agent Thread Started", agent = "zed-text");
let context = self let context = self
.context_store .context_store
.update(cx, |context_store, cx| context_store.create(cx)); .update(cx, |context_store, cx| context_store.create(cx));
@ -1079,14 +1089,17 @@ impl AgentPanel {
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
let ext_agent = match agent_choice { let ext_agent = match agent_choice {
Some(agent) => { Some(agent) => {
cx.background_spawn(async move { cx.background_spawn({
if let Some(serialized) = let agent = agent.clone();
serde_json::to_string(&LastUsedExternalAgent { agent }).log_err() async move {
{ if let Some(serialized) =
KEY_VALUE_STORE serde_json::to_string(&LastUsedExternalAgent { agent }).log_err()
.write_kvp(LAST_USED_EXTERNAL_AGENT_KEY.to_string(), serialized) {
.await KEY_VALUE_STORE
.log_err(); .write_kvp(LAST_USED_EXTERNAL_AGENT_KEY.to_string(), serialized)
.await
.log_err();
}
} }
}) })
.detach(); .detach();
@ -1108,11 +1121,15 @@ impl AgentPanel {
} }
}; };
telemetry::event!("Agent Thread Started", agent = ext_agent.name());
let server = ext_agent.server(fs, history); let server = ext_agent.server(fs, history);
this.update_in(cx, |this, window, cx| { this.update_in(cx, |this, window, cx| {
match ext_agent { match ext_agent {
crate::ExternalAgent::Gemini | crate::ExternalAgent::NativeAgent => { crate::ExternalAgent::Gemini
| crate::ExternalAgent::NativeAgent
| crate::ExternalAgent::Custom { .. } => {
if !cx.has_flag::<GeminiAndNativeFeatureFlag>() { if !cx.has_flag::<GeminiAndNativeFeatureFlag>() {
return; return;
} }
@ -1463,6 +1480,7 @@ impl AgentPanel {
tools, tools,
self.language_registry.clone(), self.language_registry.clone(),
self.workspace.clone(), self.workspace.clone(),
self.project.downgrade(),
window, window,
cx, cx,
) )
@ -1841,14 +1859,14 @@ impl AgentPanel {
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
if self.selected_agent != agent { if self.selected_agent != agent {
self.selected_agent = agent; self.selected_agent = agent.clone();
self.serialize(cx); self.serialize(cx);
} }
self.new_agent_thread(agent, window, cx); self.new_agent_thread(agent, window, cx);
} }
pub fn selected_agent(&self) -> AgentType { pub fn selected_agent(&self) -> AgentType {
self.selected_agent self.selected_agent.clone()
} }
pub fn new_agent_thread( pub fn new_agent_thread(
@ -1887,6 +1905,13 @@ impl AgentPanel {
window, window,
cx, cx,
), ),
AgentType::Custom { name, settings } => self.external_thread(
Some(crate::ExternalAgent::Custom { name, settings }),
None,
None,
window,
cx,
),
} }
} }
@ -2041,9 +2066,11 @@ impl AgentPanel {
match state { match state {
ThreadSummary::Pending => Label::new(ThreadSummary::DEFAULT) ThreadSummary::Pending => Label::new(ThreadSummary::DEFAULT)
.truncate() .truncate()
.color(Color::Muted)
.into_any_element(), .into_any_element(),
ThreadSummary::Generating => Label::new(LOADING_SUMMARY_PLACEHOLDER) ThreadSummary::Generating => Label::new(LOADING_SUMMARY_PLACEHOLDER)
.truncate() .truncate()
.color(Color::Muted)
.into_any_element(), .into_any_element(),
ThreadSummary::Ready(_) => div() ThreadSummary::Ready(_) => div()
.w_full() .w_full()
@ -2097,7 +2124,8 @@ impl AgentPanel {
.child(title_editor) .child(title_editor)
.into_any_element() .into_any_element()
} else { } else {
Label::new(thread_view.read(cx).title(cx)) Label::new(thread_view.read(cx).title())
.color(Color::Muted)
.truncate() .truncate()
.into_any_element() .into_any_element()
} }
@ -2111,6 +2139,7 @@ impl AgentPanel {
match summary { match summary {
ContextSummary::Pending => Label::new(ContextSummary::DEFAULT) ContextSummary::Pending => Label::new(ContextSummary::DEFAULT)
.color(Color::Muted)
.truncate() .truncate()
.into_any_element(), .into_any_element(),
ContextSummary::Content(summary) => { ContextSummary::Content(summary) => {
@ -2122,6 +2151,7 @@ impl AgentPanel {
} else { } else {
Label::new(LOADING_SUMMARY_PLACEHOLDER) Label::new(LOADING_SUMMARY_PLACEHOLDER)
.truncate() .truncate()
.color(Color::Muted)
.into_any_element() .into_any_element()
} }
} }
@ -2182,6 +2212,8 @@ impl AgentPanel {
"Enable Full Screen" "Enable Full Screen"
}; };
let selected_agent = self.selected_agent.clone();
PopoverMenu::new("agent-options-menu") PopoverMenu::new("agent-options-menu")
.trigger_with_tooltip( .trigger_with_tooltip(
IconButton::new("agent-options-menu", IconName::Ellipsis) IconButton::new("agent-options-menu", IconName::Ellipsis)
@ -2261,6 +2293,11 @@ impl AgentPanel {
.action("Settings", Box::new(OpenSettings)) .action("Settings", Box::new(OpenSettings))
.separator() .separator()
.action(full_screen_label, Box::new(ToggleZoom)); .action(full_screen_label, Box::new(ToggleZoom));
if selected_agent == AgentType::Gemini {
menu = menu.action("Reauthenticate", Box::new(ReauthenticateAgent))
}
menu menu
})) }))
} }
@ -2295,6 +2332,8 @@ impl AgentPanel {
.menu({ .menu({
let menu = self.assistant_navigation_menu.clone(); let menu = self.assistant_navigation_menu.clone();
move |window, cx| { move |window, cx| {
telemetry::event!("View Thread History Clicked");
if let Some(menu) = menu.as_ref() { if let Some(menu) = menu.as_ref() {
menu.update(cx, |_, cx| { menu.update(cx, |_, cx| {
cx.defer_in(window, |menu, window, cx| { cx.defer_in(window, |menu, window, cx| {
@ -2473,6 +2512,8 @@ impl AgentPanel {
let workspace = self.workspace.clone(); let workspace = self.workspace.clone();
move |window, cx| { move |window, cx| {
telemetry::event!("New Thread Clicked");
let active_thread = active_thread.clone(); let active_thread = active_thread.clone();
Some(ContextMenu::build(window, cx, |mut menu, _window, cx| { Some(ContextMenu::build(window, cx, |mut menu, _window, cx| {
menu = menu menu = menu
@ -2607,13 +2648,64 @@ impl AgentPanel {
} }
}), }),
) )
})
.when(cx.has_flag::<GeminiAndNativeFeatureFlag>(), |mut menu| {
// Add custom agents from settings
let settings =
agent_servers::AllAgentServersSettings::get_global(cx);
for (agent_name, agent_settings) in &settings.custom {
menu = menu.item(
ContextMenuEntry::new(format!("New {} Thread", agent_name))
.icon(IconName::Terminal)
.icon_color(Color::Muted)
.handler({
let workspace = workspace.clone();
let agent_name = agent_name.clone();
let agent_settings = agent_settings.clone();
move |window, cx| {
if let Some(workspace) = workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
if let Some(panel) =
workspace.panel::<AgentPanel>(cx)
{
panel.update(cx, |panel, cx| {
panel.set_selected_agent(
AgentType::Custom {
name: agent_name
.clone(),
settings:
agent_settings
.clone(),
},
window,
cx,
);
});
}
});
}
}
}),
);
}
menu
})
.when(cx.has_flag::<GeminiAndNativeFeatureFlag>(), |menu| {
menu.separator().link(
"Add Your Own Agent",
OpenBrowser {
url: "https://agentclientprotocol.com/".into(),
}
.boxed_clone(),
)
}); });
menu menu
})) }))
} }
}); });
let selected_agent_label = self.selected_agent.label().into(); let selected_agent_label = self.selected_agent.label();
let selected_agent = div() let selected_agent = div()
.id("selected_agent_icon") .id("selected_agent_icon")
.when_some(self.selected_agent.icon(), |this, icon| { .when_some(self.selected_agent.icon(), |this, icon| {
@ -3198,17 +3290,6 @@ impl AgentPanel {
ConfigurationError::ModelNotFound ConfigurationError::ModelNotFound
| ConfigurationError::ProviderNotAuthenticated(_) | ConfigurationError::ProviderNotAuthenticated(_)
| ConfigurationError::NoProvider => callout.into_any_element(), | ConfigurationError::NoProvider => callout.into_any_element(),
ConfigurationError::ProviderPendingTermsAcceptance(provider) => {
Banner::new()
.severity(Severity::Warning)
.child(h_flex().w_full().children(
provider.render_accept_terms(
LanguageModelProviderTosView::ThreadEmptyState,
cx,
),
))
.into_any_element()
}
} }
} }
@ -3698,6 +3779,11 @@ impl Render for AgentPanel {
} }
})) }))
.on_action(cx.listener(Self::toggle_burn_mode)) .on_action(cx.listener(Self::toggle_burn_mode))
.on_action(cx.listener(|this, _: &ReauthenticateAgent, window, cx| {
if let Some(thread_view) = this.active_thread_view() {
thread_view.update(cx, |thread_view, cx| thread_view.reauthenticate(window, cx))
}
}))
.child(self.render_toolbar(window, cx)) .child(self.render_toolbar(window, cx))
.children(self.render_onboarding(window, cx)) .children(self.render_onboarding(window, cx))
.map(|parent| match &self.active_view { .map(|parent| match &self.active_view {

View file

@ -28,13 +28,14 @@ use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
use agent::{Thread, ThreadId}; use agent::{Thread, ThreadId};
use agent_servers::AgentServerSettings;
use agent_settings::{AgentProfileId, AgentSettings, LanguageModelSelection}; use agent_settings::{AgentProfileId, AgentSettings, LanguageModelSelection};
use assistant_slash_command::SlashCommandRegistry; use assistant_slash_command::SlashCommandRegistry;
use client::Client; use client::Client;
use command_palette_hooks::CommandPaletteFilter; use command_palette_hooks::CommandPaletteFilter;
use feature_flags::FeatureFlagAppExt as _; use feature_flags::FeatureFlagAppExt as _;
use fs::Fs; use fs::Fs;
use gpui::{Action, App, Entity, actions}; use gpui::{Action, App, Entity, SharedString, actions};
use language::LanguageRegistry; use language::LanguageRegistry;
use language_model::{ use language_model::{
ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry,
@ -159,25 +160,43 @@ pub struct NewNativeAgentThreadFromSummary {
from_session_id: agent_client_protocol::SessionId, from_session_id: agent_client_protocol::SessionId,
} }
#[derive(Default, Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)] // TODO unify this with AgentType
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
enum ExternalAgent { enum ExternalAgent {
#[default] #[default]
Gemini, Gemini,
ClaudeCode, ClaudeCode,
NativeAgent, NativeAgent,
Custom {
name: SharedString,
settings: AgentServerSettings,
},
} }
impl ExternalAgent { impl ExternalAgent {
fn name(&self) -> &'static str {
match self {
Self::NativeAgent => "zed",
Self::Gemini => "gemini-cli",
Self::ClaudeCode => "claude-code",
Self::Custom { .. } => "custom",
}
}
pub fn server( pub fn server(
&self, &self,
fs: Arc<dyn fs::Fs>, fs: Arc<dyn fs::Fs>,
history: Entity<agent2::HistoryStore>, history: Entity<agent2::HistoryStore>,
) -> Rc<dyn agent_servers::AgentServer> { ) -> Rc<dyn agent_servers::AgentServer> {
match self { match self {
ExternalAgent::Gemini => Rc::new(agent_servers::Gemini), Self::Gemini => Rc::new(agent_servers::Gemini),
ExternalAgent::ClaudeCode => Rc::new(agent_servers::ClaudeCode), Self::ClaudeCode => Rc::new(agent_servers::ClaudeCode),
ExternalAgent::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs, history)), Self::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs, history)),
Self::Custom { name, settings } => Rc::new(agent_servers::CustomAgentServer::new(
name.clone(),
settings,
)),
} }
} }
} }

View file

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

View file

@ -378,18 +378,13 @@ impl MessageEditor {
} }
fn send_to_model(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn send_to_model(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(ConfiguredModel { model, provider }) = self let Some(ConfiguredModel { model, .. }) = self
.thread .thread
.update(cx, |thread, cx| thread.get_or_init_configured_model(cx)) .update(cx, |thread, cx| thread.get_or_init_configured_model(cx))
else { else {
return; return;
}; };
if provider.must_accept_terms(cx) {
cx.notify();
return;
}
let (user_message, user_message_creases) = self.editor.update(cx, |editor, cx| { let (user_message, user_message_creases) = self.editor.update(cx, |editor, cx| {
let creases = extract_message_creases(editor, cx); let creases = extract_message_creases(editor, cx);
let text = editor.text(cx); let text = editor.text(cx);

View file

@ -190,7 +190,6 @@ pub struct TextThreadEditor {
invoked_slash_command_creases: HashMap<InvokedSlashCommandId, CreaseId>, invoked_slash_command_creases: HashMap<InvokedSlashCommandId, CreaseId>,
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
last_error: Option<AssistError>, last_error: Option<AssistError>,
show_accept_terms: bool,
pub(crate) slash_menu_handle: pub(crate) slash_menu_handle:
PopoverMenuHandle<Picker<slash_command_picker::SlashCommandDelegate>>, PopoverMenuHandle<Picker<slash_command_picker::SlashCommandDelegate>>,
// dragged_file_worktrees is used to keep references to worktrees that were added // dragged_file_worktrees is used to keep references to worktrees that were added
@ -289,7 +288,6 @@ impl TextThreadEditor {
invoked_slash_command_creases: HashMap::default(), invoked_slash_command_creases: HashMap::default(),
_subscriptions, _subscriptions,
last_error: None, last_error: None,
show_accept_terms: false,
slash_menu_handle: Default::default(), slash_menu_handle: Default::default(),
dragged_file_worktrees: Vec::new(), dragged_file_worktrees: Vec::new(),
language_model_selector: cx.new(|cx| { language_model_selector: cx.new(|cx| {
@ -363,24 +361,12 @@ impl TextThreadEditor {
if self.sending_disabled(cx) { if self.sending_disabled(cx) {
return; return;
} }
telemetry::event!("Agent Message Sent", agent = "zed-text");
self.send_to_model(window, cx); self.send_to_model(window, cx);
} }
fn send_to_model(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn send_to_model(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let provider = LanguageModelRegistry::read_global(cx)
.default_model()
.map(|default| default.provider);
if provider
.as_ref()
.is_some_and(|provider| provider.must_accept_terms(cx))
{
self.show_accept_terms = true;
cx.notify();
return;
}
self.last_error = None; self.last_error = None;
if let Some(user_message) = self.context.update(cx, |context, cx| context.assist(cx)) { if let Some(user_message) = self.context.update(cx, |context, cx| context.assist(cx)) {
let new_selection = { let new_selection = {
let cursor = user_message let cursor = user_message
@ -1930,7 +1916,6 @@ impl TextThreadEditor {
ConfigurationError::NoProvider ConfigurationError::NoProvider
| ConfigurationError::ModelNotFound | ConfigurationError::ModelNotFound
| ConfigurationError::ProviderNotAuthenticated(_) => true, | ConfigurationError::ProviderNotAuthenticated(_) => true,
ConfigurationError::ProviderPendingTermsAcceptance(_) => self.show_accept_terms,
} }
} }

View file

@ -4,9 +4,11 @@ mod context_pill;
mod end_trial_upsell; mod end_trial_upsell;
mod onboarding_modal; mod onboarding_modal;
pub mod preview; pub mod preview;
mod unavailable_editing_tooltip;
pub use agent_notification::*; pub use agent_notification::*;
pub use burn_mode_tooltip::*; pub use burn_mode_tooltip::*;
pub use context_pill::*; pub use context_pill::*;
pub use end_trial_upsell::*; pub use end_trial_upsell::*;
pub use onboarding_modal::*; pub use onboarding_modal::*;
pub use unavailable_editing_tooltip::*;

View file

@ -86,23 +86,18 @@ impl RenderOnce for UsageCallout {
(IconName::Warning, Severity::Warning) (IconName::Warning, Severity::Warning)
}; };
div() Callout::new()
.border_t_1() .icon(icon)
.border_color(cx.theme().colors().border) .severity(severity)
.child( .icon(icon)
Callout::new() .title(title)
.icon(icon) .description(message)
.severity(severity) .actions_slot(
.icon(icon) Button::new("upgrade", button_text)
.title(title) .label_size(LabelSize::Small)
.description(message) .on_click(move |_, _, cx| {
.actions_slot( cx.open_url(&url);
Button::new("upgrade", button_text) }),
.label_size(LabelSize::Small)
.on_click(move |_, _, cx| {
cx.open_url(&url);
}),
),
) )
.into_any_element() .into_any_element()
} }

View file

@ -0,0 +1,29 @@
use gpui::{Context, IntoElement, Render, Window};
use ui::{prelude::*, tooltip_container};
pub struct UnavailableEditingTooltip {
agent_name: SharedString,
}
impl UnavailableEditingTooltip {
pub fn new(agent_name: SharedString) -> Self {
Self { agent_name }
}
}
impl Render for UnavailableEditingTooltip {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
tooltip_container(window, cx, |this, _, _| {
this.child(Label::new("Unavailable Editing")).child(
div().max_w_64().child(
Label::new(format!(
"Editing previous messages is not available for {} yet.",
self.agent_name
))
.size(LabelSize::Small)
.color(Color::Muted),
),
)
})
}
}

View file

@ -19,7 +19,7 @@ use std::sync::Arc;
use client::{Client, UserStore, zed_urls}; use client::{Client, UserStore, zed_urls};
use gpui::{AnyElement, Entity, IntoElement, ParentElement}; use gpui::{AnyElement, Entity, IntoElement, ParentElement};
use ui::{Divider, RegisterComponent, TintColor, Tooltip, prelude::*}; use ui::{Divider, RegisterComponent, Tooltip, prelude::*};
#[derive(PartialEq)] #[derive(PartialEq)]
pub enum SignInStatus { pub enum SignInStatus {
@ -43,12 +43,10 @@ impl From<client::Status> for SignInStatus {
#[derive(RegisterComponent, IntoElement)] #[derive(RegisterComponent, IntoElement)]
pub struct ZedAiOnboarding { pub struct ZedAiOnboarding {
pub sign_in_status: SignInStatus, pub sign_in_status: SignInStatus,
pub has_accepted_terms_of_service: bool,
pub plan: Option<Plan>, pub plan: Option<Plan>,
pub account_too_young: bool, pub account_too_young: bool,
pub continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>, pub continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>, pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
pub accept_terms_of_service: Arc<dyn Fn(&mut Window, &mut App)>,
pub dismiss_onboarding: Option<Arc<dyn Fn(&mut Window, &mut App)>>, pub dismiss_onboarding: Option<Arc<dyn Fn(&mut Window, &mut App)>>,
} }
@ -64,17 +62,9 @@ impl ZedAiOnboarding {
Self { Self {
sign_in_status: status.into(), sign_in_status: status.into(),
has_accepted_terms_of_service: store.has_accepted_terms_of_service(),
plan: store.plan(), plan: store.plan(),
account_too_young: store.account_too_young(), account_too_young: store.account_too_young(),
continue_with_zed_ai, continue_with_zed_ai,
accept_terms_of_service: Arc::new({
let store = user_store.clone();
move |_window, cx| {
let task = store.update(cx, |store, cx| store.accept_terms_of_service(cx));
task.detach_and_log_err(cx);
}
}),
sign_in: Arc::new(move |_window, cx| { sign_in: Arc::new(move |_window, cx| {
cx.spawn({ cx.spawn({
let client = client.clone(); let client = client.clone();
@ -94,42 +84,6 @@ impl ZedAiOnboarding {
self self
} }
fn render_accept_terms_of_service(&self) -> AnyElement {
v_flex()
.gap_1()
.w_full()
.child(Headline::new("Accept Terms of Service"))
.child(
Label::new("We dont sell your data, track you across the web, or compromise your privacy.")
.color(Color::Muted)
.mb_2(),
)
.child(
Button::new("terms_of_service", "Review Terms of Service")
.full_width()
.style(ButtonStyle::Outlined)
.icon(IconName::ArrowUpRight)
.icon_color(Color::Muted)
.icon_size(IconSize::Small)
.on_click(move |_, _window, cx| {
telemetry::event!("Review Terms of Service Clicked");
cx.open_url(&zed_urls::terms_of_service(cx))
}),
)
.child(
Button::new("accept_terms", "Accept")
.full_width()
.style(ButtonStyle::Tinted(TintColor::Accent))
.on_click({
let callback = self.accept_terms_of_service.clone();
move |_, window, cx| {
telemetry::event!("Terms of Service Accepted");
(callback)(window, cx)}
}),
)
.into_any_element()
}
fn render_sign_in_disclaimer(&self, _cx: &mut App) -> AnyElement { fn render_sign_in_disclaimer(&self, _cx: &mut App) -> AnyElement {
let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn); let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn);
let plan_definitions = PlanDefinitions; let plan_definitions = PlanDefinitions;
@ -359,14 +313,10 @@ impl ZedAiOnboarding {
impl RenderOnce for ZedAiOnboarding { impl RenderOnce for ZedAiOnboarding {
fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement { fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement {
if matches!(self.sign_in_status, SignInStatus::SignedIn) { if matches!(self.sign_in_status, SignInStatus::SignedIn) {
if self.has_accepted_terms_of_service { match self.plan {
match self.plan { None | Some(Plan::ZedFree) => self.render_free_plan_state(cx),
None | Some(Plan::ZedFree) => self.render_free_plan_state(cx), Some(Plan::ZedProTrial) => self.render_trial_state(cx),
Some(Plan::ZedProTrial) => self.render_trial_state(cx), Some(Plan::ZedPro) => self.render_pro_plan_state(cx),
Some(Plan::ZedPro) => self.render_pro_plan_state(cx),
}
} else {
self.render_accept_terms_of_service()
} }
} else { } else {
self.render_sign_in_disclaimer(cx) self.render_sign_in_disclaimer(cx)
@ -390,18 +340,15 @@ impl Component for ZedAiOnboarding {
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> { fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
fn onboarding( fn onboarding(
sign_in_status: SignInStatus, sign_in_status: SignInStatus,
has_accepted_terms_of_service: bool,
plan: Option<Plan>, plan: Option<Plan>,
account_too_young: bool, account_too_young: bool,
) -> AnyElement { ) -> AnyElement {
ZedAiOnboarding { ZedAiOnboarding {
sign_in_status, sign_in_status,
has_accepted_terms_of_service,
plan, plan,
account_too_young, account_too_young,
continue_with_zed_ai: Arc::new(|_, _| {}), continue_with_zed_ai: Arc::new(|_, _| {}),
sign_in: Arc::new(|_, _| {}), sign_in: Arc::new(|_, _| {}),
accept_terms_of_service: Arc::new(|_, _| {}),
dismiss_onboarding: None, dismiss_onboarding: None,
} }
.into_any_element() .into_any_element()
@ -415,27 +362,23 @@ impl Component for ZedAiOnboarding {
.children(vec![ .children(vec![
single_example( single_example(
"Not Signed-in", "Not Signed-in",
onboarding(SignInStatus::SignedOut, false, None, false), onboarding(SignInStatus::SignedOut, None, false),
),
single_example(
"Not Accepted ToS",
onboarding(SignInStatus::SignedIn, false, None, false),
), ),
single_example( single_example(
"Young Account", "Young Account",
onboarding(SignInStatus::SignedIn, true, None, true), onboarding(SignInStatus::SignedIn, None, true),
), ),
single_example( single_example(
"Free Plan", "Free Plan",
onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedFree), false), onboarding(SignInStatus::SignedIn, Some(Plan::ZedFree), false),
), ),
single_example( single_example(
"Pro Trial", "Pro Trial",
onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedProTrial), false), onboarding(SignInStatus::SignedIn, Some(Plan::ZedProTrial), false),
), ),
single_example( single_example(
"Pro Plan", "Pro Plan",
onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedPro), false), onboarding(SignInStatus::SignedIn, Some(Plan::ZedPro), false),
), ),
]) ])
.into_any_element(), .into_any_element(),

View file

@ -12,11 +12,11 @@ use crate::{SignInStatus, YoungAccountBanner, plan_definitions::PlanDefinitions}
#[derive(IntoElement, RegisterComponent)] #[derive(IntoElement, RegisterComponent)]
pub struct AiUpsellCard { pub struct AiUpsellCard {
pub sign_in_status: SignInStatus, sign_in_status: SignInStatus,
pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>, sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
pub account_too_young: bool, account_too_young: bool,
pub user_plan: Option<Plan>, user_plan: Option<Plan>,
pub tab_index: Option<isize>, tab_index: Option<isize>,
} }
impl AiUpsellCard { impl AiUpsellCard {
@ -43,6 +43,11 @@ impl AiUpsellCard {
tab_index: None, tab_index: None,
} }
} }
pub fn tab_index(mut self, tab_index: Option<isize>) -> Self {
self.tab_index = tab_index;
self
}
} }
impl RenderOnce for AiUpsellCard { impl RenderOnce for AiUpsellCard {

View file

@ -118,7 +118,7 @@ impl Tool for FetchTool {
} }
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool { fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
false true
} }
fn may_perform_edits(&self) -> bool { fn may_perform_edits(&self) -> bool {

View file

@ -435,8 +435,8 @@ mod test {
assert_eq!( assert_eq!(
matches, matches,
&[ &[
PathBuf::from("root/apple/banana/carrot"), PathBuf::from(path!("root/apple/banana/carrot")),
PathBuf::from("root/apple/bandana/carbonara") PathBuf::from(path!("root/apple/bandana/carbonara"))
] ]
); );
@ -447,8 +447,8 @@ mod test {
assert_eq!( assert_eq!(
matches, matches,
&[ &[
PathBuf::from("root/apple/banana/carrot"), PathBuf::from(path!("root/apple/banana/carrot")),
PathBuf::from("root/apple/bandana/carbonara") PathBuf::from(path!("root/apple/bandana/carbonara"))
] ]
); );
} }

View file

@ -68,7 +68,7 @@ impl Tool for ReadFileTool {
} }
fn icon(&self) -> IconName { fn icon(&self) -> IconName {
IconName::ToolRead IconName::ToolSearch
} }
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> { fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {

View file

@ -162,6 +162,22 @@ impl BufferDiffSnapshot {
} }
} }
fn unchanged(
buffer: &text::BufferSnapshot,
base_text: language::BufferSnapshot,
) -> BufferDiffSnapshot {
debug_assert_eq!(buffer.text(), base_text.text());
BufferDiffSnapshot {
inner: BufferDiffInner {
base_text,
hunks: SumTree::new(buffer),
pending_hunks: SumTree::new(buffer),
base_text_exists: false,
},
secondary_diff: None,
}
}
fn new_with_base_text( fn new_with_base_text(
buffer: text::BufferSnapshot, buffer: text::BufferSnapshot,
base_text: Option<Arc<String>>, base_text: Option<Arc<String>>,
@ -213,7 +229,10 @@ impl BufferDiffSnapshot {
cx: &App, cx: &App,
) -> impl Future<Output = Self> + use<> { ) -> impl Future<Output = Self> + use<> {
let base_text_exists = base_text.is_some(); let base_text_exists = base_text.is_some();
let base_text_pair = base_text.map(|text| (text, base_text_snapshot.as_rope().clone())); let base_text_pair = base_text.map(|text| {
debug_assert_eq!(&*text, &base_text_snapshot.text());
(text, base_text_snapshot.as_rope().clone())
});
cx.background_executor() cx.background_executor()
.spawn_labeled(*CALCULATE_DIFF_TASK, async move { .spawn_labeled(*CALCULATE_DIFF_TASK, async move {
Self { Self {
@ -873,6 +892,18 @@ impl BufferDiff {
} }
} }
pub fn new_unchanged(
buffer: &text::BufferSnapshot,
base_text: language::BufferSnapshot,
) -> Self {
debug_assert_eq!(buffer.text(), base_text.text());
BufferDiff {
buffer_id: buffer.remote_id(),
inner: BufferDiffSnapshot::unchanged(buffer, base_text).inner,
secondary_diff: None,
}
}
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
pub fn new_with_base_text( pub fn new_with_base_text(
base_text: &str, base_text: &str,

View file

@ -66,6 +66,8 @@ pub static IMPERSONATE_LOGIN: LazyLock<Option<String>> = LazyLock::new(|| {
.and_then(|s| if s.is_empty() { None } else { Some(s) }) .and_then(|s| if s.is_empty() { None } else { Some(s) })
}); });
pub static USE_WEB_LOGIN: LazyLock<bool> = LazyLock::new(|| std::env::var("ZED_WEB_LOGIN").is_ok());
pub static ADMIN_API_TOKEN: LazyLock<Option<String>> = LazyLock::new(|| { pub static ADMIN_API_TOKEN: LazyLock<Option<String>> = LazyLock::new(|| {
std::env::var("ZED_ADMIN_API_TOKEN") std::env::var("ZED_ADMIN_API_TOKEN")
.ok() .ok()
@ -1392,11 +1394,13 @@ impl Client {
if let Some((login, token)) = if let Some((login, token)) =
IMPERSONATE_LOGIN.as_ref().zip(ADMIN_API_TOKEN.as_ref()) IMPERSONATE_LOGIN.as_ref().zip(ADMIN_API_TOKEN.as_ref())
{ {
eprintln!("authenticate as admin {login}, {token}"); if !*USE_WEB_LOGIN {
eprintln!("authenticate as admin {login}, {token}");
return this return this
.authenticate_as_admin(http, login.clone(), token.clone()) .authenticate_as_admin(http, login.clone(), token.clone())
.await; .await;
}
} }
// Start an HTTP server to receive the redirect from Zed's sign-in page. // Start an HTTP server to receive the redirect from Zed's sign-in page.

View file

@ -76,7 +76,7 @@ static ZED_CLIENT_CHECKSUM_SEED: LazyLock<Option<Vec<u8>>> = LazyLock::new(|| {
pub static MINIDUMP_ENDPOINT: LazyLock<Option<String>> = LazyLock::new(|| { pub static MINIDUMP_ENDPOINT: LazyLock<Option<String>> = LazyLock::new(|| {
option_env!("ZED_MINIDUMP_ENDPOINT") option_env!("ZED_MINIDUMP_ENDPOINT")
.map(|s| s.to_owned()) .map(str::to_string)
.or_else(|| env::var("ZED_MINIDUMP_ENDPOINT").ok()) .or_else(|| env::var("ZED_MINIDUMP_ENDPOINT").ok())
}); });

View file

@ -1,5 +1,5 @@
use super::{Client, Status, TypedEnvelope, proto}; use super::{Client, Status, TypedEnvelope, proto};
use anyhow::{Context as _, Result, anyhow}; use anyhow::{Context as _, Result};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use cloud_api_client::websocket_protocol::MessageToClient; use cloud_api_client::websocket_protocol::MessageToClient;
use cloud_api_client::{GetAuthenticatedUserResponse, PlanInfo}; use cloud_api_client::{GetAuthenticatedUserResponse, PlanInfo};
@ -46,11 +46,6 @@ impl ProjectId {
} }
} }
#[derive(
Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Serialize, serde::Deserialize,
)]
pub struct DevServerProjectId(pub u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ParticipantIndex(pub u32); pub struct ParticipantIndex(pub u32);
@ -116,7 +111,6 @@ pub struct UserStore {
edit_prediction_usage: Option<EditPredictionUsage>, edit_prediction_usage: Option<EditPredictionUsage>,
plan_info: Option<PlanInfo>, plan_info: Option<PlanInfo>,
current_user: watch::Receiver<Option<Arc<User>>>, current_user: watch::Receiver<Option<Arc<User>>>,
accepted_tos_at: Option<Option<cloud_api_client::Timestamp>>,
contacts: Vec<Arc<Contact>>, contacts: Vec<Arc<Contact>>,
incoming_contact_requests: Vec<Arc<User>>, incoming_contact_requests: Vec<Arc<User>>,
outgoing_contact_requests: Vec<Arc<User>>, outgoing_contact_requests: Vec<Arc<User>>,
@ -194,7 +188,6 @@ impl UserStore {
plan_info: None, plan_info: None,
model_request_usage: None, model_request_usage: None,
edit_prediction_usage: None, edit_prediction_usage: None,
accepted_tos_at: None,
contacts: Default::default(), contacts: Default::default(),
incoming_contact_requests: Default::default(), incoming_contact_requests: Default::default(),
participant_indices: Default::default(), participant_indices: Default::default(),
@ -271,7 +264,6 @@ impl UserStore {
Status::SignedOut => { Status::SignedOut => {
current_user_tx.send(None).await.ok(); current_user_tx.send(None).await.ok();
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
this.accepted_tos_at = None;
cx.emit(Event::PrivateUserInfoUpdated); cx.emit(Event::PrivateUserInfoUpdated);
cx.notify(); cx.notify();
this.clear_contacts() this.clear_contacts()
@ -791,19 +783,6 @@ impl UserStore {
.set_authenticated_user_info(Some(response.user.metrics_id.clone()), staff); .set_authenticated_user_info(Some(response.user.metrics_id.clone()), staff);
} }
let accepted_tos_at = {
#[cfg(debug_assertions)]
if std::env::var("ZED_IGNORE_ACCEPTED_TOS").is_ok() {
None
} else {
response.user.accepted_tos_at
}
#[cfg(not(debug_assertions))]
response.user.accepted_tos_at
};
self.accepted_tos_at = Some(accepted_tos_at);
self.model_request_usage = Some(ModelRequestUsage(RequestUsage { self.model_request_usage = Some(ModelRequestUsage(RequestUsage {
limit: response.plan.usage.model_requests.limit, limit: response.plan.usage.model_requests.limit,
amount: response.plan.usage.model_requests.used as i32, amount: response.plan.usage.model_requests.used as i32,
@ -846,32 +825,6 @@ impl UserStore {
self.current_user.clone() self.current_user.clone()
} }
pub fn has_accepted_terms_of_service(&self) -> bool {
self.accepted_tos_at
.is_some_and(|accepted_tos_at| accepted_tos_at.is_some())
}
pub fn accept_terms_of_service(&self, cx: &Context<Self>) -> Task<Result<()>> {
if self.current_user().is_none() {
return Task::ready(Err(anyhow!("no current user")));
};
let client = self.client.clone();
cx.spawn(async move |this, cx| -> anyhow::Result<()> {
let client = client.upgrade().context("client not found")?;
let response = client
.cloud_client()
.accept_terms_of_service()
.await
.context("error accepting tos")?;
this.update(cx, |this, cx| {
this.accepted_tos_at = Some(response.user.accepted_tos_at);
cx.emit(Event::PrivateUserInfoUpdated);
})?;
Ok(())
})
}
fn load_users( fn load_users(
&self, &self,
request: impl RequestMessage<Response = UsersResponse>, request: impl RequestMessage<Response = UsersResponse>,

View file

@ -115,34 +115,6 @@ impl CloudApiClient {
})) }))
} }
pub async fn accept_terms_of_service(&self) -> Result<AcceptTermsOfServiceResponse> {
let request = self.build_request(
Request::builder().method(Method::POST).uri(
self.http_client
.build_zed_cloud_url("/client/terms_of_service/accept", &[])?
.as_ref(),
),
AsyncBody::default(),
)?;
let mut response = self.http_client.send(request).await?;
if !response.status().is_success() {
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
anyhow::bail!(
"Failed to accept terms of service.\nStatus: {:?}\nBody: {body}",
response.status()
)
}
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
Ok(serde_json::from_str(&body)?)
}
pub async fn create_llm_token( pub async fn create_llm_token(
&self, &self,
system_id: Option<String>, system_id: Option<String>,

View file

@ -970,7 +970,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
// the follow. // the follow.
workspace_b.update_in(cx_b, |workspace, window, cx| { workspace_b.update_in(cx_b, |workspace, window, cx| {
workspace.active_pane().update(cx, |pane, cx| { workspace.active_pane().update(cx, |pane, cx| {
pane.activate_prev_item(true, window, cx); pane.activate_previous_item(&Default::default(), window, cx);
}); });
}); });
executor.run_until_parked(); executor.run_until_parked();
@ -1073,7 +1073,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
// Client A cycles through some tabs. // Client A cycles through some tabs.
workspace_a.update_in(cx_a, |workspace, window, cx| { workspace_a.update_in(cx_a, |workspace, window, cx| {
workspace.active_pane().update(cx, |pane, cx| { workspace.active_pane().update(cx, |pane, cx| {
pane.activate_prev_item(true, window, cx); pane.activate_previous_item(&Default::default(), window, cx);
}); });
}); });
executor.run_until_parked(); executor.run_until_parked();
@ -1117,7 +1117,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
workspace_a.update_in(cx_a, |workspace, window, cx| { workspace_a.update_in(cx_a, |workspace, window, cx| {
workspace.active_pane().update(cx, |pane, cx| { workspace.active_pane().update(cx, |pane, cx| {
pane.activate_prev_item(true, window, cx); pane.activate_previous_item(&Default::default(), window, cx);
}); });
}); });
executor.run_until_parked(); executor.run_until_parked();
@ -1164,7 +1164,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
workspace_a.update_in(cx_a, |workspace, window, cx| { workspace_a.update_in(cx_a, |workspace, window, cx| {
workspace.active_pane().update(cx, |pane, cx| { workspace.active_pane().update(cx, |pane, cx| {
pane.activate_prev_item(true, window, cx); pane.activate_previous_item(&Default::default(), window, cx);
}); });
}); });
executor.run_until_parked(); executor.run_until_parked();

View file

@ -1,6 +1,6 @@
use anyhow::Context as _; use anyhow::Context as _;
use collections::HashMap; use collections::HashMap;
use futures::{Stream, StreamExt as _, lock::Mutex}; use futures::{FutureExt, Stream, StreamExt as _, future::BoxFuture, lock::Mutex};
use gpui::BackgroundExecutor; use gpui::BackgroundExecutor;
use std::{pin::Pin, sync::Arc}; use std::{pin::Pin, sync::Arc};
@ -14,9 +14,12 @@ pub fn create_fake_transport(
executor: BackgroundExecutor, executor: BackgroundExecutor,
) -> FakeTransport { ) -> FakeTransport {
let name = name.into(); let name = name.into();
FakeTransport::new(executor).on_request::<crate::types::requests::Initialize>(move |_params| { FakeTransport::new(executor).on_request::<crate::types::requests::Initialize, _>(
create_initialize_response(name.clone()) move |_params| {
}) let name = name.clone();
async move { create_initialize_response(name.clone()) }
},
)
} }
fn create_initialize_response(server_name: String) -> InitializeResponse { fn create_initialize_response(server_name: String) -> InitializeResponse {
@ -32,8 +35,10 @@ fn create_initialize_response(server_name: String) -> InitializeResponse {
} }
pub struct FakeTransport { pub struct FakeTransport {
request_handlers: request_handlers: HashMap<
HashMap<&'static str, Arc<dyn Fn(serde_json::Value) -> serde_json::Value + Send + Sync>>, &'static str,
Arc<dyn Send + Sync + Fn(serde_json::Value) -> BoxFuture<'static, serde_json::Value>>,
>,
tx: futures::channel::mpsc::UnboundedSender<String>, tx: futures::channel::mpsc::UnboundedSender<String>,
rx: Arc<Mutex<futures::channel::mpsc::UnboundedReceiver<String>>>, rx: Arc<Mutex<futures::channel::mpsc::UnboundedReceiver<String>>>,
executor: BackgroundExecutor, executor: BackgroundExecutor,
@ -50,18 +55,25 @@ impl FakeTransport {
} }
} }
pub fn on_request<T: crate::types::Request>( pub fn on_request<T, Fut>(
mut self, mut self,
handler: impl Fn(T::Params) -> T::Response + Send + Sync + 'static, handler: impl 'static + Send + Sync + Fn(T::Params) -> Fut,
) -> Self { ) -> Self
where
T: crate::types::Request,
Fut: 'static + Send + Future<Output = T::Response>,
{
self.request_handlers.insert( self.request_handlers.insert(
T::METHOD, T::METHOD,
Arc::new(move |value| { Arc::new(move |value| {
let params = value.get("params").expect("Missing parameters").clone(); let params = value
.get("params")
.cloned()
.unwrap_or(serde_json::Value::Null);
let params: T::Params = let params: T::Params =
serde_json::from_value(params).expect("Invalid parameters received"); serde_json::from_value(params).expect("Invalid parameters received");
let response = handler(params); let response = handler(params);
serde_json::to_value(response).unwrap() async move { serde_json::to_value(response.await).unwrap() }.boxed()
}), }),
); );
self self
@ -77,7 +89,7 @@ impl Transport for FakeTransport {
if let Some(method) = msg.get("method") { if let Some(method) = msg.get("method") {
let method = method.as_str().expect("Invalid method received"); let method = method.as_str().expect("Invalid method received");
if let Some(handler) = self.request_handlers.get(method) { if let Some(handler) = self.request_handlers.get(method) {
let payload = handler(msg); let payload = handler(msg).await;
let response = serde_json::json!({ let response = serde_json::json!({
"jsonrpc": "2.0", "jsonrpc": "2.0",
"id": id, "id": id,

View file

@ -301,6 +301,7 @@ mod tests {
init_test(cx, |settings| { init_test(cx, |settings| {
settings.defaults.completions = Some(CompletionSettings { settings.defaults.completions = Some(CompletionSettings {
words: WordsCompletionMode::Disabled, words: WordsCompletionMode::Disabled,
words_min_length: 0,
lsp: true, lsp: true,
lsp_fetch_timeout_ms: 0, lsp_fetch_timeout_ms: 0,
lsp_insert_mode: LspInsertMode::Insert, lsp_insert_mode: LspInsertMode::Insert,
@ -533,6 +534,7 @@ mod tests {
init_test(cx, |settings| { init_test(cx, |settings| {
settings.defaults.completions = Some(CompletionSettings { settings.defaults.completions = Some(CompletionSettings {
words: WordsCompletionMode::Disabled, words: WordsCompletionMode::Disabled,
words_min_length: 0,
lsp: true, lsp: true,
lsp_fetch_timeout_ms: 0, lsp_fetch_timeout_ms: 0,
lsp_insert_mode: LspInsertMode::Insert, lsp_insert_mode: LspInsertMode::Insert,

View file

@ -6,6 +6,7 @@ edition.workspace = true
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
[dependencies] [dependencies]
bincode.workspace = true
crash-handler.workspace = true crash-handler.workspace = true
log.workspace = true log.workspace = true
minidumper.workspace = true minidumper.workspace = true
@ -14,6 +15,7 @@ release_channel.workspace = true
smol.workspace = true smol.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
system_specs.workspace = true
workspace-hack.workspace = true workspace-hack.workspace = true
[target.'cfg(target_os = "macos")'.dependencies] [target.'cfg(target_os = "macos")'.dependencies]

View file

@ -127,6 +127,7 @@ unsafe fn suspend_all_other_threads() {
pub struct CrashServer { pub struct CrashServer {
initialization_params: OnceLock<InitCrashHandler>, initialization_params: OnceLock<InitCrashHandler>,
panic_info: OnceLock<CrashPanic>, panic_info: OnceLock<CrashPanic>,
active_gpu: OnceLock<system_specs::GpuSpecs>,
has_connection: Arc<AtomicBool>, has_connection: Arc<AtomicBool>,
} }
@ -135,6 +136,8 @@ pub struct CrashInfo {
pub init: InitCrashHandler, pub init: InitCrashHandler,
pub panic: Option<CrashPanic>, pub panic: Option<CrashPanic>,
pub minidump_error: Option<String>, pub minidump_error: Option<String>,
pub gpus: Vec<system_specs::GpuInfo>,
pub active_gpu: Option<system_specs::GpuSpecs>,
} }
#[derive(Debug, Deserialize, Serialize, Clone)] #[derive(Debug, Deserialize, Serialize, Clone)]
@ -143,7 +146,6 @@ pub struct InitCrashHandler {
pub zed_version: String, pub zed_version: String,
pub release_channel: String, pub release_channel: String,
pub commit_sha: String, pub commit_sha: String,
// pub gpu: String,
} }
#[derive(Deserialize, Serialize, Debug, Clone)] #[derive(Deserialize, Serialize, Debug, Clone)]
@ -178,6 +180,18 @@ impl minidumper::ServerHandler for CrashServer {
Err(e) => Some(format!("{e:?}")), Err(e) => Some(format!("{e:?}")),
}; };
#[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
let gpus = vec![];
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
let gpus = match system_specs::read_gpu_info_from_sys_class_drm() {
Ok(gpus) => gpus,
Err(err) => {
log::warn!("Failed to collect GPU information for crash report: {err}");
vec![]
}
};
let crash_info = CrashInfo { let crash_info = CrashInfo {
init: self init: self
.initialization_params .initialization_params
@ -186,6 +200,8 @@ impl minidumper::ServerHandler for CrashServer {
.clone(), .clone(),
panic: self.panic_info.get().cloned(), panic: self.panic_info.get().cloned(),
minidump_error, minidump_error,
active_gpu: self.active_gpu.get().cloned(),
gpus,
}; };
let crash_data_path = paths::logs_dir() let crash_data_path = paths::logs_dir()
@ -211,6 +227,13 @@ impl minidumper::ServerHandler for CrashServer {
serde_json::from_slice::<CrashPanic>(&buffer).expect("invalid panic data"); serde_json::from_slice::<CrashPanic>(&buffer).expect("invalid panic data");
self.panic_info.set(panic_data).expect("already panicked"); self.panic_info.set(panic_data).expect("already panicked");
} }
3 => {
let gpu_specs: system_specs::GpuSpecs =
bincode::deserialize(&buffer).expect("gpu specs");
self.active_gpu
.set(gpu_specs)
.expect("already set active gpu");
}
_ => { _ => {
panic!("invalid message kind"); panic!("invalid message kind");
} }
@ -287,6 +310,7 @@ pub fn crash_server(socket: &Path) {
initialization_params: OnceLock::new(), initialization_params: OnceLock::new(),
panic_info: OnceLock::new(), panic_info: OnceLock::new(),
has_connection, has_connection,
active_gpu: OnceLock::new(),
}), }),
&shutdown, &shutdown,
Some(CRASH_HANDLER_PING_TIMEOUT), Some(CRASH_HANDLER_PING_TIMEOUT),

View file

@ -18,7 +18,6 @@ collections.workspace = true
component.workspace = true component.workspace = true
ctor.workspace = true ctor.workspace = true
editor.workspace = true editor.workspace = true
futures.workspace = true
gpui.workspace = true gpui.workspace = true
indoc.workspace = true indoc.workspace = true
language.workspace = true language.workspace = true

View file

@ -13,7 +13,6 @@ use editor::{
DEFAULT_MULTIBUFFER_CONTEXT, Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey, DEFAULT_MULTIBUFFER_CONTEXT, Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey,
display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId}, display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},
}; };
use futures::future::join_all;
use gpui::{ use gpui::{
AnyElement, AnyView, App, AsyncApp, Context, Entity, EventEmitter, FocusHandle, Focusable, AnyElement, AnyView, App, AsyncApp, Context, Entity, EventEmitter, FocusHandle, Focusable,
Global, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Global, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled,
@ -24,7 +23,6 @@ use language::{
}; };
use project::{ use project::{
DiagnosticSummary, Project, ProjectPath, DiagnosticSummary, Project, ProjectPath,
lsp_store::rust_analyzer_ext::{cancel_flycheck, run_flycheck},
project_settings::{DiagnosticSeverity, ProjectSettings}, project_settings::{DiagnosticSeverity, ProjectSettings},
}; };
use settings::Settings; use settings::Settings;
@ -79,17 +77,10 @@ pub(crate) struct ProjectDiagnosticsEditor {
paths_to_update: BTreeSet<ProjectPath>, paths_to_update: BTreeSet<ProjectPath>,
include_warnings: bool, include_warnings: bool,
update_excerpts_task: Option<Task<Result<()>>>, update_excerpts_task: Option<Task<Result<()>>>,
cargo_diagnostics_fetch: CargoDiagnosticsFetchState,
diagnostic_summary_update: Task<()>, diagnostic_summary_update: Task<()>,
_subscription: Subscription, _subscription: Subscription,
} }
struct CargoDiagnosticsFetchState {
fetch_task: Option<Task<()>>,
cancel_task: Option<Task<()>>,
diagnostic_sources: Arc<Vec<ProjectPath>>,
}
impl EventEmitter<EditorEvent> for ProjectDiagnosticsEditor {} impl EventEmitter<EditorEvent> for ProjectDiagnosticsEditor {}
const DIAGNOSTICS_UPDATE_DELAY: Duration = Duration::from_millis(50); const DIAGNOSTICS_UPDATE_DELAY: Duration = Duration::from_millis(50);
@ -260,11 +251,7 @@ impl ProjectDiagnosticsEditor {
) )
}); });
this.diagnostics.clear(); this.diagnostics.clear();
this.update_all_diagnostics(false, window, cx); this.update_all_excerpts(window, cx);
})
.detach();
cx.observe_release(&cx.entity(), |editor, _, cx| {
editor.stop_cargo_diagnostics_fetch(cx);
}) })
.detach(); .detach();
@ -281,15 +268,10 @@ impl ProjectDiagnosticsEditor {
editor, editor,
paths_to_update: Default::default(), paths_to_update: Default::default(),
update_excerpts_task: None, update_excerpts_task: None,
cargo_diagnostics_fetch: CargoDiagnosticsFetchState {
fetch_task: None,
cancel_task: None,
diagnostic_sources: Arc::new(Vec::new()),
},
diagnostic_summary_update: Task::ready(()), diagnostic_summary_update: Task::ready(()),
_subscription: project_event_subscription, _subscription: project_event_subscription,
}; };
this.update_all_diagnostics(true, window, cx); this.update_all_excerpts(window, cx);
this this
} }
@ -373,20 +355,10 @@ impl ProjectDiagnosticsEditor {
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let fetch_cargo_diagnostics = ProjectSettings::get_global(cx) if self.update_excerpts_task.is_some() {
.diagnostics
.fetch_cargo_diagnostics();
if fetch_cargo_diagnostics {
if self.cargo_diagnostics_fetch.fetch_task.is_some() {
self.stop_cargo_diagnostics_fetch(cx);
} else {
self.update_all_diagnostics(false, window, cx);
}
} else if self.update_excerpts_task.is_some() {
self.update_excerpts_task = None; self.update_excerpts_task = None;
} else { } else {
self.update_all_diagnostics(false, window, cx); self.update_all_excerpts(window, cx);
} }
cx.notify(); cx.notify();
} }
@ -404,73 +376,6 @@ impl ProjectDiagnosticsEditor {
} }
} }
fn update_all_diagnostics(
&mut self,
first_launch: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
let cargo_diagnostics_sources = self.cargo_diagnostics_sources(cx);
if cargo_diagnostics_sources.is_empty() {
self.update_all_excerpts(window, cx);
} else if first_launch && !self.summary.is_empty() {
self.update_all_excerpts(window, cx);
} else {
self.fetch_cargo_diagnostics(Arc::new(cargo_diagnostics_sources), cx);
}
}
fn fetch_cargo_diagnostics(
&mut self,
diagnostics_sources: Arc<Vec<ProjectPath>>,
cx: &mut Context<Self>,
) {
let project = self.project.clone();
self.cargo_diagnostics_fetch.cancel_task = None;
self.cargo_diagnostics_fetch.fetch_task = None;
self.cargo_diagnostics_fetch.diagnostic_sources = diagnostics_sources.clone();
if self.cargo_diagnostics_fetch.diagnostic_sources.is_empty() {
return;
}
self.cargo_diagnostics_fetch.fetch_task = Some(cx.spawn(async move |editor, cx| {
let mut fetch_tasks = Vec::new();
for buffer_path in diagnostics_sources.iter().cloned() {
if cx
.update(|cx| {
fetch_tasks.push(run_flycheck(project.clone(), buffer_path, cx));
})
.is_err()
{
break;
}
}
let _ = join_all(fetch_tasks).await;
editor
.update(cx, |editor, _| {
editor.cargo_diagnostics_fetch.fetch_task = None;
})
.ok();
}));
}
fn stop_cargo_diagnostics_fetch(&mut self, cx: &mut App) {
self.cargo_diagnostics_fetch.fetch_task = None;
let mut cancel_gasks = Vec::new();
for buffer_path in std::mem::take(&mut self.cargo_diagnostics_fetch.diagnostic_sources)
.iter()
.cloned()
{
cancel_gasks.push(cancel_flycheck(self.project.clone(), buffer_path, cx));
}
self.cargo_diagnostics_fetch.cancel_task = Some(cx.background_spawn(async move {
let _ = join_all(cancel_gasks).await;
log::info!("Finished fetching cargo diagnostics");
}));
}
/// Enqueue an update of all excerpts. Updates all paths that either /// Enqueue an update of all excerpts. Updates all paths that either
/// currently have diagnostics or are currently present in this view. /// currently have diagnostics or are currently present in this view.
fn update_all_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn update_all_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@ -695,30 +600,6 @@ impl ProjectDiagnosticsEditor {
}) })
}) })
} }
pub fn cargo_diagnostics_sources(&self, cx: &App) -> Vec<ProjectPath> {
let fetch_cargo_diagnostics = ProjectSettings::get_global(cx)
.diagnostics
.fetch_cargo_diagnostics();
if !fetch_cargo_diagnostics {
return Vec::new();
}
self.project
.read(cx)
.worktrees(cx)
.filter_map(|worktree| {
let _cargo_toml_entry = worktree.read(cx).entry_for_path("Cargo.toml")?;
let rust_file_entry = worktree.read(cx).entries(false, 0).find(|entry| {
entry
.path
.extension()
.and_then(|extension| extension.to_str())
== Some("rs")
})?;
self.project.read(cx).path_for_entry(rust_file_entry.id, cx)
})
.collect()
}
} }
impl Focusable for ProjectDiagnosticsEditor { impl Focusable for ProjectDiagnosticsEditor {

View file

@ -1,5 +1,3 @@
use std::sync::Arc;
use crate::{ProjectDiagnosticsEditor, ToggleDiagnosticsRefresh}; use crate::{ProjectDiagnosticsEditor, ToggleDiagnosticsRefresh};
use gpui::{Context, Entity, EventEmitter, ParentElement, Render, WeakEntity, Window}; use gpui::{Context, Entity, EventEmitter, ParentElement, Render, WeakEntity, Window};
use ui::prelude::*; use ui::prelude::*;
@ -15,26 +13,18 @@ impl Render for ToolbarControls {
let mut include_warnings = false; let mut include_warnings = false;
let mut has_stale_excerpts = false; let mut has_stale_excerpts = false;
let mut is_updating = false; let mut is_updating = false;
let cargo_diagnostics_sources = Arc::new(self.diagnostics().map_or(Vec::new(), |editor| {
editor.read(cx).cargo_diagnostics_sources(cx)
}));
let fetch_cargo_diagnostics = !cargo_diagnostics_sources.is_empty();
if let Some(editor) = self.diagnostics() { if let Some(editor) = self.diagnostics() {
let diagnostics = editor.read(cx); let diagnostics = editor.read(cx);
include_warnings = diagnostics.include_warnings; include_warnings = diagnostics.include_warnings;
has_stale_excerpts = !diagnostics.paths_to_update.is_empty(); has_stale_excerpts = !diagnostics.paths_to_update.is_empty();
is_updating = if fetch_cargo_diagnostics { is_updating = diagnostics.update_excerpts_task.is_some()
diagnostics.cargo_diagnostics_fetch.fetch_task.is_some() || diagnostics
} else { .project
diagnostics.update_excerpts_task.is_some() .read(cx)
|| diagnostics .language_servers_running_disk_based_diagnostics(cx)
.project .next()
.read(cx) .is_some();
.language_servers_running_disk_based_diagnostics(cx)
.next()
.is_some()
};
} }
let tooltip = if include_warnings { let tooltip = if include_warnings {
@ -64,7 +54,6 @@ impl Render for ToolbarControls {
.on_click(cx.listener(move |toolbar_controls, _, _, cx| { .on_click(cx.listener(move |toolbar_controls, _, _, cx| {
if let Some(diagnostics) = toolbar_controls.diagnostics() { if let Some(diagnostics) = toolbar_controls.diagnostics() {
diagnostics.update(cx, |diagnostics, cx| { diagnostics.update(cx, |diagnostics, cx| {
diagnostics.stop_cargo_diagnostics_fetch(cx);
diagnostics.update_excerpts_task = None; diagnostics.update_excerpts_task = None;
cx.notify(); cx.notify();
}); });
@ -76,7 +65,7 @@ impl Render for ToolbarControls {
IconButton::new("refresh-diagnostics", IconName::ArrowCircle) IconButton::new("refresh-diagnostics", IconName::ArrowCircle)
.icon_color(Color::Info) .icon_color(Color::Info)
.shape(IconButtonShape::Square) .shape(IconButtonShape::Square)
.disabled(!has_stale_excerpts && !fetch_cargo_diagnostics) .disabled(!has_stale_excerpts)
.tooltip(Tooltip::for_action_title( .tooltip(Tooltip::for_action_title(
"Refresh diagnostics", "Refresh diagnostics",
&ToggleDiagnosticsRefresh, &ToggleDiagnosticsRefresh,
@ -84,17 +73,8 @@ impl Render for ToolbarControls {
.on_click(cx.listener({ .on_click(cx.listener({
move |toolbar_controls, _, window, cx| { move |toolbar_controls, _, window, cx| {
if let Some(diagnostics) = toolbar_controls.diagnostics() { if let Some(diagnostics) = toolbar_controls.diagnostics() {
let cargo_diagnostics_sources =
Arc::clone(&cargo_diagnostics_sources);
diagnostics.update(cx, move |diagnostics, cx| { diagnostics.update(cx, move |diagnostics, cx| {
if fetch_cargo_diagnostics { diagnostics.update_all_excerpts(window, cx);
diagnostics.fetch_cargo_diagnostics(
cargo_diagnostics_sources,
cx,
);
} else {
diagnostics.update_all_excerpts(window, cx);
}
}); });
} }
} }

View file

@ -89,9 +89,6 @@ pub trait EditPredictionProvider: 'static + Sized {
debounce: bool, debounce: bool,
cx: &mut Context<Self>, cx: &mut Context<Self>,
); );
fn needs_terms_acceptance(&self, _cx: &App) -> bool {
false
}
fn cycle( fn cycle(
&mut self, &mut self,
buffer: Entity<Buffer>, buffer: Entity<Buffer>,
@ -124,7 +121,6 @@ pub trait EditPredictionProviderHandle {
fn data_collection_state(&self, cx: &App) -> DataCollectionState; fn data_collection_state(&self, cx: &App) -> DataCollectionState;
fn usage(&self, cx: &App) -> Option<EditPredictionUsage>; fn usage(&self, cx: &App) -> Option<EditPredictionUsage>;
fn toggle_data_collection(&self, cx: &mut App); fn toggle_data_collection(&self, cx: &mut App);
fn needs_terms_acceptance(&self, cx: &App) -> bool;
fn is_refreshing(&self, cx: &App) -> bool; fn is_refreshing(&self, cx: &App) -> bool;
fn refresh( fn refresh(
&self, &self,
@ -196,10 +192,6 @@ where
self.read(cx).is_enabled(buffer, cursor_position, cx) self.read(cx).is_enabled(buffer, cursor_position, cx)
} }
fn needs_terms_acceptance(&self, cx: &App) -> bool {
self.read(cx).needs_terms_acceptance(cx)
}
fn is_refreshing(&self, cx: &App) -> bool { fn is_refreshing(&self, cx: &App) -> bool {
self.read(cx).is_refreshing() self.read(cx).is_refreshing()
} }

View file

@ -242,13 +242,9 @@ impl Render for EditPredictionButton {
IconName::ZedPredictDisabled IconName::ZedPredictDisabled
}; };
if zeta::should_show_upsell_modal(&self.user_store, cx) { if zeta::should_show_upsell_modal() {
let tooltip_meta = if self.user_store.read(cx).current_user().is_some() { let tooltip_meta = if self.user_store.read(cx).current_user().is_some() {
if self.user_store.read(cx).has_accepted_terms_of_service() { "Choose a Plan"
"Choose a Plan"
} else {
"Accept the Terms of Service"
}
} else { } else {
"Sign In" "Sign In"
}; };

View file

@ -253,7 +253,6 @@ pub type RenderDiffHunkControlsFn = Arc<
enum ReportEditorEvent { enum ReportEditorEvent {
Saved { auto_saved: bool }, Saved { auto_saved: bool },
EditorOpened, EditorOpened,
ZetaTosClicked,
Closed, Closed,
} }
@ -262,7 +261,6 @@ impl ReportEditorEvent {
match self { match self {
Self::Saved { .. } => "Editor Saved", Self::Saved { .. } => "Editor Saved",
Self::EditorOpened => "Editor Opened", Self::EditorOpened => "Editor Opened",
Self::ZetaTosClicked => "Edit Prediction Provider ToS Clicked",
Self::Closed => "Editor Closed", Self::Closed => "Editor Closed",
} }
} }
@ -5576,6 +5574,11 @@ impl Editor {
.as_ref() .as_ref()
.is_none_or(|query| !query.chars().any(|c| c.is_digit(10))); .is_none_or(|query| !query.chars().any(|c| c.is_digit(10)));
let omit_word_completions = match &query {
Some(query) => query.chars().count() < completion_settings.words_min_length,
None => completion_settings.words_min_length != 0,
};
let (mut words, provider_responses) = match &provider { let (mut words, provider_responses) = match &provider {
Some(provider) => { Some(provider) => {
let provider_responses = provider.completions( let provider_responses = provider.completions(
@ -5587,9 +5590,11 @@ impl Editor {
cx, cx,
); );
let words = match completion_settings.words { let words = match (omit_word_completions, completion_settings.words) {
WordsCompletionMode::Disabled => Task::ready(BTreeMap::default()), (true, _) | (_, WordsCompletionMode::Disabled) => {
WordsCompletionMode::Enabled | WordsCompletionMode::Fallback => cx Task::ready(BTreeMap::default())
}
(false, WordsCompletionMode::Enabled | WordsCompletionMode::Fallback) => cx
.background_spawn(async move { .background_spawn(async move {
buffer_snapshot.words_in_range(WordsQuery { buffer_snapshot.words_in_range(WordsQuery {
fuzzy_contents: None, fuzzy_contents: None,
@ -5601,16 +5606,20 @@ impl Editor {
(words, provider_responses) (words, provider_responses)
} }
None => ( None => {
cx.background_spawn(async move { let words = if omit_word_completions {
buffer_snapshot.words_in_range(WordsQuery { Task::ready(BTreeMap::default())
fuzzy_contents: None, } else {
range: word_search_range, cx.background_spawn(async move {
skip_digits, buffer_snapshot.words_in_range(WordsQuery {
fuzzy_contents: None,
range: word_search_range,
skip_digits,
})
}) })
}), };
Task::ready(Ok(Vec::new())), (words, Task::ready(Ok(Vec::new())))
), }
}; };
let snippet_sort_order = EditorSettings::get_global(cx).snippet_sort_order; let snippet_sort_order = EditorSettings::get_global(cx).snippet_sort_order;
@ -9169,45 +9178,6 @@ impl Editor {
let provider = self.edit_prediction_provider.as_ref()?; let provider = self.edit_prediction_provider.as_ref()?;
let provider_icon = Self::get_prediction_provider_icon_name(&self.edit_prediction_provider); let provider_icon = Self::get_prediction_provider_icon_name(&self.edit_prediction_provider);
if provider.provider.needs_terms_acceptance(cx) {
return Some(
h_flex()
.min_w(min_width)
.flex_1()
.px_2()
.py_1()
.gap_3()
.elevation_2(cx)
.hover(|style| style.bg(cx.theme().colors().element_hover))
.id("accept-terms")
.cursor_pointer()
.on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default())
.on_click(cx.listener(|this, _event, window, cx| {
cx.stop_propagation();
this.report_editor_event(ReportEditorEvent::ZetaTosClicked, None, cx);
window.dispatch_action(
zed_actions::OpenZedPredictOnboarding.boxed_clone(),
cx,
);
}))
.child(
h_flex()
.flex_1()
.gap_2()
.child(Icon::new(provider_icon))
.child(Label::new("Accept Terms of Service"))
.child(div().w_full())
.child(
Icon::new(IconName::ArrowUpRight)
.color(Color::Muted)
.size(IconSize::Small),
)
.into_any_element(),
)
.into_any(),
);
}
let is_refreshing = provider.provider.is_refreshing(cx); let is_refreshing = provider.provider.is_refreshing(cx);
fn pending_completion_container(icon: IconName) -> Div { fn pending_completion_container(icon: IconName) -> Div {
@ -9809,6 +9779,9 @@ impl Editor {
} }
pub fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context<Self>) { pub fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context<Self>) {
if self.read_only(cx) {
return;
}
self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
self.transact(window, cx, |this, window, cx| { self.transact(window, cx, |this, window, cx| {
this.select_autoclose_pair(window, cx); this.select_autoclose_pair(window, cx);
@ -9902,6 +9875,9 @@ impl Editor {
} }
pub fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context<Self>) { pub fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context<Self>) {
if self.read_only(cx) {
return;
}
self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
self.transact(window, cx, |this, window, cx| { self.transact(window, cx, |this, window, cx| {
this.change_selections(Default::default(), window, cx, |s| { this.change_selections(Default::default(), window, cx, |s| {

View file

@ -57,7 +57,9 @@ use util::{
use workspace::{ use workspace::{
CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry, CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
OpenOptions, ViewId, OpenOptions, ViewId,
invalid_buffer_view::InvalidBufferView,
item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions}, item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
register_project_item,
}; };
#[gpui::test] #[gpui::test]
@ -12237,6 +12239,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
settings.defaults.completions = Some(CompletionSettings { settings.defaults.completions = Some(CompletionSettings {
lsp_insert_mode, lsp_insert_mode,
words: WordsCompletionMode::Disabled, words: WordsCompletionMode::Disabled,
words_min_length: 0,
lsp: true, lsp: true,
lsp_fetch_timeout_ms: 0, lsp_fetch_timeout_ms: 0,
}); });
@ -12295,6 +12298,7 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext)
update_test_language_settings(&mut cx, |settings| { update_test_language_settings(&mut cx, |settings| {
settings.defaults.completions = Some(CompletionSettings { settings.defaults.completions = Some(CompletionSettings {
words: WordsCompletionMode::Disabled, words: WordsCompletionMode::Disabled,
words_min_length: 0,
// set the opposite here to ensure that the action is overriding the default behavior // set the opposite here to ensure that the action is overriding the default behavior
lsp_insert_mode: LspInsertMode::Insert, lsp_insert_mode: LspInsertMode::Insert,
lsp: true, lsp: true,
@ -12331,6 +12335,7 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext)
update_test_language_settings(&mut cx, |settings| { update_test_language_settings(&mut cx, |settings| {
settings.defaults.completions = Some(CompletionSettings { settings.defaults.completions = Some(CompletionSettings {
words: WordsCompletionMode::Disabled, words: WordsCompletionMode::Disabled,
words_min_length: 0,
// set the opposite here to ensure that the action is overriding the default behavior // set the opposite here to ensure that the action is overriding the default behavior
lsp_insert_mode: LspInsertMode::Replace, lsp_insert_mode: LspInsertMode::Replace,
lsp: true, lsp: true,
@ -13072,6 +13077,7 @@ async fn test_word_completion(cx: &mut TestAppContext) {
init_test(cx, |language_settings| { init_test(cx, |language_settings| {
language_settings.defaults.completions = Some(CompletionSettings { language_settings.defaults.completions = Some(CompletionSettings {
words: WordsCompletionMode::Fallback, words: WordsCompletionMode::Fallback,
words_min_length: 0,
lsp: true, lsp: true,
lsp_fetch_timeout_ms: 10, lsp_fetch_timeout_ms: 10,
lsp_insert_mode: LspInsertMode::Insert, lsp_insert_mode: LspInsertMode::Insert,
@ -13168,6 +13174,7 @@ async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext
init_test(cx, |language_settings| { init_test(cx, |language_settings| {
language_settings.defaults.completions = Some(CompletionSettings { language_settings.defaults.completions = Some(CompletionSettings {
words: WordsCompletionMode::Enabled, words: WordsCompletionMode::Enabled,
words_min_length: 0,
lsp: true, lsp: true,
lsp_fetch_timeout_ms: 0, lsp_fetch_timeout_ms: 0,
lsp_insert_mode: LspInsertMode::Insert, lsp_insert_mode: LspInsertMode::Insert,
@ -13231,6 +13238,7 @@ async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
init_test(cx, |language_settings| { init_test(cx, |language_settings| {
language_settings.defaults.completions = Some(CompletionSettings { language_settings.defaults.completions = Some(CompletionSettings {
words: WordsCompletionMode::Disabled, words: WordsCompletionMode::Disabled,
words_min_length: 0,
lsp: true, lsp: true,
lsp_fetch_timeout_ms: 0, lsp_fetch_timeout_ms: 0,
lsp_insert_mode: LspInsertMode::Insert, lsp_insert_mode: LspInsertMode::Insert,
@ -13304,6 +13312,7 @@ async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
init_test(cx, |language_settings| { init_test(cx, |language_settings| {
language_settings.defaults.completions = Some(CompletionSettings { language_settings.defaults.completions = Some(CompletionSettings {
words: WordsCompletionMode::Fallback, words: WordsCompletionMode::Fallback,
words_min_length: 0,
lsp: false, lsp: false,
lsp_fetch_timeout_ms: 0, lsp_fetch_timeout_ms: 0,
lsp_insert_mode: LspInsertMode::Insert, lsp_insert_mode: LspInsertMode::Insert,
@ -13361,6 +13370,56 @@ async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
}); });
} }
#[gpui::test]
async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
init_test(cx, |language_settings| {
language_settings.defaults.completions = Some(CompletionSettings {
words: WordsCompletionMode::Enabled,
words_min_length: 3,
lsp: true,
lsp_fetch_timeout_ms: 0,
lsp_insert_mode: LspInsertMode::Insert,
});
});
let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
cx.set_state(indoc! {"ˇ
wow
wowen
wowser
"});
cx.simulate_keystroke("w");
cx.executor().run_until_parked();
cx.update_editor(|editor, _, _| {
if editor.context_menu.borrow_mut().is_some() {
panic!(
"expected completion menu to be hidden, as words completion threshold is not met"
);
}
});
cx.simulate_keystroke("o");
cx.executor().run_until_parked();
cx.update_editor(|editor, _, _| {
if editor.context_menu.borrow_mut().is_some() {
panic!(
"expected completion menu to be hidden, as words completion threshold is not met still"
);
}
});
cx.simulate_keystroke("w");
cx.executor().run_until_parked();
cx.update_editor(|editor, _, _| {
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
{
assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
} else {
panic!("expected completion menu to be open after the word completions threshold is met");
}
});
}
fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> { fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
let position = || lsp::Position { let position = || lsp::Position {
line: params.text_document_position.position.line, line: params.text_document_position.position.line,
@ -22656,7 +22715,7 @@ async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
.await .await
.unwrap(); .unwrap();
pane.update_in(cx, |pane, window, cx| { pane.update_in(cx, |pane, window, cx| {
pane.navigate_backward(window, cx); pane.navigate_backward(&Default::default(), window, cx);
}); });
cx.run_until_parked(); cx.run_until_parked();
pane.update(cx, |pane, cx| { pane.update(cx, |pane, cx| {
@ -24243,7 +24302,7 @@ async fn test_document_colors(cx: &mut TestAppContext) {
workspace workspace
.update(cx, |workspace, window, cx| { .update(cx, |workspace, window, cx| {
workspace.active_pane().update(cx, |pane, cx| { workspace.active_pane().update(cx, |pane, cx| {
pane.navigate_backward(window, cx); pane.navigate_backward(&Default::default(), window, cx);
}) })
}) })
.unwrap(); .unwrap();
@ -24291,6 +24350,41 @@ async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
}); });
} }
#[gpui::test]
async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
init_test(cx, |_| {});
cx.update(|cx| {
register_project_item::<Editor>(cx);
});
let fs = FakeFs::new(cx.executor());
fs.insert_tree("/root1", json!({})).await;
fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
.await;
let project = Project::test(fs, ["/root1".as_ref()], cx).await;
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
let worktree_id = project.update(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id()
});
let handle = workspace
.update_in(cx, |workspace, window, cx| {
let project_path = (worktree_id, "one.pdf");
workspace.open_path(project_path, None, true, window, cx)
})
.await
.unwrap();
assert_eq!(
handle.to_any().entity_type(),
TypeId::of::<InvalidBufferView>()
);
}
#[track_caller] #[track_caller]
fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> { fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
editor editor

View file

@ -74,6 +74,7 @@ use std::{
fmt::{self, Write}, fmt::{self, Write},
iter, mem, iter, mem,
ops::{Deref, Range}, ops::{Deref, Range},
path::{self, Path},
rc::Rc, rc::Rc,
sync::Arc, sync::Arc,
time::{Duration, Instant}, time::{Duration, Instant},
@ -89,8 +90,8 @@ use unicode_segmentation::UnicodeSegmentation;
use util::post_inc; use util::post_inc;
use util::{RangeExt, ResultExt, debug_panic}; use util::{RangeExt, ResultExt, debug_panic};
use workspace::{ use workspace::{
CollaboratorId, OpenInTerminal, OpenTerminal, RevealInProjectPanel, Workspace, item::Item, CollaboratorId, ItemSettings, OpenInTerminal, OpenTerminal, RevealInProjectPanel, Workspace,
notifications::NotifyTaskExt, item::Item, notifications::NotifyTaskExt,
}; };
/// Determines what kinds of highlights should be applied to a lines background. /// Determines what kinds of highlights should be applied to a lines background.
@ -3602,171 +3603,187 @@ impl EditorElement {
let focus_handle = editor.focus_handle(cx); let focus_handle = editor.focus_handle(cx);
let colors = cx.theme().colors(); let colors = cx.theme().colors();
let header = let header = div()
div() .p_1()
.p_1() .w_full()
.w_full() .h(FILE_HEADER_HEIGHT as f32 * window.line_height())
.h(FILE_HEADER_HEIGHT as f32 * window.line_height()) .child(
.child( h_flex()
h_flex() .size_full()
.size_full() .gap_2()
.gap_2() .flex_basis(Length::Definite(DefiniteLength::Fraction(0.667)))
.flex_basis(Length::Definite(DefiniteLength::Fraction(0.667))) .pl_0p5()
.pl_0p5() .pr_5()
.pr_5() .rounded_sm()
.rounded_sm() .when(is_sticky, |el| el.shadow_md())
.when(is_sticky, |el| el.shadow_md()) .border_1()
.border_1() .map(|div| {
.map(|div| { let border_color = if is_selected
let border_color = if is_selected && is_folded
&& is_folded && focus_handle.contains_focused(window, cx)
&& focus_handle.contains_focused(window, cx) {
{ colors.border_focused
colors.border_focused } else {
} else { colors.border
colors.border };
}; div.border_color(border_color)
div.border_color(border_color) })
}) .bg(colors.editor_subheader_background)
.bg(colors.editor_subheader_background) .hover(|style| style.bg(colors.element_hover))
.hover(|style| style.bg(colors.element_hover)) .map(|header| {
.map(|header| { let editor = self.editor.clone();
let editor = self.editor.clone(); let buffer_id = for_excerpt.buffer_id;
let buffer_id = for_excerpt.buffer_id; let toggle_chevron_icon =
let toggle_chevron_icon = FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path);
FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path); header.child(
header.child( div()
div() .hover(|style| style.bg(colors.element_selected))
.hover(|style| style.bg(colors.element_selected)) .rounded_xs()
.rounded_xs() .child(
.child( ButtonLike::new("toggle-buffer-fold")
ButtonLike::new("toggle-buffer-fold") .style(ui::ButtonStyle::Transparent)
.style(ui::ButtonStyle::Transparent) .height(px(28.).into())
.height(px(28.).into()) .width(px(28.))
.width(px(28.)) .children(toggle_chevron_icon)
.children(toggle_chevron_icon) .tooltip({
.tooltip({ let focus_handle = focus_handle.clone();
let focus_handle = focus_handle.clone(); move |window, cx| {
move |window, cx| { Tooltip::with_meta_in(
Tooltip::with_meta_in( "Toggle Excerpt Fold",
"Toggle Excerpt Fold", Some(&ToggleFold),
Some(&ToggleFold), "Alt+click to toggle all",
"Alt+click to toggle all", &focus_handle,
&focus_handle, window,
cx,
)
}
})
.on_click(move |event, window, cx| {
if event.modifiers().alt {
// Alt+click toggles all buffers
editor.update(cx, |editor, cx| {
editor.toggle_fold_all(
&ToggleFoldAll,
window, window,
cx, cx,
) );
} });
}) } else {
.on_click(move |event, window, cx| { // Regular click toggles single buffer
if event.modifiers().alt { if is_folded {
// Alt+click toggles all buffers
editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
editor.toggle_fold_all( editor.unfold_buffer(buffer_id, cx);
&ToggleFoldAll,
window,
cx,
);
}); });
} else { } else {
// Regular click toggles single buffer editor.update(cx, |editor, cx| {
if is_folded { editor.fold_buffer(buffer_id, cx);
editor.update(cx, |editor, cx| { });
editor.unfold_buffer(buffer_id, cx);
});
} else {
editor.update(cx, |editor, cx| {
editor.fold_buffer(buffer_id, cx);
});
}
} }
}), }
), }),
) ),
})
.children(
editor
.addons
.values()
.filter_map(|addon| {
addon.render_buffer_header_controls(for_excerpt, window, cx)
})
.take(1),
) )
.children(indicator) })
.child( .children(
h_flex() editor
.cursor_pointer() .addons
.id("path header block") .values()
.size_full() .filter_map(|addon| {
.justify_between() addon.render_buffer_header_controls(for_excerpt, window, cx)
.overflow_hidden() })
.child( .take(1),
h_flex() )
.gap_2() .child(
.child( h_flex()
Label::new( .size(Pixels(12.0))
filename .justify_center()
.map(SharedString::from) .children(indicator),
.unwrap_or_else(|| "untitled".into()), )
) .child(
.single_line() h_flex()
.when_some(file_status, |el, status| { .cursor_pointer()
el.color(if status.is_conflicted() { .id("path header block")
Color::Conflict .size_full()
} else if status.is_modified() { .justify_between()
Color::Modified .overflow_hidden()
} else if status.is_deleted() { .child(
Color::Disabled h_flex()
} else { .gap_2()
Color::Created .map(|path_header| {
}) let filename = filename
.when(status.is_deleted(), |el| el.strikethrough()) .map(SharedString::from)
}), .unwrap_or_else(|| "untitled".into());
)
.when_some(parent_path, |then, path| { path_header
then.child(div().child(path).text_color( .when(ItemSettings::get_global(cx).file_icons, |el| {
if file_status.is_some_and(FileStatus::is_deleted) { let path = path::Path::new(filename.as_str());
colors.text_disabled let icon = FileIcons::get_icon(path, cx)
} else { .unwrap_or_default();
colors.text_muted let icon =
Icon::from_path(icon).color(Color::Muted);
el.child(icon)
})
.child(Label::new(filename).single_line().when_some(
file_status,
|el, status| {
el.color(if status.is_conflicted() {
Color::Conflict
} else if status.is_modified() {
Color::Modified
} else if status.is_deleted() {
Color::Disabled
} else {
Color::Created
})
.when(status.is_deleted(), |el| {
el.strikethrough()
})
}, },
)) ))
}), })
) .when_some(parent_path, |then, path| {
.when( then.child(div().child(path).text_color(
can_open_excerpts && is_selected && relative_path.is_some(), if file_status.is_some_and(FileStatus::is_deleted) {
|el| { colors.text_disabled
el.child( } else {
h_flex() colors.text_muted
.id("jump-to-file-button") },
.gap_2p5() ))
.child(Label::new("Jump To File")) }),
.children( )
KeyBinding::for_action_in( .when(
&OpenExcerpts, can_open_excerpts && is_selected && relative_path.is_some(),
&focus_handle, |el| {
window, el.child(
cx, h_flex()
) .id("jump-to-file-button")
.map(|binding| binding.into_any_element()), .gap_2p5()
), .child(Label::new("Jump To File"))
) .children(
}, KeyBinding::for_action_in(
) &OpenExcerpts,
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) &focus_handle,
.on_click(window.listener_for(&self.editor, { window,
move |editor, e: &ClickEvent, window, cx| { cx,
editor.open_excerpts_common( )
Some(jump_data.clone()), .map(|binding| binding.into_any_element()),
e.modifiers().secondary(), ),
window, )
cx, },
); )
} .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
})), .on_click(window.listener_for(&self.editor, {
), move |editor, e: &ClickEvent, window, cx| {
); editor.open_excerpts_common(
Some(jump_data.clone()),
e.modifiers().secondary(),
window,
cx,
);
}
})),
),
);
let file = for_excerpt.buffer.file().cloned(); let file = for_excerpt.buffer.file().cloned();
let editor = self.editor.clone(); let editor = self.editor.clone();
@ -3782,25 +3799,31 @@ impl EditorElement {
&& let Some(worktree) = && let Some(worktree) =
project.read(cx).worktree_for_id(file.worktree_id(cx), cx) project.read(cx).worktree_for_id(file.worktree_id(cx), cx)
{ {
let worktree = worktree.read(cx);
let relative_path = file.path(); let relative_path = file.path();
let entry_for_path = worktree.read(cx).entry_for_path(relative_path); let entry_for_path = worktree.entry_for_path(relative_path);
let abs_path = entry_for_path.and_then(|e| e.canonical_path.as_deref()); let abs_path = entry_for_path.map(|e| {
let has_relative_path = e.canonical_path.as_deref().map_or_else(
worktree.read(cx).root_entry().is_some_and(Entry::is_dir); || worktree.abs_path().join(relative_path),
Path::to_path_buf,
)
});
let has_relative_path = worktree.root_entry().is_some_and(Entry::is_dir);
let parent_abs_path = let parent_abs_path = abs_path
abs_path.and_then(|abs_path| Some(abs_path.parent()?.to_path_buf())); .as_ref()
.and_then(|abs_path| Some(abs_path.parent()?.to_path_buf()));
let relative_path = has_relative_path let relative_path = has_relative_path
.then_some(relative_path) .then_some(relative_path)
.map(ToOwned::to_owned); .map(ToOwned::to_owned);
let visible_in_project_panel = let visible_in_project_panel =
relative_path.is_some() && worktree.read(cx).is_visible(); relative_path.is_some() && worktree.is_visible();
let reveal_in_project_panel = entry_for_path let reveal_in_project_panel = entry_for_path
.filter(|_| visible_in_project_panel) .filter(|_| visible_in_project_panel)
.map(|entry| entry.id); .map(|entry| entry.id);
menu = menu menu = menu
.when_some(abs_path.map(ToOwned::to_owned), |menu, abs_path| { .when_some(abs_path, |menu, abs_path| {
menu.entry( menu.entry(
"Copy Path", "Copy Path",
Some(Box::new(zed_actions::workspace::CopyPath)), Some(Box::new(zed_actions::workspace::CopyPath)),

View file

@ -42,6 +42,7 @@ use ui::{IconDecorationKind, prelude::*};
use util::{ResultExt, TryFutureExt, paths::PathExt}; use util::{ResultExt, TryFutureExt, paths::PathExt};
use workspace::{ use workspace::{
CollaboratorId, ItemId, ItemNavHistory, ToolbarItemLocation, ViewId, Workspace, WorkspaceId, CollaboratorId, ItemId, ItemNavHistory, ToolbarItemLocation, ViewId, Workspace, WorkspaceId,
invalid_buffer_view::InvalidBufferView,
item::{FollowableItem, Item, ItemEvent, ProjectItem, SaveOptions}, item::{FollowableItem, Item, ItemEvent, ProjectItem, SaveOptions},
searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle}, searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
}; };
@ -1401,6 +1402,16 @@ impl ProjectItem for Editor {
editor editor
} }
fn for_broken_project_item(
abs_path: PathBuf,
is_local: bool,
e: &anyhow::Error,
window: &mut Window,
cx: &mut App,
) -> Option<InvalidBufferView> {
Some(InvalidBufferView::new(abs_path, is_local, e, window, cx))
}
} }
fn clip_ranges<'a>( fn clip_ranges<'a>(

View file

@ -26,6 +26,17 @@ fn is_rust_language(language: &Language) -> bool {
} }
pub fn apply_related_actions(editor: &Entity<Editor>, window: &mut Window, cx: &mut App) { pub fn apply_related_actions(editor: &Entity<Editor>, window: &mut Window, cx: &mut App) {
if editor.read(cx).project().is_some_and(|project| {
project
.read(cx)
.language_server_statuses(cx)
.any(|(_, status)| status.name == RUST_ANALYZER_NAME)
}) {
register_action(editor, window, cancel_flycheck_action);
register_action(editor, window, run_flycheck_action);
register_action(editor, window, clear_flycheck_action);
}
if editor if editor
.read(cx) .read(cx)
.buffer() .buffer()
@ -38,9 +49,6 @@ pub fn apply_related_actions(editor: &Entity<Editor>, window: &mut Window, cx: &
register_action(editor, window, go_to_parent_module); register_action(editor, window, go_to_parent_module);
register_action(editor, window, expand_macro_recursively); register_action(editor, window, expand_macro_recursively);
register_action(editor, window, open_docs); register_action(editor, window, open_docs);
register_action(editor, window, cancel_flycheck_action);
register_action(editor, window, run_flycheck_action);
register_action(editor, window, clear_flycheck_action);
} }
} }
@ -309,7 +317,7 @@ fn cancel_flycheck_action(
let Some(project) = &editor.project else { let Some(project) = &editor.project else {
return; return;
}; };
let Some(buffer_id) = editor let buffer_id = editor
.selections .selections
.disjoint_anchors() .disjoint_anchors()
.iter() .iter()
@ -321,10 +329,7 @@ fn cancel_flycheck_action(
.read(cx) .read(cx)
.entry_id(cx)?; .entry_id(cx)?;
project.path_for_entry(entry_id, cx) project.path_for_entry(entry_id, cx)
}) });
else {
return;
};
cancel_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx); cancel_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx);
} }
@ -337,7 +342,7 @@ fn run_flycheck_action(
let Some(project) = &editor.project else { let Some(project) = &editor.project else {
return; return;
}; };
let Some(buffer_id) = editor let buffer_id = editor
.selections .selections
.disjoint_anchors() .disjoint_anchors()
.iter() .iter()
@ -349,10 +354,7 @@ fn run_flycheck_action(
.read(cx) .read(cx)
.entry_id(cx)?; .entry_id(cx)?;
project.path_for_entry(entry_id, cx) project.path_for_entry(entry_id, cx)
}) });
else {
return;
};
run_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx); run_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx);
} }
@ -365,7 +367,7 @@ fn clear_flycheck_action(
let Some(project) = &editor.project else { let Some(project) = &editor.project else {
return; return;
}; };
let Some(buffer_id) = editor let buffer_id = editor
.selections .selections
.disjoint_anchors() .disjoint_anchors()
.iter() .iter()
@ -377,9 +379,6 @@ fn clear_flycheck_action(
.read(cx) .read(cx)
.entry_id(cx)?; .entry_id(cx)?;
project.path_for_entry(entry_id, cx) project.path_for_entry(entry_id, cx)
}) });
else {
return;
};
clear_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx); clear_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx);
} }

View file

@ -15,13 +15,9 @@ path = "src/feedback.rs"
test-support = [] test-support = []
[dependencies] [dependencies]
client.workspace = true
gpui.workspace = true gpui.workspace = true
human_bytes = "0.4.1"
menu.workspace = true menu.workspace = true
release_channel.workspace = true system_specs.workspace = true
serde.workspace = true
sysinfo.workspace = true
ui.workspace = true ui.workspace = true
urlencoding.workspace = true urlencoding.workspace = true
util.workspace = true util.workspace = true

View file

@ -1,18 +1,14 @@
use gpui::{App, ClipboardItem, PromptLevel, actions}; use gpui::{App, ClipboardItem, PromptLevel, actions};
use system_specs::SystemSpecs; use system_specs::{CopySystemSpecsIntoClipboard, SystemSpecs};
use util::ResultExt; use util::ResultExt;
use workspace::Workspace; use workspace::Workspace;
use zed_actions::feedback::FileBugReport; use zed_actions::feedback::FileBugReport;
pub mod feedback_modal; pub mod feedback_modal;
pub mod system_specs;
actions!( actions!(
zed, zed,
[ [
/// Copies system specifications to the clipboard for bug reports.
CopySystemSpecsIntoClipboard,
/// Opens email client to send feedback to Zed support. /// Opens email client to send feedback to Zed support.
EmailZed, EmailZed,
/// Opens the Zed repository on GitHub. /// Opens the Zed repository on GitHub.

View file

@ -1401,13 +1401,16 @@ impl PickerDelegate for FileFinderDelegate {
#[cfg(windows)] #[cfg(windows)]
let raw_query = raw_query.trim().to_owned().replace("/", "\\"); let raw_query = raw_query.trim().to_owned().replace("/", "\\");
#[cfg(not(windows))] #[cfg(not(windows))]
let raw_query = raw_query.trim().to_owned(); let raw_query = raw_query.trim();
let file_query_end = if path_position.path.to_str().unwrap_or(&raw_query) == raw_query { let raw_query = raw_query.trim_end_matches(':').to_owned();
let path = path_position.path.to_str();
let path_trimmed = path.unwrap_or(&raw_query).trim_end_matches(':');
let file_query_end = if path_trimmed == raw_query {
None None
} else { } else {
// Safe to unwrap as we won't get here when the unwrap in if fails // Safe to unwrap as we won't get here when the unwrap in if fails
Some(path_position.path.to_str().unwrap().len()) Some(path.unwrap().len())
}; };
let query = FileSearchQuery { let query = FileSearchQuery {

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