Compare commits

..

1 Commits

Author SHA1 Message Date
Antonio Scandurra
9d3af4d2d2 WIP 2025-05-06 20:25:59 +02:00
372 changed files with 40728 additions and 14084 deletions

View File

@@ -162,23 +162,13 @@ jobs:
working-directory: ./docs
run: |
pnpm dlx prettier@${PRETTIER_VERSION} . --check || {
echo "To fix, run from the root of the Zed repo:"
echo "To fix, run from the root of the zed repo:"
echo " cd docs && pnpm dlx prettier@${PRETTIER_VERSION} . --write && cd .."
false
}
env:
PRETTIER_VERSION: 3.5.0
- name: Prettier Check on default.json
run: |
pnpm dlx prettier@${PRETTIER_VERSION} assets/settings/default.json --check || {
echo "To fix, run from the root of the Zed repo:"
echo " pnpm dlx prettier@${PRETTIER_VERSION} assets/settings/default.json --write"
false
}
env:
PRETTIER_VERSION: 3.5.0
# To support writing comments that they will certainly be revisited.
- name: Check for todo! and FIXME comments
run: script/check-todos

View File

@@ -1,3 +0,0 @@
{
"printWidth": 120
}

347
Cargo.lock generated
View File

@@ -56,13 +56,13 @@ dependencies = [
"assistant_context_editor",
"assistant_settings",
"assistant_slash_command",
"assistant_slash_commands",
"assistant_tool",
"async-watch",
"buffer_diff",
"chrono",
"client",
"collections",
"command_palette_hooks",
"component",
"context_server",
"convert_case 0.8.0",
@@ -79,7 +79,6 @@ dependencies = [
"heed",
"html_to_markdown",
"http_client",
"indexed_docs",
"indoc",
"itertools 0.14.0",
"jsonschema",
@@ -472,6 +471,68 @@ dependencies = [
"workspace-hack",
]
[[package]]
name = "assistant"
version = "0.1.0"
dependencies = [
"anyhow",
"assistant_context_editor",
"assistant_settings",
"assistant_slash_command",
"assistant_slash_commands",
"assistant_tool",
"async-watch",
"client",
"collections",
"command_palette_hooks",
"ctor",
"db",
"editor",
"env_logger 0.11.8",
"feature_flags",
"fs",
"futures 0.3.31",
"gpui",
"indexed_docs",
"indoc",
"language",
"language_model",
"language_model_selector",
"languages",
"log",
"lsp",
"menu",
"multi_buffer",
"parking_lot",
"pretty_assertions",
"project",
"prompt_store",
"proto",
"rand 0.8.5",
"rope",
"rules_library",
"schemars",
"search",
"serde",
"serde_json_lenient",
"settings",
"smol",
"streaming_diff",
"telemetry",
"telemetry_events",
"terminal",
"terminal_view",
"text",
"theme",
"tree-sitter-md",
"ui",
"unindent",
"util",
"workspace",
"workspace-hack",
"zed_actions",
]
[[package]]
name = "assistant_context_editor"
version = "0.1.0"
@@ -486,6 +547,7 @@ dependencies = [
"collections",
"context_server",
"editor",
"feature_flags",
"fs",
"futures 0.3.31",
"fuzzy",
@@ -513,6 +575,7 @@ dependencies = [
"settings",
"smallvec",
"smol",
"strum 0.27.1",
"telemetry_events",
"text",
"theme",
@@ -532,8 +595,8 @@ version = "0.1.0"
dependencies = [
"anthropic",
"anyhow",
"collections",
"deepseek",
"feature_flags",
"fs",
"gpui",
"indexmap",
@@ -549,7 +612,6 @@ dependencies = [
"serde_json_lenient",
"settings",
"workspace-hack",
"zed_llm_client",
]
[[package]]
@@ -676,7 +738,6 @@ dependencies = [
"language_models",
"linkme",
"log",
"markdown",
"open",
"paths",
"portable-pty",
@@ -1270,9 +1331,9 @@ dependencies = [
[[package]]
name = "aws-lc-rs"
version = "1.13.1"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fcc8f365936c834db5514fc45aee5b1202d677e6b40e48468aaaa8183ca8c7"
checksum = "19b756939cb2f8dc900aa6dcd505e6e2428e9cae7ff7b028c49e3946efa70878"
dependencies = [
"aws-lc-sys",
"zeroize",
@@ -1280,9 +1341,9 @@ dependencies = [
[[package]]
name = "aws-lc-sys"
version = "0.29.0"
version = "0.28.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61b1d86e7705efe1be1b569bab41d4fa1e14e220b60a160f78de2db687add079"
checksum = "bfa9b6986f250236c27e5a204062434a773a13243d2ffc2955f37bdba4c5c6a1"
dependencies = [
"bindgen 0.69.5",
"cc",
@@ -2029,7 +2090,7 @@ dependencies = [
[[package]]
name = "blade-graphics"
version = "0.6.0"
source = "git+https://github.com/kvark/blade?rev=416375211bb0b5826b3584dccdb6a43369e499ad#416375211bb0b5826b3584dccdb6a43369e499ad"
source = "git+https://github.com/kvark/blade?rev=b16f5c7bd873c7126f48c82c39e7ae64602ae74f#b16f5c7bd873c7126f48c82c39e7ae64602ae74f"
dependencies = [
"ash",
"ash-window",
@@ -2048,7 +2109,6 @@ dependencies = [
"naga",
"objc2",
"objc2-app-kit",
"objc2-core-foundation",
"objc2-foundation",
"objc2-metal",
"objc2-quartz-core",
@@ -2062,7 +2122,7 @@ dependencies = [
[[package]]
name = "blade-macros"
version = "0.3.0"
source = "git+https://github.com/kvark/blade?rev=416375211bb0b5826b3584dccdb6a43369e499ad#416375211bb0b5826b3584dccdb6a43369e499ad"
source = "git+https://github.com/kvark/blade?rev=b16f5c7bd873c7126f48c82c39e7ae64602ae74f#b16f5c7bd873c7126f48c82c39e7ae64602ae74f"
dependencies = [
"proc-macro2",
"quote",
@@ -2072,7 +2132,7 @@ dependencies = [
[[package]]
name = "blade-util"
version = "0.2.0"
source = "git+https://github.com/kvark/blade?rev=416375211bb0b5826b3584dccdb6a43369e499ad#416375211bb0b5826b3584dccdb6a43369e499ad"
source = "git+https://github.com/kvark/blade?rev=b16f5c7bd873c7126f48c82c39e7ae64602ae74f#b16f5c7bd873c7126f48c82c39e7ae64602ae74f"
dependencies = [
"blade-graphics",
"bytemuck",
@@ -2119,9 +2179,9 @@ dependencies = [
[[package]]
name = "block2"
version = "0.6.1"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "340d2f0bdb2a43c1d3cd40513185b2bd7def0aa1052f956455114bc98f82dcf2"
checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f"
dependencies = [
"objc2",
]
@@ -2943,8 +3003,8 @@ name = "collab"
version = "0.44.0"
dependencies = [
"anyhow",
"assistant",
"assistant_context_editor",
"assistant_settings",
"assistant_slash_command",
"assistant_tool",
"async-stripe",
@@ -3068,7 +3128,6 @@ dependencies = [
"gpui",
"http_client",
"language",
"log",
"menu",
"notifications",
"picker",
@@ -3184,6 +3243,32 @@ dependencies = [
"workspace-hack",
]
[[package]]
name = "component_preview"
version = "0.1.0"
dependencies = [
"agent",
"anyhow",
"assistant_tool",
"client",
"collections",
"component",
"db",
"futures 0.3.31",
"gpui",
"languages",
"log",
"notifications",
"project",
"prompt_store",
"serde",
"ui",
"ui_input",
"util",
"workspace",
"workspace-hack",
]
[[package]]
name = "concurrent-queue"
version = "2.5.0"
@@ -3508,9 +3593,9 @@ dependencies = [
[[package]]
name = "cosmic-text"
version = "0.14.0"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e1ecbb5db9a4c2ee642df67bcfa8f044dd867dbbaa21bfab139cbc204ffbf67"
checksum = "e418dd4f5128c3e93eab12246391c54a20c496811131f85754dc8152ee207892"
dependencies = [
"bitflags 2.9.0",
"fontdb 0.16.2",
@@ -3983,6 +4068,7 @@ dependencies = [
"http_client",
"language",
"log",
"lsp-types",
"node_runtime",
"parking_lot",
"paths",
@@ -4018,6 +4104,7 @@ dependencies = [
"futures 0.3.31",
"gpui",
"language",
"lsp-types",
"paths",
"serde",
"serde_json",
@@ -4162,7 +4249,6 @@ dependencies = [
"collections",
"command_palette_hooks",
"dap",
"dap_adapters",
"db",
"debugger_tools",
"editor",
@@ -4175,7 +4261,6 @@ dependencies = [
"log",
"menu",
"parking_lot",
"paths",
"picker",
"pretty_assertions",
"project",
@@ -4433,16 +4518,6 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
[[package]]
name = "dispatch2"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec"
dependencies = [
"bitflags 2.9.0",
"objc2",
]
[[package]]
name = "displaydoc"
version = "0.2.5"
@@ -6333,7 +6408,6 @@ checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9"
dependencies = [
"cfg-if",
"crunchy",
"num-traits",
]
[[package]]
@@ -7723,7 +7797,6 @@ dependencies = [
"tree-sitter-html",
"tree-sitter-json",
"tree-sitter-md",
"tree-sitter-python",
"tree-sitter-ruby",
"tree-sitter-rust",
"tree-sitter-typescript",
@@ -7788,12 +7861,9 @@ version = "0.1.0"
dependencies = [
"collections",
"feature_flags",
"futures 0.3.31",
"fuzzy",
"gpui",
"language_model",
"log",
"ordered-float 2.10.1",
"picker",
"proto",
"ui",
@@ -7834,7 +7904,6 @@ dependencies = [
"partial-json-fixer",
"project",
"proto",
"release_channel",
"schemars",
"serde",
"serde_json",
@@ -8507,7 +8576,6 @@ version = "0.1.0"
dependencies = [
"anyhow",
"assets",
"base64 0.22.1",
"env_logger 0.11.8",
"gpui",
"language",
@@ -8896,27 +8964,23 @@ checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03"
[[package]]
name = "naga"
version = "25.0.1"
version = "23.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b977c445f26e49757f9aca3631c3b8b836942cb278d69a92e7b80d3b24da632"
checksum = "364f94bc34f61332abebe8cad6f6cd82a5b65cff22c828d05d0968911462ca4f"
dependencies = [
"arrayvec",
"bit-set 0.8.0",
"bitflags 2.9.0",
"cfg_aliases 0.2.1",
"codespan-reporting 0.12.0",
"half",
"hashbrown 0.15.2",
"cfg_aliases 0.1.1",
"codespan-reporting 0.11.1",
"hexf-parse",
"indexmap",
"log",
"num-traits",
"once_cell",
"rustc-hash 1.1.0",
"spirv",
"strum 0.26.3",
"thiserror 2.0.12",
"unicode-ident",
"termcolor",
"thiserror 1.0.69",
"unicode-xid",
]
[[package]]
@@ -9371,36 +9435,95 @@ dependencies = [
]
[[package]]
name = "objc2"
version = "0.6.1"
name = "objc-sys"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88c6597e14493ab2e44ce58f2fdecf095a51f12ca57bec060a11c57332520551"
checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310"
[[package]]
name = "objc2"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804"
dependencies = [
"objc-sys",
"objc2-encode",
]
[[package]]
name = "objc2-app-kit"
version = "0.3.1"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc"
checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff"
dependencies = [
"bitflags 2.9.0",
"block2",
"libc",
"objc2",
"objc2-core-foundation",
"objc2-core-data",
"objc2-core-image",
"objc2-foundation",
"objc2-quartz-core",
]
[[package]]
name = "objc2-core-foundation"
version = "0.3.1"
name = "objc2-cloud-kit"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166"
checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009"
dependencies = [
"bitflags 2.9.0",
"dispatch2",
"block2",
"objc2",
"objc2-core-location",
"objc2-foundation",
]
[[package]]
name = "objc2-contacts"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889"
dependencies = [
"block2",
"objc2",
"objc2-foundation",
]
[[package]]
name = "objc2-core-data"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
dependencies = [
"bitflags 2.9.0",
"block2",
"objc2",
"objc2-foundation",
]
[[package]]
name = "objc2-core-image"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80"
dependencies = [
"block2",
"objc2",
"objc2-foundation",
"objc2-metal",
]
[[package]]
name = "objc2-core-location"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781"
dependencies = [
"block2",
"objc2",
"objc2-contacts",
"objc2-foundation",
]
[[package]]
@@ -9411,53 +9534,106 @@ checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
[[package]]
name = "objc2-foundation"
version = "0.3.1"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c"
checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
dependencies = [
"bitflags 2.9.0",
"block2",
"libc",
"objc2",
"objc2-core-foundation",
]
[[package]]
name = "objc2-link-presentation"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398"
dependencies = [
"block2",
"objc2",
"objc2-app-kit",
"objc2-foundation",
]
[[package]]
name = "objc2-metal"
version = "0.3.1"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f246c183239540aab1782457b35ab2040d4259175bd1d0c58e46ada7b47a874"
checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
dependencies = [
"bitflags 2.9.0",
"block2",
"dispatch2",
"objc2",
"objc2-core-foundation",
"objc2-foundation",
]
[[package]]
name = "objc2-quartz-core"
version = "0.3.1"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90ffb6a0cd5f182dc964334388560b12a57f7b74b3e2dec5e2722aa2dfb2ccd5"
checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
dependencies = [
"bitflags 2.9.0",
"block2",
"objc2",
"objc2-core-foundation",
"objc2-foundation",
"objc2-metal",
]
[[package]]
name = "objc2-ui-kit"
version = "0.3.1"
name = "objc2-symbols"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25b1312ad7bc8a0e92adae17aa10f90aae1fb618832f9b993b022b591027daed"
checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc"
dependencies = [
"objc2",
"objc2-foundation",
]
[[package]]
name = "objc2-ui-kit"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f"
dependencies = [
"bitflags 2.9.0",
"block2",
"objc2",
"objc2-core-foundation",
"objc2-cloud-kit",
"objc2-core-data",
"objc2-core-image",
"objc2-core-location",
"objc2-foundation",
"objc2-link-presentation",
"objc2-quartz-core",
"objc2-symbols",
"objc2-uniform-type-identifiers",
"objc2-user-notifications",
]
[[package]]
name = "objc2-uniform-type-identifiers"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe"
dependencies = [
"block2",
"objc2",
"objc2-foundation",
]
[[package]]
name = "objc2-user-notifications"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3"
dependencies = [
"bitflags 2.9.0",
"block2",
"objc2",
"objc2-core-location",
"objc2-foundation",
]
[[package]]
@@ -11836,8 +12012,6 @@ version = "0.1.0"
dependencies = [
"anyhow",
"askpass",
"assistant_tool",
"assistant_tools",
"async-watch",
"backtrace",
"cargo_toml",
@@ -11860,7 +12034,6 @@ dependencies = [
"http_client",
"language",
"language_extension",
"language_model",
"languages",
"libc",
"log",
@@ -14912,6 +15085,7 @@ dependencies = [
"client",
"collections",
"db",
"feature_flags",
"gpui",
"http_client",
"notifications",
@@ -18035,7 +18209,6 @@ dependencies = [
"base64ct",
"bigdecimal",
"bit-set 0.8.0",
"bit-vec 0.8.0",
"bitflags 2.9.0",
"bstr",
"bytemuck",
@@ -18047,7 +18220,6 @@ dependencies = [
"clang-sys",
"clap",
"clap_builder",
"codespan-reporting 0.12.0",
"concurrent-queue",
"core-foundation 0.9.4",
"core-foundation-sys",
@@ -18076,7 +18248,6 @@ dependencies = [
"getrandom 0.2.15",
"getrandom 0.3.2",
"gimli",
"half",
"handlebars 4.5.0",
"hashbrown 0.14.5",
"hashbrown 0.15.2",
@@ -18110,9 +18281,6 @@ dependencies = [
"num-iter",
"num-rational",
"num-traits",
"objc2",
"objc2-foundation",
"objc2-metal",
"object",
"once_cell",
"percent-encoding",
@@ -18131,7 +18299,6 @@ dependencies = [
"regex-syntax 0.8.5",
"ring",
"rust_decimal",
"rustc-hash 1.1.0",
"rustix 0.38.44",
"rustix 1.0.5",
"rustls 0.23.26",
@@ -18527,7 +18694,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.187.0"
version = "0.186.0"
dependencies = [
"activity_indicator",
"agent",
@@ -18535,9 +18702,9 @@ dependencies = [
"ashpd",
"askpass",
"assets",
"assistant",
"assistant_context_editor",
"assistant_settings",
"assistant_tool",
"assistant_tools",
"async-watch",
"audio",
@@ -18554,7 +18721,7 @@ dependencies = [
"collab_ui",
"collections",
"command_palette",
"component",
"component_preview",
"copilot",
"dap",
"dap_adapters",
@@ -18580,7 +18747,6 @@ dependencies = [
"gpui_tokio",
"http_client",
"image_viewer",
"indoc",
"inline_completion_button",
"install_cli",
"journal",
@@ -18593,7 +18759,6 @@ dependencies = [
"languages",
"libc",
"log",
"markdown",
"markdown_preview",
"menu",
"migrator",
@@ -18645,7 +18810,6 @@ dependencies = [
"tree-sitter-md",
"tree-sitter-rust",
"ui",
"ui_input",
"ui_prompt",
"url",
"urlencoding",
@@ -18720,9 +18884,9 @@ dependencies = [
[[package]]
name = "zed_llm_client"
version = "0.8.1"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16d993fc42f9ec43ab76fa46c6eb579a66e116bb08cd2bc9a67f3afcaa05d39d"
checksum = "6fe0d60001c02d0d21a4114a13bee3a905fbb9e146ada80a90435c05fda18852"
dependencies = [
"anyhow",
"serde",
@@ -18930,7 +19094,6 @@ dependencies = [
"paths",
"postage",
"project",
"proto",
"regex",
"release_channel",
"reqwest_client",

View File

@@ -6,6 +6,7 @@ members = [
"crates/anthropic",
"crates/askpass",
"crates/assets",
"crates/assistant",
"crates/assistant_context_editor",
"crates/assistant_settings",
"crates/assistant_slash_command",
@@ -31,6 +32,7 @@ members = [
"crates/command_palette",
"crates/command_palette_hooks",
"crates/component",
"crates/component_preview",
"crates/context_server",
"crates/copilot",
"crates/credentials_provider",
@@ -212,6 +214,7 @@ ai = { path = "crates/ai" }
anthropic = { path = "crates/anthropic" }
askpass = { path = "crates/askpass" }
assets = { path = "crates/assets" }
assistant = { path = "crates/assistant" }
assistant_context_editor = { path = "crates/assistant_context_editor" }
assistant_settings = { path = "crates/assistant_settings" }
assistant_slash_command = { path = "crates/assistant_slash_command" }
@@ -237,6 +240,7 @@ collections = { path = "crates/collections" }
command_palette = { path = "crates/command_palette" }
command_palette_hooks = { path = "crates/command_palette_hooks" }
component = { path = "crates/component" }
component_preview = { path = "crates/component_preview" }
context_server = { path = "crates/context_server" }
copilot = { path = "crates/copilot" }
credentials_provider = { path = "crates/credentials_provider" }
@@ -408,9 +412,9 @@ aws-smithy-runtime-api = { version = "1.7.4", features = ["http-1x", "client"] }
aws-smithy-types = { version = "1.3.0", features = ["http-body-1-x"] }
base64 = "0.22"
bitflags = "2.6.0"
blade-graphics = { git = "https://github.com/kvark/blade", rev = "416375211bb0b5826b3584dccdb6a43369e499ad" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "416375211bb0b5826b3584dccdb6a43369e499ad" }
blade-util = { git = "https://github.com/kvark/blade", rev = "416375211bb0b5826b3584dccdb6a43369e499ad" }
blade-graphics = { git = "https://github.com/kvark/blade", rev = "b16f5c7bd873c7126f48c82c39e7ae64602ae74f" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "b16f5c7bd873c7126f48c82c39e7ae64602ae74f" }
blade-util = { git = "https://github.com/kvark/blade", rev = "b16f5c7bd873c7126f48c82c39e7ae64602ae74f" }
blake3 = "1.5.3"
bytes = "1.0"
cargo_metadata = "0.19"
@@ -469,7 +473,7 @@ lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "c9c189
markup5ever_rcdom = "0.3.0"
metal = "0.29"
mlua = { version = "0.10", features = ["lua54", "vendored", "async", "send"] }
naga = { version = "25.0", features = ["wgsl-in"] }
naga = { version = "23.1.0", features = ["wgsl-in"] }
nanoid = "0.4"
nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
nix = "0.29"
@@ -606,7 +610,7 @@ wasmtime-wasi = "29"
which = "6.0.0"
wit-component = "0.221"
workspace-hack = "0.1.0"
zed_llm_client = "0.8.1"
zed_llm_client = "0.7.5"
zstd = "0.11"
[workspace.dependencies.async-stripe]

View File

Before

Width:  |  Height:  |  Size: 474 B

After

Width:  |  Height:  |  Size: 474 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5">
<path fill-rule="evenodd" d="M4.5 2A1.5 1.5 0 0 0 3 3.5v13A1.5 1.5 0 0 0 4.5 18h11a1.5 1.5 0 0 0 1.5-1.5V7.621a1.5 1.5 0 0 0-.44-1.06l-4.12-4.122A1.5 1.5 0 0 0 11.378 2H4.5Zm2.25 8.5a.75.75 0 0 0 0 1.5h6.5a.75.75 0 0 0 0-1.5h-6.5Zm0 3a.75.75 0 0 0 0 1.5h6.5a.75.75 0 0 0 0-1.5h-6.5Z" clip-rule="evenodd" />
</svg>

Before

Width:  |  Height:  |  Size: 412 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-loader-circle-icon lucide-loader-circle"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>

Before

Width:  |  Height:  |  Size: 289 B

View File

@@ -1,3 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5">
<path d="M18.905 12.75a1.25 1.25 0 1 1-2.5 0v-7.5a1.25 1.25 0 0 1 2.5 0v7.5ZM8.905 17v1.3c0 .268-.14.526-.395.607A2 2 0 0 1 5.905 17c0-.995.182-1.948.514-2.826.204-.54-.166-1.174-.744-1.174h-2.52c-1.243 0-2.261-1.01-2.146-2.247.193-2.08.651-4.082 1.341-5.974C2.752 3.678 3.833 3 5.005 3h3.192a3 3 0 0 1 1.341.317l2.734 1.366A3 3 0 0 0 13.613 5h1.292v7h-.963c-.685 0-1.258.482-1.612 1.068a4.01 4.01 0 0 1-2.166 1.73c-.432.143-.853.386-1.011.814-.16.432-.248.9-.248 1.388Z" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-thumbs-down"><path d="M17 14V2"/><path d="M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22a3.13 3.13 0 0 1-3-3.88Z"/></svg>

Before

Width:  |  Height:  |  Size: 580 B

After

Width:  |  Height:  |  Size: 405 B

View File

@@ -1,3 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5">
<path d="M1 8.25a1.25 1.25 0 1 1 2.5 0v7.5a1.25 1.25 0 1 1-2.5 0v-7.5ZM11 3V1.7c0-.268.14-.526.395-.607A2 2 0 0 1 14 3c0 .995-.182 1.948-.514 2.826-.204.54.166 1.174.744 1.174h2.52c1.243 0 2.261 1.01 2.146 2.247a23.864 23.864 0 0 1-1.341 5.974C17.153 16.323 16.072 17 14.9 17h-3.192a3 3 0 0 1-1.341-.317l-2.734-1.366A3 3 0 0 0 6.292 15H5V8h.963c.685 0 1.258-.483 1.612-1.068a4.011 4.011 0 0 1 2.166-1.73c.432-.143.853-.386 1.011-.814.16-.432.248-.9.248-1.388Z" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-thumbs-up"><path d="M7 10v12"/><path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z"/></svg>

Before

Width:  |  Height:  |  Size: 569 B

After

Width:  |  Height:  |  Size: 404 B

View File

@@ -213,22 +213,35 @@
"ctrl-shift-n": "agent::RejectAll"
}
},
{
"context": "AssistantPanel",
"bindings": {
"ctrl-k c": "assistant::CopyCode",
"ctrl-shift-e": "project_panel::ToggleFocus",
"ctrl-g": "search::SelectNextMatch",
"ctrl-shift-g": "search::SelectPreviousMatch",
"ctrl-alt-/": "agent::ToggleModelSelector",
"ctrl-k h": "assistant::DeployHistory",
"ctrl-k l": "agent::OpenRulesLibrary",
"new": "assistant::NewChat",
"ctrl-t": "assistant::NewChat",
"ctrl-n": "assistant::NewChat"
}
},
{
"context": "ContextEditor > Editor",
"bindings": {
"ctrl-enter": "assistant::Assist",
"ctrl-shift-enter": "assistant::Edit",
"ctrl-s": "workspace::Save",
"save": "workspace::Save",
"ctrl->": "assistant::QuoteSelection",
"ctrl-<": "assistant::InsertIntoEditor",
"ctrl-alt-/": "agent::ToggleModelSelector",
"shift-enter": "assistant::Split",
"ctrl-r": "assistant::CycleMessageRole",
"enter": "assistant::ConfirmCommand",
"alt-enter": "editor::Newline",
"ctrl-k c": "assistant::CopyCode",
"ctrl-g": "search::SelectNextMatch",
"ctrl-shift-g": "search::SelectPreviousMatch",
"ctrl-k l": "agent::OpenRulesLibrary"
"alt-enter": "editor::Newline"
}
},
{
@@ -245,8 +258,7 @@
"ctrl-shift-o": "agent::ToggleNavigationMenu",
"ctrl-shift-i": "agent::ToggleOptionsMenu",
"shift-escape": "agent::ExpandMessageEditor",
"ctrl-alt-e": "agent::RemoveAllContext",
"ctrl-shift-e": "project_panel::ToggleFocus"
"ctrl-alt-e": "agent::RemoveAllContext"
}
},
{
@@ -697,8 +709,8 @@
{
"context": "PromptEditor",
"bindings": {
"ctrl-[": "agent::CyclePreviousInlineAssist",
"ctrl-]": "agent::CycleNextInlineAssist",
"ctrl-[": "assistant::CyclePreviousInlineAssist",
"ctrl-]": "assistant::CycleNextInlineAssist",
"ctrl-alt-e": "agent::RemoveAllContext"
}
},
@@ -934,7 +946,6 @@
"ctrl-e": ["terminal::SendKeystroke", "ctrl-e"],
"ctrl-o": ["terminal::SendKeystroke", "ctrl-o"],
"ctrl-w": ["terminal::SendKeystroke", "ctrl-w"],
"ctrl-backspace": ["terminal::SendKeystroke", "ctrl-w"],
"ctrl-shift-a": "editor::SelectAll",
"find": "buffer_search::Deploy",
"ctrl-shift-f": "buffer_search::Deploy",
@@ -952,10 +963,7 @@
"shift-down": "terminal::ScrollLineDown",
"shift-home": "terminal::ScrollToTop",
"shift-end": "terminal::ScrollToBottom",
"ctrl-shift-space": "terminal::ToggleViMode",
"ctrl-shift-r": "terminal::RerunTask",
"ctrl-alt-r": "terminal::RerunTask",
"alt-t": "terminal::RerunTask"
"ctrl-shift-space": "terminal::ToggleViMode"
}
},
{

View File

@@ -258,11 +258,27 @@
"shift-ctrl-r": "agent::OpenAgentDiff"
}
},
{
"context": "AssistantPanel",
"use_key_equivalents": true,
"bindings": {
"cmd-k c": "assistant::CopyCode",
"cmd-shift-e": "project_panel::ToggleFocus",
"cmd-g": "search::SelectNextMatch",
"cmd-shift-g": "search::SelectPreviousMatch",
"cmd-alt-/": "agent::ToggleModelSelector",
"cmd-k h": "assistant::DeployHistory",
"cmd-k l": "agent::OpenRulesLibrary",
"cmd-t": "assistant::NewChat",
"cmd-n": "assistant::NewChat"
}
},
{
"context": "ContextEditor > Editor",
"use_key_equivalents": true,
"bindings": {
"cmd-enter": "assistant::Assist",
"cmd-shift-enter": "assistant::Edit",
"cmd-s": "workspace::Save",
"cmd->": "assistant::QuoteSelection",
"cmd-<": "assistant::InsertIntoEditor",
@@ -764,8 +780,8 @@
"cmd-shift-a": "agent::ToggleContextPicker",
"cmd-alt-/": "agent::ToggleModelSelector",
"cmd-alt-e": "agent::RemoveAllContext",
"ctrl-[": "agent::CyclePreviousInlineAssist",
"ctrl-]": "agent::CycleNextInlineAssist"
"ctrl-[": "assistant::CyclePreviousInlineAssist",
"ctrl-]": "assistant::CycleNextInlineAssist"
}
},
{
@@ -1021,7 +1037,6 @@
"escape": ["terminal::SendKeystroke", "escape"],
"enter": ["terminal::SendKeystroke", "enter"],
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"],
"ctrl-backspace": ["terminal::SendKeystroke", "ctrl-w"],
"shift-pageup": "terminal::ScrollPageUp",
"cmd-up": "terminal::ScrollPageUp",
"shift-pagedown": "terminal::ScrollPageDown",
@@ -1038,8 +1053,7 @@
"ctrl-alt-up": "pane::SplitUp",
"ctrl-alt-down": "pane::SplitDown",
"ctrl-alt-left": "pane::SplitLeft",
"ctrl-alt-right": "pane::SplitRight",
"cmd-alt-r": "terminal::RerunTask"
"ctrl-alt-right": "pane::SplitRight"
}
},
{

View File

@@ -49,9 +49,10 @@ And here's the section to rewrite based on that prompt again for reference:
</rewrite_this>
{{#if diagnostic_errors}}
{{#each diagnostic_errors}}
Below are the diagnostic errors visible to the user. If the user requests problems to be fixed, use this information, but do not try to fix these errors if the user hasn't asked you to.
{{#each diagnostic_errors}}
<diagnostic_error>
<line_number>{{line_number}}</line_number>
<error_message>{{error_message}}</error_message>

View File

@@ -0,0 +1,206 @@
<task_description>
The user of a code editor wants to make a change to their codebase.
You must describe the change using the following XML structure:
- <patch> - A group of related code changes.
Child tags:
- <title> (required) - A high-level description of the changes. This should be as short
as possible, possibly using common abbreviations.
- <edit> (1 or more) - An edit to make at a particular range within a file.
Includes the following child tags:
- <path> (required) - The path to the file that will be changed.
- <description> (optional) - An arbitrarily-long comment that describes the purpose
of this edit.
- <old_text> (optional) - An excerpt from the file's current contents that uniquely
identifies a range within the file where the edit should occur. Required for all operations
except `create`.
- <new_text> (required) - The new text to insert into the file.
- <operation> (required) - The type of change that should occur at the given range
of the file. Must be one of the following values:
- `update`: Replaces the entire range with the new text.
- `insert_before`: Inserts the new text before the range.
- `insert_after`: Inserts new text after the range.
- `create`: Creates or overwrites a file with the given path and the new text.
- `delete`: Deletes the specified range from the file.
<guidelines>
- Never provide multiple edits whose ranges intersect each other. Instead, merge them into one edit.
- Prefer multiple edits to smaller, disjoint ranges, rather than one edit to a larger range.
- There's no need to escape angle brackets within XML tags.
- Always ensure imports are added if you're referencing symbols that are not in scope.
</guidelines>
Here are some concrete examples.
<example>
<message role="user">
```rs src/shapes.rs
pub mod rectangle;
pub mod circle;
```
```rs src/shapes/rectangle.rs
pub struct Rectangle {
width: f64,
height: f64,
}
impl Rectangle {
pub fn new(width: f64, height: f64) -> Self {
Rectangle { width, height }
}
}
```
```rs src/shapes/circle.rs
pub struct Circle {
radius: f64,
}
impl Circle {
pub fn new(radius: f64) -> Self {
Circle { radius }
}
}
```
Update all shapes to store their origin as an (x, y) tuple and implement Display.
</message>
<message role="assistant">
We'll need to update both the rectangle and circle modules.
<patch>
<title>Add origins and display impls to shapes</title>
<edit>
<path>src/shapes/rectangle.rs</path>
<description>Add the origin field to Rectangle struct</description>
<operation>insert_after</operation>
<old_text>
pub struct Rectangle {
</old_text>
<new_text>
origin: (f64, f64),
</new_text>
</edit>
<edit>
<path>src/shapes/rectangle.rs</path>
<operation>update</operation>
<old_text>
fn new(width: f64, height: f64) -> Self {
Rectangle { width, height }
}
</old_text>
<new_text>
fn new(origin: (f64, f64), width: f64, height: f64) -> Self {
Rectangle { origin, width, height }
}
</new_text>
</edit>
<edit>
<path>src/shapes/circle.rs</path>
<description>Add the origin field to Circle struct</description>
<operation>insert_after</operation>
<old_text>
pub struct Circle {
radius: f64,
</old_text>
<new_text>
origin: (f64, f64),
</new_text>
</edit>
<edit>
<path>src/shapes/circle.rs</path>
<operation>update</operation>
<old_text>
fn new(radius: f64) -> Self {
Circle { radius }
}
</old_text>
<new_text>
fn new(origin: (f64, f64), radius: f64) -> Self {
Circle { origin, radius }
}
</new_text>
</edit>
</step>
<edit>
<path>src/shapes/rectangle.rs</path>
<operation>insert_before</operation>
<old_text>
struct Rectangle {
</old_text>
<new_text>
use std::fmt;
</new_text>
</edit>
<edit>
<path>src/shapes/rectangle.rs</path>
<description>
Add a manual Display implementation for Rectangle.
Currently, this is the same as a derived Display implementation.
</description>
<operation>insert_after</operation>
<old_text>
Rectangle { width, height }
}
}
</old_text>
<new_text>
impl fmt::Display for Rectangle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.format_struct(f, "Rectangle")
.field("origin", &self.origin)
.field("width", &self.width)
.field("height", &self.height)
.finish()
}
}
</new_text>
</edit>
<edit>
<path>src/shapes/circle.rs</path>
<operation>insert_before</operation>
<old_text>
struct Circle {
</old_text>
<new_text>
use std::fmt;
</new_text>
</edit>
<edit>
<path>src/shapes/circle.rs</path>
<operation>insert_after</operation>
<old_text>
Circle { radius }
}
}
</old_text>
<new_text>
impl fmt::Display for Rectangle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.format_struct(f, "Rectangle")
.field("origin", &self.origin)
.field("width", &self.width)
.field("height", &self.height)
.finish()
}
}
</new_text>
</edit>
</patch>
</message>
</example>
</task_description>

View File

@@ -218,23 +218,6 @@
// 1. Do nothing: `none`
// 2. Find references for the same symbol: `find_all_references` (default)
"go_to_definition_fallback": "find_all_references",
// Which level to use to filter out diagnostics displayed in the editor.
//
// Affects the editor rendering only, and does not interrupt
// the functionality of diagnostics fetching and project diagnostics editor.
// Which files containing diagnostic errors/warnings to mark in the tabs.
// Diagnostics are only shown when file icons are also active.
// This setting only works when can take the following three values:
//
// Which diagnostic indicators to show in the scrollbar, their level should be more or equal to the specified severity level.
// Possible values:
// - "off" — no diagnostics are allowed
// - "error"
// - "warning" (default)
// - "info"
// - "hint"
// - null — allow all diagnostics
"diagnostics_max_severity": "warning",
// Whether to show wrap guides (vertical rulers) in the editor.
// Setting this to true will show a guide at the 'preferred_line_length' value
// if 'soft_wrap' is set to 'preferred_line_length', and will show any
@@ -326,12 +309,8 @@
},
// Titlebar related settings
"title_bar": {
// Whether to show the branch icon beside branch switcher in the titlebar.
"show_branch_icon": false,
// Whether to show onboarding banners in the titlebar.
"show_onboarding_banner": true,
// Whether to show user picture in the titlebar.
"show_user_picture": true
// Whether to show the branch icon beside branch switcher in the title bar.
"show_branch_icon": false
},
// Scrollbar related settings
"scrollbar": {
@@ -373,45 +352,6 @@
"vertical": true
}
},
// Minimap related settings
"minimap": {
// When to show the minimap in the editor.
// This setting can take three values:
// 1. Show the minimap if the editor's scrollbar is visible:
// "auto"
// 2. Always show the minimap:
// "always"
// 3. Never show the minimap:
// "never" (default)
"show": "never",
// When to show the minimap thumb.
// This setting can take two values:
// 1. Show the minimap thumb if the mouse is over the minimap:
// "hover"
// 2. Always show the minimap thumb:
// "always" (default)
"thumb": "always",
// How the minimap thumb border should look.
// This setting can take five values:
// 1. Display a border on all sides of the thumb:
// "thumb_border": "full"
// 2. Display a border on all sides except the left side of the thumb:
// "thumb_border": "left_open" (default)
// 3. Display a border on all sides except the right side of the thumb:
// "thumb_border": "right_open"
// 4. Display a border only on the left side of the thumb:
// "thumb_border": "left_only"
// 5. Display the thumb without any border:
// "thumb_border": "none"
"thumb_border": "left_open",
// How to highlight the current line in the minimap.
// This setting can take the following values:
//
// 1. `null` to inherit the editor `current_line_highlight` setting (default)
// 2. "line" or "all" to highlight the current line in the minimap.
// 3. "gutter" or "none" to not highlight the current line in the minimap.
"current_line_highlight": null
},
// Enable middle-click paste on Linux.
"middle_click_paste": true,
// What to do when multibuffer is double clicked in some of its excerpts
@@ -426,6 +366,8 @@
"gutter": {
// Whether to show line numbers in the gutter.
"line_numbers": true,
// Whether to show code action buttons in the gutter.
"code_actions": true,
// Whether to show runnables buttons in the gutter.
"runnables": true,
// Whether to show breakpoints in the gutter.
@@ -668,11 +610,13 @@
//
// Default: main
"fallback_branch_name": "main",
// Whether to sort entries in the panel by path
// or by status (the default).
//
// Default: false
"sort_by_path": false,
"scrollbar": {
// When to show the scrollbar in the git panel.
//
@@ -698,8 +642,6 @@
"version": "2",
// Whether the agent is enabled.
"enabled": true,
/// What completion mode to start new threads in, if available. Can be 'normal' or 'max'.
"preferred_completion_mode": "normal",
// Whether to show the agent panel button in the status bar.
"button": true,
// Where to dock the agent panel. Can be 'left', 'right' or 'bottom'.
@@ -722,28 +664,6 @@
// The model to use.
"model": "claude-3-7-sonnet-latest"
},
// Additional parameters for language model requests. When making a request to a model, parameters will be taken
// from the last entry in this list that matches the model's provider and name. In each entry, both provider
// and model are optional, so that you can specify parameters for either one.
"model_parameters": [
// To set parameters for all requests to OpenAI models:
// {
// "provider": "openai",
// "temperature": 0.5
// }
//
// To set parameters for all requests in general:
// {
// "temperature": 0
// }
//
// To set parameters for a specific provider and model:
// {
// "provider": "zed.dev",
// "model": "claude-3-7-sonnet-latest",
// "temperature": 1.0
// }
],
// When enabled, the agent can run potentially destructive actions without asking for your confirmation.
"always_allow_tool_actions": false,
// When enabled, the agent will stream edits.
@@ -1019,7 +939,7 @@
// longer than this value will still push diagnostics further to the right.
"min_column": 0,
// The minimum severity of the diagnostics to show inline.
// Inherits editor's diagnostics' max severity settings when `null`.
// Shows all diagnostics when not specified.
"max_severity": null
},
"cargo": {
@@ -1297,22 +1217,21 @@
"JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json", "**/.vscode/**/*.json"],
"Shell Script": [".env.*"]
},
// Settings for which version of Node.js and NPM to use when installing
// language servers and Copilot.
//
// Note: changing this setting currently requires restarting Zed.
"node": {
// By default, Zed will look for `node` and `npm` on your `$PATH`, and use the
// existing executables if their version is recent enough. Set this to `true`
// to prevent this, and force Zed to always download and install its own
// version of Node.
"ignore_system_version": false,
// You can also specify alternative paths to Node and NPM. If you specify
// `path`, but not `npm_path`, Zed will assume that `npm` is located at
// `${path}/../npm`.
"path": null,
"npm_path": null
},
// By default use a recent system version of node, or install our own.
// You can override this to use a version of node that is not in $PATH with:
// {
// "node": {
// "path": "/path/to/node"
// "npm_path": "/path/to/npm" (defaults to node_path/../npm)
// }
// }
// or to ensure Zed always downloads and installs an isolated version of node:
// {
// "node": {
// "ignore_system_version": true,
// }
// NOTE: changing this setting currently requires restarting Zed.
"node": {},
// The extensions that Zed should automatically install on startup.
//
// If you don't want any of these extensions, add this field to your settings
@@ -1687,6 +1606,8 @@
// "W": "workspace::Save"
// }
"command_aliases": {},
// Whether to show user picture in titlebar.
"show_user_picture": true,
// ssh_connections is an array of ssh connections.
// You can configure these from `project: Open Remote` in the command palette.
// Zed's ssh support will pull configuration from your ~/.ssh too.

View File

@@ -43,7 +43,6 @@ pub struct ActivityIndicator {
context_menu_handle: PopoverMenuHandle<ContextMenu>,
}
#[derive(Debug)]
struct ServerStatus {
name: SharedString,
status: BinaryStatus,
@@ -71,7 +70,6 @@ impl ActivityIndicator {
) -> Entity<ActivityIndicator> {
let project = workspace.project().clone();
let auto_updater = AutoUpdater::get(cx);
let workspace_handle = cx.entity();
let this = cx.new(|cx| {
let mut status_events = languages.language_server_binary_statuses();
cx.spawn(async move |this, cx| {
@@ -86,25 +84,6 @@ impl ActivityIndicator {
})
.detach();
cx.subscribe_in(
&workspace_handle,
window,
|activity_indicator, _, event, window, cx| match event {
workspace::Event::ClearActivityIndicator { .. } => {
if activity_indicator.statuses.pop().is_some() {
activity_indicator.dismiss_error_message(
&DismissErrorMessage,
window,
cx,
);
cx.notify();
}
}
_ => {}
},
)
.detach();
cx.subscribe(
&project.read(cx).lsp_store(),
|_, _, event, cx| match event {
@@ -136,7 +115,7 @@ impl ActivityIndicator {
}
Self {
statuses: Vec::new(),
statuses: Default::default(),
project: project.clone(),
auto_updater,
context_menu_handle: Default::default(),
@@ -206,8 +185,11 @@ impl ActivityIndicator {
cx: &mut Context<Self>,
) {
if let Some(updater) = &self.auto_updater {
updater.update(cx, |updater, cx| updater.dismiss_error(cx));
updater.update(cx, |updater, cx| {
updater.dismiss_error(cx);
});
}
cx.notify();
}
fn pending_language_server_work<'a>(

View File

@@ -9,7 +9,7 @@ license = "GPL-3.0-or-later"
workspace = true
[lib]
path = "src/agent.rs"
path = "src/assistant.rs"
doctest = false
[features]
@@ -23,13 +23,13 @@ anyhow.workspace = true
assistant_context_editor.workspace = true
assistant_settings.workspace = true
assistant_slash_command.workspace = true
assistant_slash_commands.workspace = true
assistant_tool.workspace = true
async-watch.workspace = true
buffer_diff.workspace = true
chrono.workspace = true
client.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
component.workspace = true
context_server.workspace = true
convert_case.workspace = true
@@ -46,7 +46,6 @@ gpui.workspace = true
heed.workspace = true
html_to_markdown.workspace = true
http_client.workspace = true
indexed_docs.workspace = true
itertools.workspace = true
jsonschema.workspace = true
language.workspace = true

View File

@@ -1,12 +1,11 @@
use crate::AgentPanel;
use crate::AssistantPanel;
use crate::context::{AgentContextHandle, RULES_ICON};
use crate::context_picker::{ContextPicker, MentionLink};
use crate::context_store::ContextStore;
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
use crate::message_editor::insert_message_creases;
use crate::thread::{
LastRestoreCheckpoint, MessageCrease, MessageId, MessageSegment, Thread, ThreadError,
ThreadEvent, ThreadFeedback, ThreadSummary,
LastRestoreCheckpoint, MessageId, MessageSegment, Thread, ThreadError, ThreadEvent,
ThreadFeedback,
};
use crate::thread_store::{RulesLoadingError, TextThreadStore, ThreadStore};
use crate::tool_use::{PendingToolUseStatus, ToolUse};
@@ -44,6 +43,7 @@ use std::sync::Arc;
use std::time::Duration;
use text::ToPoint;
use theme::ThemeSettings;
use ui::utils::WithRemSize;
use ui::{
Disclosure, IconButton, KeyBinding, PopoverMenuHandle, Scrollbar, ScrollbarState, TextSize,
Tooltip, prelude::*,
@@ -328,7 +328,6 @@ fn tool_use_markdown_style(window: &Window, cx: &mut App) -> MarkdownStyle {
}
}
const CODEBLOCK_CONTAINER_GROUP: &str = "codeblock_container";
const MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK: usize = 10;
fn render_markdown_code_block(
@@ -487,18 +486,12 @@ fn render_markdown_code_block(
.copied_code_block_ids
.contains(&(message_id, ix));
let can_expand = metadata.line_count > MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK;
let is_expanded = if can_expand {
active_thread
.read(cx)
.expanded_code_blocks
.get(&(message_id, ix))
.copied()
.unwrap_or(false)
} else {
false
};
let is_expanded = active_thread
.read(cx)
.expanded_code_blocks
.get(&(message_id, ix))
.copied()
.unwrap_or(true);
let codeblock_header_bg = cx
.theme()
@@ -519,7 +512,7 @@ fn render_markdown_code_block(
.children(label)
.child(
h_flex()
.visible_on_hover(CODEBLOCK_CONTAINER_GROUP)
.visible_on_hover("codeblock_container")
.gap_1()
.child(
IconButton::new(
@@ -561,42 +554,45 @@ fn render_markdown_code_block(
}
}),
)
.when(can_expand, |header| {
header.child(
IconButton::new(
("expand-collapse-code", ix),
if is_expanded {
IconName::ChevronUp
.when(
metadata.line_count > MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK,
|header| {
header.child(
IconButton::new(
("expand-collapse-code", ix),
if is_expanded {
IconName::ChevronUp
} else {
IconName::ChevronDown
},
)
.icon_color(Color::Muted)
.shape(ui::IconButtonShape::Square)
.tooltip(Tooltip::text(if is_expanded {
"Collapse Code"
} else {
IconName::ChevronDown
},
"Expand Code"
}))
.on_click({
let active_thread = active_thread.clone();
move |_event, _window, cx| {
active_thread.update(cx, |this, cx| {
let is_expanded = this
.expanded_code_blocks
.entry((message_id, ix))
.or_insert(true);
*is_expanded = !*is_expanded;
cx.notify();
});
}
}),
)
.icon_color(Color::Muted)
.shape(ui::IconButtonShape::Square)
.tooltip(Tooltip::text(if is_expanded {
"Collapse Code"
} else {
"Expand Code"
}))
.on_click({
let active_thread = active_thread.clone();
move |_event, _window, cx| {
active_thread.update(cx, |this, cx| {
let is_expanded = this
.expanded_code_blocks
.entry((message_id, ix))
.or_insert(true);
*is_expanded = !*is_expanded;
cx.notify();
});
}
}),
)
}),
},
),
);
v_flex()
.group(CODEBLOCK_CONTAINER_GROUP)
.group("codeblock_container")
.my_2()
.overflow_hidden()
.rounded_lg()
@@ -604,7 +600,16 @@ fn render_markdown_code_block(
.border_color(cx.theme().colors().border.opacity(0.6))
.bg(cx.theme().colors().editor_background)
.child(codeblock_header)
.when(can_expand && !is_expanded, |this| this.max_h_80())
.when(
metadata.line_count > MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK,
|this| {
if is_expanded {
this.h_full()
} else {
this.max_h_80()
}
},
)
}
fn render_code_language(
@@ -708,7 +713,7 @@ fn open_markdown_link(
.detach_and_log_err(cx);
}
Some(MentionLink::Thread(thread_id)) => workspace.update(cx, |workspace, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
panel.update(cx, |panel, cx| {
panel
.open_thread_by_id(&thread_id, window, cx)
@@ -717,7 +722,7 @@ fn open_markdown_link(
}
}),
Some(MentionLink::TextThread(path)) => workspace.update(cx, |workspace, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
panel.update(cx, |panel, cx| {
panel
.open_saved_prompt_editor(path, window, cx)
@@ -823,12 +828,12 @@ impl ActiveThread {
self.messages.is_empty()
}
pub fn summary<'a>(&'a self, cx: &'a App) -> &'a ThreadSummary {
pub fn summary(&self, cx: &App) -> Option<SharedString> {
self.thread.read(cx).summary()
}
pub fn regenerate_summary(&self, cx: &mut App) {
self.thread.update(cx, |thread, cx| thread.summarize(cx))
pub fn summary_or_default(&self, cx: &App) -> SharedString {
self.thread.read(cx).summary_or_default()
}
pub fn cancel_last_completion(&mut self, window: &mut Window, cx: &mut App) -> bool {
@@ -1134,7 +1139,11 @@ impl ActiveThread {
return;
}
let title = self.thread.read(cx).summary().unwrap_or("Agent Panel");
let title = self
.thread
.read(cx)
.summary()
.unwrap_or("Agent Panel".into());
match AssistantSettings::get_global(cx).notify_when_agent_waiting {
NotifyWhenAgentWaiting::PrimaryScreen => {
@@ -1203,7 +1212,8 @@ impl ActiveThread {
if let Some(workspace) = workspace_handle.upgrade() {
workspace.update(_cx, |workspace, cx| {
workspace.focus_panel::<AgentPanel>(window, cx);
workspace
.focus_panel::<AssistantPanel>(window, cx);
});
}
})
@@ -1264,7 +1274,6 @@ impl ActiveThread {
&mut self,
message_id: MessageId,
message_segments: &[MessageSegment],
message_creases: &[MessageCrease],
window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -1274,6 +1283,9 @@ impl ActiveThread {
return;
};
// Cancel any ongoing streaming when user starts editing a previous message
self.cancel_last_completion(window, cx);
let editor = crate::message_editor::create_editor(
self.workspace.clone(),
self.context_store.downgrade(),
@@ -1284,7 +1296,6 @@ impl ActiveThread {
);
editor.update(cx, |editor, cx| {
editor.set_text(message_text.clone(), window, cx);
insert_message_creases(editor, message_creases, &self.context_store, window, cx);
editor.focus_handle(cx).focus(window);
editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
});
@@ -1405,12 +1416,8 @@ impl ActiveThread {
mode: None,
messages: vec![request_message],
tools: vec![],
tool_choice: None,
stop: vec![],
temperature: AssistantSettings::temperature_for_model(
&configured_model.model,
cx,
),
temperature: None,
};
Some(configured_model.model.count_tokens(request, cx))
@@ -1739,7 +1746,6 @@ impl ActiveThread {
let Some(message) = self.thread.read(cx).message(message_id) else {
return Empty.into_any();
};
let message_creases = message.creases.clone();
let Some(rendered_message) = self.rendered_messages_by_id.get(&message_id) else {
return Empty.into_any();
@@ -1775,7 +1781,8 @@ impl ActiveThread {
let colors = cx.theme().colors();
let editor_bg_color = colors.editor_background;
let open_as_markdown = IconButton::new(("open-as-markdown", ix), IconName::DocumentText)
let open_as_markdown = IconButton::new(("open-as-markdown", ix), IconName::FileCode)
.shape(ui::IconButtonShape::Square)
.icon_size(IconSize::XSmall)
.icon_color(Color::Ignored)
.tooltip(Tooltip::text("Open Thread as Markdown"))
@@ -1800,16 +1807,13 @@ impl ActiveThread {
.mt_1()
.py_2()
.px(RESPONSE_PADDING_X)
.mr_1()
.opacity(0.4)
.hover(|style| style.opacity(1.))
.gap_1p5()
.gap_1()
.flex_wrap()
.justify_end();
let feedback_items = match self.thread.read(cx).message_feedback(message_id) {
Some(feedback) => feedback_container
.child(
div().visible_on_hover("feedback_container").child(
div().mr_1().visible_on_hover("feedback_container").child(
Label::new(match feedback {
ThreadFeedback::Positive => "Thanks for your feedback!",
ThreadFeedback::Negative => {
@@ -1822,8 +1826,11 @@ impl ActiveThread {
)
.child(
h_flex()
.pr_1()
.gap_1()
.child(
IconButton::new(("feedback-thumbs-up", ix), IconName::ThumbsUp)
.shape(ui::IconButtonShape::Square)
.icon_size(IconSize::XSmall)
.icon_color(match feedback {
ThreadFeedback::Positive => Color::Accent,
@@ -1841,6 +1848,7 @@ impl ActiveThread {
)
.child(
IconButton::new(("feedback-thumbs-down", ix), IconName::ThumbsDown)
.shape(ui::IconButtonShape::Square)
.icon_size(IconSize::XSmall)
.icon_color(match feedback {
ThreadFeedback::Positive => Color::Ignored,
@@ -1861,7 +1869,7 @@ impl ActiveThread {
.into_any_element(),
None => feedback_container
.child(
div().visible_on_hover("feedback_container").child(
div().mr_1().visible_on_hover("feedback_container").child(
Label::new(
"Rating the thread sends all of your current conversation to the Zed team.",
)
@@ -1871,10 +1879,13 @@ impl ActiveThread {
)
.child(
h_flex()
.pr_1()
.gap_1()
.child(
IconButton::new(("feedback-thumbs-up", ix), IconName::ThumbsUp)
.icon_size(IconSize::XSmall)
.icon_color(Color::Ignored)
.shape(ui::IconButtonShape::Square)
.tooltip(Tooltip::text("Helpful Response"))
.on_click(cx.listener(move |this, _, window, cx| {
this.handle_feedback_click(
@@ -1889,6 +1900,7 @@ impl ActiveThread {
IconButton::new(("feedback-thumbs-down", ix), IconName::ThumbsDown)
.icon_size(IconSize::XSmall)
.icon_color(Color::Ignored)
.shape(ui::IconButtonShape::Square)
.tooltip(Tooltip::text("Not Helpful"))
.on_click(cx.listener(move |this, _, window, cx| {
this.handle_feedback_click(
@@ -2031,7 +2043,6 @@ impl ActiveThread {
this.start_editing_message(
message_id,
&message_segments,
&message_creases,
window,
cx,
);
@@ -2065,185 +2076,202 @@ impl ActiveThread {
let panel_background = cx.theme().colors().panel_background;
v_flex()
.w_full()
.map(|parent| {
if let Some(checkpoint) = checkpoint.filter(|_| !is_generating) {
let mut is_pending = false;
let mut error = None;
if let Some(last_restore_checkpoint) =
self.thread.read(cx).last_restore_checkpoint()
{
if last_restore_checkpoint.message_id() == message_id {
match last_restore_checkpoint {
LastRestoreCheckpoint::Pending { .. } => is_pending = true,
LastRestoreCheckpoint::Error { error: err, .. } => {
error = Some(err.clone());
WithRemSize::new(ThemeSettings::get_global(cx).agent_font_size(cx))
.size_full()
.child(
v_flex()
.w_full()
.map(|parent| {
if let Some(checkpoint) = checkpoint.filter(|_| !is_generating) {
let mut is_pending = false;
let mut error = None;
if let Some(last_restore_checkpoint) =
self.thread.read(cx).last_restore_checkpoint()
{
if last_restore_checkpoint.message_id() == message_id {
match last_restore_checkpoint {
LastRestoreCheckpoint::Pending { .. } => is_pending = true,
LastRestoreCheckpoint::Error { error: err, .. } => {
error = Some(err.clone());
}
}
}
}
}
}
let restore_checkpoint_button =
Button::new(("restore-checkpoint", ix), "Restore Checkpoint")
.icon(if error.is_some() {
IconName::XCircle
} else {
IconName::Undo
})
.icon_size(IconSize::XSmall)
.icon_position(IconPosition::Start)
.icon_color(if error.is_some() {
Some(Color::Error)
} else {
None
})
.label_size(LabelSize::XSmall)
.disabled(is_pending)
.on_click(cx.listener(move |this, _, _window, cx| {
this.thread.update(cx, |thread, cx| {
thread
.restore_checkpoint(checkpoint.clone(), cx)
.detach_and_log_err(cx);
});
}));
let restore_checkpoint_button =
Button::new(("restore-checkpoint", ix), "Restore Checkpoint")
.icon(if error.is_some() {
IconName::XCircle
} else {
IconName::Undo
})
.icon_size(IconSize::XSmall)
.icon_position(IconPosition::Start)
.icon_color(if error.is_some() {
Some(Color::Error)
} else {
None
})
.label_size(LabelSize::XSmall)
.disabled(is_pending)
.on_click(cx.listener(move |this, _, _window, cx| {
this.thread.update(cx, |thread, cx| {
thread
.restore_checkpoint(checkpoint.clone(), cx)
.detach_and_log_err(cx);
});
}));
let restore_checkpoint_button = if is_pending {
restore_checkpoint_button
.with_animation(
("pulsating-restore-checkpoint-button", ix),
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.6, 1.)),
|label, delta| label.alpha(delta),
let restore_checkpoint_button = if is_pending {
restore_checkpoint_button
.with_animation(
("pulsating-restore-checkpoint-button", ix),
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.6, 1.)),
|label, delta| label.alpha(delta),
)
.into_any_element()
} else if let Some(error) = error {
restore_checkpoint_button
.tooltip(Tooltip::text(error.to_string()))
.into_any_element()
} else {
restore_checkpoint_button.into_any_element()
};
parent.child(
h_flex()
.pt_2p5()
.px_2p5()
.w_full()
.gap_1()
.child(ui::Divider::horizontal())
.child(restore_checkpoint_button)
.child(ui::Divider::horizontal()),
)
.into_any_element()
} else if let Some(error) = error {
restore_checkpoint_button
.tooltip(Tooltip::text(error.to_string()))
.into_any_element()
} else {
restore_checkpoint_button.into_any_element()
};
parent.child(
h_flex()
.pt_2p5()
.px_2p5()
.w_full()
.gap_1()
.child(ui::Divider::horizontal())
.child(restore_checkpoint_button)
.child(ui::Divider::horizontal()),
)
} else {
parent
}
})
.when(is_first_message, |parent| {
parent.child(self.render_rules_item(cx))
})
.child(styled_message)
.when(is_generating && is_last_message, |this| {
this.child(
h_flex()
.h_8()
.mt_2()
.mb_4()
.ml_4()
.py_1p5()
.when_some(loading_dots, |this, loading_dots| this.child(loading_dots)),
)
})
.when(show_feedback, move |parent| {
parent.child(feedback_items).when_some(
self.open_feedback_editors.get(&message_id),
move |parent, feedback_editor| {
let focus_handle = feedback_editor.focus_handle(cx);
parent.child(
v_flex()
.key_context("AgentFeedbackMessageEditor")
.on_action(cx.listener(move |this, _: &menu::Cancel, _, cx| {
this.open_feedback_editors.remove(&message_id);
cx.notify();
}))
.on_action(cx.listener(move |this, _: &menu::Confirm, _, cx| {
this.submit_feedback_message(message_id, cx);
cx.notify();
}))
.on_action(cx.listener(Self::confirm_editing_message))
.mb_2()
.mx_4()
.p_2()
.rounded_md()
.border_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().editor_background)
.child(feedback_editor.clone())
.child(
h_flex()
.gap_1()
.justify_end()
.child(
Button::new("dismiss-feedback-message", "Cancel")
.label_size(LabelSize::Small)
.key_binding(
KeyBinding::for_action_in(
&menu::Cancel,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(10.))),
)
.on_click(cx.listener(
move |this, _, _window, cx| {
this.open_feedback_editors
.remove(&message_id);
cx.notify();
},
)),
)
.child(
Button::new(
"submit-feedback-message",
"Share Feedback",
)
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
.label_size(LabelSize::Small)
.key_binding(
KeyBinding::for_action_in(
&menu::Confirm,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(10.))),
)
.on_click(
cx.listener(move |this, _, _window, cx| {
this.submit_feedback_message(message_id, cx);
cx.notify()
}),
),
),
),
} else {
parent
}
})
.when(is_first_message, |parent| {
parent.child(self.render_rules_item(cx))
})
.child(styled_message)
.when(is_generating && is_last_message, |this| {
this.child(
h_flex()
.h_8()
.mt_2()
.mb_4()
.ml_4()
.py_1p5()
.when_some(loading_dots, |this, loading_dots| {
this.child(loading_dots)
}),
)
},
)
})
.when(after_editing_message, |parent| {
// Backdrop to dim out the whole thread below the editing user message
parent.relative().child(
div()
.stop_mouse_events_except_scroll()
.absolute()
.inset_0()
.size_full()
.bg(panel_background)
.opacity(0.8),
)
})
})
.when(show_feedback, move |parent| {
parent.child(feedback_items).when_some(
self.open_feedback_editors.get(&message_id),
move |parent, feedback_editor| {
let focus_handle = feedback_editor.focus_handle(cx);
parent.child(
v_flex()
.key_context("AgentFeedbackMessageEditor")
.on_action(cx.listener(
move |this, _: &menu::Cancel, _, cx| {
this.open_feedback_editors.remove(&message_id);
cx.notify();
},
))
.on_action(cx.listener(
move |this, _: &menu::Confirm, _, cx| {
this.submit_feedback_message(message_id, cx);
cx.notify();
},
))
.on_action(cx.listener(Self::confirm_editing_message))
.mb_2()
.mx_4()
.p_2()
.rounded_md()
.border_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().editor_background)
.child(feedback_editor.clone())
.child(
h_flex()
.gap_1()
.justify_end()
.child(
Button::new(
"dismiss-feedback-message",
"Cancel",
)
.label_size(LabelSize::Small)
.key_binding(
KeyBinding::for_action_in(
&menu::Cancel,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(10.))),
)
.on_click(cx.listener(
move |this, _, _window, cx| {
this.open_feedback_editors
.remove(&message_id);
cx.notify();
},
)),
)
.child(
Button::new(
"submit-feedback-message",
"Share Feedback",
)
.style(ButtonStyle::Tinted(
ui::TintColor::Accent,
))
.label_size(LabelSize::Small)
.key_binding(
KeyBinding::for_action_in(
&menu::Confirm,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(10.))),
)
.on_click(cx.listener(
move |this, _, _window, cx| {
this.submit_feedback_message(
message_id, cx,
);
cx.notify()
},
)),
),
),
)
},
)
})
.when(after_editing_message, |parent| {
// Backdrop to dim out the whole thread below the editing user message
parent.relative().child(
div()
.occlude()
.absolute()
.inset_0()
.size_full()
.bg(panel_background)
.opacity(0.8),
)
}),
)
.into_any()
}
@@ -2355,22 +2383,19 @@ impl ActiveThread {
let editor_bg = cx.theme().colors().editor_background;
move |el, range, metadata, _, cx| {
let can_expand = metadata.line_count
> MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK;
if !can_expand {
return el;
}
let is_expanded = active_thread
.read(cx)
.expanded_code_blocks
.get(&(message_id, range.start))
.copied()
.unwrap_or(false);
if is_expanded {
.unwrap_or(true);
if is_expanded
|| metadata.line_count
<= MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK
{
return el;
}
el.child(
div()
.absolute()
@@ -2396,7 +2421,6 @@ impl ActiveThread {
markdown_element.code_block_renderer(
markdown::CodeBlockRenderer::Default {
copy_button: false,
copy_button_on_hover: false,
border: true,
},
)
@@ -2716,7 +2740,6 @@ impl ActiveThread {
)
.code_block_renderer(markdown::CodeBlockRenderer::Default {
copy_button: false,
copy_button_on_hover: false,
border: false,
})
.on_url_click({
@@ -2747,7 +2770,6 @@ impl ActiveThread {
)
.code_block_renderer(markdown::CodeBlockRenderer::Default {
copy_button: false,
copy_button_on_hover: false,
border: false,
})
.on_url_click({
@@ -3254,18 +3276,15 @@ impl ActiveThread {
.map(|tool_use| tool_use.status.clone())
{
self.thread.update(cx, |thread, cx| {
if let Some(configured) = thread.get_or_init_configured_model(cx) {
thread.run_tool(
c.tool_use_id.clone(),
c.ui_text.clone(),
c.input.clone(),
c.request.clone(),
c.tool.clone(),
configured.model,
Some(window.window_handle()),
cx,
);
}
thread.run_tool(
c.tool_use_id.clone(),
c.ui_text.clone(),
c.input.clone(),
&c.messages,
c.tool.clone(),
Some(window.window_handle()),
cx,
);
});
}
}
@@ -3438,14 +3457,12 @@ pub(crate) fn open_active_thread_as_markdown(
workspace.update_in(cx, |workspace, window, cx| {
let thread = thread.read(cx);
let markdown = thread.to_markdown(cx)?;
let thread_summary = thread.summary().or_default().to_string();
let thread_summary = thread
.summary()
.map(|summary| summary.to_string())
.unwrap_or_else(|| "Thread".to_string());
let project = workspace.project().clone();
if !project.read(cx).is_local() {
anyhow::bail!("failed to open active thread as markdown in remote project");
}
let buffer = project.update(cx, |project, cx| {
project.create_local_buffer(&markdown, Some(markdown_language), cx)
});
@@ -3523,7 +3540,7 @@ pub(crate) fn open_context(
}
AgentContextHandle::Thread(thread_context) => workspace.update(cx, |workspace, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
panel.update(cx, |panel, cx| {
panel.open_thread(thread_context.thread.clone(), window, cx);
});
@@ -3532,7 +3549,7 @@ pub(crate) fn open_context(
AgentContextHandle::TextThread(text_thread_context) => {
workspace.update(cx, |workspace, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
panel.update(cx, |panel, cx| {
panel.open_prompt_editor(text_thread_context.context.clone(), window, cx)
});
@@ -3576,3 +3593,152 @@ fn open_editor_at_position(
}
})
}
#[cfg(test)]
mod tests {
use assistant_tool::{ToolRegistry, ToolWorkingSet};
use editor::EditorSettings;
use fs::FakeFs;
use gpui::{TestAppContext, VisualTestContext};
use language_model::{LanguageModel, fake_provider::FakeLanguageModel};
use project::Project;
use prompt_store::PromptBuilder;
use serde_json::json;
use settings::SettingsStore;
use util::path;
use crate::{ContextLoadResult, thread_store};
use super::*;
#[gpui::test]
async fn test_current_completion_cancelled_when_message_edited(cx: &mut TestAppContext) {
init_test_settings(cx);
let project = create_test_project(
cx,
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
)
.await;
let (cx, active_thread, thread, model) = setup_test_environment(cx, project.clone()).await;
// Insert user message without any context (empty context vector)
let message = thread.update(cx, |thread, cx| {
let message_id = thread.insert_user_message(
"What is the best way to learn Rust?",
ContextLoadResult::default(),
None,
vec![],
cx,
);
thread
.message(message_id)
.expect("message should exist")
.clone()
});
// Stream response to user message
thread.update(cx, |thread, cx| {
let request = thread.to_completion_request(model.clone(), cx);
thread.stream_completion(request, model, cx.active_window(), cx)
});
let generating = thread.update(cx, |thread, _cx| thread.is_generating());
assert!(generating, "There should be one pending completion");
// Edit the previous message
active_thread.update_in(cx, |active_thread, window, cx| {
active_thread.start_editing_message(message.id, &message.segments, window, cx);
});
// Check that the stream was cancelled
let generating = thread.update(cx, |thread, _cx| thread.is_generating());
assert!(!generating, "The completion should have been cancelled");
}
fn init_test_settings(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
Project::init_settings(cx);
AssistantSettings::register(cx);
prompt_store::init(cx);
thread_store::init(cx);
workspace::init_settings(cx);
language_model::init_settings(cx);
ThemeSettings::register(cx);
EditorSettings::register(cx);
ToolRegistry::default_global(cx);
});
}
// Helper to create a test project with test files
async fn create_test_project(
cx: &mut TestAppContext,
files: serde_json::Value,
) -> Entity<Project> {
let fs = FakeFs::new(cx.executor());
fs.insert_tree(path!("/test"), files).await;
Project::test(fs, [path!("/test").as_ref()], cx).await
}
async fn setup_test_environment(
cx: &mut TestAppContext,
project: Entity<Project>,
) -> (
&mut VisualTestContext,
Entity<ActiveThread>,
Entity<Thread>,
Arc<dyn LanguageModel>,
) {
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let thread_store = cx
.update(|_, cx| {
ThreadStore::load(
project.clone(),
cx.new(|_| ToolWorkingSet::default()),
None,
prompt_builder.clone(),
cx,
)
})
.await
.unwrap();
let text_thread_store = cx
.update(|_, cx| {
TextThreadStore::new(project.clone(), prompt_builder, Default::default(), cx)
})
.await
.unwrap();
let thread = thread_store.update(cx, |store, cx| store.create_thread(cx));
let context_store = cx.new(|_cx| ContextStore::new(project.downgrade(), None));
let model = FakeLanguageModel::default();
let model: Arc<dyn LanguageModel> = Arc::new(model);
let language_registry = LanguageRegistry::new(cx.executor());
let language_registry = Arc::new(language_registry);
let active_thread = cx.update(|window, cx| {
cx.new(|cx| {
ActiveThread::new(
thread.clone(),
thread_store.clone(),
text_thread_store.clone(),
context_store.clone(),
language_registry.clone(),
workspace.downgrade(),
window,
cx,
)
})
});
(cx, active_thread, thread, model)
}
}

View File

@@ -1,4 +1,6 @@
use crate::{Keep, KeepAll, OpenAgentDiff, Reject, RejectAll, Thread, ThreadEvent};
use crate::{
Keep, KeepAll, OpenAgentDiff, Reject, RejectAll, Thread, ThreadEvent, ui::AnimatedLabel,
};
use anyhow::Result;
use assistant_settings::AssistantSettings;
use buffer_diff::DiffHunkStatus;
@@ -9,9 +11,8 @@ use editor::{
scroll::Autoscroll,
};
use gpui::{
Action, Animation, AnimationExt, AnyElement, AnyView, App, AppContext, Empty, Entity,
EventEmitter, FocusHandle, Focusable, Global, SharedString, Subscription, Task, Transformation,
WeakEntity, Window, percentage, prelude::*,
Action, AnyElement, AnyView, App, AppContext, Empty, Entity, EventEmitter, FocusHandle,
Focusable, Global, SharedString, Subscription, Task, WeakEntity, Window, prelude::*,
};
use language::{Buffer, Capability, DiskState, OffsetRangeExt, Point};
@@ -24,7 +25,6 @@ use std::{
collections::hash_map::Entry,
ops::Range,
sync::Arc,
time::Duration,
};
use ui::{IconButtonShape, KeyBinding, Tooltip, prelude::*, vertical_divider};
use util::ResultExt;
@@ -215,7 +215,11 @@ impl AgentDiffPane {
}
fn update_title(&mut self, cx: &mut Context<Self>) {
let new_title = self.thread.read(cx).summary().unwrap_or("Agent Changes");
let new_title = self
.thread
.read(cx)
.summary()
.unwrap_or("Agent Changes".into());
if new_title != self.title {
self.title = new_title;
cx.emit(EditorEvent::TitleChanged);
@@ -465,7 +469,11 @@ impl Item for AgentDiffPane {
}
fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
let summary = self.thread.read(cx).summary().unwrap_or("Agent Changes");
let summary = self
.thread
.read(cx)
.summary()
.unwrap_or("Agent Changes".into());
Label::new(format!("Review: {}", summary))
.color(if params.selected {
Color::Default
@@ -970,20 +978,9 @@ impl ToolbarItemView for AgentDiffToolbar {
impl Render for AgentDiffToolbar {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let spinner_icon = div()
.px_0p5()
.id("generating")
.tooltip(Tooltip::text("Generating Changes…"))
.child(
Icon::new(IconName::LoadCircle)
.size(IconSize::Small)
.color(Color::Accent)
.with_animation(
"load_circle",
Animation::new(Duration::from_secs(3)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
),
)
let generating_label = div()
.w(rems_from_px(110.)) // Arbitrary size so the label doesn't dance around
.child(AnimatedLabel::new("Generating"))
.into_any();
let Some(active_item) = self.active_item.as_ref() else {
@@ -1000,7 +997,7 @@ impl Render for AgentDiffToolbar {
let content = match state {
EditorState::Idle => return Empty.into_any(),
EditorState::Generating => vec![spinner_icon],
EditorState::Generating => vec![generating_label],
EditorState::Reviewing => vec![
h_flex()
.child(
@@ -1118,7 +1115,7 @@ impl Render for AgentDiffToolbar {
let is_generating = agent_diff.read(cx).thread.read(cx).is_generating();
if is_generating {
return div().px_2().child(spinner_icon).into_any();
return div().px_2().child(generating_label).into_any();
}
let is_empty = agent_diff.read(cx).multibuffer.read(cx).is_empty();

View File

@@ -0,0 +1,160 @@
mod active_thread;
mod agent_diff;
mod assistant_configuration;
mod assistant_model_selector;
mod assistant_panel;
mod buffer_codegen;
mod context;
mod context_picker;
mod context_server_configuration;
mod context_server_tool;
mod context_store;
mod context_strip;
mod debug;
mod history_store;
mod inline_assistant;
mod inline_prompt_editor;
mod message_editor;
mod profile_selector;
mod terminal_codegen;
mod terminal_inline_assistant;
mod thread;
mod thread_history;
mod thread_store;
mod tool_compatibility;
mod tool_use;
mod ui;
use std::sync::Arc;
use assistant_settings::{AgentProfileId, AssistantSettings};
use client::Client;
use command_palette_hooks::CommandPaletteFilter;
use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt};
use fs::Fs;
use gpui::{App, actions, impl_actions};
use language::LanguageRegistry;
use prompt_store::PromptBuilder;
use schemars::JsonSchema;
use serde::Deserialize;
use settings::Settings as _;
use thread::ThreadId;
pub use crate::active_thread::ActiveThread;
use crate::assistant_configuration::{AddContextServerModal, ManageProfilesModal};
pub use crate::assistant_panel::{AssistantPanel, ConcreteAssistantPanelDelegate};
pub use crate::context::{ContextLoadResult, LoadedContext};
pub use crate::inline_assistant::InlineAssistant;
pub use crate::thread::{Message, MessageSegment, Thread, ThreadEvent};
pub use crate::thread_store::{TextThreadStore, ThreadStore};
pub use agent_diff::{AgentDiffPane, AgentDiffToolbar};
pub use context_store::ContextStore;
pub use ui::preview::{all_agent_previews, get_agent_preview};
actions!(
agent,
[
NewTextThread,
ToggleContextPicker,
ToggleNavigationMenu,
ToggleOptionsMenu,
DeleteRecentlyOpenThread,
ToggleProfileSelector,
RemoveAllContext,
ExpandMessageEditor,
OpenHistory,
AddContextServer,
RemoveSelectedThread,
Chat,
CycleNextInlineAssist,
CyclePreviousInlineAssist,
FocusUp,
FocusDown,
FocusLeft,
FocusRight,
RemoveFocusedContext,
AcceptSuggestedContext,
OpenActiveThreadAsMarkdown,
OpenAgentDiff,
Keep,
Reject,
RejectAll,
KeepAll,
Follow,
ResetTrialUpsell,
]
);
#[derive(Default, Clone, PartialEq, Deserialize, JsonSchema)]
pub struct NewThread {
#[serde(default)]
from_thread_id: Option<ThreadId>,
}
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema)]
pub struct ManageProfiles {
#[serde(default)]
pub customize_tools: Option<AgentProfileId>,
}
impl ManageProfiles {
pub fn customize_tools(profile_id: AgentProfileId) -> Self {
Self {
customize_tools: Some(profile_id),
}
}
}
impl_actions!(agent, [NewThread, ManageProfiles]);
const NAMESPACE: &str = "agent";
/// Initializes the `agent` crate.
pub fn init(
fs: Arc<dyn Fs>,
client: Arc<Client>,
prompt_builder: Arc<PromptBuilder>,
language_registry: Arc<LanguageRegistry>,
cx: &mut App,
) {
AssistantSettings::register(cx);
thread_store::init(cx);
assistant_panel::init(cx);
context_server_configuration::init(language_registry, cx);
inline_assistant::init(
fs.clone(),
prompt_builder.clone(),
client.telemetry().clone(),
cx,
);
terminal_inline_assistant::init(
fs.clone(),
prompt_builder.clone(),
client.telemetry().clone(),
cx,
);
cx.observe_new(AddContextServerModal::register).detach();
cx.observe_new(ManageProfilesModal::register).detach();
feature_gate_agent_actions(cx);
}
fn feature_gate_agent_actions(cx: &mut App) {
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.hide_namespace(NAMESPACE);
});
cx.observe_flag::<Assistant2FeatureFlag, _>(move |is_enabled, cx| {
if is_enabled {
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.show_namespace(NAMESPACE);
});
} else {
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.hide_namespace(NAMESPACE);
});
}
})
.detach();
}

View File

@@ -30,7 +30,7 @@ pub(crate) use manage_profiles_modal::ManageProfilesModal;
use crate::AddContextServer;
pub struct AgentConfiguration {
pub struct AssistantConfiguration {
fs: Arc<dyn Fs>,
focus_handle: FocusHandle,
configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>,
@@ -42,7 +42,7 @@ pub struct AgentConfiguration {
scrollbar_state: ScrollbarState,
}
impl AgentConfiguration {
impl AssistantConfiguration {
pub fn new(
fs: Arc<dyn Fs>,
context_server_store: Entity<ContextServerStore>,
@@ -110,7 +110,7 @@ impl AgentConfiguration {
}
}
impl Focusable for AgentConfiguration {
impl Focusable for AssistantConfiguration {
fn focus_handle(&self, _: &App) -> FocusHandle {
self.focus_handle.clone()
}
@@ -120,9 +120,9 @@ pub enum AssistantConfigurationEvent {
NewThread(Arc<dyn LanguageModelProvider>),
}
impl EventEmitter<AssistantConfigurationEvent> for AgentConfiguration {}
impl EventEmitter<AssistantConfigurationEvent> for AssistantConfiguration {}
impl AgentConfiguration {
impl AssistantConfiguration {
fn render_provider_configuration_block(
&mut self,
provider: &Arc<dyn LanguageModelProvider>,
@@ -571,7 +571,7 @@ impl AgentConfiguration {
}
}
impl Render for AgentConfiguration {
impl Render for AssistantConfiguration {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.id("assistant-configuration")

View File

@@ -18,9 +18,9 @@ use ui::{
use util::ResultExt as _;
use workspace::{ModalView, Workspace};
use crate::agent_configuration::manage_profiles_modal::profile_modal_header::ProfileModalHeader;
use crate::agent_configuration::tool_picker::{ToolPicker, ToolPickerDelegate};
use crate::{AgentPanel, ManageProfiles, ThreadStore};
use crate::assistant_configuration::manage_profiles_modal::profile_modal_header::ProfileModalHeader;
use crate::assistant_configuration::tool_picker::{ToolPicker, ToolPickerDelegate};
use crate::{AssistantPanel, ManageProfiles, ThreadStore};
use super::tool_picker::ToolPickerMode;
@@ -115,7 +115,7 @@ impl ManageProfilesModal {
_cx: &mut Context<Workspace>,
) {
workspace.register_action(|workspace, action: &ManageProfiles, window, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
let fs = workspace.app_state().fs.clone();
let thread_store = panel.read(cx).thread_store();
let tools = thread_store.read(cx).tools();
@@ -124,7 +124,7 @@ impl ManageProfilesModal {
let mut this = Self::new(fs, tools, thread_store, window, cx);
if let Some(profile_id) = action.customize_tools.clone() {
this.configure_builtin_tools(profile_id, window, cx);
this.configure_tools(profile_id, window, cx);
}
this
@@ -190,7 +190,7 @@ impl ManageProfilesModal {
self.focus_handle(cx).focus(window);
}
fn configure_mcp_tools(
fn configure_mcps(
&mut self,
profile_id: AgentProfileId,
window: &mut Window,
@@ -228,7 +228,7 @@ impl ManageProfilesModal {
self.focus_handle(cx).focus(window);
}
fn configure_builtin_tools(
fn configure_tools(
&mut self,
profile_id: AgentProfileId,
window: &mut Window,
@@ -581,20 +581,16 @@ impl ManageProfilesModal {
)
.child(
div()
.id("configure-builtin-tools")
.id("configure-tools")
.track_focus(&mode.configure_tools.focus_handle)
.on_action({
let profile_id = mode.profile_id.clone();
cx.listener(move |this, _: &menu::Confirm, window, cx| {
this.configure_builtin_tools(
profile_id.clone(),
window,
cx,
);
this.configure_tools(profile_id.clone(), window, cx);
})
})
.child(
ListItem::new("configure-builtin-tools-item")
ListItem::new("configure-tools")
.toggle_state(
mode.configure_tools
.focus_handle
@@ -607,11 +603,11 @@ impl ManageProfilesModal {
.size(IconSize::Small)
.color(Color::Muted),
)
.child(Label::new("Configure Built-in Tools"))
.child(Label::new("Configure Tools"))
.on_click({
let profile_id = mode.profile_id.clone();
cx.listener(move |this, _, window, cx| {
this.configure_builtin_tools(
this.configure_tools(
profile_id.clone(),
window,
cx,
@@ -627,11 +623,11 @@ impl ManageProfilesModal {
.on_action({
let profile_id = mode.profile_id.clone();
cx.listener(move |this, _: &menu::Confirm, window, cx| {
this.configure_mcp_tools(profile_id.clone(), window, cx);
this.configure_mcps(profile_id.clone(), window, cx);
})
})
.child(
ListItem::new("configure-mcp-tools")
ListItem::new("configure-mcps")
.toggle_state(
mode.configure_mcps
.focus_handle
@@ -644,15 +640,11 @@ impl ManageProfilesModal {
.size(IconSize::Small)
.color(Color::Muted),
)
.child(Label::new("Configure MCP Tools"))
.child(Label::new("Configure MCP Servers"))
.on_click({
let profile_id = mode.profile_id.clone();
cx.listener(move |this, _, window, cx| {
this.configure_mcp_tools(
profile_id.clone(),
window,
cx,
);
this.configure_mcps(profile_id.clone(), window, cx);
})
}),
),
@@ -785,7 +777,7 @@ impl Render for ManageProfilesModal {
v_flex()
.pb_1()
.child(ProfileModalHeader::new(
format!("{profile_name} — Configure Built-in Tools"),
format!("{profile_name} — Configure Tools"),
Some(IconName::Cog),
))
.child(ListSeparator)
@@ -808,7 +800,7 @@ impl Render for ManageProfilesModal {
v_flex()
.pb_1()
.child(ProfileModalHeader::new(
format!("{profile_name} — Configure MCP Tools"),
format!("{profile_name} — Configure MCP Servers"),
Some(IconName::Hammer),
))
.child(ListSeparator)

View File

@@ -176,7 +176,7 @@ impl PickerDelegate for ToolPickerDelegate {
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
match self.mode {
ToolPickerMode::BuiltinTools => "Search built-in tools…",
ToolPickerMode::McpTools => "Search MCP tools…",
ToolPickerMode::McpTools => "Search MCP servers…",
}
.into()
}

View File

@@ -17,13 +17,13 @@ pub enum ModelType {
InlineAssistant,
}
pub struct AgentModelSelector {
pub struct AssistantModelSelector {
selector: Entity<LanguageModelSelector>,
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
focus_handle: FocusHandle,
}
impl AgentModelSelector {
impl AssistantModelSelector {
pub(crate) fn new(
fs: Arc<dyn Fs>,
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
@@ -99,7 +99,7 @@ impl AgentModelSelector {
}
}
impl Render for AgentModelSelector {
impl Render for AssistantModelSelector {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let focus_handle = self.focus_handle.clone();

View File

@@ -9,9 +9,9 @@ use serde::{Deserialize, Serialize};
use anyhow::{Result, anyhow};
use assistant_context_editor::{
AgentPanelDelegate, AssistantContext, ConfigurationError, ContextEditor, ContextEvent,
ContextSummary, SlashCommandCompletionProvider, humanize_token_count,
make_lsp_adapter_delegate, render_remaining_tokens,
AssistantContext, AssistantPanelDelegate, ConfigurationError, ContextEditor, ContextEvent,
SlashCommandCompletionProvider, humanize_token_count, make_lsp_adapter_delegate,
render_remaining_tokens,
};
use assistant_settings::{AssistantDockPosition, AssistantSettings};
use assistant_slash_command::SlashCommandWorkingSet;
@@ -39,30 +39,26 @@ use search::{BufferSearchBar, buffer_search};
use settings::{Settings, update_settings_file};
use theme::ThemeSettings;
use time::UtcOffset;
use ui::utils::WithRemSize;
use ui::{
Banner, CheckboxWithLabel, ContextMenu, KeyBinding, PopoverMenu, PopoverMenuHandle,
ProgressBar, Tab, Tooltip, Vector, VectorName, prelude::*,
};
use util::{ResultExt as _, maybe};
use workspace::dock::{DockPosition, Panel, PanelEvent};
use workspace::{
CollaboratorId, DraggedSelection, DraggedTab, ToggleZoom, ToolbarItemView, Workspace,
};
use zed_actions::agent::{OpenConfiguration, OpenOnboardingModal, ResetOnboarding};
use workspace::{CollaboratorId, DraggedSelection, DraggedTab, ToolbarItemView, Workspace};
use zed_actions::agent::OpenConfiguration;
use zed_actions::assistant::{OpenRulesLibrary, ToggleFocus};
use zed_actions::{DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize};
use zed_llm_client::UsageLimit;
use crate::active_thread::{self, ActiveThread, ActiveThreadEvent};
use crate::agent_configuration::{AgentConfiguration, AssistantConfigurationEvent};
use crate::agent_diff::AgentDiff;
use crate::history_store::{HistoryStore, RecentEntry};
use crate::assistant_configuration::{AssistantConfiguration, AssistantConfigurationEvent};
use crate::history_store::{HistoryEntry, HistoryStore, RecentEntry};
use crate::message_editor::{MessageEditor, MessageEditorEvent};
use crate::thread::{Thread, ThreadError, ThreadId, ThreadSummary, TokenUsageRatio};
use crate::thread_history::{HistoryEntryElement, ThreadHistory};
use crate::thread::{Thread, ThreadError, ThreadId, TokenUsageRatio};
use crate::thread_history::{EntryTimeFormat, PastContext, PastThread, ThreadHistory};
use crate::thread_store::ThreadStore;
use crate::ui::AgentOnboardingModal;
use crate::{
AddContextServer, AgentDiffPane, ContextStore, DeleteRecentlyOpenThread, ExpandMessageEditor,
Follow, InlineAssistant, NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff,
@@ -73,7 +69,7 @@ use crate::{
const AGENT_PANEL_KEY: &str = "agent_panel";
#[derive(Serialize, Deserialize)]
struct SerializedAgentPanel {
struct SerializedAssistantPanel {
width: Option<Pixels>,
}
@@ -82,40 +78,40 @@ pub fn init(cx: &mut App) {
|workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
workspace
.register_action(|workspace, action: &NewThread, window, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
panel.update(cx, |panel, cx| panel.new_thread(action, window, cx));
workspace.focus_panel::<AgentPanel>(window, cx);
workspace.focus_panel::<AssistantPanel>(window, cx);
}
})
.register_action(|workspace, _: &OpenHistory, window, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx);
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
workspace.focus_panel::<AssistantPanel>(window, cx);
panel.update(cx, |panel, cx| panel.open_history(window, cx));
}
})
.register_action(|workspace, _: &OpenConfiguration, window, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx);
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
workspace.focus_panel::<AssistantPanel>(window, cx);
panel.update(cx, |panel, cx| panel.open_configuration(window, cx));
}
})
.register_action(|workspace, _: &NewTextThread, window, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx);
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
workspace.focus_panel::<AssistantPanel>(window, cx);
panel.update(cx, |panel, cx| panel.new_prompt_editor(window, cx));
}
})
.register_action(|workspace, action: &OpenRulesLibrary, window, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx);
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
workspace.focus_panel::<AssistantPanel>(window, cx);
panel.update(cx, |panel, cx| {
panel.deploy_rules_library(action, window, cx)
});
}
})
.register_action(|workspace, _: &OpenAgentDiff, window, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx);
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
workspace.focus_panel::<AssistantPanel>(window, cx);
let thread = panel.read(cx).thread.read(cx).thread().clone();
AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx);
}
@@ -124,8 +120,8 @@ pub fn init(cx: &mut App) {
workspace.follow(CollaboratorId::Agent, window, cx);
})
.register_action(|workspace, _: &ExpandMessageEditor, window, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx);
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
workspace.focus_panel::<AssistantPanel>(window, cx);
panel.update(cx, |panel, cx| {
panel.message_editor.update(cx, |editor, cx| {
editor.expand_message_editor(&ExpandMessageEditor, window, cx);
@@ -134,28 +130,21 @@ pub fn init(cx: &mut App) {
}
})
.register_action(|workspace, _: &ToggleNavigationMenu, window, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx);
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
workspace.focus_panel::<AssistantPanel>(window, cx);
panel.update(cx, |panel, cx| {
panel.toggle_navigation_menu(&ToggleNavigationMenu, window, cx);
});
}
})
.register_action(|workspace, _: &ToggleOptionsMenu, window, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx);
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
workspace.focus_panel::<AssistantPanel>(window, cx);
panel.update(cx, |panel, cx| {
panel.toggle_options_menu(&ToggleOptionsMenu, window, cx);
});
}
})
.register_action(|workspace, _: &OpenOnboardingModal, window, cx| {
AgentOnboardingModal::toggle(workspace, window, cx)
})
.register_action(|_workspace, _: &ResetOnboarding, window, cx| {
window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx);
window.refresh();
})
.register_action(|_workspace, _: &ResetTrialUpsell, _window, cx| {
set_trial_upsell_dismissed(false, cx);
});
@@ -180,23 +169,9 @@ enum ActiveView {
Configuration,
}
enum WhichFontSize {
AgentFont,
BufferFont,
None,
}
impl ActiveView {
pub fn which_font_size_used(&self) -> WhichFontSize {
match self {
ActiveView::Thread { .. } | ActiveView::History => WhichFontSize::AgentFont,
ActiveView::PromptEditor { .. } => WhichFontSize::BufferFont,
ActiveView::Configuration => WhichFontSize::None,
}
}
pub fn thread(thread: Entity<Thread>, window: &mut Window, cx: &mut App) -> Self {
let summary = thread.read(cx).summary().or_default();
let summary = thread.read(cx).summary_or_default();
let editor = cx.new(|cx| {
let mut editor = Editor::single_line(window, cx);
@@ -218,7 +193,7 @@ impl ActiveView {
}
EditorEvent::Blurred => {
if editor.read(cx).text(cx).is_empty() {
let summary = thread.read(cx).summary().or_default();
let summary = thread.read(cx).summary_or_default();
editor.update(cx, |editor, cx| {
editor.set_text(summary, window, cx);
@@ -233,7 +208,7 @@ impl ActiveView {
let editor = editor.clone();
move |thread, event, window, cx| match event {
ThreadEvent::SummaryGenerated => {
let summary = thread.read(cx).summary().or_default();
let summary = thread.read(cx).summary_or_default();
editor.update(cx, |editor, cx| {
editor.set_text(summary, window, cx);
@@ -296,8 +271,7 @@ impl ActiveView {
.read(cx)
.context()
.read(cx)
.summary()
.or_default();
.summary_or_default();
editor.update(cx, |editor, cx| {
editor.set_text(summary, window, cx);
@@ -312,7 +286,7 @@ impl ActiveView {
let editor = editor.clone();
move |assistant_context, event, window, cx| match event {
ContextEvent::SummaryGenerated => {
let summary = assistant_context.read(cx).summary().or_default();
let summary = assistant_context.read(cx).summary_or_default();
editor.update(cx, |editor, cx| {
editor.set_text(summary, window, cx);
@@ -338,7 +312,7 @@ impl ActiveView {
}
}
pub struct AgentPanel {
pub struct AssistantPanel {
workspace: WeakEntity<Workspace>,
user_store: Entity<UserStore>,
project: Entity<Project>,
@@ -352,33 +326,31 @@ pub struct AgentPanel {
context_store: Entity<TextThreadStore>,
prompt_store: Option<Entity<PromptStore>>,
inline_assist_context_store: Entity<crate::context_store::ContextStore>,
configuration: Option<Entity<AgentConfiguration>>,
configuration: Option<Entity<AssistantConfiguration>>,
configuration_subscription: Option<Subscription>,
local_timezone: UtcOffset,
active_view: ActiveView,
previous_view: Option<ActiveView>,
history_store: Entity<HistoryStore>,
history: Entity<ThreadHistory>,
hovered_recent_history_item: Option<usize>,
assistant_dropdown_menu_handle: PopoverMenuHandle<ContextMenu>,
assistant_navigation_menu_handle: PopoverMenuHandle<ContextMenu>,
assistant_navigation_menu: Option<Entity<ContextMenu>>,
width: Option<Pixels>,
height: Option<Pixels>,
zoomed: bool,
pending_serialization: Option<Task<Result<()>>>,
hide_trial_upsell: bool,
_trial_markdown: Entity<Markdown>,
}
impl AgentPanel {
impl AssistantPanel {
fn serialize(&mut self, cx: &mut Context<Self>) {
let width = self.width;
self.pending_serialization = Some(cx.background_spawn(async move {
KEY_VALUE_STORE
.write_kvp(
AGENT_PANEL_KEY.into(),
serde_json::to_string(&SerializedAgentPanel { width })?,
serde_json::to_string(&SerializedAssistantPanel { width })?,
)
.await?;
anyhow::Ok(())
@@ -428,7 +400,7 @@ impl AgentPanel {
.log_err()
.flatten()
{
Some(serde_json::from_str::<SerializedAgentPanel>(&panel)?)
Some(serde_json::from_str::<SerializedAssistantPanel>(&panel)?)
} else {
None
};
@@ -496,6 +468,7 @@ impl AgentPanel {
thread_store.downgrade(),
context_store.downgrade(),
thread.clone(),
agent_panel_dock_position(cx),
window,
cx,
)
@@ -514,7 +487,6 @@ impl AgentPanel {
thread_store.clone(),
context_store.clone(),
[RecentEntry::Thread(thread_id, thread.clone())],
window,
cx,
)
});
@@ -701,13 +673,11 @@ impl AgentPanel {
previous_view: None,
history_store: history_store.clone(),
history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, window, cx)),
hovered_recent_history_item: None,
assistant_dropdown_menu_handle: PopoverMenuHandle::default(),
assistant_navigation_menu_handle: PopoverMenuHandle::default(),
assistant_navigation_menu: None,
width: None,
height: None,
zoomed: false,
pending_serialization: None,
hide_trial_upsell: false,
_trial_markdown: trial_markdown,
@@ -771,9 +741,9 @@ impl AgentPanel {
});
if let Some(other_thread_id) = action.from_thread_id.clone() {
let other_thread_task = self.thread_store.update(cx, |this, cx| {
this.open_thread(&other_thread_id, window, cx)
});
let other_thread_task = self
.thread_store
.update(cx, |this, cx| this.open_thread(&other_thread_id, cx));
cx.spawn({
let context_store = context_store.clone();
@@ -828,6 +798,7 @@ impl AgentPanel {
self.thread_store.downgrade(),
self.context_store.downgrade(),
thread,
agent_panel_dock_position(cx),
window,
cx,
)
@@ -973,7 +944,7 @@ impl AgentPanel {
) -> Task<Result<()>> {
let open_thread_task = self
.thread_store
.update(cx, |this, cx| this.open_thread(thread_id, window, cx));
.update(cx, |this, cx| this.open_thread(thread_id, cx));
cx.spawn_in(window, async move |this, cx| {
let thread = open_thread_task.await?;
this.update_in(cx, |this, window, cx| {
@@ -1036,6 +1007,7 @@ impl AgentPanel {
self.thread_store.downgrade(),
self.context_store.downgrade(),
thread,
agent_panel_dock_position(cx),
window,
cx,
)
@@ -1092,7 +1064,7 @@ impl AgentPanel {
_: &mut Window,
cx: &mut Context<Self>,
) {
self.handle_font_size_action(action.persist, px(1.0), cx);
self.adjust_font_size(action.persist, px(1.0), cx);
}
pub fn decrease_font_size(
@@ -1101,36 +1073,21 @@ impl AgentPanel {
_: &mut Window,
cx: &mut Context<Self>,
) {
self.handle_font_size_action(action.persist, px(-1.0), cx);
self.adjust_font_size(action.persist, px(-1.0), cx);
}
fn handle_font_size_action(&mut self, persist: bool, delta: Pixels, cx: &mut Context<Self>) {
match self.active_view.which_font_size_used() {
WhichFontSize::AgentFont => {
if persist {
update_settings_file::<ThemeSettings>(
self.fs.clone(),
cx,
move |settings, cx| {
let agent_font_size =
ThemeSettings::get_global(cx).agent_font_size(cx) + delta;
let _ = settings
.agent_font_size
.insert(theme::clamp_font_size(agent_font_size).0);
},
);
} else {
theme::adjust_agent_font_size(cx, |size| {
*size += delta;
});
}
}
WhichFontSize::BufferFont => {
// Prompt editor uses the buffer font size, so allow the action to propagate to the
// default handler that changes that font size.
cx.propagate();
}
WhichFontSize::None => {}
fn adjust_font_size(&mut self, persist: bool, delta: Pixels, cx: &mut Context<Self>) {
if persist {
update_settings_file::<ThemeSettings>(self.fs.clone(), cx, move |settings, cx| {
let agent_font_size = ThemeSettings::get_global(cx).agent_font_size(cx) + delta;
let _ = settings
.agent_font_size
.insert(theme::clamp_font_size(agent_font_size).0);
});
} else {
theme::adjust_agent_font_size(cx, |size| {
*size += delta;
});
}
}
@@ -1149,17 +1106,6 @@ impl AgentPanel {
}
}
pub fn toggle_zoom(&mut self, _: &ToggleZoom, window: &mut Window, cx: &mut Context<Self>) {
if self.zoomed {
cx.emit(PanelEvent::ZoomOut);
} else {
if !self.focus_handle(cx).contains_focused(window, cx) {
cx.focus_self(window);
}
cx.emit(PanelEvent::ZoomIn);
}
}
pub fn open_agent_diff(
&mut self,
_: &OpenAgentDiff,
@@ -1181,13 +1127,15 @@ impl AgentPanel {
self.set_active_view(ActiveView::Configuration, window, cx);
self.configuration =
Some(cx.new(|cx| AgentConfiguration::new(fs, context_server_store, tools, window, cx)));
Some(cx.new(|cx| {
AssistantConfiguration::new(fs, context_server_store, tools, window, cx)
}));
if let Some(configuration) = self.configuration.as_ref() {
self.configuration_subscription = Some(cx.subscribe_in(
configuration,
window,
Self::handle_agent_configuration_event,
Self::handle_assistant_configuration_event,
));
configuration.focus_handle(cx).focus(window);
@@ -1217,9 +1165,9 @@ impl AgentPanel {
.detach_and_log_err(cx);
}
fn handle_agent_configuration_event(
fn handle_assistant_configuration_event(
&mut self,
_entity: &Entity<AgentConfiguration>,
_entity: &Entity<AssistantConfiguration>,
event: &AssistantConfigurationEvent,
window: &mut Window,
cx: &mut Context<Self>,
@@ -1332,7 +1280,7 @@ impl AgentPanel {
}
}
impl Focusable for AgentPanel {
impl Focusable for AssistantPanel {
fn focus_handle(&self, cx: &App) -> FocusHandle {
match &self.active_view {
ActiveView::Thread { .. } => self.message_editor.focus_handle(cx),
@@ -1357,9 +1305,9 @@ fn agent_panel_dock_position(cx: &App) -> DockPosition {
}
}
impl EventEmitter<PanelEvent> for AgentPanel {}
impl EventEmitter<PanelEvent> for AssistantPanel {}
impl Panel for AgentPanel {
impl Panel for AssistantPanel {
fn persistent_name() -> &'static str {
"AgentPanel"
}
@@ -1373,6 +1321,10 @@ impl Panel for AgentPanel {
}
fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
self.message_editor.update(cx, |message_editor, cx| {
message_editor.set_dock_position(position, cx);
});
settings::update_settings_file::<AssistantSettings>(
self.fs.clone(),
cx,
@@ -1432,18 +1384,9 @@ impl Panel for AgentPanel {
fn enabled(&self, cx: &App) -> bool {
AssistantSettings::get_global(cx).enabled
}
fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
self.zoomed
}
fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, cx: &mut Context<Self>) {
self.zoomed = zoomed;
cx.notify();
}
}
impl AgentPanel {
impl AssistantPanel {
fn render_title_view(&self, _window: &mut Window, cx: &Context<Self>) -> AnyElement {
const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…";
@@ -1453,45 +1396,23 @@ impl AgentPanel {
..
} => {
let active_thread = self.thread.read(cx);
let state = if active_thread.is_empty() {
&ThreadSummary::Pending
} else {
active_thread.summary(cx)
};
let is_empty = active_thread.is_empty();
match state {
ThreadSummary::Pending => Label::new(ThreadSummary::DEFAULT.clone())
let summary = active_thread.summary(cx);
if is_empty {
Label::new(Thread::DEFAULT_SUMMARY.clone())
.truncate()
.into_any_element(),
ThreadSummary::Generating => Label::new(LOADING_SUMMARY_PLACEHOLDER)
.into_any_element()
} else if summary.is_none() {
Label::new(LOADING_SUMMARY_PLACEHOLDER)
.truncate()
.into_any_element(),
ThreadSummary::Ready(_) => div()
.into_any_element()
} else {
div()
.w_full()
.child(change_title_editor.clone())
.into_any_element(),
ThreadSummary::Error => h_flex()
.w_full()
.child(change_title_editor.clone())
.child(
ui::IconButton::new("retry-summary-generation", IconName::RotateCcw)
.on_click({
let active_thread = self.thread.clone();
move |_, _window, cx| {
active_thread.update(cx, |thread, cx| {
thread.regenerate_summary(cx);
});
}
})
.tooltip(move |_window, cx| {
cx.new(|_| {
Tooltip::new("Failed to generate title")
.meta("Click to try again")
})
.into()
}),
)
.into_any_element(),
.into_any_element()
}
}
ActiveView::PromptEditor {
@@ -1499,13 +1420,14 @@ impl AgentPanel {
context_editor,
..
} => {
let summary = context_editor.read(cx).context().read(cx).summary();
let context_editor = context_editor.read(cx);
let summary = context_editor.context().read(cx).summary();
match summary {
ContextSummary::Pending => Label::new(ContextSummary::DEFAULT)
None => Label::new(AssistantContext::DEFAULT_SUMMARY.clone())
.truncate()
.into_any_element(),
ContextSummary::Content(summary) => {
Some(summary) => {
if summary.done {
div()
.w_full()
@@ -1517,28 +1439,6 @@ impl AgentPanel {
.into_any_element()
}
}
ContextSummary::Error => h_flex()
.w_full()
.child(title_editor.clone())
.child(
ui::IconButton::new("retry-summary-generation", IconName::RotateCcw)
.on_click({
let context_editor = context_editor.clone();
move |_, _window, cx| {
context_editor.update(cx, |context_editor, cx| {
context_editor.regenerate_summary(cx);
});
}
})
.tooltip(move |_window, cx| {
cx.new(|_| {
Tooltip::new("Failed to generate title")
.meta("Click to try again")
})
.into()
}),
)
.into_any_element(),
}
}
ActiveView::History => Label::new("History").truncate().into_any_element(),
@@ -1562,7 +1462,6 @@ impl AgentPanel {
let thread = active_thread.thread().read(cx);
let thread_id = thread.id().clone();
let is_empty = active_thread.is_empty();
let editor_empty = self.message_editor.read(cx).is_editor_fully_empty(cx);
let last_usage = active_thread.thread().read(cx).last_usage().or_else(|| {
maybe!({
let amount = user_store.model_request_usage_amount()?;
@@ -1585,7 +1484,7 @@ impl AgentPanel {
let account_url = zed_urls::account_url(cx);
let show_token_count = match &self.active_view {
ActiveView::Thread { .. } => !is_empty || !editor_empty,
ActiveView::Thread { .. } => !is_empty,
ActiveView::PromptEditor { .. } => true,
_ => false,
};
@@ -1905,10 +1804,6 @@ impl AgentPanel {
}
fn should_render_upsell(&self, cx: &mut Context<Self>) -> bool {
if !matches!(self.active_view, ActiveView::Thread { .. }) {
return false;
}
if self.hide_trial_upsell || dismissed_trial_upsell() {
return false;
}
@@ -2045,9 +1940,9 @@ impl AgentPanel {
.style(ButtonStyle::Transparent)
.color(Color::Muted)
.on_click({
let agent_panel = cx.entity();
let assistant_panel = cx.entity();
move |_, _, cx| {
agent_panel.update(
assistant_panel.update(
cx,
|this, cx| {
let hidden =
@@ -2282,7 +2177,7 @@ impl AgentPanel {
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.child(
Label::new("Recent")
Label::new("Past Interactions")
.size(LabelSize::Small)
.color(Color::Muted),
)
@@ -2307,20 +2202,18 @@ impl AgentPanel {
v_flex()
.gap_1()
.children(
recent_history.into_iter().enumerate().map(|(index, entry)| {
recent_history.into_iter().map(|entry| {
// TODO: Add keyboard navigation.
let is_hovered = self.hovered_recent_history_item == Some(index);
HistoryEntryElement::new(entry.clone(), cx.entity().downgrade())
.hovered(is_hovered)
.on_hover(cx.listener(move |this, is_hovered, _window, cx| {
if *is_hovered {
this.hovered_recent_history_item = Some(index);
} else if this.hovered_recent_history_item == Some(index) {
this.hovered_recent_history_item = None;
}
cx.notify();
}))
.into_any_element()
match entry {
HistoryEntry::Thread(thread) => {
PastThread::new(thread, cx.entity().downgrade(), false, vec![], EntryTimeFormat::DateAndTime)
.into_any_element()
}
HistoryEntry::Context(context) => {
PastContext::new(context, cx.entity().downgrade(), false, vec![], EntryTimeFormat::DateAndTime)
.into_any_element()
}
}
}),
)
)
@@ -2408,13 +2301,14 @@ impl AgentPanel {
""
};
let banner = Banner::new()
.severity(ui::Severity::Info)
.child(h_flex().child(Label::new(format!(
"Consecutive tool use limit reached.{max_mode_upsell}"
))));
Some(div().px_2().pb_2().child(banner).into_any_element())
Some(
Banner::new()
.severity(ui::Severity::Info)
.child(h_flex().child(Label::new(format!(
"Consecutive tool use limit reached.{max_mode_upsell}"
))))
.into_any_element(),
)
}
fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
@@ -2653,46 +2547,6 @@ impl AgentPanel {
.into_any()
}
fn render_prompt_editor(
&self,
context_editor: &Entity<ContextEditor>,
buffer_search_bar: &Entity<BufferSearchBar>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Div {
let mut registrar = buffer_search::DivRegistrar::new(
|this, _, _cx| match &this.active_view {
ActiveView::PromptEditor {
buffer_search_bar, ..
} => Some(buffer_search_bar.clone()),
_ => None,
},
cx,
);
BufferSearchBar::register(&mut registrar);
registrar
.into_div()
.size_full()
.relative()
.map(|parent| {
buffer_search_bar.update(cx, |buffer_search_bar, cx| {
if buffer_search_bar.is_dismissed() {
return parent;
}
parent.child(
div()
.p(DynamicSpacing::Base08.rems(cx))
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.bg(cx.theme().colors().editor_background)
.child(buffer_search_bar.render(window, cx)),
)
})
})
.child(context_editor.clone())
.child(self.render_drag_target(cx))
}
fn render_drag_target(&self, cx: &Context<Self>) -> Div {
let is_local = self.project.read(cx).is_local();
div()
@@ -2814,18 +2668,9 @@ impl AgentPanel {
}
}
impl Render for AgentPanel {
impl Render for AssistantPanel {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
// WARNING: Changes to this element hierarchy can have
// non-obvious implications to the layout of children.
//
// If you need to change it, please confirm:
// - The message editor expands (⌘esc) correctly
// - When expanded, the buttons at the bottom of the panel are displayed correctly
// - Font size works as expected and can be changed with ⌘+/⌘-
// - Scrolling in all views works as expected
// - Files can be dropped into the panel
let content = v_flex()
v_flex()
.key_context(self.key_context())
.justify_between()
.size_full()
@@ -2848,40 +2693,62 @@ impl Render for AgentPanel {
.on_action(cx.listener(Self::increase_font_size))
.on_action(cx.listener(Self::decrease_font_size))
.on_action(cx.listener(Self::reset_font_size))
.on_action(cx.listener(Self::toggle_zoom))
.child(self.render_toolbar(window, cx))
.children(self.render_trial_upsell(window, cx))
.map(|parent| match &self.active_view {
ActiveView::Thread { .. } => parent
.relative()
.child(self.render_active_thread_or_empty_state(window, cx))
.children(self.render_tool_use_limit_reached(cx))
.child(h_flex().child(self.message_editor.clone()))
.children(self.render_last_error(cx))
.child(self.render_drag_target(cx)),
ActiveView::Thread { .. } => parent.child(
v_flex()
.relative()
.justify_between()
.size_full()
.child(self.render_active_thread_or_empty_state(window, cx))
.children(self.render_tool_use_limit_reached(cx))
.child(h_flex().child(self.message_editor.clone()))
.children(self.render_last_error(cx))
.child(self.render_drag_target(cx)),
),
ActiveView::History => parent.child(self.history.clone()),
ActiveView::PromptEditor {
context_editor,
buffer_search_bar,
..
} => parent.child(self.render_prompt_editor(
context_editor,
buffer_search_bar,
window,
cx,
)),
} => {
let mut registrar = buffer_search::DivRegistrar::new(
|this, _, _cx| match &this.active_view {
ActiveView::PromptEditor {
buffer_search_bar, ..
} => Some(buffer_search_bar.clone()),
_ => None,
},
cx,
);
BufferSearchBar::register(&mut registrar);
parent.child(
registrar
.into_div()
.size_full()
.relative()
.map(|parent| {
buffer_search_bar.update(cx, |buffer_search_bar, cx| {
if buffer_search_bar.is_dismissed() {
return parent;
}
parent.child(
div()
.p(DynamicSpacing::Base08.rems(cx))
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.bg(cx.theme().colors().editor_background)
.child(buffer_search_bar.render(window, cx)),
)
})
})
.child(context_editor.clone())
.child(self.render_drag_target(cx)),
)
}
ActiveView::Configuration => parent.children(self.configuration.clone()),
});
match self.active_view.which_font_size_used() {
WhichFontSize::AgentFont => {
WithRemSize::new(ThemeSettings::get_global(cx).agent_font_size(cx))
.size_full()
.child(content)
.into_any()
}
_ => content.into_any(),
}
})
}
}
@@ -2930,26 +2797,28 @@ impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
})
}
fn focus_agent_panel(
fn focus_assistant_panel(
&self,
workspace: &mut Workspace,
window: &mut Window,
cx: &mut Context<Workspace>,
) -> bool {
workspace.focus_panel::<AgentPanel>(window, cx).is_some()
workspace
.focus_panel::<AssistantPanel>(window, cx)
.is_some()
}
}
pub struct ConcreteAssistantPanelDelegate;
impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
fn active_context_editor(
&self,
workspace: &mut Workspace,
_window: &mut Window,
cx: &mut Context<Workspace>,
) -> Option<Entity<ContextEditor>> {
let panel = workspace.panel::<AgentPanel>(cx)?;
let panel = workspace.panel::<AssistantPanel>(cx)?;
panel.read(cx).active_context_editor()
}
@@ -2960,7 +2829,7 @@ impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
window: &mut Window,
cx: &mut Context<Workspace>,
) -> Task<Result<()>> {
let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
return Task::ready(Err(anyhow!("Agent panel not found")));
};
@@ -2987,12 +2856,12 @@ impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
window: &mut Window,
cx: &mut Context<Workspace>,
) {
let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
return;
};
if !panel.focus_handle(cx).contains_focused(window, cx) {
workspace.toggle_panel_focus::<AgentPanel>(window, cx);
workspace.toggle_panel_focus::<AssistantPanel>(window, cx);
}
panel.update(cx, |_, cx| {

View File

@@ -2,7 +2,6 @@ use crate::context::ContextLoadResult;
use crate::inline_prompt_editor::CodegenStatus;
use crate::{context::load_context, context_store::ContextStore};
use anyhow::Result;
use assistant_settings::AssistantSettings;
use client::telemetry::Telemetry;
use collections::HashSet;
use editor::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint};
@@ -384,7 +383,7 @@ impl CodegenAlternative {
if user_prompt.trim().to_lowercase() == "delete" {
async { Ok(LanguageModelTextStream::default()) }.boxed_local()
} else {
let request = self.build_request(&model, user_prompt, cx)?;
let request = self.build_request(user_prompt, cx)?;
cx.spawn(async move |_, cx| model.stream_completion_text(request.await, &cx).await)
.boxed_local()
};
@@ -394,7 +393,6 @@ impl CodegenAlternative {
fn build_request(
&self,
model: &Arc<dyn LanguageModel>,
user_prompt: String,
cx: &mut App,
) -> Result<Task<LanguageModelRequest>> {
@@ -443,8 +441,6 @@ impl CodegenAlternative {
}
});
let temperature = AssistantSettings::temperature_for_model(&model, cx);
Ok(cx.spawn(async move |_cx| {
let mut request_message = LanguageModelRequestMessage {
role: Role::User,
@@ -466,9 +462,8 @@ impl CodegenAlternative {
prompt_id: None,
mode: None,
tools: Vec::new(),
tool_choice: None,
stop: Vec::new(),
temperature,
temperature: None,
messages: vec![request_message],
}
}))

View File

@@ -586,7 +586,10 @@ impl ThreadContextHandle {
}
pub fn title(&self, cx: &App) -> SharedString {
self.thread.read(cx).summary().or_default()
self.thread
.read(cx)
.summary()
.unwrap_or_else(|| "New thread".into())
}
fn load(self, cx: &App) -> Task<Option<(AgentContext, Vec<Entity<Buffer>>)>> {
@@ -594,7 +597,9 @@ impl ThreadContextHandle {
let text = Thread::wait_for_detailed_summary_or_text(&self.thread, cx).await?;
let title = self
.thread
.read_with(cx, |thread, _cx| thread.summary().or_default())
.read_with(cx, |thread, _cx| {
thread.summary().unwrap_or_else(|| "New thread".into())
})
.ok()?;
let context = AgentContext::Thread(ThreadContext {
title,
@@ -637,7 +642,7 @@ impl TextThreadContextHandle {
}
pub fn title(&self, cx: &App) -> SharedString {
self.context.read(cx).summary().or_default()
self.context.read(cx).summary_or_default()
}
fn load(self, cx: &App) -> Task<Option<(AgentContext, Vec<Entity<Buffer>>)>> {
@@ -749,11 +754,11 @@ pub enum ImageStatus {
impl ImageContext {
pub fn eq_for_key(&self, other: &Self) -> bool {
self.original_image.id() == other.original_image.id()
self.original_image.id == other.original_image.id
}
pub fn hash_for_key<H: Hasher>(&self, state: &mut H) {
self.original_image.id().hash(state);
self.original_image.id.hash(state);
}
pub fn image(&self) -> Option<LanguageModelImage> {
@@ -825,20 +830,23 @@ pub fn load_context(
prompt_store: &Option<Entity<PromptStore>>,
cx: &mut App,
) -> Task<ContextLoadResult> {
let load_tasks: Vec<_> = contexts
.into_iter()
.map(|context| match context {
AgentContextHandle::File(context) => context.load(cx),
AgentContextHandle::Directory(context) => context.load(project.clone(), cx),
AgentContextHandle::Symbol(context) => context.load(cx),
AgentContextHandle::Selection(context) => context.load(cx),
AgentContextHandle::FetchedUrl(context) => context.load(),
AgentContextHandle::Thread(context) => context.load(cx),
AgentContextHandle::TextThread(context) => context.load(cx),
AgentContextHandle::Rules(context) => context.load(prompt_store, cx),
AgentContextHandle::Image(context) => context.load(cx),
})
.collect();
let mut load_tasks = Vec::new();
for context in contexts.iter().cloned() {
match context {
AgentContextHandle::File(context) => load_tasks.push(context.load(cx)),
AgentContextHandle::Directory(context) => {
load_tasks.push(context.load(project.clone(), cx))
}
AgentContextHandle::Symbol(context) => load_tasks.push(context.load(cx)),
AgentContextHandle::Selection(context) => load_tasks.push(context.load(cx)),
AgentContextHandle::FetchedUrl(context) => load_tasks.push(context.load()),
AgentContextHandle::Thread(context) => load_tasks.push(context.load(cx)),
AgentContextHandle::TextThread(context) => load_tasks.push(context.load(cx)),
AgentContextHandle::Rules(context) => load_tasks.push(context.load(prompt_store, cx)),
AgentContextHandle::Image(context) => load_tasks.push(context.load(cx)),
}
}
cx.background_spawn(async move {
let load_results = future::join_all(load_tasks).await;

View File

@@ -36,7 +36,7 @@ use ui::{
use uuid::Uuid;
use workspace::{Workspace, notifications::NotifyResultExt};
use crate::AgentPanel;
use crate::AssistantPanel;
use crate::context::RULES_ICON;
use crate::context_store::ContextStore;
use crate::thread::ThreadId;
@@ -381,16 +381,6 @@ impl ContextPicker {
cx.focus_self(window);
}
pub fn select_first(&mut self, window: &mut Window, cx: &mut Context<Self>) {
match &self.mode {
ContextPickerState::Default(entity) => entity.update(cx, |entity, cx| {
entity.select_first(&Default::default(), window, cx)
}),
// Other variants already select their first entry on open automatically
_ => {}
}
}
fn recent_menu_item(
&self,
context_picker: Entity<ContextPicker>,
@@ -435,9 +425,9 @@ impl ContextPicker {
render_thread_context_entry(&view_thread, context_store.clone(), cx)
.into_any()
},
move |window, cx| {
move |_window, cx| {
context_picker.update(cx, |this, cx| {
this.add_recent_thread(thread.clone(), window, cx)
this.add_recent_thread(thread.clone(), cx)
.detach_and_log_err(cx);
})
},
@@ -469,7 +459,6 @@ impl ContextPicker {
fn add_recent_thread(
&self,
entry: ThreadContextEntry,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let Some(context_store) = self.context_store.upgrade() else {
@@ -487,7 +476,7 @@ impl ContextPicker {
};
let open_thread_task =
thread_store.update(cx, |this, cx| this.open_thread(&id, window, cx));
thread_store.update(cx, |this, cx| this.open_thread(&id, cx));
cx.spawn(async move |this, cx| {
let thread = open_thread_task.await?;
context_store.update(cx, |context_store, cx| {
@@ -658,7 +647,7 @@ fn recent_context_picker_entries(
let current_threads = context_store.read(cx).thread_ids();
let active_thread_id = workspace
.panel::<AgentPanel>(cx)
.panel::<AssistantPanel>(cx)
.and_then(|panel| Some(panel.read(cx).active_thread()?.read(cx).id()));
if let Some((thread_store, text_thread_store)) = thread_store

View File

@@ -438,15 +438,15 @@ impl ContextPickerCompletionProvider {
new_text_len,
editor.clone(),
context_store.clone(),
move |window, cx| match &thread_entry {
move |cx| match &thread_entry {
ThreadContextEntry::Thread { id, .. } => {
let thread_id = id.clone();
let context_store = context_store.clone();
let thread_store = thread_store.clone();
window.spawn::<_, Option<_>>(cx, async move |cx| {
cx.spawn::<_, Option<_>>(async move |cx| {
let thread: Entity<Thread> = thread_store
.update_in(cx, |thread_store, window, cx| {
thread_store.open_thread(&thread_id, window, cx)
.update(cx, |thread_store, cx| {
thread_store.open_thread(&thread_id, cx)
})
.ok()?
.await
@@ -507,7 +507,7 @@ impl ContextPickerCompletionProvider {
new_text_len,
editor.clone(),
context_store.clone(),
move |_, cx| {
move |cx| {
let user_prompt_id = rules.prompt_id;
let context = context_store.update(cx, |context_store, cx| {
context_store.add_rules(user_prompt_id, false, cx)
@@ -544,7 +544,7 @@ impl ContextPickerCompletionProvider {
new_text_len,
editor.clone(),
context_store.clone(),
move |_, cx| {
move |cx| {
let context_store = context_store.clone();
let http_client = http_client.clone();
let url_to_fetch = url_to_fetch.clone();
@@ -629,7 +629,7 @@ impl ContextPickerCompletionProvider {
new_text_len,
editor,
context_store.clone(),
move |_, cx| {
move |cx| {
if is_directory {
Task::ready(
context_store
@@ -700,7 +700,7 @@ impl ContextPickerCompletionProvider {
new_text_len,
editor.clone(),
context_store.clone(),
move |_, cx| {
move |cx| {
let symbol = symbol.clone();
let context_store = context_store.clone();
let workspace = workspace.clone();
@@ -954,13 +954,10 @@ fn confirm_completion_callback(
content_len: usize,
editor: Entity<Editor>,
context_store: Entity<ContextStore>,
add_context_fn: impl Fn(&mut Window, &mut App) -> Task<Option<AgentContextHandle>>
+ Send
+ Sync
+ 'static,
add_context_fn: impl Fn(&mut App) -> Task<Option<AgentContextHandle>> + Send + Sync + 'static,
) -> Arc<dyn Fn(CompletionIntent, &mut Window, &mut App) -> bool + Send + Sync> {
Arc::new(move |_, window, cx| {
let context = add_context_fn(window, cx);
let context = add_context_fn(cx);
let crease_text = crease_text.clone();
let crease_icon_path = crease_icon_path.clone();

View File

@@ -154,7 +154,7 @@ impl PickerDelegate for ThreadContextPickerDelegate {
})
}
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
let Some(entry) = self.matches.get(self.selected_index) else {
return;
};
@@ -165,7 +165,7 @@ impl PickerDelegate for ThreadContextPickerDelegate {
return;
};
let open_thread_task =
thread_store.update(cx, |this, cx| this.open_thread(&id, window, cx));
thread_store.update(cx, |this, cx| this.open_thread(&id, cx));
cx.spawn(async move |this, cx| {
let thread = open_thread_task.await?;

View File

@@ -10,7 +10,7 @@ use ui::prelude::*;
use util::ResultExt;
use workspace::Workspace;
use crate::agent_configuration::ConfigureContextServerModal;
use crate::assistant_configuration::ConfigureContextServerModal;
pub(crate) fn init(language_registry: Arc<LanguageRegistry>, cx: &mut App) {
cx.observe_new(move |_: &mut Workspace, window, cx| {

View File

@@ -4,7 +4,7 @@ use anyhow::{Result, anyhow, bail};
use assistant_tool::{ActionLog, Tool, ToolResult, ToolSource};
use context_server::{ContextServerId, types};
use gpui::{AnyWindowHandle, App, Entity, Task};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::{Project, context_server_store::ContextServerStore};
use ui::IconName;
@@ -72,10 +72,9 @@ impl Tool for ContextServerTool {
fn run(
self: Arc<Self>,
input: serde_json::Value,
_request: Arc<LanguageModelRequest>,
_messages: &[LanguageModelRequestMessage],
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
_model: Arc<dyn LanguageModel>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
@@ -116,7 +115,7 @@ impl Tool for ContextServerTool {
}
}
}
Ok(result.into())
Ok(result)
})
.into()
} else {

View File

@@ -22,7 +22,7 @@ use crate::thread::Thread;
use crate::thread_store::{TextThreadStore, ThreadStore};
use crate::ui::{AddedContext, ContextPill};
use crate::{
AcceptSuggestedContext, AgentPanel, FocusDown, FocusLeft, FocusRight, FocusUp,
AcceptSuggestedContext, AssistantPanel, FocusDown, FocusLeft, FocusRight, FocusUp,
RemoveAllContext, RemoveFocusedContext, ToggleContextPicker,
};
@@ -144,7 +144,7 @@ impl ContextStrip {
}
let workspace = self.workspace.upgrade()?;
let panel = workspace.read(cx).panel::<AgentPanel>(cx)?.read(cx);
let panel = workspace.read(cx).panel::<AssistantPanel>(cx)?.read(cx);
if let Some(active_thread) = panel.active_thread() {
let weak_active_thread = active_thread.downgrade();
@@ -160,7 +160,7 @@ impl ContextStrip {
}
Some(SuggestedContext::Thread {
name: active_thread.summary().or_default(),
name: active_thread.summary_or_default(),
thread: weak_active_thread,
})
} else if let Some(active_context_editor) = panel.active_context_editor() {
@@ -174,7 +174,7 @@ impl ContextStrip {
}
Some(SuggestedContext::TextThread {
name: context.summary().or_default(),
name: context.summary_or_default(),
context: weak_context,
})
} else {
@@ -420,25 +420,12 @@ impl Render for ContextStrip {
})
.child(
PopoverMenu::new("context-picker")
.menu({
let context_picker = context_picker.clone();
move |window, cx| {
context_picker.update(cx, |this, cx| {
this.init(window, cx);
});
.menu(move |window, cx| {
context_picker.update(cx, |this, cx| {
this.init(window, cx);
});
Some(context_picker.clone())
}
})
.on_open({
let context_picker = context_picker.downgrade();
Rc::new(move |window, cx| {
context_picker
.update(cx, |context_picker, cx| {
context_picker.select_first(window, cx);
})
.ok();
})
Some(context_picker.clone())
})
.trigger_with_tooltip(
IconButton::new("add-context", IconName::Plus)

View File

@@ -75,7 +75,7 @@ impl Default for DebugAccountState {
Self {
enabled: false,
trial_expired: false,
plan: Plan::ZedFree,
plan: Plan::Free,
custom_prompt_usage: RequestUsage {
limit: UsageLimit::Unlimited,
amount: 0,

View File

@@ -1,4 +1,4 @@
use std::{collections::VecDeque, path::Path, sync::Arc};
use std::{collections::VecDeque, path::Path};
use anyhow::{Context as _, anyhow};
use assistant_context_editor::{AssistantContext, SavedContextMetadata};
@@ -8,7 +8,7 @@ use gpui::{Entity, Task, prelude::*};
use serde::{Deserialize, Serialize};
use smol::future::FutureExt;
use std::time::Duration;
use ui::{App, SharedString, Window};
use ui::{App, SharedString};
use util::ResultExt as _;
use crate::{
@@ -34,20 +34,6 @@ impl HistoryEntry {
HistoryEntry::Context(context) => context.mtime.to_utc(),
}
}
pub fn id(&self) -> HistoryEntryId {
match self {
HistoryEntry::Thread(thread) => HistoryEntryId::Thread(thread.id.clone()),
HistoryEntry::Context(context) => HistoryEntryId::Context(context.path.clone()),
}
}
}
/// Generic identifier for a history entry.
#[derive(Clone, PartialEq, Eq)]
pub enum HistoryEntryId {
Thread(ThreadId),
Context(Arc<Path>),
}
#[derive(Clone, Debug)]
@@ -71,8 +57,8 @@ impl Eq for RecentEntry {}
impl RecentEntry {
pub(crate) fn summary(&self, cx: &App) -> SharedString {
match self {
RecentEntry::Thread(_, thread) => thread.read(cx).summary().or_default(),
RecentEntry::Context(context) => context.read(cx).summary().or_default(),
RecentEntry::Thread(_, thread) => thread.read(cx).summary_or_default(),
RecentEntry::Context(context) => context.read(cx).summary_or_default(),
}
}
}
@@ -96,7 +82,6 @@ impl HistoryStore {
thread_store: Entity<ThreadStore>,
context_store: Entity<assistant_context_editor::ContextStore>,
initial_recent_entries: impl IntoIterator<Item = RecentEntry>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let subscriptions = vec![
@@ -104,62 +89,56 @@ impl HistoryStore {
cx.observe(&context_store, |_, _, cx| cx.notify()),
];
window
.spawn(cx, {
let thread_store = thread_store.downgrade();
let context_store = context_store.downgrade();
let this = cx.weak_entity();
async move |cx| {
let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH);
let contents = cx
.background_spawn(async move { std::fs::read_to_string(path) })
.await
.ok()?;
let entries = serde_json::from_str::<Vec<SerializedRecentEntry>>(&contents)
.context("deserializing persisted agent panel navigation history")
.log_err()?
.into_iter()
.take(MAX_RECENTLY_OPENED_ENTRIES)
.map(|serialized| match serialized {
SerializedRecentEntry::Thread(id) => thread_store
.update_in(cx, |thread_store, window, cx| {
let thread_id = ThreadId::from(id.as_str());
thread_store
.open_thread(&thread_id, window, cx)
.map_ok(|thread| RecentEntry::Thread(thread_id, thread))
.boxed()
})
.unwrap_or_else(|_| {
async { Err(anyhow!("no thread store")) }.boxed()
}),
SerializedRecentEntry::Context(id) => context_store
.update(cx, |context_store, cx| {
context_store
.open_local_context(Path::new(&id).into(), cx)
.map_ok(RecentEntry::Context)
.boxed()
})
.unwrap_or_else(|_| {
async { Err(anyhow!("no context store")) }.boxed()
}),
});
let entries = join_all(entries)
.await
.into_iter()
.filter_map(|result| result.log_err())
.collect::<VecDeque<_>>();
cx.spawn({
let thread_store = thread_store.downgrade();
let context_store = context_store.downgrade();
async move |this, cx| {
let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH);
let contents = cx
.background_spawn(async move { std::fs::read_to_string(path) })
.await
.ok()?;
let entries = serde_json::from_str::<Vec<SerializedRecentEntry>>(&contents)
.context("deserializing persisted agent panel navigation history")
.log_err()?
.into_iter()
.take(MAX_RECENTLY_OPENED_ENTRIES)
.map(|serialized| match serialized {
SerializedRecentEntry::Thread(id) => thread_store
.update(cx, |thread_store, cx| {
let thread_id = ThreadId::from(id.as_str());
thread_store
.open_thread(&thread_id, cx)
.map_ok(|thread| RecentEntry::Thread(thread_id, thread))
.boxed()
})
.unwrap_or_else(|_| async { Err(anyhow!("no thread store")) }.boxed()),
SerializedRecentEntry::Context(id) => context_store
.update(cx, |context_store, cx| {
context_store
.open_local_context(Path::new(&id).into(), cx)
.map_ok(RecentEntry::Context)
.boxed()
})
.unwrap_or_else(|_| async { Err(anyhow!("no context store")) }.boxed()),
});
let entries = join_all(entries)
.await
.into_iter()
.filter_map(|result| result.log_err())
.collect::<VecDeque<_>>();
this.update(cx, |this, _| {
this.recently_opened_entries.extend(entries);
this.recently_opened_entries
.truncate(MAX_RECENTLY_OPENED_ENTRIES);
})
.ok();
this.update(cx, |this, _| {
this.recently_opened_entries.extend(entries);
this.recently_opened_entries
.truncate(MAX_RECENTLY_OPENED_ENTRIES);
})
.ok();
Some(())
}
})
.detach();
Some(())
}
})
.detach();
Self {
thread_store,

View File

@@ -8,16 +8,16 @@ use anyhow::{Context as _, Result};
use assistant_settings::AssistantSettings;
use client::telemetry::Telemetry;
use collections::{HashMap, HashSet, VecDeque, hash_map};
use editor::display_map::EditorMargins;
use editor::{
Anchor, AnchorRangeExt, CodeActionProvider, Editor, EditorEvent, ExcerptId, ExcerptRange,
MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint,
GutterDimensions, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint,
actions::SelectAll,
display_map::{
BlockContext, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, RenderBlock,
ToDisplayPoint,
},
};
use feature_flags::{Assistant2FeatureFlag, FeatureFlagViewExt as _};
use fs::Fs;
use gpui::{
App, Context, Entity, Focusable, Global, HighlightStyle, Subscription, Task, UpdateGlobal,
@@ -43,7 +43,7 @@ use util::ResultExt;
use workspace::{ItemHandle, Toast, Workspace, dock::Panel, notifications::NotificationId};
use zed_actions::agent::OpenConfiguration;
use crate::AgentPanel;
use crate::AssistantPanel;
use crate::buffer_codegen::{BufferCodegen, CodegenAlternative, CodegenEvent};
use crate::context_store::ContextStore;
use crate::inline_prompt_editor::{CodegenStatus, InlineAssistId, PromptEditor, PromptEditorEvent};
@@ -66,6 +66,15 @@ pub fn init(
InlineAssistant::update_global(cx, |inline_assistant, cx| {
inline_assistant.register_workspace(&workspace, window, cx)
});
cx.observe_flag::<Assistant2FeatureFlag, _>(window, {
|is_assistant2_enabled, _workspace, _window, cx| {
InlineAssistant::update_global(cx, |inline_assistant, _cx| {
inline_assistant.is_assistant2_enabled = is_assistant2_enabled;
});
}
})
.detach();
})
.detach();
}
@@ -88,6 +97,7 @@ pub struct InlineAssistant {
prompt_builder: Arc<PromptBuilder>,
telemetry: Arc<Telemetry>,
fs: Arc<dyn Fs>,
is_assistant2_enabled: bool,
}
impl Global for InlineAssistant {}
@@ -109,6 +119,7 @@ impl InlineAssistant {
prompt_builder,
telemetry,
fs,
is_assistant2_enabled: false,
}
}
@@ -177,17 +188,18 @@ impl InlineAssistant {
window: &mut Window,
cx: &mut App,
) {
let is_assistant2_enabled = true;
let is_assistant2_enabled = self.is_assistant2_enabled;
if let Some(editor) = item.act_as::<Editor>(cx) {
editor.update(cx, |editor, cx| {
if is_assistant2_enabled {
let panel = workspace.read(cx).panel::<AgentPanel>(cx);
let panel = workspace.read(cx).panel::<AssistantPanel>(cx);
let thread_store = panel
.as_ref()
.map(|agent_panel| agent_panel.read(cx).thread_store().downgrade());
let text_thread_store = panel
.map(|agent_panel| agent_panel.read(cx).text_thread_store().downgrade());
.map(|assistant_panel| assistant_panel.read(cx).thread_store().downgrade());
let text_thread_store = panel.map(|assistant_panel| {
assistant_panel.read(cx).text_thread_store().downgrade()
});
editor.add_code_action_provider(
Rc::new(AssistantCodeActionProvider {
@@ -226,7 +238,7 @@ impl InlineAssistant {
let Some(inline_assist_target) = Self::resolve_inline_assist_target(
workspace,
workspace.panel::<AgentPanel>(cx),
workspace.panel::<AssistantPanel>(cx),
window,
cx,
) else {
@@ -239,15 +251,15 @@ impl InlineAssistant {
.map_or(false, |model| model.provider.is_authenticated(cx))
};
let Some(agent_panel) = workspace.panel::<AgentPanel>(cx) else {
let Some(assistant_panel) = workspace.panel::<AssistantPanel>(cx) else {
return;
};
let agent_panel = agent_panel.read(cx);
let assistant_panel = assistant_panel.read(cx);
let prompt_store = agent_panel.prompt_store().as_ref().cloned();
let thread_store = Some(agent_panel.thread_store().downgrade());
let text_thread_store = Some(agent_panel.text_thread_store().downgrade());
let context_store = agent_panel.inline_assist_context_store().clone();
let prompt_store = assistant_panel.prompt_store().as_ref().cloned();
let thread_store = Some(assistant_panel.thread_store().downgrade());
let text_thread_store = Some(assistant_panel.text_thread_store().downgrade());
let context_store = assistant_panel.inline_assist_context_store().clone();
let handle_assist =
|window: &mut Window, cx: &mut Context<Workspace>| match inline_assist_target {
@@ -458,11 +470,11 @@ impl InlineAssistant {
)
});
let editor_margins = Arc::new(Mutex::new(EditorMargins::default()));
let gutter_dimensions = Arc::new(Mutex::new(GutterDimensions::default()));
let prompt_editor = cx.new(|cx| {
PromptEditor::new_buffer(
assist_id,
editor_margins,
gutter_dimensions.clone(),
self.prompt_history.clone(),
prompt_buffer.clone(),
codegen.clone(),
@@ -577,11 +589,11 @@ impl InlineAssistant {
)
});
let editor_margins = Arc::new(Mutex::new(EditorMargins::default()));
let gutter_dimensions = Arc::new(Mutex::new(GutterDimensions::default()));
let prompt_editor = cx.new(|cx| {
PromptEditor::new_buffer(
assist_id,
editor_margins,
gutter_dimensions.clone(),
self.prompt_history.clone(),
prompt_buffer.clone(),
codegen.clone(),
@@ -650,7 +662,6 @@ impl InlineAssistant {
height: Some(prompt_editor_height),
render: build_assist_editor_renderer(prompt_editor),
priority: 0,
render_in_minimap: false,
},
BlockProperties {
style: BlockStyle::Sticky,
@@ -665,7 +676,6 @@ impl InlineAssistant {
.into_any_element()
}),
priority: 0,
render_in_minimap: false,
},
];
@@ -1407,11 +1417,11 @@ impl InlineAssistant {
enum DeletedLines {}
let mut editor = Editor::for_multibuffer(multi_buffer, None, window, cx);
editor.disable_scrollbars_and_minimap(window, cx);
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx);
editor.set_show_wrap_guides(false, cx);
editor.set_show_gutter(false, cx);
editor.scroll_manager.set_forbid_vertical_scroll(true);
editor.set_show_scrollbars(false, cx);
editor.set_read_only(true);
editor.set_show_edit_predictions(Some(false), window, cx);
editor.highlight_rows::<DeletedLines>(
@@ -1435,12 +1445,11 @@ impl InlineAssistant {
.bg(cx.theme().status().deleted_background)
.size_full()
.h(height as f32 * cx.window.line_height())
.pl(cx.margins.gutter.full_width())
.pl(cx.gutter_dimensions.full_width())
.child(deleted_lines_editor.clone())
.into_any_element()
}),
priority: 0,
render_in_minimap: false,
});
}
@@ -1453,7 +1462,7 @@ impl InlineAssistant {
fn resolve_inline_assist_target(
workspace: &mut Workspace,
agent_panel: Option<Entity<AgentPanel>>,
assistant_panel: Option<Entity<AssistantPanel>>,
window: &mut Window,
cx: &mut App,
) -> Option<InlineAssistTarget> {
@@ -1473,7 +1482,7 @@ impl InlineAssistant {
}
}
let context_editor = agent_panel
let context_editor = assistant_panel
.and_then(|panel| panel.read(cx).active_context_editor())
.and_then(|editor| {
let editor = &editor.read(cx).editor().clone();
@@ -1598,9 +1607,9 @@ fn build_assist_editor_renderer(editor: &Entity<PromptEditor<BufferCodegen>>) ->
let editor = editor.clone();
Arc::new(move |cx: &mut BlockContext| {
let editor_margins = editor.read(cx).editor_margins();
let gutter_dimensions = editor.read(cx).gutter_dimensions();
*editor_margins.lock() = *cx.margins;
*gutter_dimensions.lock() = *cx.gutter_dimensions;
editor.clone().into_any_element()
})
}

View File

@@ -1,4 +1,4 @@
use crate::agent_model_selector::{AgentModelSelector, ModelType};
use crate::assistant_model_selector::{AssistantModelSelector, ModelType};
use crate::buffer_codegen::BufferCodegen;
use crate::context::ContextCreasesAddon;
use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider};
@@ -11,9 +11,9 @@ use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist};
use crate::{RemoveAllContext, ToggleContextPicker};
use client::ErrorExt;
use collections::VecDeque;
use editor::display_map::EditorMargins;
use editor::{
ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle,
GutterDimensions, MultiBuffer,
actions::{MoveDown, MoveUp},
};
use feature_flags::{FeatureFlagAppExt as _, ZedProFeatureFlag};
@@ -42,7 +42,7 @@ pub struct PromptEditor<T> {
context_store: Entity<ContextStore>,
context_strip: Entity<ContextStrip>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
model_selector: Entity<AgentModelSelector>,
model_selector: Entity<AssistantModelSelector>,
edited_since_done: bool,
prompt_history: VecDeque<String>,
prompt_history_ix: Option<usize>,
@@ -61,13 +61,11 @@ impl<T: 'static> Render for PromptEditor<T> {
let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx);
let mut buttons = Vec::new();
const RIGHT_PADDING: Pixels = px(9.);
let (left_gutter_width, right_padding) = match &self.mode {
let left_gutter_width = match &self.mode {
PromptEditorMode::Buffer {
id: _,
codegen,
editor_margins,
gutter_dimensions,
} => {
let codegen = codegen.read(cx);
@@ -75,17 +73,13 @@ impl<T: 'static> Render for PromptEditor<T> {
buttons.push(self.render_cycle_controls(&codegen, cx));
}
let editor_margins = editor_margins.lock();
let gutter = editor_margins.gutter;
let gutter_dimensions = gutter_dimensions.lock();
let left_gutter_width = gutter.full_width() + (gutter.margin / 2.0);
let right_padding = editor_margins.right + RIGHT_PADDING;
(left_gutter_width, right_padding)
gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0)
}
PromptEditorMode::Terminal { .. } => {
// Give the equivalent of the same left-padding that we're using on the right
(Pixels::from(40.0), Pixels::from(24.))
Pixels::from(40.0)
}
};
@@ -106,7 +100,7 @@ impl<T: 'static> Render for PromptEditor<T> {
.size_full()
.pt_0p5()
.pb(bottom_padding)
.pr(right_padding)
.pr_6()
.child(
h_flex()
.items_start()
@@ -290,12 +284,12 @@ impl<T: 'static> PromptEditor<T> {
PromptEditorMode::Terminal { .. } => "Generate",
};
let agent_panel_keybinding =
let assistant_panel_keybinding =
ui::text_for_action(&zed_actions::assistant::ToggleFocus, window, cx)
.map(|keybinding| format!("{keybinding} to chat ― "))
.unwrap_or_default();
format!("{action}… ({agent_panel_keybinding}↓↑ for history)")
format!("{action}… ({assistant_panel_keybinding}↓↑ for history)")
}
pub fn prompt(&self, cx: &App) -> String {
@@ -812,7 +806,7 @@ pub enum PromptEditorMode {
Buffer {
id: InlineAssistId,
codegen: Entity<BufferCodegen>,
editor_margins: Arc<Mutex<EditorMargins>>,
gutter_dimensions: Arc<Mutex<GutterDimensions>>,
},
Terminal {
id: TerminalInlineAssistId,
@@ -844,7 +838,7 @@ impl InlineAssistId {
impl PromptEditor<BufferCodegen> {
pub fn new_buffer(
id: InlineAssistId,
editor_margins: Arc<Mutex<EditorMargins>>,
gutter_dimensions: Arc<Mutex<GutterDimensions>>,
prompt_history: VecDeque<String>,
prompt_buffer: Entity<MultiBuffer>,
codegen: Entity<BufferCodegen>,
@@ -861,7 +855,7 @@ impl PromptEditor<BufferCodegen> {
let mode = PromptEditorMode::Buffer {
id,
codegen,
editor_margins,
gutter_dimensions,
};
let prompt_editor = cx.new(|cx| {
@@ -927,7 +921,7 @@ impl PromptEditor<BufferCodegen> {
context_strip,
context_picker_menu_handle,
model_selector: cx.new(|cx| {
AgentModelSelector::new(
AssistantModelSelector::new(
fs,
model_selector_menu_handle,
prompt_editor.focus_handle(cx),
@@ -1001,9 +995,11 @@ impl PromptEditor<BufferCodegen> {
}
}
pub fn editor_margins(&self) -> &Arc<Mutex<EditorMargins>> {
pub fn gutter_dimensions(&self) -> &Arc<Mutex<GutterDimensions>> {
match &self.mode {
PromptEditorMode::Buffer { editor_margins, .. } => editor_margins,
PromptEditorMode::Buffer {
gutter_dimensions, ..
} => gutter_dimensions,
PromptEditorMode::Terminal { .. } => unreachable!(),
}
}
@@ -1098,7 +1094,7 @@ impl PromptEditor<TerminalCodegen> {
context_strip,
context_picker_menu_handle,
model_selector: cx.new(|cx| {
AgentModelSelector::new(
AssistantModelSelector::new(
fs,
model_selector_menu_handle.clone(),
prompt_editor.focus_handle(cx),

View File

@@ -1,14 +1,13 @@
use std::collections::BTreeMap;
use std::sync::Arc;
use crate::agent_model_selector::{AgentModelSelector, ModelType};
use crate::assistant_model_selector::{AssistantModelSelector, ModelType};
use crate::context::{AgentContextKey, ContextCreasesAddon, ContextLoadResult, load_context};
use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip};
use crate::ui::{
AnimatedLabel, MaxModeTooltip,
preview::{AgentPreview, UsageCallout},
};
use assistant_settings::{AssistantSettings, CompletionMode};
use buffer_diff::BufferDiff;
use client::UserStore;
use collections::{HashMap, HashSet};
@@ -17,6 +16,7 @@ use editor::{
AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorEvent,
EditorMode, EditorStyle, MultiBuffer,
};
use feature_flags::{FeatureFlagAppExt, NewBillingFeatureFlag};
use file_icons::FileIcons;
use fs::Fs;
use futures::future::Shared;
@@ -38,9 +38,11 @@ use proto::Plan;
use settings::Settings;
use std::time::Duration;
use theme::ThemeSettings;
use ui::{Disclosure, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*};
use ui::{Disclosure, DocumentationSide, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*};
use util::{ResultExt as _, maybe};
use workspace::dock::DockPosition;
use workspace::{CollaboratorId, Workspace};
use zed_llm_client::CompletionMode;
use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention};
use crate::context_store::ContextStore;
@@ -65,7 +67,7 @@ pub struct MessageEditor {
prompt_store: Option<Entity<PromptStore>>,
context_strip: Entity<ContextStrip>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
model_selector: Entity<AgentModelSelector>,
model_selector: Entity<AssistantModelSelector>,
last_loaded_context: Option<ContextLoadResult>,
load_context_task: Option<Shared<Task<()>>>,
profile_selector: Entity<ProfileSelector>,
@@ -132,6 +134,14 @@ pub(crate) fn create_editor(
editor
}
fn documentation_side(position: DockPosition) -> DocumentationSide {
match position {
DockPosition::Left => DocumentationSide::Right,
DockPosition::Bottom => DocumentationSide::Left,
DockPosition::Right => DocumentationSide::Left,
}
}
impl MessageEditor {
pub fn new(
fs: Arc<dyn Fs>,
@@ -142,6 +152,7 @@ impl MessageEditor {
thread_store: WeakEntity<ThreadStore>,
text_thread_store: WeakEntity<TextThreadStore>,
thread: Entity<Thread>,
dock_position: DockPosition,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
@@ -189,7 +200,7 @@ impl MessageEditor {
];
let model_selector = cx.new(|cx| {
AgentModelSelector::new(
AssistantModelSelector::new(
fs.clone(),
model_selector_menu_handle,
editor.focus_handle(cx),
@@ -199,16 +210,6 @@ impl MessageEditor {
)
});
let profile_selector = cx.new(|cx| {
ProfileSelector::new(
fs,
thread.clone(),
thread_store,
editor.focus_handle(cx),
cx,
)
});
Self {
editor: editor.clone(),
project: thread.read(cx).project().clone(),
@@ -225,7 +226,15 @@ impl MessageEditor {
model_selector,
edits_expanded: false,
editor_is_expanded: false,
profile_selector,
profile_selector: cx.new(|cx| {
ProfileSelector::new(
fs,
thread_store,
editor.focus_handle(cx),
documentation_side(dock_position),
cx,
)
}),
last_estimated_token_count: None,
update_token_count_task: None,
_subscriptions: subscriptions,
@@ -306,10 +315,6 @@ impl MessageEditor {
self.editor.read(cx).text(cx).trim().is_empty()
}
pub fn is_editor_fully_empty(&self, cx: &App) -> bool {
self.editor.read(cx).is_empty(cx)
}
fn send_to_model(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(ConfiguredModel { model, provider }) = self
.thread
@@ -455,6 +460,10 @@ impl MessageEditor {
}
fn render_max_mode_toggle(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
if !cx.has_flag::<NewBillingFeatureFlag>() {
return None;
}
let thread = self.thread.read(cx);
let model = thread.configured_model();
if !model?.model.supports_max_mode() {
@@ -629,7 +638,7 @@ impl MessageEditor {
this.h(vh(0.8, window)).justify_between()
})
.child(
v_flex()
div()
.min_h_16()
.when(is_editor_expanded, |this| this.h_full())
.child({
@@ -1061,6 +1070,10 @@ impl MessageEditor {
}
fn render_usage_callout(&self, line_height: Pixels, cx: &mut Context<Self>) -> Option<Div> {
if !cx.has_flag::<NewBillingFeatureFlag>() {
return None;
}
let is_using_zed_provider = self
.thread
.read(cx)
@@ -1085,11 +1098,11 @@ impl MessageEditor {
let plan = user_store
.current_plan()
.map(|plan| match plan {
Plan::Free => zed_llm_client::Plan::ZedFree,
Plan::Free => zed_llm_client::Plan::Free,
Plan::ZedPro => zed_llm_client::Plan::ZedPro,
Plan::ZedProTrial => zed_llm_client::Plan::ZedProTrial,
})
.unwrap_or(zed_llm_client::Plan::ZedFree);
.unwrap_or(zed_llm_client::Plan::Free);
let usage = self.thread.read(cx).last_usage().or_else(|| {
maybe!({
let amount = user_store.model_request_usage_amount()?;
@@ -1122,6 +1135,10 @@ impl MessageEditor {
token_usage_ratio: TokenUsageRatio,
cx: &mut Context<Self>,
) -> Option<Div> {
if !cx.has_flag::<NewBillingFeatureFlag>() {
return None;
}
let title = if token_usage_ratio == TokenUsageRatio::Exceeded {
"Thread reached the token limit"
} else {
@@ -1251,9 +1268,8 @@ impl MessageEditor {
mode: None,
messages: vec![request_message],
tools: vec![],
tool_choice: None,
stop: vec![],
temperature: AssistantSettings::temperature_for_model(&model.model, cx),
temperature: None,
};
Some(model.model.count_tokens(request, cx))
@@ -1276,6 +1292,12 @@ impl MessageEditor {
.ok();
}));
}
pub fn set_dock_position(&mut self, position: DockPosition, cx: &mut Context<Self>) {
self.profile_selector.update(cx, |profile_selector, cx| {
profile_selector.set_documentation_side(documentation_side(position), cx)
});
}
}
pub fn extract_message_creases(
@@ -1449,6 +1471,7 @@ impl AgentPreview for MessageEditor {
thread_store.downgrade(),
text_thread_store.downgrade(),
thread,
DockPosition::Left,
window,
cx,
)

View File

@@ -1,8 +1,7 @@
use std::sync::Arc;
use assistant_settings::{
AgentProfile, AgentProfileId, AssistantDockPosition, AssistantSettings, GroupedAgentProfiles,
builtin_profiles,
AgentProfile, AgentProfileId, AssistantSettings, GroupedAgentProfiles, builtin_profiles,
};
use fs::Fs;
use gpui::{Action, Entity, FocusHandle, Subscription, WeakEntity, prelude::*};
@@ -14,24 +13,24 @@ use ui::{
};
use util::ResultExt as _;
use crate::{ManageProfiles, Thread, ThreadStore, ToggleProfileSelector};
use crate::{ManageProfiles, ThreadStore, ToggleProfileSelector};
pub struct ProfileSelector {
profiles: GroupedAgentProfiles,
fs: Arc<dyn Fs>,
thread: Entity<Thread>,
thread_store: WeakEntity<ThreadStore>,
menu_handle: PopoverMenuHandle<ContextMenu>,
focus_handle: FocusHandle,
_subscriptions: Vec<Subscription>,
documentation_side: DocumentationSide,
}
impl ProfileSelector {
pub fn new(
fs: Arc<dyn Fs>,
thread: Entity<Thread>,
thread_store: WeakEntity<ThreadStore>,
focus_handle: FocusHandle,
documentation_side: DocumentationSide,
cx: &mut Context<Self>,
) -> Self {
let settings_subscription = cx.observe_global::<SettingsStore>(move |this, cx| {
@@ -41,14 +40,19 @@ impl ProfileSelector {
Self {
profiles: GroupedAgentProfiles::from_settings(AssistantSettings::get_global(cx)),
fs,
thread,
thread_store,
menu_handle: PopoverMenuHandle::default(),
focus_handle,
_subscriptions: vec![settings_subscription],
documentation_side,
}
}
pub fn set_documentation_side(&mut self, side: DocumentationSide, cx: &mut Context<Self>) {
self.documentation_side = side;
cx.notify();
}
pub fn menu_handle(&self) -> PopoverMenuHandle<ContextMenu> {
self.menu_handle.clone()
}
@@ -65,12 +69,8 @@ impl ProfileSelector {
ContextMenu::build(window, cx, |mut menu, _window, cx| {
let settings = AssistantSettings::get_global(cx);
for (profile_id, profile) in self.profiles.builtin.iter() {
menu = menu.item(self.menu_entry_for_profile(
profile_id.clone(),
profile,
settings,
cx,
));
menu =
menu.item(self.menu_entry_for_profile(profile_id.clone(), profile, settings));
}
if !self.profiles.custom.is_empty() {
@@ -80,7 +80,6 @@ impl ProfileSelector {
profile_id.clone(),
profile,
settings,
cx,
));
}
}
@@ -101,7 +100,6 @@ impl ProfileSelector {
profile_id: AgentProfileId,
profile: &AgentProfile,
settings: &AssistantSettings,
_cx: &App,
) -> ContextMenuEntry {
let documentation = match profile.name.to_lowercase().as_str() {
builtin_profiles::WRITE => Some("Get help to write anything."),
@@ -114,7 +112,7 @@ impl ProfileSelector {
.toggleable(IconPosition::End, profile_id == settings.default_profile);
let entry = if let Some(doc_text) = documentation {
entry.documentation_aside(documentation_side(settings.dock), move |_| {
entry.documentation_aside(self.documentation_side, move |_| {
Label::new(doc_text).into_any_element()
})
} else {
@@ -153,67 +151,51 @@ impl Render for ProfileSelector {
.map(|profile| profile.name.clone())
.unwrap_or_else(|| "Unknown".into());
let configured_model = self
.thread
.read_with(cx, |thread, _cx| thread.configured_model())
.or_else(|| {
let model_registry = LanguageModelRegistry::read_global(cx);
model_registry.default_model()
});
let supports_tools =
configured_model.map_or(false, |default| default.model.supports_tools());
let model_registry = LanguageModelRegistry::read_global(cx);
let supports_tools = model_registry
.default_model()
.map_or(false, |default| default.model.supports_tools());
if supports_tools {
let this = cx.entity().clone();
let focus_handle = self.focus_handle.clone();
let trigger_button = Button::new("profile-selector-model", selected_profile)
let this = cx.entity().clone();
let focus_handle = self.focus_handle.clone();
let trigger_button = if supports_tools {
Button::new("profile-selector-model", selected_profile)
.label_size(LabelSize::Small)
.color(Color::Muted)
.icon(IconName::ChevronDown)
.icon_size(IconSize::XSmall)
.icon_position(IconPosition::End)
.icon_color(Color::Muted);
PopoverMenu::new("profile-selector")
.trigger_with_tooltip(trigger_button, {
let focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(
"Toggle Profile Menu",
&ToggleProfileSelector,
&focus_handle,
window,
cx,
)
}
})
.anchor(
if documentation_side(settings.dock) == DocumentationSide::Left {
gpui::Corner::BottomRight
} else {
gpui::Corner::BottomLeft
},
)
.with_handle(self.menu_handle.clone())
.menu(move |window, cx| {
Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
})
.into_any_element()
.icon_color(Color::Muted)
} else {
Button::new("tools-not-supported-button", "Tools Unsupported")
Button::new("tools-not-supported-button", "No Tools")
.disabled(true)
.label_size(LabelSize::Small)
.color(Color::Muted)
.tooltip(Tooltip::text("This model does not support tools."))
.into_any_element()
}
}
}
.tooltip(Tooltip::text("The current model does not support tools."))
};
fn documentation_side(position: AssistantDockPosition) -> DocumentationSide {
match position {
AssistantDockPosition::Left => DocumentationSide::Right,
AssistantDockPosition::Bottom => DocumentationSide::Left,
AssistantDockPosition::Right => DocumentationSide::Left,
PopoverMenu::new("profile-selector")
.trigger_with_tooltip(trigger_button, {
let focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(
"Toggle Profile Menu",
&ToggleProfileSelector,
&focus_handle,
window,
cx,
)
}
})
.anchor(if self.documentation_side == DocumentationSide::Left {
gpui::Corner::BottomRight
} else {
gpui::Corner::BottomLeft
})
.with_handle(self.menu_handle.clone())
.menu(move |window, cx| {
Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
})
}
}

View File

@@ -6,7 +6,6 @@ use crate::inline_prompt_editor::{
use crate::terminal_codegen::{CLEAR_INPUT, CodegenEvent, TerminalCodegen};
use crate::thread_store::{TextThreadStore, ThreadStore};
use anyhow::{Context as _, Result};
use assistant_settings::AssistantSettings;
use client::telemetry::Telemetry;
use collections::{HashMap, VecDeque};
use editor::{MultiBuffer, actions::SelectAll};
@@ -267,12 +266,6 @@ impl TerminalInlineAssistant {
load_context(contexts, project, &assist.prompt_store, cx)
})?;
let ConfiguredModel { model, .. } = LanguageModelRegistry::read_global(cx)
.inline_assistant_model()
.context("No inline assistant model")?;
let temperature = AssistantSettings::temperature_for_model(&model, cx);
Ok(cx.background_spawn(async move {
let mut request_message = LanguageModelRequestMessage {
role: Role::User,
@@ -293,9 +286,8 @@ impl TerminalInlineAssistant {
mode: None,
messages: vec![request_message],
tools: Vec::new(),
tool_choice: None,
stop: Vec::new(),
temperature,
temperature: None,
}
}))
}

View File

@@ -5,7 +5,7 @@ use std::sync::Arc;
use std::time::Instant;
use anyhow::{Result, anyhow};
use assistant_settings::{AssistantSettings, CompletionMode};
use assistant_settings::AssistantSettings;
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet};
use chrono::{DateTime, Utc};
use collections::HashMap;
@@ -35,10 +35,9 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
use thiserror::Error;
use ui::Window;
use util::{ResultExt as _, post_inc};
use util::{ResultExt as _, TryFutureExt as _, post_inc};
use uuid::Uuid;
use zed_llm_client::CompletionRequestStatus;
use zed_llm_client::{CompletionMode, CompletionRequestStatus};
use crate::ThreadStore;
use crate::context::{AgentContext, AgentContextHandle, ContextLoadResult, LoadedContext};
@@ -268,7 +267,7 @@ impl DetailedSummaryState {
}
}
#[derive(Default, Debug)]
#[derive(Default)]
pub struct TotalTokenUsage {
pub total: usize,
pub max: usize,
@@ -313,6 +312,14 @@ pub enum TokenUsageRatio {
Exceeded,
}
fn default_completion_mode(cx: &App) -> CompletionMode {
if cx.is_staff() {
CompletionMode::Max
} else {
CompletionMode::Normal
}
}
#[derive(Debug, Clone, Copy)]
pub enum QueueState {
Sending,
@@ -324,12 +331,12 @@ pub enum QueueState {
pub struct Thread {
id: ThreadId,
updated_at: DateTime<Utc>,
summary: ThreadSummary,
summary: Option<SharedString>,
pending_summary: Task<Option<()>>,
detailed_summary_task: Task<Option<()>>,
detailed_summary_tx: postage::watch::Sender<DetailedSummaryState>,
detailed_summary_rx: postage::watch::Receiver<DetailedSummaryState>,
completion_mode: assistant_settings::CompletionMode,
completion_mode: CompletionMode,
messages: Vec<Message>,
next_message_id: MessageId,
last_prompt_id: PromptId,
@@ -361,33 +368,6 @@ pub struct Thread {
configured_model: Option<ConfiguredModel>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ThreadSummary {
Pending,
Generating,
Ready(SharedString),
Error,
}
impl ThreadSummary {
pub const DEFAULT: SharedString = SharedString::new_static("New Thread");
pub fn or_default(&self) -> SharedString {
self.unwrap_or(Self::DEFAULT)
}
pub fn unwrap_or(&self, message: impl Into<SharedString>) -> SharedString {
self.ready().unwrap_or_else(|| message.into())
}
pub fn ready(&self) -> Option<SharedString> {
match self {
ThreadSummary::Ready(summary) => Some(summary.clone()),
ThreadSummary::Pending | ThreadSummary::Generating | ThreadSummary::Error => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExceededWindowError {
/// Model used when last message exceeded context window
@@ -410,12 +390,12 @@ impl Thread {
Self {
id: ThreadId::new(),
updated_at: Utc::now(),
summary: ThreadSummary::Pending,
summary: None,
pending_summary: Task::ready(None),
detailed_summary_task: Task::ready(None),
detailed_summary_tx,
detailed_summary_rx,
completion_mode: AssistantSettings::get_global(cx).preferred_completion_mode,
completion_mode: default_completion_mode(cx),
messages: Vec::new(),
next_message_id: MessageId(0),
last_prompt_id: PromptId::new(),
@@ -458,7 +438,6 @@ impl Thread {
tools: Entity<ToolWorkingSet>,
prompt_builder: Arc<PromptBuilder>,
project_context: SharedProjectContext,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let next_message_id = MessageId(
@@ -468,13 +447,7 @@ impl Thread {
.map(|message| message.id.0 + 1)
.unwrap_or(0),
);
let tool_use = ToolUseState::from_serialized_messages(
tools.clone(),
&serialized.messages,
project.clone(),
window,
cx,
);
let tool_use = ToolUseState::from_serialized_messages(tools.clone(), &serialized.messages);
let (detailed_summary_tx, detailed_summary_rx) =
postage::watch::channel_with(serialized.detailed_summary_state);
@@ -491,19 +464,15 @@ impl Thread {
.or_else(|| registry.default_model())
});
let completion_mode = serialized
.completion_mode
.unwrap_or_else(|| AssistantSettings::get_global(cx).preferred_completion_mode);
Self {
id,
updated_at: serialized.updated_at,
summary: ThreadSummary::Ready(serialized.summary),
summary: Some(serialized.summary),
pending_summary: Task::ready(None),
detailed_summary_task: Task::ready(None),
detailed_summary_tx,
detailed_summary_rx,
completion_mode,
completion_mode: default_completion_mode(cx),
messages: serialized
.messages
.into_iter()
@@ -599,6 +568,10 @@ impl Thread {
self.last_prompt_id = PromptId::new();
}
pub fn summary(&self) -> Option<SharedString> {
self.summary.clone()
}
pub fn project_context(&self) -> SharedProjectContext {
self.project_context.clone()
}
@@ -619,25 +592,26 @@ impl Thread {
cx.notify();
}
pub fn summary(&self) -> &ThreadSummary {
&self.summary
pub const DEFAULT_SUMMARY: SharedString = SharedString::new_static("New Thread");
pub fn summary_or_default(&self) -> SharedString {
self.summary.clone().unwrap_or(Self::DEFAULT_SUMMARY)
}
pub fn set_summary(&mut self, new_summary: impl Into<SharedString>, cx: &mut Context<Self>) {
let current_summary = match &self.summary {
ThreadSummary::Pending | ThreadSummary::Generating => return,
ThreadSummary::Ready(summary) => summary,
ThreadSummary::Error => &ThreadSummary::DEFAULT,
let Some(current_summary) = &self.summary else {
// Don't allow setting summary until generated
return;
};
let mut new_summary = new_summary.into();
if new_summary.is_empty() {
new_summary = ThreadSummary::DEFAULT;
new_summary = Self::DEFAULT_SUMMARY;
}
if current_summary != &new_summary {
self.summary = ThreadSummary::Ready(new_summary);
self.summary = Some(new_summary);
cx.emit(ThreadEvent::SummaryChanged);
}
}
@@ -1051,7 +1025,7 @@ impl Thread {
let initial_project_snapshot = initial_project_snapshot.await;
this.read_with(cx, |this, cx| SerializedThread {
version: SerializedThread::VERSION.to_string(),
summary: this.summary().or_default(),
summary: this.summary_or_default(),
updated_at: this.updated_at(),
messages: this
.messages()
@@ -1094,7 +1068,6 @@ impl Thread {
tool_use_id: tool_result.tool_use_id.clone(),
is_error: tool_result.is_error,
content: tool_result.content.clone(),
output: tool_result.output.clone(),
})
.collect(),
context: message.loaded_context.text.clone(),
@@ -1122,7 +1095,6 @@ impl Thread {
provider: model.provider.id().0.to_string(),
model: model.model.id().0.to_string(),
}),
completion_mode: Some(this.completion_mode),
})
})
}
@@ -1175,9 +1147,8 @@ impl Thread {
mode: None,
messages: vec![],
tools: Vec::new(),
tool_choice: None,
stop: Vec::new(),
temperature: AssistantSettings::temperature_for_model(&model, cx),
temperature: None,
};
let available_tools = self.available_tools(cx, model.clone());
@@ -1220,7 +1191,6 @@ impl Thread {
}));
}
let mut message_ix_to_cache = None;
for message in &self.messages {
let mut request_message = LanguageModelRequestMessage {
role: message.role,
@@ -1257,86 +1227,42 @@ impl Thread {
};
}
let mut cache_message = true;
let mut tool_results_message = LanguageModelRequestMessage {
role: Role::User,
content: Vec::new(),
cache: false,
};
for (tool_use, tool_result) in self.tool_use.tool_results(message.id) {
if let Some(tool_result) = tool_result {
request_message
.content
.push(MessageContent::ToolUse(tool_use.clone()));
tool_results_message
.content
.push(MessageContent::ToolResult(LanguageModelToolResult {
tool_use_id: tool_use.id.clone(),
tool_name: tool_result.tool_name.clone(),
is_error: tool_result.is_error,
content: if tool_result.content.is_empty() {
// Surprisingly, the API fails if we return an empty string here.
// It thinks we are sending a tool use without a tool result.
"<Tool returned an empty string>".into()
} else {
tool_result.content.clone()
},
output: None,
}));
} else {
cache_message = false;
log::debug!(
"skipped tool use {:?} because it is still pending",
tool_use
);
}
}
self.tool_use
.attach_tool_uses(message.id, &mut request_message);
if cache_message {
message_ix_to_cache = Some(request.messages.len());
}
request.messages.push(request_message);
if !tool_results_message.content.is_empty() {
if cache_message {
message_ix_to_cache = Some(request.messages.len());
}
if let Some(tool_results_message) = self.tool_use.tool_results_message(message.id) {
request.messages.push(tool_results_message);
}
}
// https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching
if let Some(message_ix_to_cache) = message_ix_to_cache {
request.messages[message_ix_to_cache].cache = true;
if let Some(last) = request.messages.last_mut() {
last.cache = true;
}
self.attached_tracked_files_state(&mut request.messages, cx);
request.tools = available_tools;
request.mode = if model.supports_max_mode() {
Some(self.completion_mode.into())
Some(self.completion_mode)
} else {
Some(CompletionMode::Normal.into())
Some(CompletionMode::Normal)
};
request
}
fn to_summarize_request(
&self,
model: &Arc<dyn LanguageModel>,
added_user_message: String,
cx: &App,
) -> LanguageModelRequest {
fn to_summarize_request(&self, added_user_message: String) -> LanguageModelRequest {
let mut request = LanguageModelRequest {
thread_id: None,
prompt_id: None,
mode: None,
messages: vec![],
tools: Vec::new(),
tool_choice: None,
stop: Vec::new(),
temperature: AssistantSettings::temperature_for_model(model, cx),
temperature: None,
};
for message in &self.messages {
@@ -1609,9 +1535,9 @@ impl Thread {
completion.queue_state = QueueState::Started;
}
CompletionRequestStatus::Failed {
code, message, request_id
code, message
} => {
return Err(anyhow!("completion request failed. request_id: {request_id}, code: {code}, message: {message}"));
return Err(anyhow!("completion request failed. code: {code}, message: {message}"));
}
CompletionRequestStatus::UsageUpdated {
amount, limit
@@ -1647,7 +1573,7 @@ impl Thread {
// If there is a response without tool use, summarize the message. Otherwise,
// allow two tool uses before summarizing.
if matches!(thread.summary, ThreadSummary::Pending)
if thread.summary.is_none()
&& thread.messages.len() >= 2
&& (!thread.has_pending_tool_uses() || thread.messages.len() >= 6)
{
@@ -1761,7 +1687,6 @@ impl Thread {
pub fn summarize(&mut self, cx: &mut Context<Self>) {
let Some(model) = LanguageModelRegistry::read_global(cx).thread_summary_model() else {
println!("No thread summary model");
return;
};
@@ -1774,19 +1699,15 @@ impl Thread {
If the conversation is about a specific subject, include it in the title. \
Be descriptive. DO NOT speak in the first person.";
let request = self.to_summarize_request(&model.model, added_user_message.into(), cx);
self.summary = ThreadSummary::Generating;
let request = self.to_summarize_request(added_user_message.into());
self.pending_summary = cx.spawn(async move |this, cx| {
let result = async {
async move {
let mut messages = model.model.stream_completion(request, &cx).await?;
let mut new_summary = String::new();
while let Some(event) = messages.next().await {
let Ok(event) = event else {
continue;
};
let event = event?;
let text = match event {
LanguageModelCompletionEvent::Text(text) => text,
LanguageModelCompletionEvent::StatusUpdate(
@@ -1812,29 +1733,18 @@ impl Thread {
}
}
anyhow::Ok(new_summary)
this.update(cx, |this, cx| {
if !new_summary.is_empty() {
this.summary = Some(new_summary.into());
}
cx.emit(ThreadEvent::SummaryGenerated);
})?;
anyhow::Ok(())
}
.await;
this.update(cx, |this, cx| {
match result {
Ok(new_summary) => {
if new_summary.is_empty() {
this.summary = ThreadSummary::Error;
} else {
this.summary = ThreadSummary::Ready(new_summary.into());
}
}
Err(err) => {
this.summary = ThreadSummary::Error;
log::error!("Failed to generate thread summary: {}", err);
}
}
cx.emit(ThreadEvent::SummaryGenerated);
})
.log_err()?;
Some(())
.log_err()
.await
});
}
@@ -1875,7 +1785,7 @@ impl Thread {
4. Any action items or next steps if any\n\
Format it in Markdown with headings and bullet points.";
let request = self.to_summarize_request(&model, added_user_message.into(), cx);
let request = self.to_summarize_request(added_user_message.into());
*self.detailed_summary_tx.borrow_mut() = DetailedSummaryState::Generating {
message_id: last_message_id,
@@ -1967,7 +1877,8 @@ impl Thread {
model: Arc<dyn LanguageModel>,
) -> Vec<PendingToolUse> {
self.auto_capture_telemetry(cx);
let request = Arc::new(self.to_completion_request(model.clone(), cx));
let request = self.to_completion_request(model, cx);
let messages = Arc::new(request.messages);
let pending_tool_uses = self
.tool_use
.pending_tool_uses()
@@ -1985,7 +1896,7 @@ impl Thread {
tool_use.id.clone(),
tool_use.ui_text.clone(),
tool_use.input.clone(),
request.clone(),
messages.clone(),
tool,
);
cx.emit(ThreadEvent::ToolConfirmationNeeded);
@@ -1994,9 +1905,8 @@ impl Thread {
tool_use.id.clone(),
tool_use.ui_text.clone(),
tool_use.input.clone(),
request.clone(),
&messages,
tool,
model.clone(),
window,
cx,
);
@@ -2089,14 +1999,12 @@ impl Thread {
tool_use_id: LanguageModelToolUseId,
ui_text: impl Into<SharedString>,
input: serde_json::Value,
request: Arc<LanguageModelRequest>,
messages: &[LanguageModelRequestMessage],
tool: Arc<dyn Tool>,
model: Arc<dyn LanguageModel>,
window: Option<AnyWindowHandle>,
cx: &mut Context<Thread>,
) {
let task =
self.spawn_tool_use(tool_use_id.clone(), request, input, tool, model, window, cx);
let task = self.spawn_tool_use(tool_use_id.clone(), messages, input, tool, window, cx);
self.tool_use
.run_pending_tool(tool_use_id, ui_text.into(), task);
}
@@ -2104,10 +2012,9 @@ impl Thread {
fn spawn_tool_use(
&mut self,
tool_use_id: LanguageModelToolUseId,
request: Arc<LanguageModelRequest>,
messages: &[LanguageModelRequestMessage],
input: serde_json::Value,
tool: Arc<dyn Tool>,
model: Arc<dyn LanguageModel>,
window: Option<AnyWindowHandle>,
cx: &mut Context<Thread>,
) -> Task<()> {
@@ -2118,10 +2025,9 @@ impl Thread {
} else {
tool.run(
input,
request,
messages,
self.project.clone(),
self.action_log.clone(),
model,
window,
cx,
)
@@ -2444,8 +2350,9 @@ impl Thread {
pub fn to_markdown(&self, cx: &App) -> Result<String> {
let mut markdown = Vec::new();
let summary = self.summary().or_default();
writeln!(markdown, "# {summary}\n")?;
if let Some(summary) = self.summary() {
writeln!(markdown, "# {summary}\n")?;
};
for message in self.messages() {
writeln!(
@@ -2503,13 +2410,6 @@ impl Thread {
writeln!(markdown, "**\n")?;
writeln!(markdown, "{}", tool_result.content)?;
if let Some(output) = tool_result.output.as_ref() {
writeln!(
markdown,
"\n\nDebug Output:\n\n```json\n{}\n```\n",
serde_json::to_string_pretty(output)?
)?;
}
}
}
@@ -2758,11 +2658,11 @@ struct PendingCompletion {
mod tests {
use super::*;
use crate::{ThreadStore, context::load_context, context_store::ContextStore, thread_store};
use assistant_settings::{AssistantSettings, LanguageModelParameters};
use assistant_settings::AssistantSettings;
use assistant_tool::ToolRegistry;
use editor::EditorSettings;
use gpui::TestAppContext;
use language_model::fake_provider::{FakeLanguageModel, FakeLanguageModelProvider};
use language_model::fake_provider::FakeLanguageModel;
use project::{FakeFs, Project};
use prompt_store::PromptBuilder;
use serde_json::json;
@@ -3169,290 +3069,6 @@ fn main() {{
);
}
#[gpui::test]
async fn test_temperature_setting(cx: &mut TestAppContext) {
init_test_settings(cx);
let project = create_test_project(
cx,
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
)
.await;
let (_workspace, _thread_store, thread, _context_store, model) =
setup_test_environment(cx, project.clone()).await;
// Both model and provider
cx.update(|cx| {
AssistantSettings::override_global(
AssistantSettings {
model_parameters: vec![LanguageModelParameters {
provider: Some(model.provider_id().0.to_string().into()),
model: Some(model.id().0.clone()),
temperature: Some(0.66),
}],
..AssistantSettings::get_global(cx).clone()
},
cx,
);
});
let request = thread.update(cx, |thread, cx| {
thread.to_completion_request(model.clone(), cx)
});
assert_eq!(request.temperature, Some(0.66));
// Only model
cx.update(|cx| {
AssistantSettings::override_global(
AssistantSettings {
model_parameters: vec![LanguageModelParameters {
provider: None,
model: Some(model.id().0.clone()),
temperature: Some(0.66),
}],
..AssistantSettings::get_global(cx).clone()
},
cx,
);
});
let request = thread.update(cx, |thread, cx| {
thread.to_completion_request(model.clone(), cx)
});
assert_eq!(request.temperature, Some(0.66));
// Only provider
cx.update(|cx| {
AssistantSettings::override_global(
AssistantSettings {
model_parameters: vec![LanguageModelParameters {
provider: Some(model.provider_id().0.to_string().into()),
model: None,
temperature: Some(0.66),
}],
..AssistantSettings::get_global(cx).clone()
},
cx,
);
});
let request = thread.update(cx, |thread, cx| {
thread.to_completion_request(model.clone(), cx)
});
assert_eq!(request.temperature, Some(0.66));
// Same model name, different provider
cx.update(|cx| {
AssistantSettings::override_global(
AssistantSettings {
model_parameters: vec![LanguageModelParameters {
provider: Some("anthropic".into()),
model: Some(model.id().0.clone()),
temperature: Some(0.66),
}],
..AssistantSettings::get_global(cx).clone()
},
cx,
);
});
let request = thread.update(cx, |thread, cx| {
thread.to_completion_request(model.clone(), cx)
});
assert_eq!(request.temperature, None);
}
#[gpui::test]
async fn test_thread_summary(cx: &mut TestAppContext) {
init_test_settings(cx);
let project = create_test_project(cx, json!({})).await;
let (_, _thread_store, thread, _context_store, model) =
setup_test_environment(cx, project.clone()).await;
// Initial state should be pending
thread.read_with(cx, |thread, _| {
assert!(matches!(thread.summary(), ThreadSummary::Pending));
assert_eq!(thread.summary().or_default(), ThreadSummary::DEFAULT);
});
// Manually setting the summary should not be allowed in this state
thread.update(cx, |thread, cx| {
thread.set_summary("This should not work", cx);
});
thread.read_with(cx, |thread, _| {
assert!(matches!(thread.summary(), ThreadSummary::Pending));
});
// Send a message
thread.update(cx, |thread, cx| {
thread.insert_user_message("Hi!", ContextLoadResult::default(), None, vec![], cx);
thread.send_to_model(model.clone(), None, cx);
});
let fake_model = model.as_fake();
simulate_successful_response(&fake_model, cx);
// Should start generating summary when there are >= 2 messages
thread.read_with(cx, |thread, _| {
assert_eq!(*thread.summary(), ThreadSummary::Generating);
});
// Should not be able to set the summary while generating
thread.update(cx, |thread, cx| {
thread.set_summary("This should not work either", cx);
});
thread.read_with(cx, |thread, _| {
assert!(matches!(thread.summary(), ThreadSummary::Generating));
assert_eq!(thread.summary().or_default(), ThreadSummary::DEFAULT);
});
cx.run_until_parked();
fake_model.stream_last_completion_response("Brief".into());
fake_model.stream_last_completion_response(" Introduction".into());
fake_model.end_last_completion_stream();
cx.run_until_parked();
// Summary should be set
thread.read_with(cx, |thread, _| {
assert!(matches!(thread.summary(), ThreadSummary::Ready(_)));
assert_eq!(thread.summary().or_default(), "Brief Introduction");
});
// Now we should be able to set a summary
thread.update(cx, |thread, cx| {
thread.set_summary("Brief Intro", cx);
});
thread.read_with(cx, |thread, _| {
assert_eq!(thread.summary().or_default(), "Brief Intro");
});
// Test setting an empty summary (should default to DEFAULT)
thread.update(cx, |thread, cx| {
thread.set_summary("", cx);
});
thread.read_with(cx, |thread, _| {
assert!(matches!(thread.summary(), ThreadSummary::Ready(_)));
assert_eq!(thread.summary().or_default(), ThreadSummary::DEFAULT);
});
}
#[gpui::test]
async fn test_thread_summary_error_set_manually(cx: &mut TestAppContext) {
init_test_settings(cx);
let project = create_test_project(cx, json!({})).await;
let (_, _thread_store, thread, _context_store, model) =
setup_test_environment(cx, project.clone()).await;
test_summarize_error(&model, &thread, cx);
// Now we should be able to set a summary
thread.update(cx, |thread, cx| {
thread.set_summary("Brief Intro", cx);
});
thread.read_with(cx, |thread, _| {
assert!(matches!(thread.summary(), ThreadSummary::Ready(_)));
assert_eq!(thread.summary().or_default(), "Brief Intro");
});
}
#[gpui::test]
async fn test_thread_summary_error_retry(cx: &mut TestAppContext) {
init_test_settings(cx);
let project = create_test_project(cx, json!({})).await;
let (_, _thread_store, thread, _context_store, model) =
setup_test_environment(cx, project.clone()).await;
test_summarize_error(&model, &thread, cx);
// Sending another message should not trigger another summarize request
thread.update(cx, |thread, cx| {
thread.insert_user_message(
"How are you?",
ContextLoadResult::default(),
None,
vec![],
cx,
);
thread.send_to_model(model.clone(), None, cx);
});
let fake_model = model.as_fake();
simulate_successful_response(&fake_model, cx);
thread.read_with(cx, |thread, _| {
// State is still Error, not Generating
assert!(matches!(thread.summary(), ThreadSummary::Error));
});
// But the summarize request can be invoked manually
thread.update(cx, |thread, cx| {
thread.summarize(cx);
});
thread.read_with(cx, |thread, _| {
assert!(matches!(thread.summary(), ThreadSummary::Generating));
});
cx.run_until_parked();
fake_model.stream_last_completion_response("A successful summary".into());
fake_model.end_last_completion_stream();
cx.run_until_parked();
thread.read_with(cx, |thread, _| {
assert!(matches!(thread.summary(), ThreadSummary::Ready(_)));
assert_eq!(thread.summary().or_default(), "A successful summary");
});
}
fn test_summarize_error(
model: &Arc<dyn LanguageModel>,
thread: &Entity<Thread>,
cx: &mut TestAppContext,
) {
thread.update(cx, |thread, cx| {
thread.insert_user_message("Hi!", ContextLoadResult::default(), None, vec![], cx);
thread.send_to_model(model.clone(), None, cx);
});
let fake_model = model.as_fake();
simulate_successful_response(&fake_model, cx);
thread.read_with(cx, |thread, _| {
assert!(matches!(thread.summary(), ThreadSummary::Generating));
assert_eq!(thread.summary().or_default(), ThreadSummary::DEFAULT);
});
// Simulate summary request ending
cx.run_until_parked();
fake_model.end_last_completion_stream();
cx.run_until_parked();
// State is set to Error and default message
thread.read_with(cx, |thread, _| {
assert!(matches!(thread.summary(), ThreadSummary::Error));
assert_eq!(thread.summary().or_default(), ThreadSummary::DEFAULT);
});
}
fn simulate_successful_response(fake_model: &FakeLanguageModel, cx: &mut TestAppContext) {
cx.run_until_parked();
fake_model.stream_last_completion_response("Assistant response".into());
fake_model.end_last_completion_stream();
cx.run_until_parked();
}
fn init_test_settings(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
@@ -3509,29 +3125,9 @@ fn main() {{
let thread = thread_store.update(cx, |store, cx| store.create_thread(cx));
let context_store = cx.new(|_cx| ContextStore::new(project.downgrade(), None));
let provider = Arc::new(FakeLanguageModelProvider);
let model = provider.test_model();
let model = FakeLanguageModel::default();
let model: Arc<dyn LanguageModel> = Arc::new(model);
cx.update(|_, cx| {
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
registry.set_default_model(
Some(ConfiguredModel {
provider: provider.clone(),
model: model.clone(),
}),
cx,
);
registry.set_thread_summary_model(
Some(ConfiguredModel {
provider,
model: model.clone(),
}),
cx,
);
})
});
(workspace, thread_store, thread, context_store, model)
}

View File

@@ -2,11 +2,12 @@ use std::fmt::Display;
use std::ops::Range;
use std::sync::Arc;
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
use assistant_context_editor::SavedContextMetadata;
use chrono::{Datelike as _, NaiveDate, TimeDelta, Utc};
use editor::{Editor, EditorEvent};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
App, ClickEvent, Empty, Entity, FocusHandle, Focusable, ScrollStrategy, Stateful, Task,
App, Empty, Entity, FocusHandle, Focusable, ScrollStrategy, Stateful, Task,
UniformListScrollHandle, WeakEntity, Window, uniform_list,
};
use time::{OffsetDateTime, UtcOffset};
@@ -17,21 +18,19 @@ use ui::{
use util::ResultExt;
use crate::history_store::{HistoryEntry, HistoryStore};
use crate::{AgentPanel, RemoveSelectedThread};
use crate::thread_store::SerializedThreadMetadata;
use crate::{AssistantPanel, RemoveSelectedThread};
pub struct ThreadHistory {
agent_panel: WeakEntity<AgentPanel>,
assistant_panel: WeakEntity<AssistantPanel>,
history_store: Entity<HistoryStore>,
scroll_handle: UniformListScrollHandle,
selected_index: usize,
hovered_index: Option<usize>,
search_editor: Entity<Editor>,
all_entries: Arc<Vec<HistoryEntry>>,
// When the search is empty, we display date separators between history entries
// This vector contains an enum of either a separator or an actual entry
separated_items: Vec<ListItemType>,
// Maps entry indexes to list item indexes
separated_item_indexes: Vec<u32>,
separated_items: Vec<HistoryListItem>,
_separated_items_task: Option<Task<()>>,
search_state: SearchState,
scrollbar_visibility: bool,
@@ -51,7 +50,7 @@ enum SearchState {
},
}
enum ListItemType {
enum HistoryListItem {
BucketSeparator(TimeBucket),
Entry {
index: usize,
@@ -59,18 +58,18 @@ enum ListItemType {
},
}
impl ListItemType {
impl HistoryListItem {
fn entry_index(&self) -> Option<usize> {
match self {
ListItemType::BucketSeparator(_) => None,
ListItemType::Entry { index, .. } => Some(*index),
HistoryListItem::BucketSeparator(_) => None,
HistoryListItem::Entry { index, .. } => Some(*index),
}
}
}
impl ThreadHistory {
pub(crate) fn new(
agent_panel: WeakEntity<AgentPanel>,
assistant_panel: WeakEntity<AssistantPanel>,
history_store: Entity<HistoryStore>,
window: &mut Window,
cx: &mut Context<Self>,
@@ -97,15 +96,13 @@ impl ThreadHistory {
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
let mut this = Self {
agent_panel,
assistant_panel,
history_store,
scroll_handle,
selected_index: 0,
hovered_index: None,
search_state: SearchState::Empty,
all_entries: Default::default(),
separated_items: Default::default(),
separated_item_indexes: Default::default(),
search_editor,
scrollbar_visibility: true,
scrollbar_state,
@@ -117,74 +114,54 @@ impl ThreadHistory {
}
fn update_all_entries(&mut self, cx: &mut Context<Self>) {
let new_entries: Arc<Vec<HistoryEntry>> = self
self.all_entries = self
.history_store
.update(cx, |store, cx| store.entries(cx))
.into();
self.set_selected_index(0, cx);
self.update_separated_items(cx);
match &self.search_state {
SearchState::Empty => {}
SearchState::Searching { query, .. } | SearchState::Searched { query, .. } => {
self.search(query.clone(), cx);
}
}
cx.notify();
}
fn update_separated_items(&mut self, cx: &mut Context<Self>) {
self._separated_items_task.take();
let mut items = Vec::with_capacity(new_entries.len() + 1);
let mut indexes = Vec::with_capacity(new_entries.len() + 1);
let mut separated_items = std::mem::take(&mut self.separated_items);
separated_items.clear();
let all_entries = self.all_entries.clone();
let bg_task = cx.background_spawn(async move {
let mut bucket = None;
let today = Local::now().naive_local().date();
let today = Utc::now().naive_local().date();
for (index, entry) in new_entries.iter().enumerate() {
let entry_date = entry
.updated_at()
.with_timezone(&Local)
.naive_local()
.date();
for (index, entry) in all_entries.iter().enumerate() {
let entry_date = entry.updated_at().naive_local().date();
let entry_bucket = TimeBucket::from_dates(today, entry_date);
if Some(entry_bucket) != bucket {
bucket = Some(entry_bucket);
items.push(ListItemType::BucketSeparator(entry_bucket));
separated_items.push(HistoryListItem::BucketSeparator(entry_bucket));
}
indexes.push(items.len() as u32);
items.push(ListItemType::Entry {
separated_items.push(HistoryListItem::Entry {
index,
format: entry_bucket.into(),
});
}
(new_entries, items, indexes)
separated_items
});
let task = cx.spawn(async move |this, cx| {
let (new_entries, items, indexes) = bg_task.await;
let separated_items = 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 {
if 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);
}
}
this.separated_items = separated_items;
cx.notify();
})
.log_err();
@@ -252,7 +229,7 @@ impl ThreadHistory {
matches,
};
this.set_selected_entry_index(0, cx);
this.set_selected_index(0, cx);
cx.notify();
};
})
@@ -275,14 +252,6 @@ impl ThreadHistory {
}
}
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 {
match &self.search_state {
SearchState::Empty => false,
@@ -310,9 +279,9 @@ impl ThreadHistory {
let count = self.matched_count();
if count > 0 {
if self.selected_index == 0 {
self.set_selected_entry_index(count - 1, cx);
self.set_selected_index(count - 1, cx);
} else {
self.set_selected_entry_index(self.selected_index - 1, cx);
self.set_selected_index(self.selected_index - 1, cx);
}
}
}
@@ -326,9 +295,9 @@ impl ThreadHistory {
let count = self.matched_count();
if count > 0 {
if self.selected_index == count - 1 {
self.set_selected_entry_index(0, cx);
self.set_selected_index(0, cx);
} else {
self.set_selected_entry_index(self.selected_index + 1, cx);
self.set_selected_index(self.selected_index + 1, cx);
}
}
}
@@ -341,32 +310,21 @@ impl ThreadHistory {
) {
let count = self.matched_count();
if count > 0 {
self.set_selected_entry_index(0, cx);
self.set_selected_index(0, cx);
}
}
fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
let count = self.matched_count();
if count > 0 {
self.set_selected_entry_index(count - 1, cx);
self.set_selected_index(count - 1, cx);
}
}
fn set_selected_entry_index(&mut self, entry_index: usize, cx: &mut Context<Self>) {
self.selected_index = entry_index;
let scroll_ix = match self.search_state {
SearchState::Empty | SearchState::Searching { .. } => self
.separated_item_indexes
.get(entry_index)
.map(|ix| *ix as usize)
.unwrap_or(entry_index + 1),
SearchState::Searched { .. } => entry_index,
};
fn set_selected_index(&mut self, index: usize, cx: &mut Context<Self>) {
self.selected_index = index;
self.scroll_handle
.scroll_to_item(scroll_ix, ScrollStrategy::Top);
.scroll_to_item(index, ScrollStrategy::Top);
cx.notify();
}
@@ -410,12 +368,14 @@ impl ThreadHistory {
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
if let Some(entry) = self.get_match(self.selected_index) {
let task_result = match entry {
HistoryEntry::Thread(thread) => self.agent_panel.update(cx, move |this, cx| {
HistoryEntry::Thread(thread) => self.assistant_panel.update(cx, move |this, cx| {
this.open_thread_by_id(&thread.id, window, cx)
}),
HistoryEntry::Context(context) => self.agent_panel.update(cx, move |this, cx| {
this.open_saved_prompt_editor(context.path.clone(), window, cx)
}),
HistoryEntry::Context(context) => {
self.assistant_panel.update(cx, move |this, cx| {
this.open_saved_prompt_editor(context.path.clone(), window, cx)
})
}
};
if let Some(task) = task_result.log_err() {
@@ -435,10 +395,10 @@ impl ThreadHistory {
if let Some(entry) = self.get_match(self.selected_index) {
let task_result = match entry {
HistoryEntry::Thread(thread) => self
.agent_panel
.assistant_panel
.update(cx, |this, cx| this.delete_thread(&thread.id, cx)),
HistoryEntry::Context(context) => self
.agent_panel
.assistant_panel
.update(cx, |this, cx| this.delete_context(context.path.clone(), cx)),
};
@@ -475,7 +435,7 @@ impl ThreadHistory {
.map(|(ix, m)| {
self.render_list_item(
Some(range_start + ix),
&ListItemType::Entry {
&HistoryListItem::Entry {
index: m.candidate_id,
format: EntryTimeFormat::DateAndTime,
},
@@ -493,36 +453,25 @@ impl ThreadHistory {
fn render_list_item(
&self,
list_entry_ix: Option<usize>,
item: &ListItemType,
item: &HistoryListItem,
highlight_positions: Vec<usize>,
cx: &Context<Self>,
cx: &App,
) -> AnyElement {
match item {
ListItemType::Entry { index, format } => match self.all_entries.get(*index) {
HistoryListItem::Entry { index, format } => match self.all_entries.get(*index) {
Some(entry) => h_flex()
.w_full()
.pb_1()
.child(
HistoryEntryElement::new(entry.clone(), self.agent_panel.clone())
.highlight_positions(highlight_positions)
.timestamp_format(*format)
.selected(list_entry_ix == Some(self.selected_index))
.hovered(list_entry_ix == self.hovered_index)
.on_hover(cx.listener(move |this, is_hovered, _window, cx| {
if *is_hovered {
this.hovered_index = list_entry_ix;
} else if this.hovered_index == list_entry_ix {
this.hovered_index = None;
}
cx.notify();
}))
.into_any_element(),
)
.child(self.render_history_entry(
entry,
list_entry_ix == Some(self.selected_index),
highlight_positions,
*format,
))
.into_any(),
None => Empty.into_any_element(),
},
ListItemType::BucketSeparator(bucket) => div()
HistoryListItem::BucketSeparator(bucket) => div()
.px(DynamicSpacing::Base06.rems(cx))
.pt_2()
.pb_1()
@@ -534,6 +483,33 @@ impl ThreadHistory {
.into_any_element(),
}
}
fn render_history_entry(
&self,
entry: &HistoryEntry,
is_active: bool,
highlight_positions: Vec<usize>,
format: EntryTimeFormat,
) -> AnyElement {
match entry {
HistoryEntry::Thread(thread) => PastThread::new(
thread.clone(),
self.assistant_panel.clone(),
is_active,
highlight_positions,
format,
)
.into_any_element(),
HistoryEntry::Context(context) => PastContext::new(
context.clone(),
self.assistant_panel.clone(),
is_active,
highlight_positions,
format,
)
.into_any_element(),
}
}
}
impl Focusable for ThreadHistory {
@@ -599,7 +575,7 @@ impl Render for ThreadHistory {
uniform_list(
cx.entity().clone(),
"thread-history",
self.list_item_count(),
self.matched_count(),
Self::list_items,
)
.p_1()
@@ -615,97 +591,155 @@ impl Render for ThreadHistory {
}
#[derive(IntoElement)]
pub struct HistoryEntryElement {
entry: HistoryEntry,
agent_panel: WeakEntity<AgentPanel>,
pub struct PastThread {
thread: SerializedThreadMetadata,
assistant_panel: WeakEntity<AssistantPanel>,
selected: bool,
hovered: bool,
highlight_positions: Vec<usize>,
timestamp_format: EntryTimeFormat,
on_hover: Box<dyn Fn(&bool, &mut Window, &mut App) + 'static>,
}
impl HistoryEntryElement {
pub fn new(entry: HistoryEntry, agent_panel: WeakEntity<AgentPanel>) -> Self {
impl PastThread {
pub fn new(
thread: SerializedThreadMetadata,
assistant_panel: WeakEntity<AssistantPanel>,
selected: bool,
highlight_positions: Vec<usize>,
timestamp_format: EntryTimeFormat,
) -> Self {
Self {
entry,
agent_panel,
selected: false,
hovered: false,
highlight_positions: vec![],
timestamp_format: EntryTimeFormat::DateAndTime,
on_hover: Box::new(|_, _, _| {}),
thread,
assistant_panel,
selected,
highlight_positions,
timestamp_format,
}
}
pub fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
pub fn hovered(mut self, hovered: bool) -> Self {
self.hovered = hovered;
self
}
pub fn highlight_positions(mut self, positions: Vec<usize>) -> Self {
self.highlight_positions = positions;
self
}
pub fn on_hover(mut self, on_hover: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self {
self.on_hover = Box::new(on_hover);
self
}
pub fn timestamp_format(mut self, format: EntryTimeFormat) -> Self {
self.timestamp_format = format;
self
}
}
impl RenderOnce for HistoryEntryElement {
impl RenderOnce for PastThread {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let (id, summary, timestamp) = match &self.entry {
HistoryEntry::Thread(thread) => (
thread.id.to_string(),
thread.summary.clone(),
thread.updated_at.timestamp(),
),
HistoryEntry::Context(context) => (
context.path.to_string_lossy().to_string(),
context.title.clone().into(),
context.mtime.timestamp(),
),
};
let summary = self.thread.summary;
let thread_timestamp =
self.timestamp_format
.format_timestamp(&self.agent_panel, timestamp, cx);
let thread_timestamp = self.timestamp_format.format_timestamp(
&self.assistant_panel,
self.thread.updated_at.timestamp(),
cx,
);
ListItem::new(SharedString::from(id))
ListItem::new(SharedString::from(self.thread.id.to_string()))
.rounded()
.toggle_state(self.selected)
.spacing(ListItemSpacing::Sparse)
.start_slot(
div().max_w_4_5().child(
HighlightedLabel::new(summary, self.highlight_positions)
.size(LabelSize::Small)
.truncate(),
),
)
.end_slot(
h_flex()
.w_full()
.gap_2()
.justify_between()
.child(
HighlightedLabel::new(summary, self.highlight_positions)
.size(LabelSize::Small)
.truncate(),
)
.gap_1p5()
.child(
Label::new(thread_timestamp)
.color(Color::Muted)
.size(LabelSize::XSmall),
)
.child(
IconButton::new("delete", IconName::TrashAlt)
.shape(IconButtonShape::Square)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.tooltip(move |window, cx| {
Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx)
})
.on_click({
let assistant_panel = self.assistant_panel.clone();
let id = self.thread.id.clone();
move |_event, _window, cx| {
assistant_panel
.update(cx, |this, cx| {
this.delete_thread(&id, cx).detach_and_log_err(cx);
})
.ok();
}
}),
),
)
.on_hover(self.on_hover)
.end_slot::<IconButton>(if self.hovered || self.selected {
Some(
.on_click({
let assistant_panel = self.assistant_panel.clone();
let id = self.thread.id.clone();
move |_event, window, cx| {
assistant_panel
.update(cx, |this, cx| {
this.open_thread_by_id(&id, window, cx)
.detach_and_log_err(cx);
})
.ok();
}
})
}
}
#[derive(IntoElement)]
pub struct PastContext {
context: SavedContextMetadata,
assistant_panel: WeakEntity<AssistantPanel>,
selected: bool,
highlight_positions: Vec<usize>,
timestamp_format: EntryTimeFormat,
}
impl PastContext {
pub fn new(
context: SavedContextMetadata,
assistant_panel: WeakEntity<AssistantPanel>,
selected: bool,
highlight_positions: Vec<usize>,
timestamp_format: EntryTimeFormat,
) -> Self {
Self {
context,
assistant_panel,
selected,
highlight_positions,
timestamp_format,
}
}
}
impl RenderOnce for PastContext {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let summary = self.context.title;
let context_timestamp = self.timestamp_format.format_timestamp(
&self.assistant_panel,
self.context.mtime.timestamp(),
cx,
);
ListItem::new(SharedString::from(
self.context.path.to_string_lossy().to_string(),
))
.rounded()
.toggle_state(self.selected)
.spacing(ListItemSpacing::Sparse)
.start_slot(
div().max_w_4_5().child(
HighlightedLabel::new(summary, self.highlight_positions)
.size(LabelSize::Small)
.truncate(),
),
)
.end_slot(
h_flex()
.gap_1p5()
.child(
Label::new(context_timestamp)
.color(Color::Muted)
.size(LabelSize::XSmall),
)
.child(
IconButton::new("delete", IconName::TrashAlt)
.shape(IconButtonShape::Square)
.icon_size(IconSize::XSmall)
@@ -714,71 +748,31 @@ impl RenderOnce for HistoryEntryElement {
Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx)
})
.on_click({
let agent_panel = self.agent_panel.clone();
let f: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static> =
match &self.entry {
HistoryEntry::Thread(thread) => {
let id = thread.id.clone();
Box::new(move |_event, _window, cx| {
agent_panel
.update(cx, |this, cx| {
this.delete_thread(&id, cx)
.detach_and_log_err(cx);
})
.ok();
})
}
HistoryEntry::Context(context) => {
let path = context.path.clone();
Box::new(move |_event, _window, cx| {
agent_panel
.update(cx, |this, cx| {
this.delete_context(path.clone(), cx)
.detach_and_log_err(cx);
})
.ok();
})
}
};
f
let assistant_panel = self.assistant_panel.clone();
let path = self.context.path.clone();
move |_event, _window, cx| {
assistant_panel
.update(cx, |this, cx| {
this.delete_context(path.clone(), cx)
.detach_and_log_err(cx);
})
.ok();
}
}),
)
} else {
None
})
.on_click({
let agent_panel = self.agent_panel.clone();
let f: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static> = match &self.entry
{
HistoryEntry::Thread(thread) => {
let id = thread.id.clone();
Box::new(move |_event, window, cx| {
agent_panel
.update(cx, |this, cx| {
this.open_thread_by_id(&id, window, cx)
.detach_and_log_err(cx);
})
.ok();
})
}
HistoryEntry::Context(context) => {
let path = context.path.clone();
Box::new(move |_event, window, cx| {
agent_panel
.update(cx, |this, cx| {
this.open_saved_prompt_editor(path.clone(), window, cx)
.detach_and_log_err(cx);
})
.ok();
})
}
};
f
})
),
)
.on_click({
let assistant_panel = self.assistant_panel.clone();
let path = self.context.path.clone();
move |_event, window, cx| {
assistant_panel
.update(cx, |this, cx| {
this.open_saved_prompt_editor(path.clone(), window, cx)
.detach_and_log_err(cx);
})
.ok();
}
})
}
}
@@ -791,12 +785,12 @@ pub enum EntryTimeFormat {
impl EntryTimeFormat {
fn format_timestamp(
&self,
agent_panel: &WeakEntity<AgentPanel>,
assistant_panel: &WeakEntity<AssistantPanel>,
timestamp: i64,
cx: &App,
) -> String {
let timestamp = OffsetDateTime::from_unix_timestamp(timestamp).unwrap();
let timezone = agent_panel
let timezone = assistant_panel
.read_with(cx, |this, _cx| this.local_timezone())
.unwrap_or(UtcOffset::UTC);

View File

@@ -5,7 +5,7 @@ use std::rc::Rc;
use std::sync::Arc;
use anyhow::{Context as _, Result, anyhow};
use assistant_settings::{AgentProfile, AgentProfileId, AssistantSettings, CompletionMode};
use assistant_settings::{AgentProfile, AgentProfileId, AssistantSettings};
use assistant_tool::{ToolId, ToolSource, ToolWorkingSet};
use chrono::{DateTime, Utc};
use collections::HashMap;
@@ -28,7 +28,6 @@ use prompt_store::{
};
use serde::{Deserialize, Serialize};
use settings::{Settings as _, SettingsStore};
use ui::Window;
use util::ResultExt as _;
use crate::context_server_tool::ContextServerTool;
@@ -389,20 +388,18 @@ impl ThreadStore {
pub fn open_thread(
&self,
id: &ThreadId,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<Entity<Thread>>> {
let id = id.clone();
let database_future = ThreadsDatabase::global_future(cx);
let this = cx.weak_entity();
window.spawn(cx, async move |cx| {
cx.spawn(async move |this, cx| {
let database = database_future.await.map_err(|err| anyhow!(err))?;
let thread = database
.try_find_thread(id.clone())
.await?
.ok_or_else(|| anyhow!("no thread found with ID: {id:?}"))?;
let thread = this.update_in(cx, |this, window, cx| {
let thread = this.update(cx, |this, cx| {
cx.new(|cx| {
Thread::deserialize(
id.clone(),
@@ -411,7 +408,6 @@ impl ThreadStore {
this.tools.clone(),
this.prompt_builder.clone(),
this.project_context.clone(),
window,
cx,
)
})
@@ -655,8 +651,6 @@ pub struct SerializedThread {
pub exceeded_window_error: Option<ExceededWindowError>,
#[serde(default)]
pub model: Option<SerializedLanguageModel>,
#[serde(default)]
pub completion_mode: Option<CompletionMode>,
}
#[derive(Serialize, Deserialize, Debug)]
@@ -776,7 +770,6 @@ pub struct SerializedToolResult {
pub tool_use_id: LanguageModelToolUseId,
pub is_error: bool,
pub content: Arc<str>,
pub output: Option<serde_json::Value>,
}
#[derive(Serialize, Deserialize)]
@@ -801,7 +794,6 @@ impl LegacySerializedThread {
detailed_summary_state: DetailedSummaryState::default(),
exceeded_window_error: None,
model: None,
completion_mode: None,
}
}
}

View File

@@ -1,17 +1,16 @@
use std::sync::Arc;
use anyhow::Result;
use assistant_tool::{AnyToolCard, Tool, ToolResultOutput, ToolUseStatus, ToolWorkingSet};
use assistant_tool::{AnyToolCard, Tool, ToolUseStatus, ToolWorkingSet};
use collections::HashMap;
use futures::FutureExt as _;
use futures::future::Shared;
use gpui::{App, Entity, SharedString, Task};
use language_model::{
ConfiguredModel, LanguageModel, LanguageModelRequest, LanguageModelToolResult,
LanguageModelToolUse, LanguageModelToolUseId, Role,
ConfiguredModel, LanguageModel, LanguageModelRequestMessage, LanguageModelToolResult,
LanguageModelToolUse, LanguageModelToolUseId, MessageContent, Role,
};
use project::Project;
use ui::{IconName, Window};
use ui::IconName;
use util::truncate_lines_to_byte_limit;
use crate::thread::{MessageId, PromptId, ThreadId};
@@ -55,9 +54,6 @@ impl ToolUseState {
pub fn from_serialized_messages(
tools: Entity<ToolWorkingSet>,
messages: &[SerializedMessage],
project: Entity<Project>,
window: &mut Window,
cx: &mut App,
) -> Self {
let mut this = Self::new(tools);
let mut tool_names_by_id = HashMap::default();
@@ -97,23 +93,12 @@ impl ToolUseState {
this.tool_results.insert(
tool_use_id.clone(),
LanguageModelToolResult {
tool_use_id: tool_use_id.clone(),
tool_use_id,
tool_name: tool_use.clone(),
is_error: tool_result.is_error,
content: tool_result.content.clone(),
output: tool_result.output.clone(),
},
);
if let Some(tool) = this.tools.read(cx).tool(tool_use, cx) {
if let Some(output) = tool_result.output.clone() {
if let Some(card) =
tool.deserialize_card(output, project.clone(), window, cx)
{
this.tool_result_cards.insert(tool_use_id, card);
}
}
}
}
}
}
@@ -139,7 +124,6 @@ impl ToolUseState {
tool_use_id: tool_use_id.clone(),
tool_name: tool_use.name.clone(),
content,
output: None,
is_error: true,
},
);
@@ -354,7 +338,7 @@ impl ToolUseState {
tool_use_id: LanguageModelToolUseId,
ui_text: impl Into<Arc<str>>,
input: serde_json::Value,
request: Arc<LanguageModelRequest>,
messages: Arc<Vec<LanguageModelRequestMessage>>,
tool: Arc<dyn Tool>,
) {
if let Some(tool_use) = self.pending_tool_uses_by_id.get_mut(&tool_use_id) {
@@ -363,7 +347,7 @@ impl ToolUseState {
let confirmation = Confirmation {
tool_use_id,
input,
request,
messages,
tool,
ui_text,
};
@@ -375,7 +359,7 @@ impl ToolUseState {
&mut self,
tool_use_id: LanguageModelToolUseId,
tool_name: Arc<str>,
output: Result<ToolResultOutput>,
output: Result<String>,
configured_model: Option<&ConfiguredModel>,
) -> Option<PendingToolUse> {
let metadata = self.tool_use_metadata_by_id.remove(&tool_use_id);
@@ -395,8 +379,7 @@ impl ToolUseState {
);
match output {
Ok(output) => {
let tool_result = output.content;
Ok(tool_result) => {
const BYTES_PER_TOKEN_ESTIMATE: usize = 3;
// Protect from clearly large output
@@ -423,7 +406,6 @@ impl ToolUseState {
tool_name,
content: tool_result.into(),
is_error: false,
output: output.output,
},
);
self.pending_tool_uses_by_id.remove(&tool_use_id)
@@ -436,7 +418,6 @@ impl ToolUseState {
tool_name,
content: err.to_string().into(),
is_error: true,
output: None,
},
);
@@ -449,20 +430,71 @@ impl ToolUseState {
}
}
pub fn attach_tool_uses(
&self,
message_id: MessageId,
request_message: &mut LanguageModelRequestMessage,
) {
if let Some(tool_uses) = self.tool_uses_by_assistant_message.get(&message_id) {
for tool_use in tool_uses {
if self.tool_results.contains_key(&tool_use.id) {
// Do not send tool uses until they are completed
request_message
.content
.push(MessageContent::ToolUse(tool_use.clone()));
} else {
log::debug!(
"skipped tool use {:?} because it is still pending",
tool_use
);
}
}
}
}
pub fn has_tool_results(&self, assistant_message_id: MessageId) -> bool {
self.tool_uses_by_assistant_message
.contains_key(&assistant_message_id)
}
pub fn tool_results(
pub fn tool_results_message(
&self,
assistant_message_id: MessageId,
) -> impl Iterator<Item = (&LanguageModelToolUse, Option<&LanguageModelToolResult>)> {
self.tool_uses_by_assistant_message
.get(&assistant_message_id)
.into_iter()
.flatten()
.map(|tool_use| (tool_use, self.tool_results.get(&tool_use.id)))
) -> Option<LanguageModelRequestMessage> {
let tool_uses = self
.tool_uses_by_assistant_message
.get(&assistant_message_id)?;
if tool_uses.is_empty() {
return None;
}
let mut request_message = LanguageModelRequestMessage {
role: Role::User,
content: vec![],
cache: false,
};
for tool_use in tool_uses {
if let Some(tool_result) = self.tool_results.get(&tool_use.id) {
request_message
.content
.push(MessageContent::ToolResult(LanguageModelToolResult {
tool_use_id: tool_use.id.clone(),
tool_name: tool_result.tool_name.clone(),
is_error: tool_result.is_error,
content: if tool_result.content.is_empty() {
// Surprisingly, the API fails if we return an empty string here.
// It thinks we are sending a tool use without a tool result.
"<Tool returned an empty string>".into()
} else {
tool_result.content.clone()
},
}));
}
}
Some(request_message)
}
}
@@ -483,7 +515,7 @@ pub struct Confirmation {
pub tool_use_id: LanguageModelToolUseId,
pub input: serde_json::Value,
pub ui_text: Arc<str>,
pub request: Arc<LanguageModelRequest>,
pub messages: Arc<Vec<LanguageModelRequestMessage>>,
pub tool: Arc<dyn Tool>,
}

View File

@@ -2,7 +2,6 @@ mod agent_notification;
mod animated_label;
mod context_pill;
mod max_mode_tooltip;
mod onboarding_modal;
pub mod preview;
mod upsell;
@@ -10,4 +9,3 @@ pub use agent_notification::*;
pub use animated_label::*;
pub use context_pill::*;
pub use max_mode_tooltip::*;
pub use onboarding_modal::*;

View File

@@ -1,174 +0,0 @@
use gpui::{
ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseDownEvent, Render,
};
use ui::{TintColor, Vector, VectorName, prelude::*};
use workspace::{ModalView, Workspace};
use crate::agent_panel::AgentPanel;
macro_rules! agent_onboarding_event {
($name:expr) => {
telemetry::event!($name, source = "Agent Onboarding");
};
($name:expr, $($key:ident $(= $value:expr)?),+ $(,)?) => {
telemetry::event!($name, source = "Agent Onboarding", $($key $(= $value)?),+);
};
}
pub struct AgentOnboardingModal {
focus_handle: FocusHandle,
workspace: Entity<Workspace>,
}
impl AgentOnboardingModal {
pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
let workspace_entity = cx.entity();
workspace.toggle_modal(window, cx, |_window, cx| Self {
workspace: workspace_entity,
focus_handle: cx.focus_handle(),
});
}
fn open_panel(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
self.workspace.update(cx, |workspace, cx| {
workspace.focus_panel::<AgentPanel>(window, cx);
});
cx.emit(DismissEvent);
agent_onboarding_event!("Open Panel Clicked");
}
fn view_blog(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
cx.open_url("http://zed.dev/blog/fastest-ai-code-editor");
cx.notify();
agent_onboarding_event!("Blog Link Clicked");
}
fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
cx.emit(DismissEvent);
}
}
impl EventEmitter<DismissEvent> for AgentOnboardingModal {}
impl Focusable for AgentOnboardingModal {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl ModalView for AgentOnboardingModal {}
impl Render for AgentOnboardingModal {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let window_height = window.viewport_size().height;
let max_height = window_height - px(200.);
let base = v_flex()
.id("agent-onboarding")
.key_context("AgentOnboardingModal")
.relative()
.w(px(450.))
.h_full()
.max_h(max_height)
.p_4()
.gap_2()
.elevation_3(cx)
.track_focus(&self.focus_handle(cx))
.overflow_hidden()
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| {
agent_onboarding_event!("Canceled", trigger = "Action");
cx.emit(DismissEvent);
}))
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
this.focus_handle.focus(window);
}))
.child(
div()
.absolute()
.top_0()
.right(px(-1.0))
.w(px(441.))
.h(px(240.))
.child(
Vector::new(VectorName::Grid, rems_from_px(441.), rems_from_px(240.))
.color(ui::Color::Custom(cx.theme().colors().text.alpha(0.05))),
),
)
.child(
div()
.absolute()
.top(px(-8.0))
.right_0()
.w(px(400.))
.h(px(92.))
.child(
Vector::new(VectorName::AiGrid, rems_from_px(400.), rems_from_px(92.))
.color(ui::Color::Custom(cx.theme().colors().text.alpha(0.32))),
),
)
.child(
div()
.absolute()
.inset_0()
.size_full()
.bg(gpui::linear_gradient(
175.,
gpui::linear_color_stop(
cx.theme().colors().elevated_surface_background,
0.,
),
gpui::linear_color_stop(
cx.theme().colors().elevated_surface_background.opacity(0.),
0.8,
),
)),
)
.child(
v_flex()
.w_full()
.gap_1()
.child(
Label::new("Introducing")
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(Headline::new("Agentic Editing in Zed").size(HeadlineSize::Large)),
)
.child(h_flex().absolute().top_2().right_2().child(
IconButton::new("cancel", IconName::X).on_click(cx.listener(
|_, _: &ClickEvent, _window, cx| {
agent_onboarding_event!("Cancelled", trigger = "X click");
cx.emit(DismissEvent);
},
)),
));
let open_panel_button = Button::new("open-panel", "Get Started with the Agent Panel")
.icon_size(IconSize::Indicator)
.style(ButtonStyle::Tinted(TintColor::Accent))
.full_width()
.on_click(cx.listener(Self::open_panel));
let blog_post_button = Button::new("view-blog", "Check out the Blog Post")
.icon(IconName::ArrowUpRight)
.icon_size(IconSize::Indicator)
.icon_color(Color::Muted)
.full_width()
.on_click(cx.listener(Self::view_blog));
let copy = "Zed now natively supports agentic editing, enabling fluid collaboration between humans and AI.";
base.child(Label::new(copy).color(Color::Muted)).child(
v_flex()
.w_full()
.mt_2()
.gap_2()
.child(open_panel_button)
.child(blog_post_button),
)
}
}

View File

@@ -39,7 +39,7 @@ impl RenderOnce for UsageCallout {
let (title, message, button_text, url) = if is_limit_reached {
match self.plan {
Plan::ZedFree => (
Plan::Free => (
"Out of free prompts",
"Upgrade to continue, wait for the next reset, or switch to API key."
.to_string(),
@@ -61,7 +61,7 @@ impl RenderOnce for UsageCallout {
}
} else {
match self.plan {
Plan::ZedFree => (
Plan::Free => (
"Reaching free plan limit soon",
format!(
"{remaining} remaining - Upgrade to increase limit, or switch providers",
@@ -120,7 +120,7 @@ impl Component for UsageCallout {
single_example(
"Approaching limit (90%)",
UsageCallout::new(
Plan::ZedFree,
Plan::Free,
RequestUsage {
limit: UsageLimit::Limited(50),
amount: 45, // 90% of limit
@@ -131,7 +131,7 @@ impl Component for UsageCallout {
single_example(
"Limit reached (100%)",
UsageCallout::new(
Plan::ZedFree,
Plan::Free,
RequestUsage {
limit: UsageLimit::Limited(50),
amount: 50, // 100% of limit

View File

@@ -578,7 +578,6 @@ pub enum ToolChoice {
Auto,
Any,
Tool { name: String },
None,
}
#[derive(Debug, Serialize, Deserialize)]

View File

@@ -167,20 +167,16 @@ fn get_shell_safe_zed_path() -> anyhow::Result<String> {
.to_string_lossy()
.to_string();
// NOTE: this was previously enabled, however, it caused errors when it shouldn't have
// (see https://github.com/zed-industries/zed/issues/29819)
// The zed path failing to execute within the askpass script results in very vague ssh
// authentication failed errors, so this was done to try and surface a better error
//
// use std::os::unix::fs::MetadataExt;
// let metadata = std::fs::metadata(&zed_path)
// .context("Failed to check metadata of Zed executable path for use in askpass")?;
// let is_executable = metadata.is_file() && metadata.mode() & 0o111 != 0;
// anyhow::ensure!(
// is_executable,
// "Failed to verify Zed executable path for use in askpass"
// );
// sanity check on unix systems that the path exists and is executable
// todo(windows): implement this check for windows (or just use `is-executable` crate)
use std::os::unix::fs::MetadataExt;
let metadata = std::fs::metadata(&zed_path)
.context("Failed to check metadata of Zed executable path for use in askpass")?;
let is_executable = metadata.is_file() && metadata.mode() & 0o111 != 0;
anyhow::ensure!(
is_executable,
"Failed to verify Zed executable path for use in askpass"
);
// As of writing, this can only be fail if the path contains a null byte, which shouldn't be possible
// but shlex has annotated the error as #[non_exhaustive] so we can't make it a compile error if other
// errors are introduced in the future :(

View File

@@ -0,0 +1,88 @@
[package]
name = "assistant"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/assistant.rs"
doctest = false
[features]
test-support = [
"editor/test-support",
"language/test-support",
"project/test-support",
"text/test-support",
]
[dependencies]
anyhow.workspace = true
assistant_context_editor.workspace = true
assistant_settings.workspace = true
assistant_slash_command.workspace = true
assistant_slash_commands.workspace = true
assistant_tool.workspace = true
async-watch.workspace = true
client.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
db.workspace = true
editor.workspace = true
feature_flags.workspace = true
fs.workspace = true
futures.workspace = true
gpui.workspace = true
indexed_docs.workspace = true
indoc.workspace = true
language.workspace = true
language_model.workspace = true
language_model_selector.workspace = true
log.workspace = true
lsp.workspace = true
menu.workspace = true
multi_buffer.workspace = true
parking_lot.workspace = true
project.workspace = true
rules_library.workspace = true
prompt_store.workspace = true
proto.workspace = true
rope.workspace = true
schemars.workspace = true
search.workspace = true
serde.workspace = true
settings.workspace = true
smol.workspace = true
streaming_diff.workspace = true
telemetry.workspace = true
telemetry_events.workspace = true
terminal.workspace = true
terminal_view.workspace = true
text.workspace = true
theme.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
zed_actions.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
ctor.workspace = true
editor = { workspace = true, features = ["test-support"] }
env_logger.workspace = true
language = { workspace = true, features = ["test-support"] }
language_model = { workspace = true, features = ["test-support"] }
languages = { workspace = true, features = ["test-support"] }
log.workspace = true
pretty_assertions.workspace = true
project = { workspace = true, features = ["test-support"] }
rand.workspace = true
serde_json_lenient.workspace = true
terminal_view = { workspace = true, features = ["test-support"] }
text = { workspace = true, features = ["test-support"] }
tree-sitter-md.workspace = true
unindent.workspace = true

View File

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

View File

@@ -1,123 +1,102 @@
mod active_thread;
mod agent_configuration;
mod agent_diff;
mod agent_model_selector;
mod agent_panel;
mod buffer_codegen;
mod context;
mod context_picker;
mod context_server_configuration;
mod context_server_tool;
mod context_store;
mod context_strip;
mod debug;
mod history_store;
#![cfg_attr(target_os = "windows", allow(unused, dead_code))]
mod assistant_configuration;
pub mod assistant_panel;
mod inline_assistant;
mod inline_prompt_editor;
mod message_editor;
mod profile_selector;
mod slash_command_settings;
mod terminal_codegen;
pub mod slash_command_settings;
mod terminal_inline_assistant;
mod thread;
mod thread_history;
mod thread_store;
mod tool_compatibility;
mod tool_use;
mod ui;
use std::sync::Arc;
use assistant_settings::{AgentProfileId, AssistantSettings, LanguageModelSelection};
use assistant_settings::{AssistantSettings, LanguageModelSelection};
use assistant_slash_command::SlashCommandRegistry;
use client::Client;
use feature_flags::FeatureFlagAppExt as _;
use command_palette_hooks::CommandPaletteFilter;
use feature_flags::FeatureFlagAppExt;
use fs::Fs;
use gpui::{App, actions, impl_actions};
use language::LanguageRegistry;
use language_model::{LanguageModelId, LanguageModelProviderId, LanguageModelRegistry};
use gpui::{App, Global, ReadGlobal, UpdateGlobal, actions};
use language_model::{
LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, LanguageModelResponseMessage,
};
use prompt_store::PromptBuilder;
use schemars::JsonSchema;
use serde::Deserialize;
use settings::{Settings as _, SettingsStore};
use thread::ThreadId;
use settings::{Settings, SettingsStore};
pub use crate::active_thread::ActiveThread;
use crate::agent_configuration::{AddContextServerModal, ManageProfilesModal};
pub use crate::agent_panel::{AgentPanel, ConcreteAssistantPanelDelegate};
pub use crate::context::{ContextLoadResult, LoadedContext};
pub use crate::inline_assistant::InlineAssistant;
pub use crate::assistant_panel::{AssistantPanel, AssistantPanelEvent};
pub(crate) use crate::inline_assistant::*;
use crate::slash_command_settings::SlashCommandSettings;
pub use crate::thread::{Message, MessageSegment, Thread, ThreadEvent};
pub use crate::thread_store::{TextThreadStore, ThreadStore};
pub use agent_diff::{AgentDiffPane, AgentDiffToolbar};
pub use context_store::ContextStore;
pub use ui::preview::{all_agent_previews, get_agent_preview};
actions!(
agent,
assistant,
[
NewTextThread,
ToggleContextPicker,
ToggleNavigationMenu,
ToggleOptionsMenu,
DeleteRecentlyOpenThread,
ToggleProfileSelector,
RemoveAllContext,
ExpandMessageEditor,
OpenHistory,
AddContextServer,
RemoveSelectedThread,
Chat,
InsertActivePrompt,
DeployHistory,
NewChat,
CycleNextInlineAssist,
CyclePreviousInlineAssist,
FocusUp,
FocusDown,
FocusLeft,
FocusRight,
RemoveFocusedContext,
AcceptSuggestedContext,
OpenActiveThreadAsMarkdown,
OpenAgentDiff,
Keep,
Reject,
RejectAll,
KeepAll,
Follow,
ResetTrialUpsell,
CyclePreviousInlineAssist
]
);
#[derive(Default, Clone, PartialEq, Deserialize, JsonSchema)]
pub struct NewThread {
#[serde(default)]
from_thread_id: Option<ThreadId>,
const DEFAULT_CONTEXT_LINES: usize = 50;
#[derive(Deserialize, Debug)]
pub struct LanguageModelUsage {
pub prompt_tokens: u32,
pub completion_tokens: u32,
pub total_tokens: u32,
}
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema)]
pub struct ManageProfiles {
#[serde(default)]
pub customize_tools: Option<AgentProfileId>,
#[derive(Deserialize, Debug)]
pub struct LanguageModelChoiceDelta {
pub index: u32,
pub delta: LanguageModelResponseMessage,
pub finish_reason: Option<String>,
}
impl ManageProfiles {
pub fn customize_tools(profile_id: AgentProfileId) -> Self {
Self {
customize_tools: Some(profile_id),
/// The state pertaining to the Assistant.
#[derive(Default)]
struct Assistant {
/// Whether the Assistant is enabled.
enabled: bool,
}
impl Global for Assistant {}
impl Assistant {
const NAMESPACE: &'static str = "assistant";
fn set_enabled(&mut self, enabled: bool, cx: &mut App) {
if self.enabled == enabled {
return;
}
self.enabled = enabled;
if !enabled {
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.hide_namespace(Self::NAMESPACE);
});
return;
}
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.show_namespace(Self::NAMESPACE);
});
}
pub fn enabled(cx: &App) -> bool {
Self::global(cx).enabled
}
}
impl_actions!(agent, [NewThread, ManageProfiles]);
/// Initializes the `agent` crate.
pub fn init(
fs: Arc<dyn Fs>,
client: Arc<Client>,
prompt_builder: Arc<PromptBuilder>,
language_registry: Arc<LanguageRegistry>,
cx: &mut App,
) {
cx.set_global(Assistant::default());
AssistantSettings::register(cx);
SlashCommandSettings::register(cx);
@@ -125,9 +104,8 @@ pub fn init(
rules_library::init(cx);
init_language_model_settings(cx);
assistant_slash_command::init(cx);
thread_store::init(cx);
agent_panel::init(cx);
context_server_configuration::init(language_registry, cx);
assistant_tool::init(cx);
assistant_panel::init(cx);
register_slash_commands(cx);
inline_assistant::init(
@@ -143,8 +121,22 @@ pub fn init(
cx,
);
indexed_docs::init(cx);
cx.observe_new(AddContextServerModal::register).detach();
cx.observe_new(ManageProfilesModal::register).detach();
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.hide_namespace(Assistant::NAMESPACE);
});
Assistant::update_global(cx, |assistant, cx| {
let settings = AssistantSettings::get_global(cx);
assistant.set_enabled(settings.enabled, cx);
});
cx.observe_global::<SettingsStore>(|cx| {
Assistant::update_global(cx, |assistant, cx| {
let settings = AssistantSettings::get_global(cx);
assistant.set_enabled(settings.enabled, cx);
});
})
.detach();
}
fn init_language_model_settings(cx: &mut App) {
@@ -171,7 +163,7 @@ fn update_active_language_model_from_settings(cx: &mut App) {
fn to_selected_model(selection: &LanguageModelSelection) -> language_model::SelectedModel {
language_model::SelectedModel {
provider: LanguageModelProviderId::from(selection.provider.0.clone()),
provider: LanguageModelProviderId::from(selection.provider.clone()),
model: LanguageModelId::from(selection.model.clone()),
}
}
@@ -258,3 +250,11 @@ fn update_slash_commands_from_settings(cx: &mut App) {
.unregister_command(assistant_slash_commands::CargoWorkspaceSlashCommand);
}
}
#[cfg(test)]
#[ctor::ctor]
fn init_logger() {
if std::env::var("RUST_LOG").is_ok() {
env_logger::init();
}
}

View File

@@ -0,0 +1,199 @@
use std::sync::Arc;
use collections::HashMap;
use gpui::{AnyView, App, EventEmitter, FocusHandle, Focusable, Subscription, canvas};
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
use ui::{ElevationIndex, prelude::*};
use workspace::Item;
pub struct ConfigurationView {
focus_handle: FocusHandle,
configuration_views: HashMap<LanguageModelProviderId, AnyView>,
_registry_subscription: Subscription,
}
impl ConfigurationView {
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let focus_handle = cx.focus_handle();
let registry_subscription = cx.subscribe_in(
&LanguageModelRegistry::global(cx),
window,
|this, _, event: &language_model::Event, window, cx| match event {
language_model::Event::AddedProvider(provider_id) => {
let provider = LanguageModelRegistry::read_global(cx).provider(provider_id);
if let Some(provider) = provider {
this.add_configuration_view(&provider, window, cx);
}
}
language_model::Event::RemovedProvider(provider_id) => {
this.remove_configuration_view(provider_id);
}
_ => {}
},
);
let mut this = Self {
focus_handle,
configuration_views: HashMap::default(),
_registry_subscription: registry_subscription,
};
this.build_configuration_views(window, cx);
this
}
fn build_configuration_views(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let providers = LanguageModelRegistry::read_global(cx).providers();
for provider in providers {
self.add_configuration_view(&provider, window, cx);
}
}
fn remove_configuration_view(&mut self, provider_id: &LanguageModelProviderId) {
self.configuration_views.remove(provider_id);
}
fn add_configuration_view(
&mut self,
provider: &Arc<dyn LanguageModelProvider>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let configuration_view = provider.configuration_view(window, cx);
self.configuration_views
.insert(provider.id(), configuration_view);
}
fn render_provider_view(
&mut self,
provider: &Arc<dyn LanguageModelProvider>,
cx: &mut Context<Self>,
) -> Div {
let provider_id = provider.id().0.clone();
let provider_name = provider.name().0.clone();
let configuration_view = self.configuration_views.get(&provider.id()).cloned();
let open_new_context = cx.listener({
let provider = provider.clone();
move |_, _, _window, cx| {
cx.emit(ConfigurationViewEvent::NewProviderContextEditor(
provider.clone(),
))
}
});
v_flex()
.gap_2()
.child(
h_flex()
.justify_between()
.child(Headline::new(provider_name.clone()).size(HeadlineSize::Small))
.when(provider.is_authenticated(cx), move |this| {
this.child(
h_flex().justify_end().child(
Button::new(
SharedString::from(format!("new-context-{provider_id}")),
"Open New Chat",
)
.icon_position(IconPosition::Start)
.icon(IconName::Plus)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.on_click(open_new_context),
),
)
}),
)
.child(
div()
.p(DynamicSpacing::Base08.rems(cx))
.bg(cx.theme().colors().surface_background)
.border_1()
.border_color(cx.theme().colors().border_variant)
.rounded_sm()
.when(configuration_view.is_none(), |this| {
this.child(div().child(Label::new(format!(
"No configuration view for {}",
provider_name
))))
})
.when_some(configuration_view, |this, configuration_view| {
this.child(configuration_view)
}),
)
}
}
impl Render for ConfigurationView {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let providers = LanguageModelRegistry::read_global(cx).providers();
let provider_views = providers
.into_iter()
.map(|provider| self.render_provider_view(&provider, cx))
.collect::<Vec<_>>();
let mut element = v_flex()
.id("assistant-configuration-view")
.track_focus(&self.focus_handle(cx))
.bg(cx.theme().colors().editor_background)
.size_full()
.overflow_y_scroll()
.child(
v_flex()
.p(DynamicSpacing::Base16.rems(cx))
.border_b_1()
.border_color(cx.theme().colors().border)
.gap_1()
.child(Headline::new("Configure your Assistant").size(HeadlineSize::Medium))
.child(
Label::new(
"At least one LLM provider must be configured to use the Assistant.",
)
.color(Color::Muted),
),
)
.child(
v_flex()
.p(DynamicSpacing::Base16.rems(cx))
.mt_1()
.gap_6()
.flex_1()
.children(provider_views),
)
.into_any();
// We use a canvas here to get scrolling to work in the ConfigurationView. It's a workaround
// because we couldn't the element to take up the size of the parent.
canvas(
move |bounds, window, cx| {
element.prepaint_as_root(bounds.origin, bounds.size.into(), window, cx);
element
},
|_, mut element, window, cx| {
element.paint(window, cx);
},
)
.flex_1()
.w_full()
}
}
pub enum ConfigurationViewEvent {
NewProviderContextEditor(Arc<dyn LanguageModelProvider>),
}
impl EventEmitter<ConfigurationViewEvent> for ConfigurationView {}
impl Focusable for ConfigurationView {
fn focus_handle(&self, _: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Item for ConfigurationView {
type Event = ConfigurationViewEvent;
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
"Configuration".into()
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -22,6 +22,7 @@ clock.workspace = true
collections.workspace = true
context_server.workspace = true
editor.workspace = true
feature_flags.workspace = true
fs.workspace = true
futures.workspace = true
fuzzy.workspace = true
@@ -46,6 +47,7 @@ serde_json.workspace = true
settings.workspace = true
smallvec.workspace = true
smol.workspace = true
strum.workspace = true
telemetry_events.workspace = true
text.workspace = true
theme.workspace = true

View File

@@ -2,33 +2,22 @@ mod context;
mod context_editor;
mod context_history;
mod context_store;
mod patch;
mod slash_command;
mod slash_command_picker;
use std::sync::Arc;
use client::Client;
use gpui::{App, Context};
use workspace::Workspace;
use gpui::App;
pub use crate::context::*;
pub use crate::context_editor::*;
pub use crate::context_history::*;
pub use crate::context_store::*;
pub use crate::patch::*;
pub use crate::slash_command::*;
pub fn init(client: Arc<Client>, cx: &mut App) {
pub fn init(client: Arc<Client>, _cx: &mut App) {
context_store::init(&client.into());
workspace::FollowableViewRegistry::register::<ContextEditor>(cx);
cx.observe_new(
|workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
workspace
.register_action(ContextEditor::quote_selection)
.register_action(ContextEditor::insert_selection)
.register_action(ContextEditor::copy_code)
.register_action(ContextEditor::handle_insert_dragged_files);
},
)
.detach();
}

View File

@@ -1,8 +1,8 @@
#[cfg(test)]
mod context_tests;
use anyhow::{Context as _, Result, anyhow, bail};
use assistant_settings::AssistantSettings;
use crate::patch::{AssistantEdit, AssistantPatch, AssistantPatchStatus};
use anyhow::{Context as _, Result, anyhow};
use assistant_slash_command::{
SlashCommandContent, SlashCommandEvent, SlashCommandLine, SlashCommandOutputSection,
SlashCommandResult, SlashCommandWorkingSet,
@@ -36,6 +36,7 @@ use std::{
iter, mem,
ops::Range,
path::Path,
str::FromStr as _,
sync::Arc,
time::{Duration, Instant},
};
@@ -120,6 +121,14 @@ impl MessageStatus {
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RequestType {
/// Request a normal chat response from the model.
Chat,
/// Add a preamble to the message, which tells the model to return a structured response that suggests edits.
SuggestEdits,
}
#[derive(Clone, Debug)]
pub enum ContextOperation {
InsertMessage {
@@ -133,7 +142,7 @@ pub enum ContextOperation {
version: clock::Global,
},
UpdateSummary {
summary: ContextSummaryContent,
summary: ContextSummary,
version: clock::Global,
},
SlashCommandStarted {
@@ -203,7 +212,7 @@ impl ContextOperation {
version: language::proto::deserialize_version(&update.version),
}),
proto::context_operation::Variant::UpdateSummary(update) => Ok(Self::UpdateSummary {
summary: ContextSummaryContent {
summary: ContextSummary {
text: update.summary,
done: update.done,
timestamp: language::proto::deserialize_timestamp(
@@ -454,6 +463,10 @@ pub enum ContextEvent {
StreamedCompletion,
StartedThoughtProcess(Range<language::Anchor>),
EndedThoughtProcess(language::Anchor),
PatchesUpdated {
removed: Vec<Range<language::Anchor>>,
updated: Vec<Range<language::Anchor>>,
},
InvokedSlashCommandChanged {
command_id: InvokedSlashCommandId,
},
@@ -467,73 +480,11 @@ pub enum ContextEvent {
Operation(ContextOperation),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ContextSummary {
Pending,
Content(ContextSummaryContent),
Error,
}
#[derive(Default, Clone, Debug, Eq, PartialEq)]
pub struct ContextSummaryContent {
#[derive(Clone, Default, Debug)]
pub struct ContextSummary {
pub text: String,
pub done: bool,
pub timestamp: clock::Lamport,
}
impl ContextSummary {
pub const DEFAULT: &str = "New Text Thread";
pub fn or_default(&self) -> SharedString {
self.unwrap_or(Self::DEFAULT)
}
pub fn unwrap_or(&self, message: impl Into<SharedString>) -> SharedString {
self.content()
.map_or_else(|| message.into(), |content| content.text.clone().into())
}
pub fn content(&self) -> Option<&ContextSummaryContent> {
match self {
ContextSummary::Content(content) => Some(content),
ContextSummary::Pending | ContextSummary::Error => None,
}
}
fn content_as_mut(&mut self) -> Option<&mut ContextSummaryContent> {
match self {
ContextSummary::Content(content) => Some(content),
ContextSummary::Pending | ContextSummary::Error => None,
}
}
fn content_or_set_empty(&mut self) -> &mut ContextSummaryContent {
match self {
ContextSummary::Content(content) => content,
ContextSummary::Pending | ContextSummary::Error => {
let content = ContextSummaryContent::default();
*self = ContextSummary::Content(content);
self.content_as_mut().unwrap()
}
}
}
pub fn is_pending(&self) -> bool {
matches!(self, ContextSummary::Pending)
}
fn timestamp(&self) -> Option<clock::Lamport> {
match self {
ContextSummary::Content(content) => Some(content.timestamp),
ContextSummary::Pending | ContextSummary::Error => None,
}
}
}
impl PartialOrd for ContextSummary {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.timestamp().partial_cmp(&other.timestamp())
}
timestamp: clock::Lamport,
}
#[derive(Clone, Debug, Eq, PartialEq)]
@@ -653,6 +604,26 @@ struct PendingCompletion {
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
pub struct InvokedSlashCommandId(clock::Lamport);
#[derive(Clone, Debug)]
pub struct XmlTag {
pub kind: XmlTagKind,
pub range: Range<text::Anchor>,
pub is_open_tag: bool,
}
#[derive(Copy, Clone, Debug, strum::EnumString, PartialEq, Eq, strum::AsRefStr)]
#[strum(serialize_all = "snake_case")]
pub enum XmlTagKind {
Patch,
Title,
Edit,
Path,
Description,
OldText,
NewText,
Operation,
}
pub struct AssistantContext {
id: ContextId,
timestamp: clock::Lamport,
@@ -669,7 +640,7 @@ pub struct AssistantContext {
message_anchors: Vec<MessageAnchor>,
contents: Vec<Content>,
messages_metadata: HashMap<MessageId, MessageMetadata>,
summary: ContextSummary,
summary: Option<ContextSummary>,
summary_task: Task<Option<()>>,
completion_count: usize,
pending_completions: Vec<PendingCompletion>,
@@ -681,6 +652,8 @@ pub struct AssistantContext {
_subscriptions: Vec<Subscription>,
telemetry: Option<Arc<Telemetry>>,
language_registry: Arc<LanguageRegistry>,
patches: Vec<AssistantPatch>,
xml_tags: Vec<XmlTag>,
project: Option<Entity<Project>>,
prompt_builder: Arc<PromptBuilder>,
}
@@ -695,6 +668,18 @@ impl ContextAnnotation for ParsedSlashCommand {
}
}
impl ContextAnnotation for AssistantPatch {
fn range(&self) -> &Range<language::Anchor> {
&self.range
}
}
impl ContextAnnotation for XmlTag {
fn range(&self) -> &Range<language::Anchor> {
&self.range
}
}
impl EventEmitter<ContextEvent> for AssistantContext {}
impl AssistantContext {
@@ -756,7 +741,7 @@ impl AssistantContext {
slash_command_output_sections: Vec::new(),
thought_process_output_sections: Vec::new(),
edits_since_last_parse: edits_since_last_slash_command_parse,
summary: ContextSummary::Pending,
summary: None,
summary_task: Task::ready(None),
completion_count: Default::default(),
pending_completions: Default::default(),
@@ -771,6 +756,8 @@ impl AssistantContext {
project,
language_registry,
slash_commands,
patches: Vec::new(),
xml_tags: Vec::new(),
prompt_builder,
};
@@ -815,7 +802,7 @@ impl AssistantContext {
.collect(),
summary: self
.summary
.content()
.as_ref()
.map(|summary| summary.text.clone())
.unwrap_or_default(),
slash_command_output_sections: self
@@ -1001,10 +988,12 @@ impl AssistantContext {
summary: new_summary,
..
} => {
if self.summary.timestamp().map_or(true, |current_timestamp| {
new_summary.timestamp > current_timestamp
}) {
self.summary = ContextSummary::Content(new_summary);
if self
.summary
.as_ref()
.map_or(true, |summary| new_summary.timestamp > summary.timestamp)
{
self.summary = Some(new_summary);
summary_generated = true;
}
}
@@ -1162,8 +1151,50 @@ impl AssistantContext {
self.path.as_ref()
}
pub fn summary(&self) -> &ContextSummary {
&self.summary
pub fn summary(&self) -> Option<&ContextSummary> {
self.summary.as_ref()
}
pub fn patch_containing(&self, position: Point, cx: &App) -> Option<&AssistantPatch> {
let buffer = self.buffer.read(cx);
let index = self.patches.binary_search_by(|patch| {
let patch_range = patch.range.to_point(&buffer);
if position < patch_range.start {
Ordering::Greater
} else if position > patch_range.end {
Ordering::Less
} else {
Ordering::Equal
}
});
if let Ok(ix) = index {
Some(&self.patches[ix])
} else {
None
}
}
pub fn patch_ranges(&self) -> impl Iterator<Item = Range<language::Anchor>> + '_ {
self.patches.iter().map(|patch| patch.range.clone())
}
pub fn patch_for_range(
&self,
range: &Range<language::Anchor>,
cx: &App,
) -> Option<&AssistantPatch> {
let buffer = self.buffer.read(cx);
let index = self.patch_index_for_range(range, buffer).ok()?;
Some(&self.patches[index])
}
fn patch_index_for_range(
&self,
tagged_range: &Range<text::Anchor>,
buffer: &text::BufferSnapshot,
) -> Result<usize, usize> {
self.patches
.binary_search_by(|probe| probe.range.cmp(&tagged_range, buffer))
}
pub fn parsed_slash_commands(&self) -> &[ParsedSlashCommand] {
@@ -1242,10 +1273,10 @@ impl AssistantContext {
pub(crate) fn count_remaining_tokens(&mut self, cx: &mut Context<Self>) {
// Assume it will be a Chat request, even though that takes fewer tokens (and risks going over the limit),
// because otherwise you see in the UI that your empty message has a bunch of tokens already used.
let request = self.to_completion_request(RequestType::Chat, cx);
let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
return;
};
let request = self.to_completion_request(Some(&model.model), cx);
let debounce = self.token_count.is_some();
self.pending_token_count = cx.spawn(async move |this, cx| {
async move {
@@ -1391,7 +1422,7 @@ impl AssistantContext {
}
let request = {
let mut req = self.to_completion_request(Some(&model), cx);
let mut req = self.to_completion_request(RequestType::Chat, cx);
// Skip the last message because it's likely to change and
// therefore would be a waste to cache.
req.messages.pop();
@@ -1466,6 +1497,8 @@ impl AssistantContext {
let mut removed_parsed_slash_command_ranges = Vec::new();
let mut updated_parsed_slash_commands = Vec::new();
let mut removed_patches = Vec::new();
let mut updated_patches = Vec::new();
while let Some(mut row_range) = row_ranges.next() {
while let Some(next_row_range) = row_ranges.peek() {
if row_range.end >= next_row_range.start {
@@ -1490,6 +1523,13 @@ impl AssistantContext {
cx,
);
self.invalidate_pending_slash_commands(&buffer, cx);
self.reparse_patches_in_range(
start..end,
&buffer,
&mut updated_patches,
&mut removed_patches,
cx,
);
}
if !updated_parsed_slash_commands.is_empty()
@@ -1500,6 +1540,13 @@ impl AssistantContext {
updated: updated_parsed_slash_commands,
});
}
if !updated_patches.is_empty() || !removed_patches.is_empty() {
cx.emit(ContextEvent::PatchesUpdated {
removed: removed_patches,
updated: updated_patches,
});
}
}
fn reparse_slash_commands_in_range(
@@ -1590,6 +1637,267 @@ impl AssistantContext {
}
}
fn reparse_patches_in_range(
&mut self,
range: Range<text::Anchor>,
buffer: &BufferSnapshot,
updated: &mut Vec<Range<text::Anchor>>,
removed: &mut Vec<Range<text::Anchor>>,
cx: &mut Context<Self>,
) {
// Rebuild the XML tags in the edited range.
let intersecting_tags_range =
self.indices_intersecting_buffer_range(&self.xml_tags, range.clone(), cx);
let new_tags = self.parse_xml_tags_in_range(buffer, range.clone(), cx);
self.xml_tags
.splice(intersecting_tags_range.clone(), new_tags);
// Find which patches intersect the changed range.
let intersecting_patches_range =
self.indices_intersecting_buffer_range(&self.patches, range.clone(), cx);
// Reparse all tags after the last unchanged patch before the change.
let mut tags_start_ix = 0;
if let Some(preceding_unchanged_patch) =
self.patches[..intersecting_patches_range.start].last()
{
tags_start_ix = match self.xml_tags.binary_search_by(|tag| {
tag.range
.start
.cmp(&preceding_unchanged_patch.range.end, buffer)
.then(Ordering::Less)
}) {
Ok(ix) | Err(ix) => ix,
};
}
// Rebuild the patches in the range.
let new_patches = self.parse_patches(tags_start_ix, range.end, buffer, cx);
updated.extend(new_patches.iter().map(|patch| patch.range.clone()));
let removed_patches = self.patches.splice(intersecting_patches_range, new_patches);
removed.extend(
removed_patches
.map(|patch| patch.range)
.filter(|range| !updated.contains(&range)),
);
}
fn parse_xml_tags_in_range(
&self,
buffer: &BufferSnapshot,
range: Range<text::Anchor>,
cx: &App,
) -> Vec<XmlTag> {
let mut messages = self.messages(cx).peekable();
let mut tags = Vec::new();
let mut lines = buffer.text_for_range(range).lines();
let mut offset = lines.offset();
while let Some(line) = lines.next() {
while let Some(message) = messages.peek() {
if offset < message.offset_range.end {
break;
} else {
messages.next();
}
}
let is_assistant_message = messages
.peek()
.map_or(false, |message| message.role == Role::Assistant);
if is_assistant_message {
for (start_ix, _) in line.match_indices('<') {
let mut name_start_ix = start_ix + 1;
let closing_bracket_ix = line[start_ix..].find('>').map(|i| start_ix + i);
if let Some(closing_bracket_ix) = closing_bracket_ix {
let end_ix = closing_bracket_ix + 1;
let mut is_open_tag = true;
if line[name_start_ix..closing_bracket_ix].starts_with('/') {
name_start_ix += 1;
is_open_tag = false;
}
let tag_inner = &line[name_start_ix..closing_bracket_ix];
let tag_name_len = tag_inner
.find(|c: char| c.is_whitespace())
.unwrap_or(tag_inner.len());
if let Ok(kind) = XmlTagKind::from_str(&tag_inner[..tag_name_len]) {
tags.push(XmlTag {
range: buffer.anchor_after(offset + start_ix)
..buffer.anchor_before(offset + end_ix),
is_open_tag,
kind,
});
};
}
}
}
offset = lines.offset();
}
tags
}
fn parse_patches(
&mut self,
tags_start_ix: usize,
buffer_end: text::Anchor,
buffer: &BufferSnapshot,
cx: &App,
) -> Vec<AssistantPatch> {
let mut new_patches = Vec::new();
let mut pending_patch = None;
let mut patch_tag_depth = 0;
let mut tags = self.xml_tags[tags_start_ix..].iter().peekable();
'tags: while let Some(tag) = tags.next() {
if tag.range.start.cmp(&buffer_end, buffer).is_gt() && patch_tag_depth == 0 {
break;
}
if tag.kind == XmlTagKind::Patch && tag.is_open_tag {
patch_tag_depth += 1;
let patch_start = tag.range.start;
let mut edits = Vec::<Result<AssistantEdit>>::new();
let mut patch = AssistantPatch {
range: patch_start..patch_start,
title: String::new().into(),
edits: Default::default(),
status: crate::AssistantPatchStatus::Pending,
};
while let Some(tag) = tags.next() {
if tag.kind == XmlTagKind::Patch && !tag.is_open_tag {
patch_tag_depth -= 1;
if patch_tag_depth == 0 {
patch.range.end = tag.range.end;
// Include the line immediately after this <patch> tag if it's empty.
let patch_end_offset = patch.range.end.to_offset(buffer);
let mut patch_end_chars = buffer.chars_at(patch_end_offset);
if patch_end_chars.next() == Some('\n')
&& patch_end_chars.next().map_or(true, |ch| ch == '\n')
{
let messages = self.messages_for_offsets(
[patch_end_offset, patch_end_offset + 1],
cx,
);
if messages.len() == 1 {
patch.range.end = buffer.anchor_before(patch_end_offset + 1);
}
}
edits.sort_unstable_by(|a, b| {
if let (Ok(a), Ok(b)) = (a, b) {
a.path.cmp(&b.path)
} else {
Ordering::Equal
}
});
patch.edits = edits.into();
patch.status = AssistantPatchStatus::Ready;
new_patches.push(patch);
continue 'tags;
}
}
if tag.kind == XmlTagKind::Title && tag.is_open_tag {
let content_start = tag.range.end;
while let Some(tag) = tags.next() {
if tag.kind == XmlTagKind::Title && !tag.is_open_tag {
let content_end = tag.range.start;
patch.title =
trimmed_text_in_range(buffer, content_start..content_end)
.into();
break;
}
}
}
if tag.kind == XmlTagKind::Edit && tag.is_open_tag {
let mut path = None;
let mut old_text = None;
let mut new_text = None;
let mut operation = None;
let mut description = None;
while let Some(tag) = tags.next() {
if tag.kind == XmlTagKind::Edit && !tag.is_open_tag {
edits.push(AssistantEdit::new(
path,
operation,
old_text,
new_text,
description,
));
break;
}
if tag.is_open_tag
&& [
XmlTagKind::Path,
XmlTagKind::OldText,
XmlTagKind::NewText,
XmlTagKind::Operation,
XmlTagKind::Description,
]
.contains(&tag.kind)
{
let kind = tag.kind;
let content_start = tag.range.end;
if let Some(tag) = tags.peek() {
if tag.kind == kind && !tag.is_open_tag {
let tag = tags.next().unwrap();
let content_end = tag.range.start;
let content = trimmed_text_in_range(
buffer,
content_start..content_end,
);
match kind {
XmlTagKind::Path => path = Some(content),
XmlTagKind::Operation => operation = Some(content),
XmlTagKind::OldText => {
old_text = Some(content).filter(|s| !s.is_empty())
}
XmlTagKind::NewText => {
new_text = Some(content).filter(|s| !s.is_empty())
}
XmlTagKind::Description => {
description =
Some(content).filter(|s| !s.is_empty())
}
_ => {}
}
}
}
}
}
}
}
patch.edits = edits.into();
pending_patch = Some(patch);
}
}
if let Some(mut pending_patch) = pending_patch {
let patch_start = pending_patch.range.start.to_offset(buffer);
if let Some(message) = self.message_for_offset(patch_start, cx) {
if message.anchor_range.end == text::Anchor::MAX {
pending_patch.range.end = text::Anchor::MAX;
} else {
let message_end = buffer.anchor_after(message.offset_range.end - 1);
pending_patch.range.end = message_end;
}
} else {
pending_patch.range.end = text::Anchor::MAX;
}
new_patches.push(pending_patch);
}
new_patches
}
pub fn pending_command_for_position(
&mut self,
position: language::Anchor,
@@ -1994,7 +2302,11 @@ impl AssistantContext {
})
}
pub fn assist(&mut self, cx: &mut Context<Self>) -> Option<MessageAnchor> {
pub fn assist(
&mut self,
request_type: RequestType,
cx: &mut Context<Self>,
) -> Option<MessageAnchor> {
let model_registry = LanguageModelRegistry::read_global(cx);
let model = model_registry.default_model()?;
let last_message_id = self.get_last_valid_message_id(cx)?;
@@ -2009,7 +2321,7 @@ impl AssistantContext {
// Compute which messages to cache, including the last one.
self.mark_cache_anchors(&model.cache_configuration(), false, cx);
let request = self.to_completion_request(Some(&model), cx);
let request = self.to_completion_request(request_type, cx);
let assistant_message = self
.insert_message_after(last_message_id, Role::Assistant, MessageStatus::Pending, cx)
@@ -2249,7 +2561,7 @@ impl AssistantContext {
pub fn to_completion_request(
&self,
model: Option<&Arc<dyn LanguageModel>>,
request_type: RequestType,
cx: &App,
) -> LanguageModelRequest {
let buffer = self.buffer.read(cx);
@@ -2271,10 +2583,8 @@ impl AssistantContext {
mode: None,
messages: Vec::new(),
tools: Vec::new(),
tool_choice: None,
stop: Vec::new(),
temperature: model
.and_then(|model| AssistantSettings::temperature_for_model(model, cx)),
temperature: None,
};
for message in self.messages(cx) {
if message.status != MessageStatus::Done {
@@ -2330,6 +2640,25 @@ impl AssistantContext {
}
}
if let RequestType::SuggestEdits = request_type {
if let Ok(preamble) = self.prompt_builder.generate_suggest_edits_prompt() {
let last_elem_index = completion_request.messages.len();
completion_request
.messages
.push(LanguageModelRequestMessage {
role: Role::User,
content: vec![MessageContent::Text(preamble)],
cache: false,
});
// The preamble message should be sent right before the last actual user message.
completion_request
.messages
.swap(last_elem_index, last_elem_index.saturating_sub(1));
}
}
completion_request
}
@@ -2364,6 +2693,17 @@ impl AssistantContext {
ranges.push(message.anchor_range.clone());
}
}
let buffer = self.buffer.read(cx).text_snapshot();
let mut updated = Vec::new();
let mut removed = Vec::new();
for range in ranges {
self.reparse_patches_in_range(range, &buffer, &mut updated, &mut removed, cx);
}
if !updated.is_empty() || !removed.is_empty() {
cx.emit(ContextEvent::PatchesUpdated { removed, updated })
}
}
pub fn update_metadata(
@@ -2636,12 +2976,12 @@ impl AssistantContext {
return;
};
if replace_old || (self.message_anchors.len() >= 2 && self.summary.is_pending()) {
if replace_old || (self.message_anchors.len() >= 2 && self.summary.is_none()) {
if !model.provider.is_authenticated(cx) {
return;
}
let mut request = self.to_completion_request(Some(&model.model), cx);
let mut request = self.to_completion_request(RequestType::Chat, cx);
request.messages.push(LanguageModelRequestMessage {
role: Role::User,
content: vec![
@@ -2653,20 +2993,17 @@ impl AssistantContext {
// If there is no summary, it is set with `done: false` so that "Loading Summary…" can
// be displayed.
match self.summary {
ContextSummary::Pending | ContextSummary::Error => {
self.summary = ContextSummary::Content(ContextSummaryContent {
text: "".to_string(),
done: false,
timestamp: clock::Lamport::default(),
});
replace_old = true;
}
ContextSummary::Content(_) => {}
if self.summary.is_none() {
self.summary = Some(ContextSummary {
text: "".to_string(),
done: false,
timestamp: clock::Lamport::default(),
});
replace_old = true;
}
self.summary_task = cx.spawn(async move |this, cx| {
let result = async {
async move {
let stream = model.model.stream_completion_text(request, &cx);
let mut messages = stream.await?;
@@ -2677,7 +3014,7 @@ impl AssistantContext {
this.update(cx, |this, cx| {
let version = this.version.clone();
let timestamp = this.next_timestamp();
let summary = this.summary.content_or_set_empty();
let summary = this.summary.get_or_insert(ContextSummary::default());
if !replaced && replace_old {
summary.text.clear();
replaced = true;
@@ -2699,19 +3036,10 @@ impl AssistantContext {
}
}
this.read_with(cx, |this, _cx| {
if let Some(summary) = this.summary.content() {
if summary.text.is_empty() {
bail!("Model generated an empty summary");
}
}
Ok(())
})??;
this.update(cx, |this, cx| {
let version = this.version.clone();
let timestamp = this.next_timestamp();
if let Some(summary) = this.summary.content_as_mut() {
if let Some(summary) = this.summary.as_mut() {
summary.done = true;
summary.timestamp = timestamp;
let operation = ContextOperation::UpdateSummary {
@@ -2726,18 +3054,8 @@ impl AssistantContext {
anyhow::Ok(())
}
.await;
if let Err(err) = result {
this.update(cx, |this, cx| {
this.summary = ContextSummary::Error;
cx.emit(ContextEvent::SummaryChanged);
})
.log_err();
log::error!("Error generating context summary: {}", err);
}
Some(())
.log_err()
.await
});
}
}
@@ -2851,7 +3169,7 @@ impl AssistantContext {
let (old_path, summary) = this.read_with(cx, |this, _| {
let path = this.path.clone();
let summary = if let Some(summary) = this.summary.content() {
let summary = if let Some(summary) = this.summary.as_ref() {
if summary.done {
Some(summary.text.clone())
} else {
@@ -2905,12 +3223,39 @@ impl AssistantContext {
pub fn set_custom_summary(&mut self, custom_summary: String, cx: &mut Context<Self>) {
let timestamp = self.next_timestamp();
let summary = self.summary.content_or_set_empty();
let summary = self.summary.get_or_insert(ContextSummary::default());
summary.timestamp = timestamp;
summary.done = true;
summary.text = custom_summary;
cx.emit(ContextEvent::SummaryChanged);
}
pub const DEFAULT_SUMMARY: SharedString = SharedString::new_static("New Text Thread");
pub fn summary_or_default(&self) -> SharedString {
self.summary
.as_ref()
.map(|summary| summary.text.clone().into())
.unwrap_or(Self::DEFAULT_SUMMARY)
}
}
fn trimmed_text_in_range(buffer: &BufferSnapshot, range: Range<text::Anchor>) -> String {
let mut is_start = true;
let mut content = buffer
.text_for_range(range)
.map(|mut chunk| {
if is_start {
chunk = chunk.trim_start_matches('\n');
if !chunk.is_empty() {
is_start = false;
}
}
chunk
})
.collect::<String>();
content.truncate(content.trim_end().len());
content
}
#[derive(Debug, Default)]
@@ -3126,7 +3471,7 @@ impl SavedContext {
let timestamp = next_timestamp.tick();
operations.push(ContextOperation::UpdateSummary {
summary: ContextSummaryContent {
summary: ContextSummary {
text: self.summary,
done: true,
timestamp,

View File

@@ -1,6 +1,6 @@
use crate::{
AssistantContext, CacheStatus, ContextEvent, ContextId, ContextOperation, ContextSummary,
InvokedSlashCommandId, MessageCacheMetadata, MessageId, MessageStatus,
AssistantContext, AssistantEdit, AssistantEditKind, CacheStatus, ContextEvent, ContextId,
ContextOperation, InvokedSlashCommandId, MessageCacheMetadata, MessageId, MessageStatus,
};
use anyhow::Result;
use assistant_slash_command::{
@@ -16,10 +16,7 @@ use futures::{
};
use gpui::{App, Entity, SharedString, Task, TestAppContext, WeakEntity, prelude::*};
use language::{Buffer, BufferSnapshot, LanguageRegistry, LspAdapterDelegate};
use language_model::{
ConfiguredModel, LanguageModelCacheConfiguration, LanguageModelRegistry, Role,
fake_provider::{FakeLanguageModel, FakeLanguageModelProvider},
};
use language_model::{LanguageModelCacheConfiguration, LanguageModelRegistry, Role};
use parking_lot::Mutex;
use pretty_assertions::assert_eq;
use project::Project;
@@ -35,16 +32,20 @@ use std::{
rc::Rc,
sync::{Arc, atomic::AtomicBool},
};
use text::{ReplicaId, ToOffset, network::Network};
use text::{OffsetRangeExt as _, ReplicaId, ToOffset, network::Network};
use ui::{IconName, Window};
use unindent::Unindent;
use util::RandomCharIter;
use util::{
RandomCharIter,
test::{generate_marked_text, marked_text_ranges},
};
use workspace::Workspace;
#[gpui::test]
fn test_inserting_and_removing_messages(cx: &mut App) {
init_test(cx);
let settings_store = SettingsStore::test(cx);
LanguageModelRegistry::test(cx);
cx.set_global(settings_store);
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context = cx.new(|cx| {
@@ -181,8 +182,9 @@ fn test_inserting_and_removing_messages(cx: &mut App) {
#[gpui::test]
fn test_message_splitting(cx: &mut App) {
init_test(cx);
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
LanguageModelRegistry::test(cx);
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
@@ -283,8 +285,9 @@ fn test_message_splitting(cx: &mut App) {
#[gpui::test]
fn test_messages_for_offsets(cx: &mut App) {
init_test(cx);
let settings_store = SettingsStore::test(cx);
LanguageModelRegistry::test(cx);
cx.set_global(settings_store);
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context = cx.new(|cx| {
@@ -375,8 +378,10 @@ fn test_messages_for_offsets(cx: &mut App) {
#[gpui::test]
async fn test_slash_commands(cx: &mut TestAppContext) {
cx.update(init_test);
let settings_store = cx.update(SettingsStore::test);
cx.set_global(settings_store);
cx.update(LanguageModelRegistry::test);
cx.update(Project::init_settings);
let fs = FakeFs::new(cx.background_executor.clone());
fs.insert_tree(
@@ -665,9 +670,408 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
}
#[gpui::test]
async fn test_serialization(cx: &mut TestAppContext) {
cx.update(init_test);
async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
cx.update(prompt_store::init);
let mut settings_store = cx.update(SettingsStore::test);
cx.update(|cx| {
settings_store
.set_user_settings(
r#"{ "assistant": { "enable_experimental_live_diffs": true } }"#,
cx,
)
.unwrap()
});
cx.set_global(settings_store);
cx.update(language::init);
cx.update(Project::init_settings);
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, [Path::new("/root")], cx).await;
cx.update(LanguageModelRegistry::test);
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
// Create a new context
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context = cx.new(|cx| {
AssistantContext::local(
registry.clone(),
Some(project),
None,
prompt_builder.clone(),
Arc::new(SlashCommandWorkingSet::default()),
cx,
)
});
// Insert an assistant message to simulate a response.
let assistant_message_id = context.update(cx, |context, cx| {
let user_message_id = context.messages(cx).next().unwrap().id;
context
.insert_message_after(user_message_id, Role::Assistant, MessageStatus::Done, cx)
.unwrap()
.id
});
// No edit tags
edit(
&context,
"
«one
two
»",
cx,
);
expect_patches(
&context,
"
one
two
",
&[],
cx,
);
// Partial edit step tag is added
edit(
&context,
"
one
two
«
<patch»",
cx,
);
expect_patches(
&context,
"
one
two
<patch",
&[],
cx,
);
// The rest of the step tag is added. The unclosed
// step is treated as incomplete.
edit(
&context,
"
one
two
<patch«>
<edit>»",
cx,
);
expect_patches(
&context,
"
one
two
«<patch>
<edit>»",
&[&[]],
cx,
);
// The full patch is added
edit(
&context,
"
one
two
<patch>
<edit>«
<description>add a `two` function</description>
<path>src/lib.rs</path>
<operation>insert_after</operation>
<old_text>fn one</old_text>
<new_text>
fn two() {}
</new_text>
</edit>
</patch>
also,»",
cx,
);
expect_patches(
&context,
"
one
two
«<patch>
<edit>
<description>add a `two` function</description>
<path>src/lib.rs</path>
<operation>insert_after</operation>
<old_text>fn one</old_text>
<new_text>
fn two() {}
</new_text>
</edit>
</patch>
»
also,",
&[&[AssistantEdit {
path: "src/lib.rs".into(),
kind: AssistantEditKind::InsertAfter {
old_text: "fn one".into(),
new_text: "fn two() {}".into(),
description: Some("add a `two` function".into()),
},
}]],
cx,
);
// The step is manually edited.
edit(
&context,
"
one
two
<patch>
<edit>
<description>add a `two` function</description>
<path>src/lib.rs</path>
<operation>insert_after</operation>
<old_text>«fn zero»</old_text>
<new_text>
fn two() {}
</new_text>
</edit>
</patch>
also,",
cx,
);
expect_patches(
&context,
"
one
two
«<patch>
<edit>
<description>add a `two` function</description>
<path>src/lib.rs</path>
<operation>insert_after</operation>
<old_text>fn zero</old_text>
<new_text>
fn two() {}
</new_text>
</edit>
</patch>
»
also,",
&[&[AssistantEdit {
path: "src/lib.rs".into(),
kind: AssistantEditKind::InsertAfter {
old_text: "fn zero".into(),
new_text: "fn two() {}".into(),
description: Some("add a `two` function".into()),
},
}]],
cx,
);
// When setting the message role to User, the steps are cleared.
context.update(cx, |context, cx| {
context.cycle_message_roles(HashSet::from_iter([assistant_message_id]), cx);
context.cycle_message_roles(HashSet::from_iter([assistant_message_id]), cx);
});
expect_patches(
&context,
"
one
two
<patch>
<edit>
<description>add a `two` function</description>
<path>src/lib.rs</path>
<operation>insert_after</operation>
<old_text>fn zero</old_text>
<new_text>
fn two() {}
</new_text>
</edit>
</patch>
also,",
&[],
cx,
);
// When setting the message role back to Assistant, the steps are reparsed.
context.update(cx, |context, cx| {
context.cycle_message_roles(HashSet::from_iter([assistant_message_id]), cx);
});
expect_patches(
&context,
"
one
two
«<patch>
<edit>
<description>add a `two` function</description>
<path>src/lib.rs</path>
<operation>insert_after</operation>
<old_text>fn zero</old_text>
<new_text>
fn two() {}
</new_text>
</edit>
</patch>
»
also,",
&[&[AssistantEdit {
path: "src/lib.rs".into(),
kind: AssistantEditKind::InsertAfter {
old_text: "fn zero".into(),
new_text: "fn two() {}".into(),
description: Some("add a `two` function".into()),
},
}]],
cx,
);
// Ensure steps are re-parsed when deserializing.
let serialized_context = context.read_with(cx, |context, cx| context.serialize(cx));
let deserialized_context = cx.new(|cx| {
AssistantContext::deserialize(
serialized_context,
Path::new("").into(),
registry.clone(),
prompt_builder.clone(),
Arc::new(SlashCommandWorkingSet::default()),
None,
None,
cx,
)
});
expect_patches(
&deserialized_context,
"
one
two
«<patch>
<edit>
<description>add a `two` function</description>
<path>src/lib.rs</path>
<operation>insert_after</operation>
<old_text>fn zero</old_text>
<new_text>
fn two() {}
</new_text>
</edit>
</patch>
»
also,",
&[&[AssistantEdit {
path: "src/lib.rs".into(),
kind: AssistantEditKind::InsertAfter {
old_text: "fn zero".into(),
new_text: "fn two() {}".into(),
description: Some("add a `two` function".into()),
},
}]],
cx,
);
fn edit(
context: &Entity<AssistantContext>,
new_text_marked_with_edits: &str,
cx: &mut TestAppContext,
) {
context.update(cx, |context, cx| {
context.buffer.update(cx, |buffer, cx| {
buffer.edit_via_marked_text(&new_text_marked_with_edits.unindent(), None, cx);
});
});
cx.executor().run_until_parked();
}
#[track_caller]
fn expect_patches(
context: &Entity<AssistantContext>,
expected_marked_text: &str,
expected_suggestions: &[&[AssistantEdit]],
cx: &mut TestAppContext,
) {
let expected_marked_text = expected_marked_text.unindent();
let (expected_text, _) = marked_text_ranges(&expected_marked_text, false);
let (buffer_text, ranges, patches) = context.update(cx, |context, cx| {
context.buffer.read_with(cx, |buffer, _| {
let ranges = context
.patches
.iter()
.map(|entry| entry.range.to_offset(buffer))
.collect::<Vec<_>>();
(
buffer.text(),
ranges,
context
.patches
.iter()
.map(|step| step.edits.clone())
.collect::<Vec<_>>(),
)
})
});
assert_eq!(buffer_text, expected_text);
let actual_marked_text = generate_marked_text(&expected_text, &ranges, false);
assert_eq!(actual_marked_text, expected_marked_text);
assert_eq!(
patches
.iter()
.map(|patch| {
patch
.iter()
.map(|edit| {
let edit = edit.as_ref().unwrap();
AssistantEdit {
path: edit.path.clone(),
kind: edit.kind.clone(),
}
})
.collect::<Vec<_>>()
})
.collect::<Vec<_>>(),
expected_suggestions
);
}
}
#[gpui::test]
async fn test_serialization(cx: &mut TestAppContext) {
let settings_store = cx.update(SettingsStore::test);
cx.set_global(settings_store);
cx.update(LanguageModelRegistry::test);
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context = cx.new(|cx| {
@@ -743,8 +1147,6 @@ async fn test_serialization(cx: &mut TestAppContext) {
#[gpui::test(iterations = 100)]
async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: StdRng) {
cx.update(init_test);
let min_peers = env::var("MIN_PEERS")
.map(|i| i.parse().expect("invalid `MIN_PEERS` variable"))
.unwrap_or(2);
@@ -755,6 +1157,10 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
.unwrap_or(50);
let settings_store = cx.update(SettingsStore::test);
cx.set_global(settings_store);
cx.update(LanguageModelRegistry::test);
let slash_commands = cx.update(SlashCommandRegistry::default_global);
slash_commands.register_command(FakeSlashCommand("cmd-1".into()), false);
slash_commands.register_command(FakeSlashCommand("cmd-2".into()), false);
@@ -1023,8 +1429,9 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
#[gpui::test]
fn test_mark_cache_anchors(cx: &mut App) {
init_test(cx);
let settings_store = SettingsStore::test(cx);
LanguageModelRegistry::test(cx);
cx.set_global(settings_store);
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context = cx.new(|cx| {
@@ -1180,187 +1587,6 @@ fn test_mark_cache_anchors(cx: &mut App) {
);
}
#[gpui::test]
async fn test_summarization(cx: &mut TestAppContext) {
let (context, fake_model) = setup_context_editor_with_fake_model(cx);
// Initial state should be pending
context.read_with(cx, |context, _| {
assert!(matches!(context.summary(), ContextSummary::Pending));
assert_eq!(context.summary().or_default(), ContextSummary::DEFAULT);
});
let message_1 = context.read_with(cx, |context, _cx| context.message_anchors[0].clone());
context.update(cx, |context, cx| {
context
.insert_message_after(message_1.id, Role::Assistant, MessageStatus::Done, cx)
.unwrap();
});
// Send a message
context.update(cx, |context, cx| {
context.assist(cx);
});
simulate_successful_response(&fake_model, cx);
// Should start generating summary when there are >= 2 messages
context.read_with(cx, |context, _| {
assert!(!context.summary().content().unwrap().done);
});
cx.run_until_parked();
fake_model.stream_last_completion_response("Brief".into());
fake_model.stream_last_completion_response(" Introduction".into());
fake_model.end_last_completion_stream();
cx.run_until_parked();
// Summary should be set
context.read_with(cx, |context, _| {
assert_eq!(context.summary().or_default(), "Brief Introduction");
});
// We should be able to manually set a summary
context.update(cx, |context, cx| {
context.set_custom_summary("Brief Intro".into(), cx);
});
context.read_with(cx, |context, _| {
assert_eq!(context.summary().or_default(), "Brief Intro");
});
}
#[gpui::test]
async fn test_thread_summary_error_set_manually(cx: &mut TestAppContext) {
let (context, fake_model) = setup_context_editor_with_fake_model(cx);
test_summarize_error(&fake_model, &context, cx);
// Now we should be able to set a summary
context.update(cx, |context, cx| {
context.set_custom_summary("Brief Intro".into(), cx);
});
context.read_with(cx, |context, _| {
assert_eq!(context.summary().or_default(), "Brief Intro");
});
}
#[gpui::test]
async fn test_thread_summary_error_retry(cx: &mut TestAppContext) {
let (context, fake_model) = setup_context_editor_with_fake_model(cx);
test_summarize_error(&fake_model, &context, cx);
// Sending another message should not trigger another summarize request
context.update(cx, |context, cx| {
context.assist(cx);
});
simulate_successful_response(&fake_model, cx);
context.read_with(cx, |context, _| {
// State is still Error, not Generating
assert!(matches!(context.summary(), ContextSummary::Error));
});
// But the summarize request can be invoked manually
context.update(cx, |context, cx| {
context.summarize(true, cx);
});
context.read_with(cx, |context, _| {
assert!(!context.summary().content().unwrap().done);
});
cx.run_until_parked();
fake_model.stream_last_completion_response("A successful summary".into());
fake_model.end_last_completion_stream();
cx.run_until_parked();
context.read_with(cx, |context, _| {
assert_eq!(context.summary().or_default(), "A successful summary");
});
}
fn test_summarize_error(
model: &Arc<FakeLanguageModel>,
context: &Entity<AssistantContext>,
cx: &mut TestAppContext,
) {
let message_1 = context.read_with(cx, |context, _cx| context.message_anchors[0].clone());
context.update(cx, |context, cx| {
context
.insert_message_after(message_1.id, Role::Assistant, MessageStatus::Done, cx)
.unwrap();
});
// Send a message
context.update(cx, |context, cx| {
context.assist(cx);
});
simulate_successful_response(&model, cx);
context.read_with(cx, |context, _| {
assert!(!context.summary().content().unwrap().done);
});
// Simulate summary request ending
cx.run_until_parked();
model.end_last_completion_stream();
cx.run_until_parked();
// State is set to Error and default message
context.read_with(cx, |context, _| {
assert_eq!(*context.summary(), ContextSummary::Error);
assert_eq!(context.summary().or_default(), ContextSummary::DEFAULT);
});
}
fn setup_context_editor_with_fake_model(
cx: &mut TestAppContext,
) -> (Entity<AssistantContext>, Arc<FakeLanguageModel>) {
let registry = Arc::new(LanguageRegistry::test(cx.executor().clone()));
let fake_provider = Arc::new(FakeLanguageModelProvider);
let fake_model = Arc::new(fake_provider.test_model());
cx.update(|cx| {
init_test(cx);
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
registry.set_default_model(
Some(ConfiguredModel {
provider: fake_provider.clone(),
model: fake_model.clone(),
}),
cx,
)
})
});
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context = cx.new(|cx| {
AssistantContext::local(
registry,
None,
None,
prompt_builder.clone(),
Arc::new(SlashCommandWorkingSet::default()),
cx,
)
});
(context, fake_model)
}
fn simulate_successful_response(fake_model: &FakeLanguageModel, cx: &mut TestAppContext) {
cx.run_until_parked();
fake_model.stream_last_completion_response("Assistant response".into());
fake_model.end_last_completion_stream();
cx.run_until_parked();
}
fn messages(context: &Entity<AssistantContext>, cx: &App) -> Vec<(MessageId, Role, Range<usize>)> {
context
.read(cx)
@@ -1380,16 +1606,6 @@ fn messages_cache(
.collect()
}
fn init_test(cx: &mut App) {
let settings_store = SettingsStore::test(cx);
prompt_store::init(cx);
LanguageModelRegistry::test(cx);
cx.set_global(settings_store);
language::init(cx);
assistant_settings::init(cx);
Project::init_settings(cx);
}
#[derive(Clone)]
struct FakeSlashCommand(String);

View File

@@ -9,23 +9,25 @@ use client::{proto, zed_urls};
use collections::{BTreeSet, HashMap, HashSet, hash_map};
use editor::{
Anchor, Editor, EditorEvent, MenuInlineCompletionsPolicy, MultiBuffer, MultiBufferSnapshot,
RowExt, ToOffset as _, ToPoint,
ProposedChangeLocation, ProposedChangesEditor, RowExt, ToOffset as _, ToPoint,
actions::{MoveToEndOfLine, Newline, ShowCompletions},
display_map::{
BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata, CustomBlockId, FoldId,
RenderBlock, ToDisplayPoint,
BlockContext, BlockId, BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata,
CustomBlockId, FoldId, RenderBlock, ToDisplayPoint,
},
scroll::Autoscroll,
};
use editor::{FoldPlaceholder, display_map::CreaseId};
use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt as _};
use fs::Fs;
use futures::FutureExt;
use gpui::{
Animation, AnimationExt, AnyElement, AnyView, App, ClipboardEntry, ClipboardItem, Empty,
Entity, EventEmitter, FocusHandle, Focusable, FontWeight, Global, InteractiveElement,
IntoElement, ParentElement, Pixels, Render, RenderImage, SharedString, Size,
StatefulInteractiveElement, Styled, Subscription, Task, Transformation, WeakEntity, actions,
div, img, impl_internal_actions, percentage, point, prelude::*, pulsating_between, size,
Animation, AnimationExt, AnyElement, AnyView, App, AsyncWindowContext, ClipboardEntry,
ClipboardItem, CursorStyle, Empty, Entity, EventEmitter, FocusHandle, Focusable, FontWeight,
Global, InteractiveElement, IntoElement, ParentElement, Pixels, Render, RenderImage,
SharedString, Size, StatefulInteractiveElement, Styled, Subscription, Task, Transformation,
WeakEntity, actions, div, img, impl_internal_actions, percentage, point, prelude::*,
pulsating_between, size,
};
use indexed_docs::IndexedDocsStore;
use language::{
@@ -68,14 +70,14 @@ use workspace::{
Save, Toast, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
item::{self, FollowableItem, Item, ItemHandle},
notifications::NotificationId,
pane,
pane::{self, SaveIntent},
searchable::{SearchEvent, SearchableItem},
};
use crate::{
AssistantContext, CacheStatus, Content, ContextEvent, ContextId, InvokedSlashCommandId,
InvokedSlashCommandStatus, Message, MessageId, MessageMetadata, MessageStatus,
ParsedSlashCommand, PendingSlashCommandStatus,
AssistantContext, AssistantPatch, AssistantPatchStatus, CacheStatus, Content, ContextEvent,
ContextId, InvokedSlashCommandId, InvokedSlashCommandStatus, Message, MessageId,
MessageMetadata, MessageStatus, ParsedSlashCommand, PendingSlashCommandStatus, RequestType,
};
use crate::{
ThoughtProcessOutputSection, slash_command::SlashCommandCompletionProvider,
@@ -89,6 +91,7 @@ actions!(
ConfirmCommand,
CopyCode,
CycleMessageRole,
Edit,
InsertIntoEditor,
QuoteSelection,
Split,
@@ -109,10 +112,22 @@ struct ScrollPosition {
cursor: Anchor,
}
struct PatchViewState {
crease_id: CreaseId,
editor: Option<PatchEditorState>,
update_task: Option<Task<()>>,
}
struct PatchEditorState {
editor: WeakEntity<ProposedChangesEditor>,
opened_patch: AssistantPatch,
}
type MessageHeader = MessageMetadata;
#[derive(Clone)]
enum AssistError {
FileRequired,
PaymentRequired,
MaxMonthlySpendReached,
Message(SharedString),
@@ -123,7 +138,7 @@ pub enum ThoughtProcessStatus {
Completed,
}
pub trait AgentPanelDelegate {
pub trait AssistantPanelDelegate {
fn active_context_editor(
&self,
workspace: &mut Workspace,
@@ -157,7 +172,7 @@ pub trait AgentPanelDelegate {
);
}
impl dyn AgentPanelDelegate {
impl dyn AssistantPanelDelegate {
/// Returns the global [`AssistantPanelDelegate`], if it exists.
pub fn try_global(cx: &App) -> Option<Arc<Self>> {
cx.try_global::<GlobalAssistantPanelDelegate>()
@@ -170,7 +185,7 @@ impl dyn AgentPanelDelegate {
}
}
struct GlobalAssistantPanelDelegate(Arc<dyn AgentPanelDelegate>);
struct GlobalAssistantPanelDelegate(Arc<dyn AssistantPanelDelegate>);
impl Global for GlobalAssistantPanelDelegate {}
@@ -190,6 +205,8 @@ pub struct ContextEditor {
pending_slash_command_creases: HashMap<Range<language::Anchor>, CreaseId>,
invoked_slash_command_creases: HashMap<InvokedSlashCommandId, CreaseId>,
_subscriptions: Vec<Subscription>,
patches: HashMap<Range<language::Anchor>, PatchViewState>,
active_patch: Option<Range<language::Anchor>>,
last_error: Option<AssistError>,
show_accept_terms: bool,
pub(crate) slash_menu_handle:
@@ -226,9 +243,9 @@ impl ContextEditor {
let editor = cx.new(|cx| {
let mut editor =
Editor::for_buffer(context.read(cx).buffer().clone(), None, window, cx);
editor.disable_scrollbars_and_minimap(window, cx);
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
editor.set_show_line_numbers(false, cx);
editor.set_show_scrollbars(false, cx);
editor.set_show_git_diff_gutter(false, cx);
editor.set_show_code_actions(false, cx);
editor.set_show_runnables(false, cx);
@@ -258,6 +275,7 @@ impl ContextEditor {
let slash_command_sections = context.read(cx).slash_command_output_sections().to_vec();
let thought_process_sections = context.read(cx).thought_process_output_sections().to_vec();
let patch_ranges = context.read(cx).patch_ranges().collect::<Vec<_>>();
let slash_commands = context.read(cx).slash_commands().clone();
let mut this = Self {
context,
@@ -275,6 +293,8 @@ impl ContextEditor {
pending_slash_command_creases: HashMap::default(),
invoked_slash_command_creases: HashMap::default(),
_subscriptions,
patches: HashMap::default(),
active_patch: None,
last_error: None,
show_accept_terms: false,
slash_menu_handle: Default::default(),
@@ -305,6 +325,7 @@ impl ContextEditor {
window,
cx,
);
this.patches_updated(&Vec::new(), &patch_ranges, window, cx);
this
}
@@ -347,13 +368,34 @@ impl ContextEditor {
}
fn assist(&mut self, _: &Assist, window: &mut Window, cx: &mut Context<Self>) {
if self.sending_disabled(cx) {
return;
}
self.send_to_model(window, cx);
self.send_to_model(RequestType::Chat, window, cx);
}
fn send_to_model(&mut self, window: &mut Window, cx: &mut Context<Self>) {
fn edit(&mut self, _: &Edit, window: &mut Window, cx: &mut Context<Self>) {
self.send_to_model(RequestType::SuggestEdits, window, cx);
}
fn focus_active_patch(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
if let Some((_range, patch)) = self.active_patch() {
if let Some(editor) = patch
.editor
.as_ref()
.and_then(|state| state.editor.upgrade())
{
editor.focus_handle(cx).focus(window);
return true;
}
}
false
}
fn send_to_model(
&mut self,
request_type: RequestType,
window: &mut Window,
cx: &mut Context<Self>,
) {
let provider = LanguageModelRegistry::read_global(cx)
.default_model()
.map(|default| default.provider);
@@ -366,9 +408,19 @@ impl ContextEditor {
return;
}
if self.focus_active_patch(window, cx) {
return;
}
self.last_error = None;
if let Some(user_message) = self.context.update(cx, |context, cx| context.assist(cx)) {
if request_type == RequestType::SuggestEdits && !self.context.read(cx).contains_files(cx) {
self.last_error = Some(AssistError::FileRequired);
cx.notify();
} else if let Some(user_message) = self
.context
.update(cx, |context, cx| context.assist(request_type, cx))
{
let new_selection = {
let cursor = user_message
.start
@@ -633,6 +685,9 @@ impl ContextEditor {
}
});
}
ContextEvent::PatchesUpdated { removed, updated } => {
self.patches_updated(removed, updated, window, cx);
}
ContextEvent::ParsedSlashCommandsUpdated { removed, updated } => {
self.editor.update(cx, |editor, cx| {
let buffer = editor.buffer().read(cx).snapshot(cx);
@@ -837,6 +892,131 @@ impl ContextEditor {
});
}
fn patches_updated(
&mut self,
removed: &Vec<Range<text::Anchor>>,
updated: &Vec<Range<text::Anchor>>,
window: &mut Window,
cx: &mut Context<ContextEditor>,
) {
let this = cx.entity().downgrade();
let mut editors_to_close = Vec::new();
self.editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(window, cx);
let multibuffer = &snapshot.buffer_snapshot;
let (&excerpt_id, _, _) = multibuffer.as_singleton().unwrap();
let mut removed_crease_ids = Vec::new();
let mut ranges_to_unfold: Vec<Range<Anchor>> = Vec::new();
for range in removed {
if let Some(state) = self.patches.remove(range) {
let patch_start = multibuffer
.anchor_in_excerpt(excerpt_id, range.start)
.unwrap();
let patch_end = multibuffer
.anchor_in_excerpt(excerpt_id, range.end)
.unwrap();
editors_to_close.extend(state.editor.and_then(|state| state.editor.upgrade()));
ranges_to_unfold.push(patch_start..patch_end);
removed_crease_ids.push(state.crease_id);
}
}
editor.unfold_ranges(&ranges_to_unfold, true, false, cx);
editor.remove_creases(removed_crease_ids, cx);
for range in updated {
let Some(patch) = self.context.read(cx).patch_for_range(&range, cx).cloned() else {
continue;
};
let path_count = patch.path_count();
let patch_start = multibuffer
.anchor_in_excerpt(excerpt_id, patch.range.start)
.unwrap();
let patch_end = multibuffer
.anchor_in_excerpt(excerpt_id, patch.range.end)
.unwrap();
let render_block: RenderBlock = Arc::new({
let this = this.clone();
let patch_range = range.clone();
move |cx: &mut BlockContext| {
let max_width = cx.max_width;
let gutter_width = cx.gutter_dimensions.full_width();
let block_id = cx.block_id;
let selected = cx.selected;
let window = &mut cx.window;
this.update(cx.app, |this, cx| {
this.render_patch_block(
patch_range.clone(),
max_width,
gutter_width,
block_id,
selected,
window,
cx,
)
})
.ok()
.flatten()
.unwrap_or_else(|| Empty.into_any())
}
});
let height = path_count as u32 + 1;
let crease = Crease::block(
patch_start..patch_end,
height,
BlockStyle::Flex,
render_block.clone(),
);
let should_refold;
if let Some(state) = self.patches.get_mut(&range) {
if let Some(editor_state) = &state.editor {
if editor_state.opened_patch != patch {
state.update_task = Some({
let this = this.clone();
cx.spawn_in(window, async move |_, cx| {
Self::update_patch_editor(this.clone(), patch, cx)
.await
.log_err();
})
});
}
}
should_refold =
snapshot.intersects_fold(patch_start.to_offset(&snapshot.buffer_snapshot));
} else {
let crease_id = editor.insert_creases([crease.clone()], cx)[0];
self.patches.insert(
range.clone(),
PatchViewState {
crease_id,
editor: None,
update_task: None,
},
);
should_refold = true;
}
if should_refold {
editor.unfold_ranges(&[patch_start..patch_end], true, false, cx);
editor.fold_creases(vec![crease], false, window, cx);
}
}
});
for editor in editors_to_close {
self.close_patch_editor(editor, window, cx);
}
self.update_active_patch(window, cx);
}
fn insert_thought_process_output_sections(
&mut self,
sections: impl IntoIterator<
@@ -965,12 +1145,177 @@ impl ContextEditor {
}
EditorEvent::SelectionsChanged { .. } => {
self.scroll_position = self.cursor_scroll_position(window, cx);
self.update_active_patch(window, cx);
}
_ => {}
}
cx.emit(event.clone());
}
fn active_patch(&self) -> Option<(Range<text::Anchor>, &PatchViewState)> {
let patch = self.active_patch.as_ref()?;
Some((patch.clone(), self.patches.get(&patch)?))
}
fn update_active_patch(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let newest_cursor = self.editor.update(cx, |editor, cx| {
editor.selections.newest::<Point>(cx).head()
});
let context = self.context.read(cx);
let new_patch = context.patch_containing(newest_cursor, cx).cloned();
if new_patch.as_ref().map(|p| &p.range) == self.active_patch.as_ref() {
return;
}
if let Some(old_patch_range) = self.active_patch.take() {
if let Some(patch_state) = self.patches.get_mut(&old_patch_range) {
if let Some(state) = patch_state.editor.take() {
if let Some(editor) = state.editor.upgrade() {
self.close_patch_editor(editor, window, cx);
}
}
}
}
if let Some(new_patch) = new_patch {
self.active_patch = Some(new_patch.range.clone());
if let Some(patch_state) = self.patches.get_mut(&new_patch.range) {
let mut editor = None;
if let Some(state) = &patch_state.editor {
if let Some(opened_editor) = state.editor.upgrade() {
editor = Some(opened_editor);
}
}
if let Some(editor) = editor {
self.workspace
.update(cx, |workspace, cx| {
workspace.activate_item(&editor, true, false, window, cx);
})
.ok();
} else {
patch_state.update_task = Some(cx.spawn_in(window, async move |this, cx| {
Self::open_patch_editor(this, new_patch, cx).await.log_err();
}));
}
}
}
}
fn close_patch_editor(
&mut self,
editor: Entity<ProposedChangesEditor>,
window: &mut Window,
cx: &mut Context<ContextEditor>,
) {
self.workspace
.update(cx, |workspace, cx| {
if let Some(pane) = workspace.pane_for(&editor) {
pane.update(cx, |pane, cx| {
let item_id = editor.entity_id();
if !editor.read(cx).focus_handle(cx).is_focused(window) {
pane.close_item_by_id(item_id, SaveIntent::Skip, window, cx)
.detach_and_log_err(cx);
}
});
}
})
.ok();
}
async fn open_patch_editor(
this: WeakEntity<Self>,
patch: AssistantPatch,
cx: &mut AsyncWindowContext,
) -> Result<()> {
let project = this.read_with(cx, |this, _| this.project.clone())?;
let resolved_patch = patch.resolve(project.clone(), cx).await;
let editor = cx.new_window_entity(|window, cx| {
let editor = ProposedChangesEditor::new(
patch.title.clone(),
resolved_patch
.edit_groups
.iter()
.map(|(buffer, groups)| ProposedChangeLocation {
buffer: buffer.clone(),
ranges: groups
.iter()
.map(|group| group.context_range.clone())
.collect(),
})
.collect(),
Some(project.clone()),
window,
cx,
);
resolved_patch.apply(&editor, cx);
editor
})?;
this.update(cx, |this, _| {
if let Some(patch_state) = this.patches.get_mut(&patch.range) {
patch_state.editor = Some(PatchEditorState {
editor: editor.downgrade(),
opened_patch: patch,
});
patch_state.update_task.take();
}
})?;
this.read_with(cx, |this, _| this.workspace.clone())?
.update_in(cx, |workspace, window, cx| {
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, false, window, cx)
})
.log_err();
Ok(())
}
async fn update_patch_editor(
this: WeakEntity<Self>,
patch: AssistantPatch,
cx: &mut AsyncWindowContext,
) -> Result<()> {
let project = this.update(cx, |this, _| this.project.clone())?;
let resolved_patch = patch.resolve(project.clone(), cx).await;
this.update_in(cx, |this, window, cx| {
let patch_state = this.patches.get_mut(&patch.range)?;
let locations = resolved_patch
.edit_groups
.iter()
.map(|(buffer, groups)| ProposedChangeLocation {
buffer: buffer.clone(),
ranges: groups
.iter()
.map(|group| group.context_range.clone())
.collect(),
})
.collect();
if let Some(state) = &mut patch_state.editor {
if let Some(editor) = state.editor.upgrade() {
editor.update(cx, |editor, cx| {
editor.set_title(patch.title.clone(), cx);
editor.reset_locations(locations, window, cx);
resolved_patch.apply(editor, cx);
});
state.opened_patch = patch;
} else {
patch_state.editor.take();
}
}
patch_state.update_task.take();
Some(())
})?;
Ok(())
}
fn handle_editor_search_event(
&mut self,
_: &Entity<Editor>,
@@ -1144,7 +1489,7 @@ impl ContextEditor {
h_flex()
.id(("message_header", message_id.as_u64()))
.pl(cx.margins.gutter.full_width())
.pl(cx.gutter_dimensions.full_width())
.h_11()
.w_full()
.relative()
@@ -1239,7 +1584,6 @@ impl ContextEditor {
),
priority: usize::MAX,
render: render_block(MessageMetadata::from(message)),
render_in_minimap: false,
};
let mut new_blocks = vec![];
let mut block_index_to_message = vec![];
@@ -1322,11 +1666,11 @@ impl ContextEditor {
window: &mut Window,
cx: &mut Context<Workspace>,
) {
let Some(agent_panel_delegate) = <dyn AgentPanelDelegate>::try_global(cx) else {
let Some(assistant_panel_delegate) = <dyn AssistantPanelDelegate>::try_global(cx) else {
return;
};
let Some(context_editor_view) =
agent_panel_delegate.active_context_editor(workspace, window, cx)
assistant_panel_delegate.active_context_editor(workspace, window, cx)
else {
return;
};
@@ -1352,9 +1696,9 @@ impl ContextEditor {
cx: &mut Context<Workspace>,
) {
let result = maybe!({
let agent_panel_delegate = <dyn AgentPanelDelegate>::try_global(cx)?;
let assistant_panel_delegate = <dyn AssistantPanelDelegate>::try_global(cx)?;
let context_editor_view =
agent_panel_delegate.active_context_editor(workspace, window, cx)?;
assistant_panel_delegate.active_context_editor(workspace, window, cx)?;
Self::get_selection_or_code_block(&context_editor_view, cx)
});
let Some((text, is_code_block)) = result else {
@@ -1387,11 +1731,11 @@ impl ContextEditor {
window: &mut Window,
cx: &mut Context<Workspace>,
) {
let Some(agent_panel_delegate) = <dyn AgentPanelDelegate>::try_global(cx) else {
let Some(assistant_panel_delegate) = <dyn AssistantPanelDelegate>::try_global(cx) else {
return;
};
let Some(context_editor_view) =
agent_panel_delegate.active_context_editor(workspace, window, cx)
assistant_panel_delegate.active_context_editor(workspace, window, cx)
else {
return;
};
@@ -1477,7 +1821,7 @@ impl ContextEditor {
window: &mut Window,
cx: &mut Context<Workspace>,
) {
let Some(agent_panel_delegate) = <dyn AgentPanelDelegate>::try_global(cx) else {
let Some(assistant_panel_delegate) = <dyn AssistantPanelDelegate>::try_global(cx) else {
return;
};
@@ -1508,7 +1852,7 @@ impl ContextEditor {
return;
}
agent_panel_delegate.quote_selection(workspace, selections, buffer, window, cx);
assistant_panel_delegate.quote_selection(workspace, selections, buffer, window, cx);
}
pub fn quote_ranges(
@@ -1814,12 +2158,12 @@ impl ContextEditor {
let image_size = size_for_image(
&image,
size(
cx.max_width - cx.margins.gutter.full_width(),
cx.max_width - cx.gutter_dimensions.full_width(),
MAX_HEIGHT_IN_LINES as f32 * cx.line_height,
),
);
h_flex()
.pl(cx.margins.gutter.full_width())
.pl(cx.gutter_dimensions.full_width())
.child(
img(image.clone())
.object_fit(gpui::ObjectFit::ScaleDown)
@@ -1829,7 +2173,6 @@ impl ContextEditor {
.into_any_element()
}),
priority: 0,
render_in_minimap: false,
})
})
.collect::<Vec<_>>();
@@ -1860,12 +2203,119 @@ impl ContextEditor {
}
pub fn title(&self, cx: &App) -> SharedString {
self.context.read(cx).summary().or_default()
self.context.read(cx).summary_or_default()
}
pub fn regenerate_summary(&mut self, cx: &mut Context<Self>) {
self.context
.update(cx, |context, cx| context.summarize(true, cx));
fn render_patch_block(
&mut self,
range: Range<text::Anchor>,
max_width: Pixels,
gutter_width: Pixels,
id: BlockId,
selected: bool,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<AnyElement> {
let snapshot = self
.editor
.update(cx, |editor, cx| editor.snapshot(window, cx));
let (excerpt_id, _buffer_id, _) = snapshot.buffer_snapshot.as_singleton().unwrap();
let excerpt_id = *excerpt_id;
let anchor = snapshot
.buffer_snapshot
.anchor_in_excerpt(excerpt_id, range.start)
.unwrap();
let theme = cx.theme().clone();
let patch = self.context.read(cx).patch_for_range(&range, cx)?;
let paths = patch
.paths()
.map(|p| SharedString::from(p.to_string()))
.collect::<BTreeSet<_>>();
Some(
v_flex()
.id(id)
.bg(theme.colors().editor_background)
.ml(gutter_width)
.pb_1()
.w(max_width - gutter_width)
.rounded_sm()
.border_1()
.border_color(theme.colors().border_variant)
.overflow_hidden()
.hover(|style| style.border_color(theme.colors().text_accent))
.when(selected, |this| {
this.border_color(theme.colors().text_accent)
})
.cursor(CursorStyle::PointingHand)
.on_click(cx.listener(move |this, _, window, cx| {
this.editor.update(cx, |editor, cx| {
editor.change_selections(None, window, cx, |selections| {
selections.select_ranges(vec![anchor..anchor]);
});
});
this.focus_active_patch(window, cx);
}))
.child(
div()
.px_2()
.py_1()
.overflow_hidden()
.text_ellipsis()
.border_b_1()
.border_color(theme.colors().border_variant)
.bg(theme.colors().element_background)
.child(
Label::new(patch.title.clone())
.size(LabelSize::Small)
.color(Color::Muted),
),
)
.children(paths.into_iter().map(|path| {
h_flex()
.px_2()
.pt_1()
.gap_1p5()
.child(Icon::new(IconName::File).size(IconSize::Small))
.child(Label::new(path).size(LabelSize::Small))
}))
.when(patch.status == AssistantPatchStatus::Pending, |div| {
div.child(
h_flex()
.pt_1()
.px_2()
.gap_1()
.child(
Icon::new(IconName::ArrowCircle)
.size(IconSize::XSmall)
.color(Color::Muted)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| {
icon.transform(Transformation::rotate(percentage(
delta,
)))
},
),
)
.child(
Label::new("Generating…")
.color(Color::Muted)
.size(LabelSize::Small)
.with_animation(
"pulsating-label",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.4, 0.8)),
|label, delta| label.alpha(delta),
),
),
)
})
.into_any(),
)
}
fn render_notice(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
@@ -1899,24 +2349,11 @@ impl ContextEditor {
.log_err();
if let Some(client) = client {
cx.spawn(async move |context_editor, cx| {
match client.authenticate_and_connect(true, cx).await {
util::ConnectionResult::Timeout => {
log::error!("Authentication timeout")
}
util::ConnectionResult::ConnectionReset => {
log::error!("Connection reset")
}
util::ConnectionResult::Result(r) => {
if r.log_err().is_some() {
context_editor
.update(cx, |_, cx| cx.notify())
.ok();
}
}
}
cx.spawn(async move |this, cx| {
client.authenticate_and_connect(true, cx).await?;
this.update(cx, |_, cx| cx.notify())
})
.detach()
.detach_and_log_err(cx)
}
})),
)
@@ -1958,11 +2395,19 @@ impl ContextEditor {
.on_click({
let focus_handle = self.focus_handle(cx).clone();
move |_event, window, cx| {
focus_handle.dispatch_action(
&zed_actions::agent::OpenConfiguration,
window,
cx,
);
if cx.has_flag::<Assistant2FeatureFlag>() {
focus_handle.dispatch_action(
&zed_actions::agent::OpenConfiguration,
window,
cx,
);
} else {
focus_handle.dispatch_action(
&zed_actions::assistant::ShowConfiguration,
window,
cx,
);
};
}
}),
)
@@ -2000,14 +2445,29 @@ impl ContextEditor {
None => (ButtonStyle::Filled, None),
};
let model = LanguageModelRegistry::read_global(cx).default_model();
let has_configuration_error = configuration_error(cx).is_some();
let needs_to_accept_terms = self.show_accept_terms
&& model
.as_ref()
.map_or(false, |model| model.provider.must_accept_terms(cx));
let disabled = has_configuration_error || needs_to_accept_terms;
ButtonLike::new("send_button")
.disabled(self.sending_disabled(cx))
.disabled(disabled)
.style(style)
.when_some(tooltip, |button, tooltip| {
button.tooltip(move |_, _| tooltip.clone())
})
.layer(ElevationIndex::ModalSurface)
.child(Label::new("Send"))
.child(Label::new(
if AssistantSettings::get_global(cx).are_live_diffs_enabled(cx) {
"Chat"
} else {
"Send"
},
))
.children(
KeyBinding::for_action_in(&Assist, &focus_handle, window, cx)
.map(|binding| binding.into_any_element()),
@@ -2017,18 +2477,59 @@ impl ContextEditor {
})
}
/// Whether or not we should allow messages to be sent.
/// Will return false if the selected provided has a configuration error or
/// if the user has not accepted the terms of service for this provider.
fn sending_disabled(&self, cx: &mut Context<'_, ContextEditor>) -> bool {
let model = LanguageModelRegistry::read_global(cx).default_model();
fn render_edit_button(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let focus_handle = self.focus_handle(cx).clone();
let (style, tooltip) = match token_state(&self.context, cx) {
Some(TokenState::NoTokensLeft { .. }) => (
ButtonStyle::Tinted(TintColor::Error),
Some(Tooltip::text("Token limit reached")(window, cx)),
),
Some(TokenState::HasMoreTokens {
over_warn_threshold,
..
}) => {
let (style, tooltip) = if over_warn_threshold {
(
ButtonStyle::Tinted(TintColor::Warning),
Some(Tooltip::text("Token limit is close to exhaustion")(
window, cx,
)),
)
} else {
(ButtonStyle::Filled, None)
};
(style, tooltip)
}
None => (ButtonStyle::Filled, None),
};
let provider = LanguageModelRegistry::read_global(cx)
.default_model()
.map(|default| default.provider);
let has_configuration_error = configuration_error(cx).is_some();
let needs_to_accept_terms = self.show_accept_terms
&& model
&& provider
.as_ref()
.map_or(false, |model| model.provider.must_accept_terms(cx));
has_configuration_error || needs_to_accept_terms
.map_or(false, |provider| provider.must_accept_terms(cx));
let disabled = has_configuration_error || needs_to_accept_terms;
ButtonLike::new("edit_button")
.disabled(disabled)
.style(style)
.when_some(tooltip, |button, tooltip| {
button.tooltip(move |_, _| tooltip.clone())
})
.layer(ElevationIndex::ModalSurface)
.child(Label::new("Suggest Edits"))
.children(
KeyBinding::for_action_in(&Edit, &focus_handle, window, cx)
.map(|binding| binding.into_any_element()),
)
.on_click(move |_event, window, cx| {
focus_handle.dispatch_action(&Edit, window, cx);
})
}
fn render_inject_context_menu(&self, cx: &mut Context<Self>) -> impl IntoElement {
@@ -2106,6 +2607,7 @@ impl ContextEditor {
.elevation_2(cx)
.occlude()
.child(match last_error {
AssistError::FileRequired => self.render_file_required_error(cx),
AssistError::PaymentRequired => self.render_payment_required_error(cx),
AssistError::MaxMonthlySpendReached => {
self.render_max_monthly_spend_reached_error(cx)
@@ -2118,6 +2620,41 @@ impl ContextEditor {
)
}
fn render_file_required_error(&self, cx: &mut Context<Self>) -> AnyElement {
v_flex()
.gap_0p5()
.child(
h_flex()
.gap_1p5()
.items_center()
.child(Icon::new(IconName::Warning).color(Color::Warning))
.child(
Label::new("Suggest Edits needs a file to edit").weight(FontWeight::MEDIUM),
),
)
.child(
div()
.id("error-message")
.max_h_24()
.overflow_y_scroll()
.child(Label::new(
"To include files, type /file or /tab in your prompt.",
)),
)
.child(
h_flex()
.justify_end()
.mt_1()
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|this, _, _window, cx| {
this.last_error = None;
cx.notify();
},
))),
)
.into_any()
}
fn render_payment_required_error(&self, cx: &mut Context<Self>) -> AnyElement {
const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used.";
@@ -2558,6 +3095,7 @@ impl Render for ContextEditor {
.capture_action(cx.listener(ContextEditor::paste))
.capture_action(cx.listener(ContextEditor::cycle_message_role))
.capture_action(cx.listener(ContextEditor::confirm_command))
.on_action(cx.listener(ContextEditor::edit))
.on_action(cx.listener(ContextEditor::assist))
.on_action(cx.listener(ContextEditor::split))
.on_action(move |_: &ToggleModelSelector, window, cx| {
@@ -2610,6 +3148,20 @@ impl Render for ContextEditor {
h_flex()
.w_full()
.justify_end()
.when(
AssistantSettings::get_global(cx).are_live_diffs_enabled(cx),
|buttons| {
buttons
.items_center()
.gap_1p5()
.child(self.render_edit_button(window, cx))
.child(
Label::new("or")
.size(LabelSize::Small)
.color(Color::Muted),
)
},
)
.child(self.render_send_button(window, cx)),
),
),
@@ -2816,10 +3368,10 @@ impl FollowableItem for ContextEditor {
let editor_state = state.editor?;
let project = workspace.read(cx).project().clone();
let agent_panel_delegate = <dyn AgentPanelDelegate>::try_global(cx)?;
let assistant_panel_delegate = <dyn AssistantPanelDelegate>::try_global(cx)?;
let context_editor_task = workspace.update(cx, |workspace, cx| {
agent_panel_delegate.open_remote_context(workspace, context_id, window, cx)
assistant_panel_delegate.open_remote_context(workspace, context_id, window, cx)
});
Some(window.spawn(cx, async move |cx| {

View File

@@ -8,7 +8,7 @@ use ui::{Avatar, ListItem, ListItemSpacing, prelude::*};
use workspace::{Item, Workspace};
use crate::{
AgentPanelDelegate, ContextStore, DEFAULT_TAB_TITLE, RemoteContextMetadata,
AssistantPanelDelegate, ContextStore, DEFAULT_TAB_TITLE, RemoteContextMetadata,
SavedContextMetadata,
};
@@ -70,19 +70,19 @@ impl ContextHistory {
) {
let SavedContextPickerEvent::Confirmed(context) = event;
let Some(agent_panel_delegate) = <dyn AgentPanelDelegate>::try_global(cx) else {
let Some(assistant_panel_delegate) = <dyn AssistantPanelDelegate>::try_global(cx) else {
return;
};
self.workspace
.update(cx, |workspace, cx| match context {
ContextMetadata::Remote(metadata) => {
agent_panel_delegate
assistant_panel_delegate
.open_remote_context(workspace, metadata.id.clone(), window, cx)
.detach_and_log_err(cx);
}
ContextMetadata::Saved(metadata) => {
agent_panel_delegate
assistant_panel_delegate
.open_saved_context(workspace, metadata.path.clone(), window, cx)
.detach_and_log_err(cx);
}

View File

@@ -648,10 +648,7 @@ impl ContextStore {
if context.replica_id() == ReplicaId::default() {
Some(proto::ContextMetadata {
context_id: context.id().to_proto(),
summary: context
.summary()
.content()
.map(|summary| summary.text.clone()),
summary: context.summary().map(|summary| summary.text.clone()),
})
} else {
None

View File

@@ -0,0 +1,957 @@
use anyhow::{Context as _, Result, anyhow};
use collections::HashMap;
use editor::ProposedChangesEditor;
use futures::{TryFutureExt as _, future};
use gpui::{App, AppContext as _, AsyncApp, Entity, SharedString};
use language::{AutoindentMode, Buffer, BufferSnapshot};
use project::{Project, ProjectPath};
use std::{cmp, ops::Range, path::Path, sync::Arc};
use text::{AnchorRangeExt as _, Bias, OffsetRangeExt as _, Point};
#[derive(Clone, Debug)]
pub struct AssistantPatch {
pub range: Range<language::Anchor>,
pub title: SharedString,
pub edits: Arc<[Result<AssistantEdit>]>,
pub status: AssistantPatchStatus,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum AssistantPatchStatus {
Pending,
Ready,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AssistantEdit {
pub path: String,
pub kind: AssistantEditKind,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum AssistantEditKind {
Update {
old_text: String,
new_text: String,
description: Option<String>,
},
Create {
new_text: String,
description: Option<String>,
},
InsertBefore {
old_text: String,
new_text: String,
description: Option<String>,
},
InsertAfter {
old_text: String,
new_text: String,
description: Option<String>,
},
Delete {
old_text: String,
},
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ResolvedPatch {
pub edit_groups: HashMap<Entity<Buffer>, Vec<ResolvedEditGroup>>,
pub errors: Vec<AssistantPatchResolutionError>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ResolvedEditGroup {
pub context_range: Range<language::Anchor>,
pub edits: Vec<ResolvedEdit>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ResolvedEdit {
range: Range<language::Anchor>,
new_text: String,
description: Option<String>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AssistantPatchResolutionError {
pub edit_ix: usize,
pub message: String,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
enum SearchDirection {
Up,
Left,
Diagonal,
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct SearchState {
cost: u32,
direction: SearchDirection,
}
impl SearchState {
fn new(cost: u32, direction: SearchDirection) -> Self {
Self { cost, direction }
}
}
struct SearchMatrix {
cols: usize,
data: Vec<SearchState>,
}
impl SearchMatrix {
fn new(rows: usize, cols: usize) -> Self {
SearchMatrix {
cols,
data: vec![SearchState::new(0, SearchDirection::Diagonal); rows * cols],
}
}
fn get(&self, row: usize, col: usize) -> SearchState {
self.data[row * self.cols + col]
}
fn set(&mut self, row: usize, col: usize, cost: SearchState) {
self.data[row * self.cols + col] = cost;
}
}
impl ResolvedPatch {
pub fn apply(&self, editor: &ProposedChangesEditor, cx: &mut App) {
for (buffer, groups) in &self.edit_groups {
let branch = editor.branch_buffer_for_base(buffer).unwrap();
Self::apply_edit_groups(groups, &branch, cx);
}
editor.recalculate_all_buffer_diffs();
}
fn apply_edit_groups(groups: &Vec<ResolvedEditGroup>, buffer: &Entity<Buffer>, cx: &mut App) {
let mut edits = Vec::new();
for group in groups {
for suggestion in &group.edits {
edits.push((suggestion.range.clone(), suggestion.new_text.clone()));
}
}
buffer.update(cx, |buffer, cx| {
buffer.edit(
edits,
Some(AutoindentMode::Block {
original_indent_columns: Vec::new(),
}),
cx,
);
});
}
}
impl ResolvedEdit {
pub fn try_merge(&mut self, other: &Self, buffer: &text::BufferSnapshot) -> bool {
let range = &self.range;
let other_range = &other.range;
// Don't merge if we don't contain the other suggestion.
if range.start.cmp(&other_range.start, buffer).is_gt()
|| range.end.cmp(&other_range.end, buffer).is_lt()
{
return false;
}
let other_offset_range = other_range.to_offset(buffer);
let offset_range = range.to_offset(buffer);
// If the other range is empty at the start of this edit's range, combine the new text
if other_offset_range.is_empty() && other_offset_range.start == offset_range.start {
self.new_text = format!("{}\n{}", other.new_text, self.new_text);
self.range.start = other_range.start;
if let Some((description, other_description)) =
self.description.as_mut().zip(other.description.as_ref())
{
*description = format!("{}\n{}", other_description, description)
}
} else {
if let Some((description, other_description)) =
self.description.as_mut().zip(other.description.as_ref())
{
description.push('\n');
description.push_str(other_description);
}
}
true
}
}
impl AssistantEdit {
pub fn new(
path: Option<String>,
operation: Option<String>,
old_text: Option<String>,
new_text: Option<String>,
description: Option<String>,
) -> Result<Self> {
let path = path.ok_or_else(|| anyhow!("missing path"))?;
let operation = operation.ok_or_else(|| anyhow!("missing operation"))?;
let kind = match operation.as_str() {
"update" => AssistantEditKind::Update {
old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,
new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,
description,
},
"insert_before" => AssistantEditKind::InsertBefore {
old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,
new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,
description,
},
"insert_after" => AssistantEditKind::InsertAfter {
old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,
new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,
description,
},
"delete" => AssistantEditKind::Delete {
old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,
},
"create" => AssistantEditKind::Create {
description,
new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,
},
_ => Err(anyhow!("unknown operation {operation:?}"))?,
};
Ok(Self { path, kind })
}
pub async fn resolve(
&self,
project: Entity<Project>,
mut cx: AsyncApp,
) -> Result<(Entity<Buffer>, ResolvedEdit)> {
let path = self.path.clone();
let kind = self.kind.clone();
let buffer = project
.update(&mut cx, |project, cx| {
let project_path = project
.find_project_path(Path::new(&path), cx)
.or_else(|| {
// If we couldn't find a project path for it, put it in the active worktree
// so that when we create the buffer, it can be saved.
let worktree = project
.active_entry()
.and_then(|entry_id| project.worktree_for_entry(entry_id, cx))
.or_else(|| project.worktrees(cx).next())?;
let worktree = worktree.read(cx);
Some(ProjectPath {
worktree_id: worktree.id(),
path: Arc::from(Path::new(&path)),
})
})
.with_context(|| format!("worktree not found for {:?}", path))?;
anyhow::Ok(project.open_buffer(project_path, cx))
})??
.await?;
let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?;
let suggestion = cx
.background_spawn(async move { kind.resolve(&snapshot) })
.await;
Ok((buffer, suggestion))
}
}
impl AssistantEditKind {
fn resolve(self, snapshot: &BufferSnapshot) -> ResolvedEdit {
match self {
Self::Update {
old_text,
new_text,
description,
} => {
let range = Self::resolve_location(&snapshot, &old_text);
ResolvedEdit {
range,
new_text,
description,
}
}
Self::Create {
new_text,
description,
} => ResolvedEdit {
range: text::Anchor::MIN..text::Anchor::MAX,
description,
new_text,
},
Self::InsertBefore {
old_text,
mut new_text,
description,
} => {
let range = Self::resolve_location(&snapshot, &old_text);
new_text.push('\n');
ResolvedEdit {
range: range.start..range.start,
new_text,
description,
}
}
Self::InsertAfter {
old_text,
mut new_text,
description,
} => {
let range = Self::resolve_location(&snapshot, &old_text);
new_text.insert(0, '\n');
ResolvedEdit {
range: range.end..range.end,
new_text,
description,
}
}
Self::Delete { old_text } => {
let range = Self::resolve_location(&snapshot, &old_text);
ResolvedEdit {
range,
new_text: String::new(),
description: None,
}
}
}
}
fn resolve_location(buffer: &text::BufferSnapshot, search_query: &str) -> Range<text::Anchor> {
const INSERTION_COST: u32 = 3;
const DELETION_COST: u32 = 10;
const WHITESPACE_INSERTION_COST: u32 = 1;
const WHITESPACE_DELETION_COST: u32 = 1;
let buffer_len = buffer.len();
let query_len = search_query.len();
let mut matrix = SearchMatrix::new(query_len + 1, buffer_len + 1);
let mut leading_deletion_cost = 0_u32;
for (row, query_byte) in search_query.bytes().enumerate() {
let deletion_cost = if query_byte.is_ascii_whitespace() {
WHITESPACE_DELETION_COST
} else {
DELETION_COST
};
leading_deletion_cost = leading_deletion_cost.saturating_add(deletion_cost);
matrix.set(
row + 1,
0,
SearchState::new(leading_deletion_cost, SearchDirection::Diagonal),
);
for (col, buffer_byte) in buffer.bytes_in_range(0..buffer.len()).flatten().enumerate() {
let insertion_cost = if buffer_byte.is_ascii_whitespace() {
WHITESPACE_INSERTION_COST
} else {
INSERTION_COST
};
let up = SearchState::new(
matrix.get(row, col + 1).cost.saturating_add(deletion_cost),
SearchDirection::Up,
);
let left = SearchState::new(
matrix.get(row + 1, col).cost.saturating_add(insertion_cost),
SearchDirection::Left,
);
let diagonal = SearchState::new(
if query_byte == *buffer_byte {
matrix.get(row, col).cost
} else {
matrix
.get(row, col)
.cost
.saturating_add(deletion_cost + insertion_cost)
},
SearchDirection::Diagonal,
);
matrix.set(row + 1, col + 1, up.min(left).min(diagonal));
}
}
// Traceback to find the best match
let mut best_buffer_end = buffer_len;
let mut best_cost = u32::MAX;
for col in 1..=buffer_len {
let cost = matrix.get(query_len, col).cost;
if cost < best_cost {
best_cost = cost;
best_buffer_end = col;
}
}
let mut query_ix = query_len;
let mut buffer_ix = best_buffer_end;
while query_ix > 0 && buffer_ix > 0 {
let current = matrix.get(query_ix, buffer_ix);
match current.direction {
SearchDirection::Diagonal => {
query_ix -= 1;
buffer_ix -= 1;
}
SearchDirection::Up => {
query_ix -= 1;
}
SearchDirection::Left => {
buffer_ix -= 1;
}
}
}
let mut start = buffer.offset_to_point(buffer.clip_offset(buffer_ix, Bias::Left));
start.column = 0;
let mut end = buffer.offset_to_point(buffer.clip_offset(best_buffer_end, Bias::Right));
if end.column > 0 {
end.column = buffer.line_len(end.row);
}
buffer.anchor_after(start)..buffer.anchor_before(end)
}
}
impl AssistantPatch {
pub async fn resolve(&self, project: Entity<Project>, cx: &mut AsyncApp) -> ResolvedPatch {
let mut resolve_tasks = Vec::new();
for (ix, edit) in self.edits.iter().enumerate() {
if let Ok(edit) = edit.as_ref() {
resolve_tasks.push(
edit.resolve(project.clone(), cx.clone())
.map_err(move |error| (ix, error)),
);
}
}
let edits = future::join_all(resolve_tasks).await;
let mut errors = Vec::new();
let mut edits_by_buffer = HashMap::default();
for entry in edits {
match entry {
Ok((buffer, edit)) => {
edits_by_buffer
.entry(buffer)
.or_insert_with(Vec::new)
.push(edit);
}
Err((edit_ix, error)) => errors.push(AssistantPatchResolutionError {
edit_ix,
message: error.to_string(),
}),
}
}
// Expand the context ranges of each edit and group edits with overlapping context ranges.
let mut edit_groups_by_buffer = HashMap::default();
for (buffer, edits) in edits_by_buffer {
if let Ok(snapshot) = buffer.update(cx, |buffer, _| buffer.text_snapshot()) {
edit_groups_by_buffer.insert(buffer, Self::group_edits(edits, &snapshot));
}
}
ResolvedPatch {
edit_groups: edit_groups_by_buffer,
errors,
}
}
fn group_edits(
mut edits: Vec<ResolvedEdit>,
snapshot: &text::BufferSnapshot,
) -> Vec<ResolvedEditGroup> {
let mut edit_groups = Vec::<ResolvedEditGroup>::new();
// Sort edits by their range so that earlier, larger ranges come first
edits.sort_by(|a, b| a.range.cmp(&b.range, &snapshot));
// Merge overlapping edits
edits.dedup_by(|a, b| b.try_merge(a, &snapshot));
// Create context ranges for each edit
for edit in edits {
let context_range = {
let edit_point_range = edit.range.to_point(&snapshot);
let start_row = edit_point_range.start.row.saturating_sub(5);
let end_row = cmp::min(edit_point_range.end.row + 5, snapshot.max_point().row);
let start = snapshot.anchor_before(Point::new(start_row, 0));
let end = snapshot.anchor_after(Point::new(end_row, snapshot.line_len(end_row)));
start..end
};
if let Some(last_group) = edit_groups.last_mut() {
if last_group
.context_range
.end
.cmp(&context_range.start, &snapshot)
.is_ge()
{
// Merge with the previous group if context ranges overlap
last_group.context_range.end = context_range.end;
last_group.edits.push(edit);
} else {
// Create a new group
edit_groups.push(ResolvedEditGroup {
context_range,
edits: vec![edit],
});
}
} else {
// Create the first group
edit_groups.push(ResolvedEditGroup {
context_range,
edits: vec![edit],
});
}
}
edit_groups
}
pub fn path_count(&self) -> usize {
self.paths().count()
}
pub fn paths(&self) -> impl '_ + Iterator<Item = &str> {
let mut prev_path = None;
self.edits.iter().filter_map(move |edit| {
if let Ok(edit) = edit {
let path = Some(edit.path.as_str());
if path != prev_path {
prev_path = path;
return path;
}
}
None
})
}
}
impl PartialEq for AssistantPatch {
fn eq(&self, other: &Self) -> bool {
self.range == other.range
&& self.title == other.title
&& Arc::ptr_eq(&self.edits, &other.edits)
}
}
impl Eq for AssistantPatch {}
#[cfg(test)]
mod tests {
use super::*;
use gpui::App;
use language::{
Language, LanguageConfig, LanguageMatcher, language_settings::AllLanguageSettings,
};
use settings::SettingsStore;
use ui::BorrowAppContext;
use unindent::Unindent as _;
use util::test::{generate_marked_text, marked_text_ranges};
#[gpui::test]
fn test_resolve_location(cx: &mut App) {
assert_location_resolution(
concat!(
" Lorem\n",
"« ipsum\n",
" dolor sit amet»\n",
" consecteur",
),
"ipsum\ndolor",
cx,
);
assert_location_resolution(
&"
«fn foo1(a: usize) -> usize {
40
fn foo2(b: usize) -> usize {
42
}
"
.unindent(),
"fn foo1(b: usize) {\n40\n}",
cx,
);
assert_location_resolution(
&"
fn main() {
« Foo
.bar()
.baz()
.qux()»
}
fn foo2(b: usize) -> usize {
42
}
"
.unindent(),
"Foo.bar.baz.qux()",
cx,
);
assert_location_resolution(
&"
class Something {
one() { return 1; }
« two() { return 2222; }
three() { return 333; }
four() { return 4444; }
five() { return 5555; }
six() { return 6666; }
» seven() { return 7; }
eight() { return 8; }
}
"
.unindent(),
&"
two() { return 2222; }
four() { return 4444; }
five() { return 5555; }
six() { return 6666; }
"
.unindent(),
cx,
);
}
#[gpui::test]
fn test_resolve_edits(cx: &mut App) {
init_test(cx);
assert_edits(
"
/// A person
struct Person {
name: String,
age: usize,
}
/// A dog
struct Dog {
weight: f32,
}
impl Person {
fn name(&self) -> &str {
&self.name
}
}
"
.unindent(),
vec![
AssistantEditKind::Update {
old_text: "
name: String,
"
.unindent(),
new_text: "
first_name: String,
last_name: String,
"
.unindent(),
description: None,
},
AssistantEditKind::Update {
old_text: "
fn name(&self) -> &str {
&self.name
}
"
.unindent(),
new_text: "
fn name(&self) -> String {
format!(\"{} {}\", self.first_name, self.last_name)
}
"
.unindent(),
description: None,
},
],
"
/// A person
struct Person {
first_name: String,
last_name: String,
age: usize,
}
/// A dog
struct Dog {
weight: f32,
}
impl Person {
fn name(&self) -> String {
format!(\"{} {}\", self.first_name, self.last_name)
}
}
"
.unindent(),
cx,
);
// Ensure InsertBefore merges correctly with Update of the same text
assert_edits(
"
fn foo() {
}
"
.unindent(),
vec![
AssistantEditKind::InsertBefore {
old_text: "
fn foo() {"
.unindent(),
new_text: "
fn bar() {
qux();
}"
.unindent(),
description: Some("implement bar".into()),
},
AssistantEditKind::Update {
old_text: "
fn foo() {
}"
.unindent(),
new_text: "
fn foo() {
bar();
}"
.unindent(),
description: Some("call bar in foo".into()),
},
AssistantEditKind::InsertAfter {
old_text: "
fn foo() {
}
"
.unindent(),
new_text: "
fn qux() {
// todo
}
"
.unindent(),
description: Some("implement qux".into()),
},
],
"
fn bar() {
qux();
}
fn foo() {
bar();
}
fn qux() {
// todo
}
"
.unindent(),
cx,
);
// Correctly indent new text when replacing multiple adjacent indented blocks.
assert_edits(
"
impl Numbers {
fn one() {
1
}
fn two() {
2
}
fn three() {
3
}
}
"
.unindent(),
vec![
AssistantEditKind::Update {
old_text: "
fn one() {
1
}
"
.unindent(),
new_text: "
fn one() {
101
}
"
.unindent(),
description: None,
},
AssistantEditKind::Update {
old_text: "
fn two() {
2
}
"
.unindent(),
new_text: "
fn two() {
102
}
"
.unindent(),
description: None,
},
AssistantEditKind::Update {
old_text: "
fn three() {
3
}
"
.unindent(),
new_text: "
fn three() {
103
}
"
.unindent(),
description: None,
},
],
"
impl Numbers {
fn one() {
101
}
fn two() {
102
}
fn three() {
103
}
}
"
.unindent(),
cx,
);
assert_edits(
"
impl Person {
fn set_name(&mut self, name: String) {
self.name = name;
}
fn name(&self) -> String {
return self.name;
}
}
"
.unindent(),
vec![
AssistantEditKind::Update {
old_text: "self.name = name;".unindent(),
new_text: "self._name = name;".unindent(),
description: None,
},
AssistantEditKind::Update {
old_text: "return self.name;\n".unindent(),
new_text: "return self._name;\n".unindent(),
description: None,
},
],
"
impl Person {
fn set_name(&mut self, name: String) {
self._name = name;
}
fn name(&self) -> String {
return self._name;
}
}
"
.unindent(),
cx,
);
}
fn init_test(cx: &mut App) {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
cx.update_global::<SettingsStore, _>(|settings, cx| {
settings.update_user_settings::<AllLanguageSettings>(cx, |_| {});
});
}
#[track_caller]
fn assert_location_resolution(text_with_expected_range: &str, query: &str, cx: &mut App) {
let (text, _) = marked_text_ranges(text_with_expected_range, false);
let buffer = cx.new(|cx| Buffer::local(text.clone(), cx));
let snapshot = buffer.read(cx).snapshot();
let range = AssistantEditKind::resolve_location(&snapshot, query).to_offset(&snapshot);
let text_with_actual_range = generate_marked_text(&text, &[range], false);
pretty_assertions::assert_eq!(text_with_actual_range, text_with_expected_range);
}
#[track_caller]
fn assert_edits(
old_text: String,
edits: Vec<AssistantEditKind>,
new_text: String,
cx: &mut App,
) {
let buffer =
cx.new(|cx| Buffer::local(old_text, cx).with_language(Arc::new(rust_lang()), cx));
let snapshot = buffer.read(cx).snapshot();
let resolved_edits = edits
.into_iter()
.map(|kind| kind.resolve(&snapshot))
.collect();
let edit_groups = AssistantPatch::group_edits(resolved_edits, &snapshot);
ResolvedPatch::apply_edit_groups(&edit_groups, &buffer, cx);
let actual_new_text = buffer.read(cx).text();
pretty_assertions::assert_eq!(actual_new_text, new_text);
}
fn rust_lang() -> Language {
Language::new(
LanguageConfig {
name: "Rust".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
..Default::default()
},
Some(language::tree_sitter_rust::LANGUAGE.into()),
)
.with_indents_query(
r#"
(call_expression) @indent
(field_expression) @indent
(_ "(" ")" @end) @indent
(_ "{" "}" @end) @indent
"#,
)
.unwrap()
}
}

View File

@@ -14,7 +14,7 @@ path = "src/assistant_settings.rs"
[dependencies]
anthropic = { workspace = true, features = ["schemars"] }
anyhow.workspace = true
collections.workspace = true
feature_flags.workspace = true
gpui.workspace = true
indexmap.workspace = true
language_model.workspace = true
@@ -27,7 +27,6 @@ schemars.workspace = true
serde.workspace = true
settings.workspace = true
workspace-hack.workspace = true
zed_llm_client.workspace = true
[dev-dependencies]
fs.workspace = true

View File

@@ -1,7 +1,7 @@
use std::sync::Arc;
use collections::IndexMap;
use gpui::SharedString;
use indexmap::IndexMap;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

View File

@@ -5,9 +5,10 @@ use std::sync::Arc;
use ::open_ai::Model as OpenAiModel;
use anthropic::Model as AnthropicModel;
use anyhow::{Result, bail};
use collections::IndexMap;
use deepseek::Model as DeepseekModel;
use gpui::{App, Pixels, SharedString};
use feature_flags::{AgentStreamEditsFeatureFlag, Assistant2FeatureFlag, FeatureFlagAppExt};
use gpui::{App, Pixels};
use indexmap::IndexMap;
use language_model::{CloudModel, LanguageModel};
use lmstudio::Model as LmStudioModel;
use ollama::Model as OllamaModel;
@@ -17,10 +18,6 @@ use settings::{Settings, SettingsSources};
pub use crate::agent_profile::*;
pub fn init(cx: &mut App) {
AssistantSettings::register(cx);
}
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum AssistantDockPosition {
@@ -85,68 +82,38 @@ pub struct AssistantSettings {
pub thread_summary_model: Option<LanguageModelSelection>,
pub inline_alternatives: Vec<LanguageModelSelection>,
pub using_outdated_settings_version: bool,
pub enable_experimental_live_diffs: bool,
pub default_profile: AgentProfileId,
pub profiles: IndexMap<AgentProfileId, AgentProfile>,
pub always_allow_tool_actions: bool,
pub notify_when_agent_waiting: NotifyWhenAgentWaiting,
pub stream_edits: bool,
pub single_file_review: bool,
pub model_parameters: Vec<LanguageModelParameters>,
pub preferred_completion_mode: CompletionMode,
}
impl AssistantSettings {
pub fn temperature_for_model(model: &Arc<dyn LanguageModel>, cx: &App) -> Option<f32> {
let settings = Self::get_global(cx);
settings
.model_parameters
.iter()
.rfind(|setting| setting.matches(model))
.and_then(|m| m.temperature)
pub fn stream_edits(&self, cx: &App) -> bool {
cx.has_flag::<AgentStreamEditsFeatureFlag>() || self.stream_edits
}
pub fn are_live_diffs_enabled(&self, cx: &App) -> bool {
if cx.has_flag::<Assistant2FeatureFlag>() {
return false;
}
cx.is_staff() || self.enable_experimental_live_diffs
}
pub fn set_inline_assistant_model(&mut self, provider: String, model: String) {
self.inline_assistant_model = Some(LanguageModelSelection {
provider: provider.into(),
model,
});
self.inline_assistant_model = Some(LanguageModelSelection { provider, model });
}
pub fn set_commit_message_model(&mut self, provider: String, model: String) {
self.commit_message_model = Some(LanguageModelSelection {
provider: provider.into(),
model,
});
self.commit_message_model = Some(LanguageModelSelection { provider, model });
}
pub fn set_thread_summary_model(&mut self, provider: String, model: String) {
self.thread_summary_model = Some(LanguageModelSelection {
provider: provider.into(),
model,
});
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct LanguageModelParameters {
pub provider: Option<LanguageModelProviderSetting>,
pub model: Option<SharedString>,
pub temperature: Option<f32>,
}
impl LanguageModelParameters {
pub fn matches(&self, model: &Arc<dyn LanguageModel>) -> bool {
if let Some(provider) = &self.provider {
if provider.0 != model.provider_id().0 {
return false;
}
}
if let Some(setting_model) = &self.model {
if *setting_model != model.id().0 {
return false;
}
}
true
self.thread_summary_model = Some(LanguageModelSelection { provider, model });
}
}
@@ -213,37 +180,37 @@ impl AssistantSettingsContent {
.and_then(|provider| match provider {
AssistantProviderContentV1::ZedDotDev { default_model } => {
default_model.map(|model| LanguageModelSelection {
provider: "zed.dev".into(),
provider: "zed.dev".to_string(),
model: model.id().to_string(),
})
}
AssistantProviderContentV1::OpenAi { default_model, .. } => {
default_model.map(|model| LanguageModelSelection {
provider: "openai".into(),
provider: "openai".to_string(),
model: model.id().to_string(),
})
}
AssistantProviderContentV1::Anthropic { default_model, .. } => {
default_model.map(|model| LanguageModelSelection {
provider: "anthropic".into(),
provider: "anthropic".to_string(),
model: model.id().to_string(),
})
}
AssistantProviderContentV1::Ollama { default_model, .. } => {
default_model.map(|model| LanguageModelSelection {
provider: "ollama".into(),
provider: "ollama".to_string(),
model: model.id().to_string(),
})
}
AssistantProviderContentV1::LmStudio { default_model, .. } => {
default_model.map(|model| LanguageModelSelection {
provider: "lmstudio".into(),
provider: "lmstudio".to_string(),
model: model.id().to_string(),
})
}
AssistantProviderContentV1::DeepSeek { default_model, .. } => {
default_model.map(|model| LanguageModelSelection {
provider: "deepseek".into(),
provider: "deepseek".to_string(),
model: model.id().to_string(),
})
}
@@ -252,14 +219,13 @@ impl AssistantSettingsContent {
commit_message_model: None,
thread_summary_model: None,
inline_alternatives: None,
enable_experimental_live_diffs: None,
default_profile: None,
profiles: None,
always_allow_tool_actions: None,
notify_when_agent_waiting: None,
stream_edits: None,
single_file_review: None,
model_parameters: Vec::new(),
preferred_completion_mode: None,
},
VersionedAssistantSettingsContent::V2(ref settings) => settings.clone(),
},
@@ -270,7 +236,7 @@ impl AssistantSettingsContent {
default_width: settings.default_width,
default_height: settings.default_height,
default_model: Some(LanguageModelSelection {
provider: "openai".into(),
provider: "openai".to_string(),
model: settings
.default_open_ai_model
.clone()
@@ -282,14 +248,13 @@ impl AssistantSettingsContent {
commit_message_model: None,
thread_summary_model: None,
inline_alternatives: None,
enable_experimental_live_diffs: None,
default_profile: None,
profiles: None,
always_allow_tool_actions: None,
notify_when_agent_waiting: None,
stream_edits: None,
single_file_review: None,
model_parameters: Vec::new(),
preferred_completion_mode: None,
},
None => AssistantSettingsContentV2::default(),
}
@@ -354,7 +319,7 @@ impl AssistantSettingsContent {
&model,
None,
None,
Some(language_model.supports_tools()),
language_model.supports_tools(),
)),
api_url,
});
@@ -402,10 +367,7 @@ impl AssistantSettingsContent {
}
}
VersionedAssistantSettingsContent::V2(ref mut settings) => {
settings.default_model = Some(LanguageModelSelection {
provider: provider.into(),
model,
});
settings.default_model = Some(LanguageModelSelection { provider, model });
}
},
Some(AssistantSettingsContentInner::Legacy(settings)) => {
@@ -416,10 +378,7 @@ impl AssistantSettingsContent {
None => {
self.inner = Some(AssistantSettingsContentInner::for_v2(
AssistantSettingsContentV2 {
default_model: Some(LanguageModelSelection {
provider: provider.into(),
model,
}),
default_model: Some(LanguageModelSelection { provider, model }),
..Default::default()
},
));
@@ -429,10 +388,7 @@ impl AssistantSettingsContent {
pub fn set_inline_assistant_model(&mut self, provider: String, model: String) {
self.v2_setting(|setting| {
setting.inline_assistant_model = Some(LanguageModelSelection {
provider: provider.into(),
model,
});
setting.inline_assistant_model = Some(LanguageModelSelection { provider, model });
Ok(())
})
.ok();
@@ -440,10 +396,7 @@ impl AssistantSettingsContent {
pub fn set_commit_message_model(&mut self, provider: String, model: String) {
self.v2_setting(|setting| {
setting.commit_message_model = Some(LanguageModelSelection {
provider: provider.into(),
model,
});
setting.commit_message_model = Some(LanguageModelSelection { provider, model });
Ok(())
})
.ok();
@@ -471,10 +424,7 @@ impl AssistantSettingsContent {
pub fn set_thread_summary_model(&mut self, provider: String, model: String) {
self.v2_setting(|setting| {
setting.thread_summary_model = Some(LanguageModelSelection {
provider: provider.into(),
model,
});
setting.thread_summary_model = Some(LanguageModelSelection { provider, model });
Ok(())
})
.ok();
@@ -563,14 +513,13 @@ impl Default for VersionedAssistantSettingsContent {
commit_message_model: None,
thread_summary_model: None,
inline_alternatives: None,
enable_experimental_live_diffs: None,
default_profile: None,
profiles: None,
always_allow_tool_actions: None,
notify_when_agent_waiting: None,
stream_edits: None,
single_file_review: None,
model_parameters: Vec::new(),
preferred_completion_mode: None,
})
}
}
@@ -607,6 +556,10 @@ pub struct AssistantSettingsContentV2 {
thread_summary_model: Option<LanguageModelSelection>,
/// Additional models with which to generate alternatives when performing inline assists.
inline_alternatives: Option<Vec<LanguageModelSelection>>,
/// Enable experimental live diffs in the assistant panel.
///
/// Default: false
enable_experimental_live_diffs: Option<bool>,
/// The default profile to use in the Agent.
///
/// Default: write
@@ -630,88 +583,37 @@ pub struct AssistantSettingsContentV2 {
///
/// Default: true
single_file_review: Option<bool>,
/// Additional parameters for language model requests. When making a request
/// to a model, parameters will be taken from the last entry in this list
/// that matches the model's provider and name. In each entry, both provider
/// and model are optional, so that you can specify parameters for either
/// one.
///
/// Default: []
#[serde(default)]
model_parameters: Vec<LanguageModelParameters>,
/// What completion mode to enable for new threads
///
/// Default: normal
preferred_completion_mode: Option<CompletionMode>,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
#[serde(rename_all = "snake_case")]
pub enum CompletionMode {
#[default]
Normal,
Max,
}
impl From<CompletionMode> for zed_llm_client::CompletionMode {
fn from(value: CompletionMode) -> Self {
match value {
CompletionMode::Normal => zed_llm_client::CompletionMode::Normal,
CompletionMode::Max => zed_llm_client::CompletionMode::Max,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct LanguageModelSelection {
pub provider: LanguageModelProviderSetting,
#[schemars(schema_with = "providers_schema")]
pub provider: String,
pub model: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct LanguageModelProviderSetting(pub String);
impl JsonSchema for LanguageModelProviderSetting {
fn schema_name() -> String {
"LanguageModelProviderSetting".into()
}
fn json_schema(_: &mut schemars::r#gen::SchemaGenerator) -> Schema {
schemars::schema::SchemaObject {
enum_values: Some(vec![
"anthropic".into(),
"bedrock".into(),
"google".into(),
"lmstudio".into(),
"ollama".into(),
"openai".into(),
"zed.dev".into(),
"copilot_chat".into(),
"deepseek".into(),
]),
..Default::default()
}
.into()
}
}
impl From<String> for LanguageModelProviderSetting {
fn from(provider: String) -> Self {
Self(provider)
}
}
impl From<&str> for LanguageModelProviderSetting {
fn from(provider: &str) -> Self {
Self(provider.to_string())
fn providers_schema(_: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
schemars::schema::SchemaObject {
enum_values: Some(vec![
"anthropic".into(),
"bedrock".into(),
"google".into(),
"lmstudio".into(),
"ollama".into(),
"openai".into(),
"zed.dev".into(),
"copilot_chat".into(),
"deepseek".into(),
]),
..Default::default()
}
.into()
}
impl Default for LanguageModelSelection {
fn default() -> Self {
Self {
provider: LanguageModelProviderSetting("openai".to_string()),
provider: "openai".to_string(),
model: "gpt-4".to_string(),
}
}
@@ -833,6 +735,10 @@ impl Settings for AssistantSettings {
.thread_summary_model
.or(settings.thread_summary_model.take());
merge(&mut settings.inline_alternatives, value.inline_alternatives);
merge(
&mut settings.enable_experimental_live_diffs,
value.enable_experimental_live_diffs,
);
merge(
&mut settings.always_allow_tool_actions,
value.always_allow_tool_actions,
@@ -844,14 +750,6 @@ impl Settings for AssistantSettings {
merge(&mut settings.stream_edits, value.stream_edits);
merge(&mut settings.single_file_review, value.single_file_review);
merge(&mut settings.default_profile, value.default_profile);
merge(
&mut settings.preferred_completion_mode,
value.preferred_completion_mode,
);
settings
.model_parameters
.extend_from_slice(&value.model_parameters);
if let Some(profiles) = value.profiles {
settings
@@ -978,14 +876,13 @@ mod tests {
dock: None,
default_width: None,
default_height: None,
enable_experimental_live_diffs: None,
default_profile: None,
profiles: None,
always_allow_tool_actions: None,
notify_when_agent_waiting: None,
stream_edits: None,
single_file_review: None,
model_parameters: Vec::new(),
preferred_completion_mode: None,
},
)),
}
@@ -1048,7 +945,7 @@ mod tests {
AssistantSettingsContentV2 {
enabled: Some(false),
default_model: Some(LanguageModelSelection {
provider: "xai".to_owned().into(),
provider: "xai".to_owned(),
model: "grok".to_owned(),
}),
..Default::default()

View File

@@ -7,7 +7,6 @@ mod tool_working_set;
use std::fmt;
use std::fmt::Debug;
use std::fmt::Formatter;
use std::ops::Deref;
use std::sync::Arc;
use anyhow::Result;
@@ -18,8 +17,7 @@ use gpui::IntoElement;
use gpui::Window;
use gpui::{App, Entity, SharedString, Task, WeakEntity};
use icons::IconName;
use language_model::LanguageModel;
use language_model::LanguageModelRequest;
use language_model::LanguageModelRequestMessage;
use language_model::LanguageModelToolSchemaFormat;
use project::Project;
use workspace::Workspace;
@@ -63,34 +61,11 @@ impl ToolUseStatus {
}
}
#[derive(Debug)]
pub struct ToolResultOutput {
pub content: String,
pub output: Option<serde_json::Value>,
}
impl From<String> for ToolResultOutput {
fn from(value: String) -> Self {
ToolResultOutput {
content: value,
output: None,
}
}
}
impl Deref for ToolResultOutput {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.content
}
}
/// The result of running a tool, containing both the asynchronous output
/// and an optional card view that can be rendered immediately.
pub struct ToolResult {
/// The asynchronous task that will eventually resolve to the tool's output
pub output: Task<Result<ToolResultOutput>>,
pub output: Task<Result<String>>,
/// An optional view to present the output of the tool.
pub card: Option<AnyToolCard>,
}
@@ -153,9 +128,9 @@ impl AnyToolCard {
}
}
impl From<Task<Result<ToolResultOutput>>> for ToolResult {
impl From<Task<Result<String>>> for ToolResult {
/// Convert from a task to a ToolResult with no card
fn from(output: Task<Result<ToolResultOutput>>) -> Self {
fn from(output: Task<Result<String>>) -> Self {
Self { output, card: None }
}
}
@@ -206,23 +181,12 @@ pub trait Tool: 'static + Send + Sync {
fn run(
self: Arc<Self>,
input: serde_json::Value,
request: Arc<LanguageModelRequest>,
messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
model: Arc<dyn LanguageModel>,
window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult;
fn deserialize_card(
self: Arc<Self>,
_output: serde_json::Value,
_project: Entity<Project>,
_window: &mut Window,
_cx: &mut App,
) -> Option<AnyToolCard> {
None
}
}
impl Debug for dyn Tool {

View File

@@ -35,17 +35,25 @@ fn adapt_to_json_schema_subset(json: &mut Value) -> Result<()> {
}
}
const KEYS_TO_REMOVE: [&str; 5] = [
const KEYS_TO_REMOVE: [&str; 4] = [
"format",
"additionalProperties",
"exclusiveMinimum",
"exclusiveMaximum",
"optional",
];
for key in KEYS_TO_REMOVE {
obj.remove(key);
}
if let Some(default) = obj.get("default") {
let is_null = default.is_null();
// Default is not supported, so we need to remove it
obj.remove("default");
if is_null {
obj.insert("nullable".to_string(), Value::Bool(true));
}
}
// If a type is not specified for an input parameter, add a default type
if matches!(obj.get("description"), Some(Value::String(_)))
&& !obj.contains_key("type")
@@ -84,6 +92,26 @@ mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_transform_default_null_to_nullable() {
let mut json = json!({
"description": "A test field",
"type": "string",
"default": null
});
adapt_to_json_schema_subset(&mut json).unwrap();
assert_eq!(
json,
json!({
"description": "A test field",
"type": "string",
"nullable": true
})
);
}
#[test]
fn test_transform_adds_type_when_missing() {
let mut json = json!({
@@ -129,8 +157,7 @@ mod tests {
"format": "uint32",
"exclusiveMinimum": 0,
"exclusiveMaximum": 100,
"additionalProperties": false,
"optional": true
"additionalProperties": false
});
adapt_to_json_schema_subset(&mut json).unwrap();

View File

@@ -17,14 +17,14 @@ eval = []
[dependencies]
aho-corasick.workspace = true
anyhow.workspace = true
assistant_settings.workspace = true
assistant_tool.workspace = true
assistant_settings.workspace = true
buffer_diff.workspace = true
chrono.workspace = true
collections.workspace = true
component.workspace = true
derive_more.workspace = true
editor.workspace = true
derive_more.workspace = true
feature_flags.workspace = true
futures.workspace = true
gpui.workspace = true
@@ -35,9 +35,8 @@ indoc.workspace = true
itertools.workspace = true
language.workspace = true
language_model.workspace = true
linkme.workspace = true
log.workspace = true
markdown.workspace = true
linkme.workspace = true
open.workspace = true
paths.workspace = true
portable-pty.workspace = true

View File

@@ -1,5 +1,6 @@
mod copy_path_tool;
mod create_directory_tool;
mod create_file_tool;
mod delete_path_tool;
mod diagnostics_tool;
mod edit_agent;
@@ -12,7 +13,9 @@ mod move_path_tool;
mod now_tool;
mod open_tool;
mod read_file_tool;
mod replace;
mod schema;
mod streaming_edit_file_tool;
mod templates;
mod terminal_tool;
mod thinking_tool;
@@ -21,12 +24,15 @@ mod web_search_tool;
use std::sync::Arc;
use assistant_settings::AssistantSettings;
use assistant_tool::ToolRegistry;
use copy_path_tool::CopyPathTool;
use feature_flags::{AgentStreamEditsFeatureFlag, FeatureFlagAppExt};
use gpui::{App, Entity};
use http_client::HttpClientWithUrl;
use language_model::LanguageModelRegistry;
use move_path_tool::MovePathTool;
use settings::{Settings, SettingsStore};
use web_search_tool::WebSearchTool;
pub(crate) use templates::*;
@@ -34,18 +40,21 @@ pub(crate) use templates::*;
use crate::create_directory_tool::CreateDirectoryTool;
use crate::delete_path_tool::DeletePathTool;
use crate::diagnostics_tool::DiagnosticsTool;
use crate::edit_file_tool::EditFileTool;
use crate::fetch_tool::FetchTool;
use crate::find_path_tool::FindPathTool;
use crate::grep_tool::GrepTool;
use crate::list_directory_tool::ListDirectoryTool;
use crate::now_tool::NowTool;
use crate::read_file_tool::ReadFileTool;
use crate::streaming_edit_file_tool::StreamingEditFileTool;
use crate::thinking_tool::ThinkingTool;
pub use edit_file_tool::EditFileToolInput;
pub use create_file_tool::{CreateFileTool, CreateFileToolInput};
pub use edit_file_tool::{EditFileTool, EditFileToolInput};
pub use find_path_tool::FindPathToolInput;
pub use open_tool::OpenTool;
pub use read_file_tool::{ReadFileTool, ReadFileToolInput};
pub use read_file_tool::ReadFileToolInput;
pub use streaming_edit_file_tool::StreamingEditFileToolInput;
pub use terminal_tool::TerminalTool;
pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
@@ -66,7 +75,12 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
registry.register_tool(GrepTool);
registry.register_tool(ThinkingTool);
registry.register_tool(FetchTool::new(http_client));
registry.register_tool(EditFileTool);
register_edit_file_tool(cx);
cx.observe_flag::<AgentStreamEditsFeatureFlag, _>(|_, cx| register_edit_file_tool(cx))
.detach();
cx.observe_global::<SettingsStore>(register_edit_file_tool)
.detach();
register_web_search_tool(&LanguageModelRegistry::global(cx), cx);
cx.subscribe(
@@ -93,16 +107,29 @@ fn register_web_search_tool(registry: &Entity<LanguageModelRegistry>, cx: &mut A
}
}
fn register_edit_file_tool(cx: &mut App) {
let registry = ToolRegistry::global(cx);
registry.unregister_tool(CreateFileTool);
registry.unregister_tool(EditFileTool);
registry.unregister_tool(StreamingEditFileTool);
if AssistantSettings::get_global(cx).stream_edits(cx) {
registry.register_tool(StreamingEditFileTool);
} else {
registry.register_tool(CreateFileTool);
registry.register_tool(EditFileTool);
}
}
#[cfg(test)]
mod tests {
use super::*;
use assistant_settings::AssistantSettings;
use client::Client;
use clock::FakeSystemClock;
use http_client::FakeHttpClient;
use schemars::JsonSchema;
use serde::Serialize;
use settings::Settings;
#[test]
fn test_json_schema() {

View File

@@ -3,8 +3,8 @@ use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::AnyWindowHandle;
use gpui::{App, AppContext, Entity, Task};
use language_model::LanguageModel;
use language_model::{LanguageModelRequest, LanguageModelToolSchemaFormat};
use language_model::LanguageModelRequestMessage;
use language_model::LanguageModelToolSchemaFormat;
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -74,10 +74,9 @@ impl Tool for CopyPathTool {
fn run(
self: Arc<Self>,
input: serde_json::Value,
_request: Arc<LanguageModelRequest>,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
_model: Arc<dyn LanguageModel>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
@@ -108,9 +107,10 @@ impl Tool for CopyPathTool {
cx.background_spawn(async move {
match copy_task.await {
Ok(_) => Ok(
format!("Copied {} to {}", input.source_path, input.destination_path).into(),
),
Ok(_) => Ok(format!(
"Copied {} to {}",
input.source_path, input.destination_path
)),
Err(err) => Err(anyhow!(
"Failed to copy {} to {}: {}",
input.source_path,

View File

@@ -3,7 +3,8 @@ use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::AnyWindowHandle;
use gpui::{App, Entity, Task};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use language_model::LanguageModelRequestMessage;
use language_model::LanguageModelToolSchemaFormat;
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -61,10 +62,9 @@ impl Tool for CreateDirectoryTool {
fn run(
self: Arc<Self>,
input: serde_json::Value,
_request: Arc<LanguageModelRequest>,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
_model: Arc<dyn LanguageModel>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
@@ -88,7 +88,7 @@ impl Tool for CreateDirectoryTool {
.await
.map_err(|err| anyhow!("Unable to create directory {destination_path}: {err}"))?;
Ok(format!("Created directory {destination_path}").into())
Ok(format!("Created directory {destination_path}"))
})
.into()
}

View File

@@ -0,0 +1,195 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::AnyWindowHandle;
use gpui::{App, Entity, Task};
use language_model::LanguageModelRequestMessage;
use language_model::LanguageModelToolSchemaFormat;
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use ui::IconName;
use util::markdown::MarkdownInlineCode;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct CreateFileToolInput {
/// The path where the file should be created.
///
/// <example>
/// If the project has the following structure:
///
/// - directory1/
/// - directory2/
///
/// You can create a new file by providing a path of "directory1/new_file.txt"
/// </example>
///
/// Make sure to include this field before the `contents` field in the input object
/// so that we can display it immediately.
pub path: String,
/// The text contents of the file to create.
///
/// <example>
/// To create a file with the text "Hello, World!", provide contents of "Hello, World!"
/// </example>
pub contents: String,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct PartialInput {
#[serde(default)]
path: String,
#[serde(default)]
contents: String,
}
pub struct CreateFileTool;
const DEFAULT_UI_TEXT: &str = "Create file";
impl Tool for CreateFileTool {
fn name(&self) -> String {
"create_file".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
false
}
fn description(&self) -> String {
include_str!("./create_file_tool/description.md").into()
}
fn icon(&self) -> IconName {
IconName::FileCreate
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
json_schema_for::<CreateFileToolInput>(format)
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<CreateFileToolInput>(input.clone()) {
Ok(input) => {
let path = MarkdownInlineCode(&input.path);
format!("Create file {path}")
}
Err(_) => DEFAULT_UI_TEXT.to_string(),
}
}
fn still_streaming_ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<PartialInput>(input.clone()).ok() {
Some(input) if !input.path.is_empty() => input.path,
_ => DEFAULT_UI_TEXT.to_string(),
}
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<CreateFileToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
let project_path = match project.read(cx).find_project_path(&input.path, cx) {
Some(project_path) => project_path,
None => {
return Task::ready(Err(anyhow!("Path to create was outside the project"))).into();
}
};
let contents: Arc<str> = input.contents.as_str().into();
let destination_path: Arc<str> = input.path.as_str().into();
cx.spawn(async move |cx| {
let buffer = project
.update(cx, |project, cx| {
project.open_buffer(project_path.clone(), cx)
})?
.await
.map_err(|err| anyhow!("Unable to open buffer for {destination_path}: {err}"))?;
cx.update(|cx| {
action_log.update(cx, |action_log, cx| {
action_log.buffer_created(buffer.clone(), cx)
});
buffer.update(cx, |buffer, cx| buffer.set_text(contents, cx));
action_log.update(cx, |action_log, cx| {
action_log.buffer_edited(buffer.clone(), cx)
});
})?;
project
.update(cx, |project, cx| project.save_buffer(buffer, cx))?
.await
.map_err(|err| anyhow!("Unable to save buffer for {destination_path}: {err}"))?;
Ok(format!("Created file {destination_path}"))
})
.into()
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn still_streaming_ui_text_with_path() {
let tool = CreateFileTool;
let input = json!({
"path": "src/main.rs",
"contents": "fn main() {\n println!(\"Hello, world!\");\n}"
});
assert_eq!(tool.still_streaming_ui_text(&input), "src/main.rs");
}
#[test]
fn still_streaming_ui_text_without_path() {
let tool = CreateFileTool;
let input = json!({
"path": "",
"contents": "fn main() {\n println!(\"Hello, world!\");\n}"
});
assert_eq!(tool.still_streaming_ui_text(&input), DEFAULT_UI_TEXT);
}
#[test]
fn still_streaming_ui_text_with_null() {
let tool = CreateFileTool;
let input = serde_json::Value::Null;
assert_eq!(tool.still_streaming_ui_text(&input), DEFAULT_UI_TEXT);
}
#[test]
fn ui_text_with_valid_input() {
let tool = CreateFileTool;
let input = json!({
"path": "src/main.rs",
"contents": "fn main() {\n println!(\"Hello, world!\");\n}"
});
assert_eq!(tool.ui_text(&input), "Create file `src/main.rs`");
}
#[test]
fn ui_text_with_invalid_input() {
let tool = CreateFileTool;
let input = json!({
"invalid": "field"
});
assert_eq!(tool.ui_text(&input), DEFAULT_UI_TEXT);
}
}

View File

@@ -0,0 +1,3 @@
Creates a new file at the specified path within the project, containing the given text content. Returns confirmation that the file was created.
This tool is the most efficient way to create new files within the project, so it should always be chosen whenever it's necessary to create a new file in the project with specific text content, or whenever a file in the project needs such a drastic change that you would prefer to replace the entire thing instead of making individual edits. This tool should not be used when making changes to parts of an existing file but not all of it. In those cases, it's better to use another approach to edit the file.

View File

@@ -3,7 +3,7 @@ use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use futures::{SinkExt, StreamExt, channel::mpsc};
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::{Project, ProjectPath};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -59,10 +59,9 @@ impl Tool for DeletePathTool {
fn run(
self: Arc<Self>,
input: serde_json::Value,
_request: Arc<LanguageModelRequest>,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
_model: Arc<dyn LanguageModel>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
@@ -128,7 +127,7 @@ impl Tool for DeletePathTool {
match delete {
Some(deletion_task) => match deletion_task.await {
Ok(()) => Ok(format!("Deleted {path_str}").into()),
Ok(()) => Ok(format!("Deleted {path_str}")),
Err(err) => Err(anyhow!("Failed to delete {path_str}: {err}")),
},
None => Err(anyhow!(

View File

@@ -3,7 +3,7 @@ use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{AnyWindowHandle, App, Entity, Task};
use language::{DiagnosticSeverity, OffsetRangeExt};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -79,10 +79,9 @@ impl Tool for DiagnosticsTool {
fn run(
self: Arc<Self>,
input: serde_json::Value,
_request: Arc<LanguageModelRequest>,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
_model: Arc<dyn LanguageModel>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
@@ -123,9 +122,9 @@ impl Tool for DiagnosticsTool {
}
if output.is_empty() {
Ok("File doesn't have errors or warnings!".to_string().into())
Ok("File doesn't have errors or warnings!".to_string())
} else {
Ok(output.into())
Ok(output)
}
})
.into()
@@ -159,12 +158,10 @@ impl Tool for DiagnosticsTool {
});
if has_diagnostics {
Task::ready(Ok(output.into())).into()
Task::ready(Ok(output)).into()
} else {
Task::ready(Ok("No errors or warnings found in the project."
.to_string()
.into()))
.into()
Task::ready(Ok("No errors or warnings found in the project.".to_string()))
.into()
}
}
}

View File

@@ -17,11 +17,10 @@ use gpui::{AppContext, AsyncApp, Entity, SharedString, Task};
use language::{Bias, Buffer, BufferSnapshot, LineIndent, Point};
use language_model::{
LanguageModel, LanguageModelCompletionError, LanguageModelRequest, LanguageModelRequestMessage,
LanguageModelToolChoice, MessageContent, Role,
MessageContent, Role,
};
use project::{AgentLocation, Project};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde::Serialize;
use std::{cmp, iter, mem, ops::Range, path::PathBuf, sync::Arc, task::Poll};
use streaming_diff::{CharOperation, StreamingDiff};
@@ -51,10 +50,10 @@ pub enum EditAgentOutputEvent {
OldTextNotFound(SharedString),
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug)]
pub struct EditAgentOutput {
pub raw_edits: String,
pub parser_metrics: EditParserMetrics,
pub _raw_edits: String,
pub _parser_metrics: EditParserMetrics,
}
#[derive(Clone)]
@@ -84,7 +83,7 @@ impl EditAgent {
&self,
buffer: Entity<Buffer>,
edit_description: String,
conversation: &LanguageModelRequest,
previous_messages: Vec<LanguageModelRequestMessage>,
cx: &mut AsyncApp,
) -> (
Task<Result<EditAgentOutput>>,
@@ -92,7 +91,6 @@ impl EditAgent {
) {
let this = self.clone();
let (events_tx, events_rx) = mpsc::unbounded();
let conversation = conversation.clone();
let output = cx.spawn(async move |cx| {
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
let path = cx.update(|cx| snapshot.resolve_file_path(cx, true))?;
@@ -101,7 +99,7 @@ impl EditAgent {
edit_description,
}
.render(&this.templates)?;
let new_chunks = this.request(conversation, prompt, cx).await?;
let new_chunks = this.request(previous_messages, prompt, cx).await?;
let (output, mut inner_events) = this.overwrite_with_chunks(buffer, new_chunks, cx);
while let Some(event) = inner_events.next().await {
@@ -187,8 +185,8 @@ impl EditAgent {
}
Ok(EditAgentOutput {
raw_edits,
parser_metrics: EditParserMetrics::default(),
_raw_edits: raw_edits,
_parser_metrics: EditParserMetrics::default(),
})
}
@@ -196,7 +194,7 @@ impl EditAgent {
&self,
buffer: Entity<Buffer>,
edit_description: String,
conversation: &LanguageModelRequest,
previous_messages: Vec<LanguageModelRequestMessage>,
cx: &mut AsyncApp,
) -> (
Task<Result<EditAgentOutput>>,
@@ -216,7 +214,6 @@ impl EditAgent {
let this = self.clone();
let (events_tx, events_rx) = mpsc::unbounded();
let conversation = conversation.clone();
let output = cx.spawn(async move |cx| {
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
let path = cx.update(|cx| snapshot.resolve_file_path(cx, true))?;
@@ -225,7 +222,7 @@ impl EditAgent {
edit_description,
}
.render(&this.templates)?;
let edit_chunks = this.request(conversation, prompt, cx).await?;
let edit_chunks = this.request(previous_messages, prompt, cx).await?;
let (output, mut inner_events) = this.apply_edit_chunks(buffer, edit_chunks, cx);
while let Some(event) = inner_events.next().await {
@@ -427,8 +424,8 @@ impl EditAgent {
}
}
Ok(EditAgentOutput {
raw_edits,
parser_metrics: parser.finish(),
_raw_edits: raw_edits,
_parser_metrics: parser.finish(),
})
});
(output, rx)
@@ -515,67 +512,33 @@ impl EditAgent {
async fn request(
&self,
mut conversation: LanguageModelRequest,
mut messages: Vec<LanguageModelRequestMessage>,
prompt: String,
cx: &mut AsyncApp,
) -> Result<BoxStream<'static, Result<String, LanguageModelCompletionError>>> {
let mut messages_iter = conversation.messages.iter_mut();
if let Some(last_message) = messages_iter.next_back() {
let mut message_content = Vec::new();
if let Some(last_message) = messages.last_mut() {
if last_message.role == Role::Assistant {
let old_content_len = last_message.content.len();
last_message
.content
.retain(|content| !matches!(content, MessageContent::ToolUse(_)));
let new_content_len = last_message.content.len();
// We just removed pending tool uses from the content of the
// last message, so it doesn't make sense to cache it anymore
// (e.g., the message will look very different on the next
// request). Thus, we move the flag to the message prior to it,
// as it will still be a valid prefix of the conversation.
if old_content_len != new_content_len && last_message.cache {
if let Some(prev_message) = messages_iter.next_back() {
last_message.cache = false;
prev_message.cache = true;
}
}
if last_message.content.is_empty() {
conversation.messages.pop();
messages.pop();
}
}
}
conversation.messages.push(LanguageModelRequestMessage {
message_content.push(MessageContent::Text(prompt));
messages.push(LanguageModelRequestMessage {
role: Role::User,
content: vec![MessageContent::Text(prompt)],
content: message_content,
cache: false,
});
// Include tools in the request so that we can take advantage of
// caching when ToolChoice::None is supported.
let mut tool_choice = None;
let mut tools = Vec::new();
if !conversation.tools.is_empty()
&& self
.model
.supports_tool_choice(LanguageModelToolChoice::None)
{
tool_choice = Some(LanguageModelToolChoice::None);
tools = conversation.tools.clone();
}
let request = LanguageModelRequest {
thread_id: conversation.thread_id,
prompt_id: conversation.prompt_id,
mode: conversation.mode,
messages: conversation.messages,
tool_choice,
tools,
stop: Vec::new(),
temperature: None,
messages,
// temperature: Some(0.5),
..Default::default()
};
Ok(self.model.stream_completion_text(request, cx).await?.stream)
}

View File

@@ -1,6 +1,4 @@
use derive_more::{Add, AddAssign};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
use std::{cmp, mem, ops::Range};
@@ -15,9 +13,7 @@ pub enum EditParserEvent {
NewTextChunk { chunk: String, done: bool },
}
#[derive(
Clone, Debug, Default, PartialEq, Eq, Add, AddAssign, Serialize, Deserialize, JsonSchema,
)]
#[derive(Clone, Debug, Default, PartialEq, Eq, Add, AddAssign)]
pub struct EditParserMetrics {
pub tags: usize,
pub mismatched_tags: usize,

View File

@@ -1,17 +1,18 @@
use super::*;
use crate::{ReadFileToolInput, edit_file_tool::EditFileToolInput, grep_tool::GrepToolInput};
use crate::{
ReadFileToolInput, grep_tool::GrepToolInput,
streaming_edit_file_tool::StreamingEditFileToolInput,
};
use Role::*;
use anyhow::anyhow;
use assistant_tool::ToolRegistry;
use client::{Client, UserStore};
use collections::HashMap;
use fs::FakeFs;
use futures::{FutureExt, future::LocalBoxFuture};
use gpui::{AppContext, TestAppContext};
use indoc::{formatdoc, indoc};
use indoc::indoc;
use language_model::{
LanguageModelRegistry, LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolUse,
LanguageModelToolUseId,
LanguageModelRegistry, LanguageModelToolResult, LanguageModelToolUse, LanguageModelToolUseId,
};
use project::Project;
use rand::prelude::*;
@@ -39,7 +40,7 @@ fn eval_extract_handle_command_output() {
conversation: vec![
message(
User,
[text(formatdoc! {"
[text(indoc! {"
Read the `{input_file_path}` file and extract a method in
the final stanza of `run_git_blame` to deal with command failures,
call it `handle_command_output` and take the std::process::Output as the only parameter.
@@ -68,7 +69,7 @@ fn eval_extract_handle_command_output() {
[tool_use(
"tool_2",
"edit_file",
EditFileToolInput {
StreamingEditFileToolInput {
display_description: edit_description.into(),
path: input_file_path.into(),
create_or_overwrite: false,
@@ -98,7 +99,7 @@ fn eval_delete_run_git_blame() {
conversation: vec![
message(
User,
[text(formatdoc! {"
[text(indoc! {"
Read the `{input_file_path}` file and delete `run_git_blame`. Just that
one function, not its usages.
"})],
@@ -124,7 +125,7 @@ fn eval_delete_run_git_blame() {
[tool_use(
"tool_2",
"edit_file",
EditFileToolInput {
StreamingEditFileToolInput {
display_description: edit_description.into(),
path: input_file_path.into(),
create_or_overwrite: false,
@@ -140,61 +141,6 @@ fn eval_delete_run_git_blame() {
);
}
#[test]
#[cfg_attr(not(feature = "eval"), ignore)]
fn eval_translate_doc_comments() {
let input_file_path = "root/canvas.rs";
let input_file_content = include_str!("evals/fixtures/translate_doc_comments/before.rs");
let edit_description = "Translate all doc comments to Italian";
eval(
200,
1.,
EvalInput {
conversation: vec![
message(
User,
[text(formatdoc! {"
Read the {input_file_path} file and edit it (without overwriting it),
translating all the doc comments to italian.
"})],
),
message(
Assistant,
[tool_use(
"tool_1",
"read_file",
ReadFileToolInput {
path: input_file_path.into(),
start_line: None,
end_line: None,
},
)],
),
message(
User,
[tool_result("tool_1", "read_file", input_file_content)],
),
message(
Assistant,
[tool_use(
"tool_2",
"edit_file",
EditFileToolInput {
display_description: edit_description.into(),
path: input_file_path.into(),
create_or_overwrite: false,
},
)],
),
],
input_path: input_file_path.into(),
input_content: Some(input_file_content.into()),
edit_description: edit_description.into(),
assertion: EvalAssertion::judge_diff("Doc comments were translated to Italian"),
},
);
}
#[test]
#[cfg_attr(not(feature = "eval"), ignore)]
fn eval_use_wasi_sdk_in_compile_parser_to_wasm() {
@@ -209,7 +155,7 @@ fn eval_use_wasi_sdk_in_compile_parser_to_wasm() {
conversation: vec![
message(
User,
[text(formatdoc! {"
[text(indoc! {"
Read the `{input_file_path}` file and change `compile_parser_to_wasm` to use `wasi-sdk` instead of emscripten.
Use `ureq` to download the SDK for the current platform and architecture.
Extract the archive into a sibling of `lib` inside the `tree-sitter` directory in the cache_dir.
@@ -217,7 +163,7 @@ fn eval_use_wasi_sdk_in_compile_parser_to_wasm() {
that's inside of the archive.
Don't re-download the SDK if that executable already exists.
Use these clang flags: -fPIC -shared -Os -Wl,--export=tree_sitter_{{language_name}}
Use these clang flags: -fPIC -shared -Os -Wl,--export=tree_sitter_{language_name}
Here are the available wasi-sdk assets:
- wasi-sdk-25.0-x86_64-macos.tar.gz
@@ -294,7 +240,7 @@ fn eval_use_wasi_sdk_in_compile_parser_to_wasm() {
[tool_use(
"tool_4",
"edit_file",
EditFileToolInput {
StreamingEditFileToolInput {
display_description: edit_description.into(),
path: input_file_path.into(),
create_or_overwrite: false,
@@ -318,10 +264,11 @@ fn eval_use_wasi_sdk_in_compile_parser_to_wasm() {
fn eval_disable_cursor_blinking() {
let input_file_path = "root/editor.rs";
let input_file_content = include_str!("evals/fixtures/disable_cursor_blinking/before.rs");
let output_file_content = include_str!("evals/fixtures/disable_cursor_blinking/after.rs");
let edit_description = "Comment out the call to `BlinkManager::enable`";
eval(
200,
0.95,
0.6, // TODO: make this eval better
EvalInput {
conversation: vec![
message(User, [text("Let's research how to cursor blinking works.")]),
@@ -369,7 +316,7 @@ fn eval_disable_cursor_blinking() {
[tool_use(
"tool_4",
"edit_file",
EditFileToolInput {
StreamingEditFileToolInput {
display_description: edit_description.into(),
path: input_file_path.into(),
create_or_overwrite: false,
@@ -380,11 +327,7 @@ fn eval_disable_cursor_blinking() {
input_path: input_file_path.into(),
input_content: Some(input_file_content.into()),
edit_description: edit_description.into(),
assertion: EvalAssertion::judge_diff(indoc! {"
- Calls to BlinkManager in `observe_window_activation` were commented out
- The call to `blink_manager.enable` above the call to show_cursor_names was commented out
- All the edits have valid indentation
"}),
assertion: EvalAssertion::assert_eq(output_file_content),
},
);
}
@@ -563,7 +506,7 @@ fn eval_from_pixels_constructor() {
[tool_use(
"tool_5",
"edit_file",
EditFileToolInput {
StreamingEditFileToolInput {
display_description: edit_description.into(),
path: input_file_path.into(),
create_or_overwrite: false,
@@ -640,7 +583,7 @@ fn eval_zode() {
tool_use(
"tool_3",
"edit_file",
EditFileToolInput {
StreamingEditFileToolInput {
display_description: edit_description.into(),
path: input_file_path.into(),
create_or_overwrite: true,
@@ -885,7 +828,7 @@ fn eval_add_overwrite_test() {
tool_use(
"tool_5",
"edit_file",
EditFileToolInput {
StreamingEditFileToolInput {
display_description: edit_description.into(),
path: input_file_path.into(),
create_or_overwrite: false,
@@ -952,7 +895,6 @@ fn tool_result(
tool_name: name.into(),
is_error: false,
content: result.into(),
output: None,
})
}
@@ -1091,8 +1033,7 @@ impl EvalAssertion {
fn eval(iterations: usize, expected_pass_ratio: f32, mut eval: EvalInput) {
let mut evaluated_count = 0;
let mut failed_count = 0;
report_progress(evaluated_count, failed_count, iterations);
report_progress(evaluated_count, 0, iterations);
let (tx, rx) = mpsc::channel();
@@ -1109,6 +1050,7 @@ fn eval(iterations: usize, expected_pass_ratio: f32, mut eval: EvalInput) {
}
drop(tx);
let mut failed_count = 0;
let mut failed_evals = HashMap::default();
let mut errored_evals = HashMap::default();
let mut eval_outputs = Vec::new();
@@ -1116,7 +1058,7 @@ fn eval(iterations: usize, expected_pass_ratio: f32, mut eval: EvalInput) {
while let Ok(output) = rx.recv() {
match output {
Ok(output) => {
cumulative_parser_metrics += output.sample.edit_output.parser_metrics.clone();
cumulative_parser_metrics += output.sample.edit_output._parser_metrics.clone();
eval_outputs.push(output.clone());
if output.assertion.score < 80 {
failed_count += 1;
@@ -1197,25 +1139,19 @@ impl Display for EvalOutput {
writeln!(
f,
"Parser Metrics:\n{:#?}",
self.sample.edit_output.parser_metrics
self.sample.edit_output._parser_metrics
)?;
writeln!(f, "Raw Edits:\n{}", self.sample.edit_output.raw_edits)?;
writeln!(f, "Raw Edits:\n{}", self.sample.edit_output._raw_edits)?;
Ok(())
}
}
fn report_progress(evaluated_count: usize, failed_count: usize, iterations: usize) {
let passed_count = evaluated_count - failed_count;
let passed_ratio = if evaluated_count == 0 {
0.0
} else {
passed_count as f64 / evaluated_count as f64
};
let failing_rate = failed_count as f32 / evaluated_count as f32;
let passing_rate = ((1. - failing_rate) * 100.).round() as usize;
print!(
"\r\x1b[KEvaluated {}/{} ({:.2}%)",
evaluated_count,
iterations,
passed_ratio * 100.0
"\r\x1b[KEvaluated {}/{} ({}% passing)",
evaluated_count, iterations, passing_rate
);
std::io::stdout().flush().unwrap();
}
@@ -1229,30 +1165,25 @@ struct EditAgentTest {
impl EditAgentTest {
async fn new(cx: &mut TestAppContext) -> Self {
cx.executor().allow_parking();
cx.update(settings::init);
cx.update(Project::init_settings);
cx.update(language::init);
cx.update(gpui_tokio::init);
cx.update(client::init_settings);
let fs = FakeFs::new(cx.executor().clone());
cx.update(|cx| {
settings::init(cx);
gpui_tokio::init(cx);
let http_client = Arc::new(ReqwestClient::user_agent("agent tests").unwrap());
cx.set_http_client(http_client);
client::init_settings(cx);
let client = Client::production(cx);
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
settings::init(cx);
Project::init_settings(cx);
language::init(cx);
language_model::init(client.clone(), cx);
language_models::init(user_store.clone(), client.clone(), fs.clone(), cx);
crate::init(client.http_client(), cx);
});
fs.insert_tree("/root", json!({})).await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let (agent_model, judge_model) = cx
.update(|cx| {
let http_client = ReqwestClient::user_agent("agent tests").unwrap();
cx.set_http_client(Arc::new(http_client));
let client = Client::production(cx);
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
language_model::init(client.clone(), cx);
language_models::init(user_store.clone(), client.clone(), fs.clone(), cx);
cx.spawn(async move |cx| {
let agent_model =
Self::load_model("anthropic", "claude-3-7-sonnet-latest", cx).await;
@@ -1301,32 +1232,12 @@ impl EditAgentTest {
.update(cx, |project, cx| project.open_buffer(path, cx))
.await
.unwrap();
let conversation = LanguageModelRequest {
messages: eval.conversation,
tools: cx.update(|cx| {
ToolRegistry::default_global(cx)
.tools()
.into_iter()
.filter_map(|tool| {
let input_schema = tool
.input_schema(self.agent.model.tool_input_format())
.ok()?;
Some(LanguageModelRequestTool {
name: tool.name(),
description: tool.description(),
input_schema,
})
})
.collect()
}),
..Default::default()
};
let edit_output = if let Some(input_content) = eval.input_content.as_deref() {
buffer.update(cx, |buffer, cx| buffer.set_text(input_content, cx));
let (edit_output, _) = self.agent.edit(
buffer.clone(),
eval.edit_description,
&conversation,
eval.conversation,
&mut cx.to_async(),
);
edit_output.await?
@@ -1334,7 +1245,7 @@ impl EditAgentTest {
let (edit_output, _) = self.agent.overwrite(
buffer.clone(),
eval.edit_description,
&conversation,
eval.conversation,
&mut cx.to_async(),
);
edit_output.await?

View File

@@ -1,339 +0,0 @@
// font-kit/src/canvas.rs
//
// Copyright © 2018 The Pathfinder Project Developers.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
//! An in-memory bitmap surface for glyph rasterization.
use lazy_static::lazy_static;
use pathfinder_geometry::rect::RectI;
use pathfinder_geometry::vector::Vector2I;
use std::cmp;
use std::fmt;
use crate::utils;
lazy_static! {
static ref BITMAP_1BPP_TO_8BPP_LUT: [[u8; 8]; 256] = {
let mut lut = [[0; 8]; 256];
for byte in 0..0x100 {
let mut value = [0; 8];
for bit in 0..8 {
if (byte & (0x80 >> bit)) != 0 {
value[bit] = 0xff;
}
}
lut[byte] = value
}
lut
};
}
/// An in-memory bitmap surface for glyph rasterization.
pub struct Canvas {
/// The raw pixel data.
pub pixels: Vec<u8>,
/// The size of the buffer, in pixels.
pub size: Vector2I,
/// The number of *bytes* between successive rows.
pub stride: usize,
/// The image format of the canvas.
pub format: Format,
}
impl Canvas {
/// Creates a new blank canvas with the given pixel size and format.
///
/// Stride is automatically calculated from width.
///
/// The canvas is initialized with transparent black (all values 0).
#[inline]
pub fn new(size: Vector2I, format: Format) -> Canvas {
Canvas::with_stride(
size,
size.x() as usize * format.bytes_per_pixel() as usize,
format,
)
}
/// Creates a new blank canvas with the given pixel size, stride (number of bytes between
/// successive rows), and format.
///
/// The canvas is initialized with transparent black (all values 0).
pub fn with_stride(size: Vector2I, stride: usize, format: Format) -> Canvas {
Canvas {
pixels: vec![0; stride * size.y() as usize],
size,
stride,
format,
}
}
#[allow(dead_code)]
pub(crate) fn blit_from_canvas(&mut self, src: &Canvas) {
self.blit_from(
Vector2I::default(),
&src.pixels,
src.size,
src.stride,
src.format,
)
}
/// Blits to a rectangle with origin at `dst_point` and size according to `src_size`.
/// If the target area overlaps the boundaries of the canvas, only the drawable region is blitted.
/// `dst_point` and `src_size` are specified in pixels. `src_stride` is specified in bytes.
/// `src_stride` must be equal or larger than the actual data length.
#[allow(dead_code)]
pub(crate) fn blit_from(
&mut self,
dst_point: Vector2I,
src_bytes: &[u8],
src_size: Vector2I,
src_stride: usize,
src_format: Format,
) {
assert_eq!(
src_stride * src_size.y() as usize,
src_bytes.len(),
"Number of pixels in src_bytes does not match stride and size."
);
assert!(
src_stride >= src_size.x() as usize * src_format.bytes_per_pixel() as usize,
"src_stride must be >= than src_size.x()"
);
let dst_rect = RectI::new(dst_point, src_size);
let dst_rect = dst_rect.intersection(RectI::new(Vector2I::default(), self.size));
let dst_rect = match dst_rect {
Some(dst_rect) => dst_rect,
None => return,
};
match (self.format, src_format) {
(Format::A8, Format::A8)
| (Format::Rgb24, Format::Rgb24)
| (Format::Rgba32, Format::Rgba32) => {
self.blit_from_with::<BlitMemcpy>(dst_rect, src_bytes, src_stride, src_format)
}
(Format::A8, Format::Rgb24) => {
self.blit_from_with::<BlitRgb24ToA8>(dst_rect, src_bytes, src_stride, src_format)
}
(Format::Rgb24, Format::A8) => {
self.blit_from_with::<BlitA8ToRgb24>(dst_rect, src_bytes, src_stride, src_format)
}
(Format::Rgb24, Format::Rgba32) => self
.blit_from_with::<BlitRgba32ToRgb24>(dst_rect, src_bytes, src_stride, src_format),
(Format::Rgba32, Format::Rgb24) => self
.blit_from_with::<BlitRgb24ToRgba32>(dst_rect, src_bytes, src_stride, src_format),
(Format::Rgba32, Format::A8) | (Format::A8, Format::Rgba32) => unimplemented!(),
}
}
#[allow(dead_code)]
pub(crate) fn blit_from_bitmap_1bpp(
&mut self,
dst_point: Vector2I,
src_bytes: &[u8],
src_size: Vector2I,
src_stride: usize,
) {
if self.format != Format::A8 {
unimplemented!()
}
let dst_rect = RectI::new(dst_point, src_size);
let dst_rect = dst_rect.intersection(RectI::new(Vector2I::default(), self.size));
let dst_rect = match dst_rect {
Some(dst_rect) => dst_rect,
None => return,
};
let size = dst_rect.size();
let dest_bytes_per_pixel = self.format.bytes_per_pixel() as usize;
let dest_row_stride = size.x() as usize * dest_bytes_per_pixel;
let src_row_stride = utils::div_round_up(size.x() as usize, 8);
for y in 0..size.y() {
let (dest_row_start, src_row_start) = (
(y + dst_rect.origin_y()) as usize * self.stride
+ dst_rect.origin_x() as usize * dest_bytes_per_pixel,
y as usize * src_stride,
);
let dest_row_end = dest_row_start + dest_row_stride;
let src_row_end = src_row_start + src_row_stride;
let dest_row_pixels = &mut self.pixels[dest_row_start..dest_row_end];
let src_row_pixels = &src_bytes[src_row_start..src_row_end];
for x in 0..src_row_stride {
let pattern = &BITMAP_1BPP_TO_8BPP_LUT[src_row_pixels[x] as usize];
let dest_start = x * 8;
let dest_end = cmp::min(dest_start + 8, dest_row_stride);
let src = &pattern[0..(dest_end - dest_start)];
dest_row_pixels[dest_start..dest_end].clone_from_slice(src);
}
}
}
/// Blits to area `rect` using the data given in the buffer `src_bytes`.
/// `src_stride` must be specified in bytes.
/// The dimensions of `rect` must be in pixels.
fn blit_from_with<B: Blit>(
&mut self,
rect: RectI,
src_bytes: &[u8],
src_stride: usize,
src_format: Format,
) {
let src_bytes_per_pixel = src_format.bytes_per_pixel() as usize;
let dest_bytes_per_pixel = self.format.bytes_per_pixel() as usize;
for y in 0..rect.height() {
let (dest_row_start, src_row_start) = (
(y + rect.origin_y()) as usize * self.stride
+ rect.origin_x() as usize * dest_bytes_per_pixel,
y as usize * src_stride,
);
let dest_row_end = dest_row_start + rect.width() as usize * dest_bytes_per_pixel;
let src_row_end = src_row_start + rect.width() as usize * src_bytes_per_pixel;
let dest_row_pixels = &mut self.pixels[dest_row_start..dest_row_end];
let src_row_pixels = &src_bytes[src_row_start..src_row_end];
B::blit(dest_row_pixels, src_row_pixels)
}
}
}
impl fmt::Debug for Canvas {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("Canvas")
.field("pixels", &self.pixels.len()) // Do not dump a vector content.
.field("size", &self.size)
.field("stride", &self.stride)
.field("format", &self.format)
.finish()
}
}
/// The image format for the canvas.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Format {
/// Premultiplied R8G8B8A8, little-endian.
Rgba32,
/// R8G8B8, little-endian.
Rgb24,
/// A8.
A8,
}
impl Format {
/// Returns the number of bits per pixel that this image format corresponds to.
#[inline]
pub fn bits_per_pixel(self) -> u8 {
match self {
Format::Rgba32 => 32,
Format::Rgb24 => 24,
Format::A8 => 8,
}
}
/// Returns the number of color channels per pixel that this image format corresponds to.
#[inline]
pub fn components_per_pixel(self) -> u8 {
match self {
Format::Rgba32 => 4,
Format::Rgb24 => 3,
Format::A8 => 1,
}
}
/// Returns the number of bits per color channel that this image format contains.
#[inline]
pub fn bits_per_component(self) -> u8 {
self.bits_per_pixel() / self.components_per_pixel()
}
/// Returns the number of bytes per pixel that this image format corresponds to.
#[inline]
pub fn bytes_per_pixel(self) -> u8 {
self.bits_per_pixel() / 8
}
}
/// The antialiasing strategy that should be used when rasterizing glyphs.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum RasterizationOptions {
/// "Black-and-white" rendering. Each pixel is either entirely on or off.
Bilevel,
/// Grayscale antialiasing. Only one channel is used.
GrayscaleAa,
/// Subpixel RGB antialiasing, for LCD screens.
SubpixelAa,
}
trait Blit {
fn blit(dest: &mut [u8], src: &[u8]);
}
struct BlitMemcpy;
impl Blit for BlitMemcpy {
#[inline]
fn blit(dest: &mut [u8], src: &[u8]) {
dest.clone_from_slice(src)
}
}
struct BlitRgb24ToA8;
impl Blit for BlitRgb24ToA8 {
#[inline]
fn blit(dest: &mut [u8], src: &[u8]) {
// TODO(pcwalton): SIMD.
for (dest, src) in dest.iter_mut().zip(src.chunks(3)) {
*dest = src[1]
}
}
}
struct BlitA8ToRgb24;
impl Blit for BlitA8ToRgb24 {
#[inline]
fn blit(dest: &mut [u8], src: &[u8]) {
for (dest, src) in dest.chunks_mut(3).zip(src.iter()) {
dest[0] = *src;
dest[1] = *src;
dest[2] = *src;
}
}
}
struct BlitRgba32ToRgb24;
impl Blit for BlitRgba32ToRgb24 {
#[inline]
fn blit(dest: &mut [u8], src: &[u8]) {
// TODO(pcwalton): SIMD.
for (dest, src) in dest.chunks_mut(3).zip(src.chunks(4)) {
dest.copy_from_slice(&src[0..3])
}
}
}
struct BlitRgb24ToRgba32;
impl Blit for BlitRgb24ToRgba32 {
fn blit(dest: &mut [u8], src: &[u8]) {
for (dest, src) in dest.chunks_mut(4).zip(src.chunks(3)) {
dest[0] = src[0];
dest[1] = src[1];
dest[2] = src[2];
dest[3] = 255;
}
}
}

View File

@@ -1,27 +1,21 @@
use crate::{
Templates,
edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent},
replace::{replace_exact, replace_with_flexible_indent},
schema::json_schema_for,
};
use anyhow::{Result, anyhow};
use assistant_tool::{
ActionLog, AnyToolCard, Tool, ToolCard, ToolResult, ToolResultOutput, ToolUseStatus,
};
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolCard, ToolResult, ToolUseStatus};
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
use editor::{Editor, EditorMode, MultiBuffer, PathKey};
use futures::StreamExt;
use editor::{Editor, EditorElement, EditorMode, EditorStyle, MultiBuffer, PathKey};
use gpui::{
Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, EntityId, Task,
TextStyleRefinement, WeakEntity, pulsating_between,
Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Context, Entity, EntityId,
Task, TextStyle, WeakEntity, pulsating_between,
};
use indoc::formatdoc;
use language::{
Anchor, Buffer, Capability, LanguageRegistry, LineEnding, OffsetRangeExt, Rope, TextBuffer,
language_settings::SoftWrap,
};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use markdown::{Markdown, MarkdownElement, MarkdownStyle};
use project::Project;
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::{AgentLocation, Project};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
@@ -31,7 +25,7 @@ use std::{
time::Duration,
};
use theme::ThemeSettings;
use ui::{Disclosure, Tooltip, prelude::*};
use ui::{Disclosure, Tooltip, Window, prelude::*};
use util::ResultExt;
use workspace::Workspace;
@@ -39,13 +33,7 @@ pub struct EditFileTool;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct EditFileToolInput {
/// A one-line, user-friendly markdown description of the edit. This will be
/// shown in the UI and also passed to another model to perform the edit.
///
/// Be terse, but also descriptive in what you want to achieve with this
/// edit. Avoid generic instructions.
///
/// NEVER mention the file path in this description.
/// A user-friendly markdown description of what's being replaced. This will be shown in the UI.
///
/// <example>Fix API endpoint URLs</example>
/// <example>Update copyright year in `page_footer`</example>
@@ -54,7 +42,7 @@ pub struct EditFileToolInput {
/// so that we can display it immediately.
pub display_description: String,
/// The full path of the file to create or modify in the project.
/// The full path of the file to modify in the project.
///
/// WARNING: When specifying which file path need changing, you MUST
/// start each path with one of the project's root directories.
@@ -75,20 +63,11 @@ pub struct EditFileToolInput {
/// </example>
pub path: PathBuf,
/// If true, this tool will recreate the file from scratch.
/// If false, this tool will produce granular edits to an existing file.
///
/// When a file already exists or you just created it, always prefer editing
/// it as opposed to recreating it from scratch.
pub create_or_overwrite: bool,
}
/// The text to replace.
pub old_string: String,
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct EditFileToolOutput {
pub original_path: PathBuf,
pub new_text: String,
pub old_text: String,
pub raw_output: Option<EditAgentOutput>,
/// The text to replace it with.
pub new_string: String,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
@@ -97,6 +76,10 @@ struct PartialInput {
path: String,
#[serde(default)]
display_description: String,
#[serde(default)]
old_string: String,
#[serde(default)]
new_string: String,
}
const DEFAULT_UI_TEXT: &str = "Editing file";
@@ -148,10 +131,9 @@ impl Tool for EditFileTool {
fn run(
self: Arc<Self>,
input: serde_json::Value,
request: Arc<LanguageModelRequest>,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
model: Arc<dyn LanguageModel>,
window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
@@ -160,14 +142,6 @@ impl Tool for EditFileTool {
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
return Task::ready(Err(anyhow!(
"Path {} not found in project",
input.path.display()
)))
.into();
};
let card = window.and_then(|window| {
window
.update(cx, |_, window, cx| {
@@ -180,7 +154,11 @@ impl Tool for EditFileTool {
let card_clone = card.clone();
let task = cx.spawn(async move |cx: &mut AsyncApp| {
let edit_agent = EditAgent::new(model, project.clone(), action_log, Templates::new());
let project_path = project.read_with(cx, |project, cx| {
project
.find_project_path(&input.path, cx)
.context("Path not found in project")
})??;
let buffer = project
.update(cx, |project, cx| {
@@ -188,89 +166,118 @@ impl Tool for EditFileTool {
})?
.await?;
let exists = buffer.read_with(cx, |buffer, _| {
buffer
.file()
.as_ref()
.map_or(false, |file| file.disk_state().exists())
})?;
if !input.create_or_overwrite && !exists {
return Err(anyhow!("{} not found", input.path.display()));
// Set the agent's location to the top of the file
project
.update(cx, |project, cx| {
project.set_agent_location(
Some(AgentLocation {
buffer: buffer.downgrade(),
position: language::Anchor::MIN,
}),
cx,
);
})
.ok();
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
if input.old_string.is_empty() {
return Err(anyhow!(
"`old_string` can't be empty, use another tool if you want to create a file."
));
}
let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
let old_text = cx
.background_spawn({
let old_snapshot = old_snapshot.clone();
async move { old_snapshot.text() }
if input.old_string == input.new_string {
return Err(anyhow!(
"The `old_string` and `new_string` are identical, so no changes would be made."
));
}
let result = cx
.background_spawn(async move {
// Try to match exactly
let diff = replace_exact(&input.old_string, &input.new_string, &snapshot)
.await
// If that fails, try being flexible about indentation
.or_else(|| {
replace_with_flexible_indent(
&input.old_string,
&input.new_string,
&snapshot,
)
})?;
if diff.edits.is_empty() {
return None;
}
let old_text = snapshot.text();
Some((old_text, diff))
})
.await;
let (output, mut events) = if input.create_or_overwrite {
edit_agent.overwrite(
buffer.clone(),
input.display_description.clone(),
&request,
cx,
)
} else {
edit_agent.edit(
buffer.clone(),
input.display_description.clone(),
&request,
cx,
)
let Some((old_text, diff)) = result else {
let err = buffer.read_with(cx, |buffer, _cx| {
let file_exists = buffer
.file()
.map_or(false, |file| file.disk_state().exists());
if !file_exists {
anyhow!("{} does not exist", input.path.display())
} else if buffer.is_empty() {
anyhow!(
"{} is empty, so the provided `old_string` wasn't found.",
input.path.display()
)
} else {
anyhow!("Failed to match the provided `old_string`")
}
})?;
return Err(err);
};
let mut hallucinated_old_text = false;
while let Some(event) = events.next().await {
match event {
EditAgentOutputEvent::Edited => {
if let Some(card) = card_clone.as_ref() {
let new_snapshot =
buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
let new_text = cx
.background_spawn({
let new_snapshot = new_snapshot.clone();
async move { new_snapshot.text() }
})
.await;
card.update(cx, |card, cx| {
card.set_diff(
project_path.path.clone(),
old_text.clone(),
new_text,
cx,
);
})
.log_err();
}
}
EditAgentOutputEvent::OldTextNotFound(_) => hallucinated_old_text = true,
let snapshot = cx.update(|cx| {
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
let base_version = diff.base_version.clone();
let snapshot = buffer.update(cx, |buffer, cx| {
buffer.finalize_last_transaction();
buffer.apply_diff(diff, cx);
buffer.finalize_last_transaction();
buffer.snapshot()
});
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
// Set the agent's location to the position of the first edit
if let Some(first_edit) = snapshot.edits_since::<usize>(&base_version).next() {
let position = snapshot.anchor_before(first_edit.new.start);
project.update(cx, |project, cx| {
project.set_agent_location(
Some(AgentLocation {
buffer: buffer.downgrade(),
position,
}),
cx,
);
})
}
}
let agent_output = output.await?;
snapshot
})?;
project
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
.update(cx, |project, cx| project.save_buffer(buffer, cx))?
.await?;
let new_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
let new_text = cx.background_spawn({
let new_snapshot = new_snapshot.clone();
async move { new_snapshot.text() }
});
let diff = cx.background_spawn(async move {
language::unified_diff(&old_snapshot.text(), &new_snapshot.text())
});
let (new_text, diff) = futures::join!(new_text, diff);
let output = EditFileToolOutput {
original_path: project_path.path.to_path_buf(),
new_text: new_text.clone(),
old_text: old_text.clone(),
raw_output: Some(agent_output),
};
let new_text = snapshot.text();
let diff_str = cx
.background_spawn({
let old_text = old_text.clone();
let new_text = new_text.clone();
async move { language::unified_diff(&old_text, &new_text) }
})
.await;
if let Some(card) = card_clone {
card.update(cx, |card, cx| {
@@ -279,23 +286,11 @@ impl Tool for EditFileTool {
.log_err();
}
let input_path = input.path.display();
if diff.is_empty() {
if hallucinated_old_text {
Err(anyhow!(formatdoc! {"
Some edits were produced but none of them could be applied.
Read the relevant sections of {input_path} again so that
I can perform the requested edits.
"}))
} else {
Ok("No edits were made.".to_string().into())
}
} else {
Ok(ToolResultOutput {
content: format!("Edited {}:\n\n```diff\n{}\n```", input_path, diff),
output: serde_json::to_value(output).ok(),
})
}
Ok(format!(
"Edited {}:\n\n```diff\n{}\n```",
input.path.display(),
diff_str
))
});
ToolResult {
@@ -303,32 +298,6 @@ impl Tool for EditFileTool {
card: card.map(AnyToolCard::from),
}
}
fn deserialize_card(
self: Arc<Self>,
output: serde_json::Value,
project: Entity<Project>,
window: &mut Window,
cx: &mut App,
) -> Option<AnyToolCard> {
let output = match serde_json::from_value::<EditFileToolOutput>(output) {
Ok(output) => output,
Err(_) => return None,
};
let card = cx.new(|cx| {
let mut card = EditFileToolCard::new(output.original_path.clone(), project, window, cx);
card.set_diff(
output.original_path.into(),
output.old_text,
output.new_text,
cx,
);
card
});
Some(card.into())
}
}
pub struct EditFileToolCard {
@@ -338,7 +307,7 @@ pub struct EditFileToolCard {
project: Entity<Project>,
diff_task: Option<Task<Result<()>>>,
preview_expanded: bool,
error_expanded: Option<Entity<Markdown>>,
error_expanded: bool,
full_height_expanded: bool,
total_lines: Option<u32>,
editor_unique_id: EntityId,
@@ -362,9 +331,9 @@ impl EditFileToolCard {
editor.set_show_gutter(false, cx);
editor.disable_inline_diagnostics();
editor.disable_expand_excerpt_buttons(cx);
editor.disable_scrollbars_and_minimap(window, cx);
editor.set_soft_wrap_mode(SoftWrap::None, cx);
editor.scroll_manager.set_forbid_vertical_scroll(true);
editor.set_show_scrollbars(false, cx);
editor.set_show_indent_guides(false, cx);
editor.set_read_only(true);
editor.set_show_breakpoints(false, cx);
@@ -381,7 +350,7 @@ impl EditFileToolCard {
multibuffer,
diff_task: None,
preview_expanded: true,
error_expanded: None,
error_expanded: false,
full_height_expanded: false,
total_lines: None,
}
@@ -438,9 +407,9 @@ impl ToolCard for EditFileToolCard {
workspace: WeakEntity<Workspace>,
cx: &mut Context<Self>,
) -> impl IntoElement {
let error_message = match status {
ToolUseStatus::Error(err) => Some(err),
_ => None,
let (failed, error_message) = match status {
ToolUseStatus::Error(err) => (true, Some(err.to_string())),
_ => (false, None),
};
let path_label_button = h_flex()
@@ -528,11 +497,9 @@ impl ToolCard for EditFileToolCard {
.gap_1()
.justify_between()
.rounded_t_md()
.when(error_message.is_none(), |header| {
header.bg(codeblock_header_bg)
})
.when(!failed, |header| header.bg(codeblock_header_bg))
.child(path_label_button)
.when_some(error_message, |header, error_message| {
.when(failed, |header| {
header.child(
h_flex()
.gap_1()
@@ -544,28 +511,19 @@ impl ToolCard for EditFileToolCard {
.child(
Disclosure::new(
("edit-file-error-disclosure", self.editor_unique_id),
self.error_expanded.is_some(),
self.error_expanded,
)
.opened_icon(IconName::ChevronUp)
.closed_icon(IconName::ChevronDown)
.on_click(cx.listener({
let error_message = error_message.clone();
move |this, _event, _window, cx| {
if this.error_expanded.is_some() {
this.error_expanded.take();
} else {
this.error_expanded = Some(cx.new(|cx| {
Markdown::new(error_message.clone(), None, None, cx)
}))
}
cx.notify();
}
})),
.on_click(cx.listener(
move |this, _event, _window, _cx| {
this.error_expanded = !this.error_expanded;
},
)),
),
)
})
.when(error_message.is_none() && self.has_diff(), |header| {
.when(!failed && self.has_diff(), |header| {
header.child(
Disclosure::new(
("edit-file-disclosure", self.editor_unique_id),
@@ -587,16 +545,33 @@ impl ToolCard for EditFileToolCard {
.map(|style| style.text.line_height_in_pixels(window.rem_size()))
.unwrap_or_default();
editor.set_text_style_refinement(TextStyleRefinement {
font_size: Some(
TextSize::Small
.rems(cx)
.to_pixels(ThemeSettings::get_global(cx).agent_font_size(cx))
.into(),
),
..TextStyleRefinement::default()
});
let element = editor.render(window, cx);
let settings = ThemeSettings::get_global(cx);
let element = EditorElement::new(
&cx.entity(),
EditorStyle {
background: cx.theme().colors().editor_background,
horizontal_padding: rems(0.25).to_pixels(window.rem_size()),
local_player: cx.theme().players().local(),
text: TextStyle {
color: cx.theme().colors().editor_foreground,
font_family: settings.buffer_font.family.clone(),
font_features: settings.buffer_font.features.clone(),
font_fallbacks: settings.buffer_font.fallbacks.clone(),
font_size: TextSize::Small
.rems(cx)
.to_pixels(settings.agent_font_size(cx))
.into(),
font_weight: settings.buffer_font.weight,
line_height: relative(settings.buffer_line_height.value()),
..Default::default()
},
scrollbar_width: EditorElement::SCROLLBAR_WIDTH,
syntax: cx.theme().syntax().clone(),
status: cx.theme().status().clone(),
..Default::default()
},
);
(element.into_any_element(), line_height)
});
@@ -606,18 +581,18 @@ impl ToolCard for EditFileToolCard {
(IconName::ChevronDown, "Expand Code Block")
};
let gradient_overlay =
div()
.absolute()
.bottom_0()
.left_0()
.w_full()
.h_2_5()
.bg(gpui::linear_gradient(
0.,
gpui::linear_color_stop(cx.theme().colors().editor_background, 0.),
gpui::linear_color_stop(cx.theme().colors().editor_background.opacity(0.), 1.),
));
let gradient_overlay = div()
.absolute()
.bottom_0()
.left_0()
.w_full()
.h_2_5()
.rounded_b_lg()
.bg(gpui::linear_gradient(
0.,
gpui::linear_color_stop(cx.theme().colors().editor_background, 0.),
gpui::linear_color_stop(cx.theme().colors().editor_background.opacity(0.), 1.),
));
let border_color = cx.theme().colors().border.opacity(0.6);
@@ -635,9 +610,8 @@ impl ToolCard for EditFileToolCard {
let mut container = v_flex()
.p_3()
.gap_1()
.gap_1p5()
.border_t_1()
.rounded_b_md()
.border_color(border_color)
.bg(cx.theme().colors().editor_background);
@@ -652,7 +626,7 @@ impl ToolCard for EditFileToolCard {
_ => div().w_1_2(),
}
.id("loading_div")
.h_1()
.h_2()
.rounded_full()
.bg(cx.theme().colors().element_active)
.with_animation(
@@ -672,12 +646,12 @@ impl ToolCard for EditFileToolCard {
v_flex()
.mb_2()
.border_1()
.when(error_message.is_some(), |card| card.border_dashed())
.when(failed, |card| card.border_dashed())
.border_color(border_color)
.rounded_md()
.rounded_lg()
.overflow_hidden()
.child(codeblock_header)
.when_some(self.error_expanded.as_ref(), |card, error_markdown| {
.when(failed && self.error_expanded, |card| {
card.child(
v_flex()
.p_2()
@@ -697,81 +671,64 @@ impl ToolCard for EditFileToolCard {
.rounded_md()
.text_ui_sm(cx)
.bg(cx.theme().colors().editor_background)
.child(MarkdownElement::new(
error_markdown.clone(),
markdown_style(window, cx),
)),
.children(
error_message
.map(|error| div().child(error).into_any_element()),
),
),
)
})
.when(!self.has_diff() && error_message.is_none(), |card| {
.when(!self.has_diff() && !failed, |card| {
card.child(waiting_for_diff)
})
.when(self.preview_expanded && self.has_diff(), |card| {
card.child(
v_flex()
.relative()
.h_full()
.when(!self.full_height_expanded, |editor_container| {
editor_container
.max_h(DEFAULT_COLLAPSED_LINES as f32 * editor_line_height)
})
.overflow_hidden()
.border_t_1()
.border_color(border_color)
.bg(cx.theme().colors().editor_background)
.child(editor)
.when(
!self.full_height_expanded && is_collapsible,
|editor_container| editor_container.child(gradient_overlay),
),
)
.when(is_collapsible, |card| {
.when(
!failed && self.preview_expanded && self.has_diff(),
|card| {
card.child(
h_flex()
.id(("expand-button", self.editor_unique_id))
.flex_none()
.cursor_pointer()
.h_5()
.justify_center()
v_flex()
.relative()
.h_full()
.when(!self.full_height_expanded, |editor_container| {
editor_container
.max_h(DEFAULT_COLLAPSED_LINES as f32 * editor_line_height)
})
.overflow_hidden()
.border_t_1()
.rounded_b_md()
.border_color(border_color)
.bg(cx.theme().colors().editor_background)
.hover(|style| style.bg(cx.theme().colors().element_hover.opacity(0.1)))
.child(
Icon::new(full_height_icon)
.size(IconSize::Small)
.color(Color::Muted),
)
.tooltip(Tooltip::text(full_height_tooltip_label))
.on_click(cx.listener(move |this, _event, _window, _cx| {
this.full_height_expanded = !this.full_height_expanded;
})),
.child(editor)
.when(
!self.full_height_expanded && is_collapsible,
|editor_container| editor_container.child(gradient_overlay),
),
)
})
})
}
}
fn markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
let theme_settings = ThemeSettings::get_global(cx);
let ui_font_size = TextSize::Default.rems(cx);
let mut text_style = window.text_style();
text_style.refine(&TextStyleRefinement {
font_family: Some(theme_settings.ui_font.family.clone()),
font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
font_features: Some(theme_settings.ui_font.features.clone()),
font_size: Some(ui_font_size.into()),
color: Some(cx.theme().colors().text),
..Default::default()
});
MarkdownStyle {
base_text_style: text_style.clone(),
selection_background_color: cx.theme().players().local().selection,
..Default::default()
.when(is_collapsible, |editor_container| {
editor_container.child(
h_flex()
.id(("expand-button", self.editor_unique_id))
.flex_none()
.cursor_pointer()
.h_5()
.justify_center()
.border_t_1()
.border_color(border_color)
.bg(cx.theme().colors().editor_background)
.hover(|style| {
style.bg(cx.theme().colors().element_hover.opacity(0.1))
})
.child(
Icon::new(full_height_icon)
.size(IconSize::Small)
.color(Color::Muted),
)
.tooltip(Tooltip::text(full_height_tooltip_label))
.on_click(cx.listener(move |this, _event, _window, _cx| {
this.full_height_expanded = !this.full_height_expanded;
})),
)
})
},
)
}
}
@@ -851,48 +808,7 @@ async fn build_buffer_diff(
#[cfg(test)]
mod tests {
use super::*;
use fs::FakeFs;
use gpui::TestAppContext;
use language_model::fake_provider::FakeLanguageModel;
use serde_json::json;
use settings::SettingsStore;
use util::path;
#[gpui::test]
async fn test_edit_nonexistent_file(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree("/root", json!({})).await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let model = Arc::new(FakeLanguageModel::default());
let result = cx
.update(|cx| {
let input = serde_json::to_value(EditFileToolInput {
display_description: "Some edit".into(),
path: "root/nonexistent_file.txt".into(),
create_or_overwrite: false,
})
.unwrap();
Arc::new(EditFileTool)
.run(
input,
Arc::default(),
project.clone(),
action_log,
model,
None,
cx,
)
.output
})
.await;
assert_eq!(
result.unwrap_err().to_string(),
"root/nonexistent_file.txt not found"
);
}
#[test]
fn still_streaming_ui_text_with_path() {
@@ -960,13 +876,4 @@ mod tests {
DEFAULT_UI_TEXT,
);
}
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
Project::init_settings(cx);
});
}
}

View File

@@ -1,4 +1,4 @@
This is a tool for creating a new file or editing an existing file. For moving or renaming files, you should generally use the `terminal` tool with the 'mv' command instead.
This is a tool for editing files. For moving or renaming files, you should generally use the `terminal` tool with the 'mv' command instead. For larger edits, use the `create_file` tool to overwrite files.
Before using this tool:
@@ -6,3 +6,40 @@ Before using this tool:
2. Verify the directory path is correct (only applicable when creating new files):
- Use the `list_directory` tool to verify the parent directory exists and is the correct location
To make a file edit, provide the following:
1. path: The full path to the file you wish to modify in the project. This path must include the root directory in the project.
2. old_string: The text to replace (must be unique within the file, and must match the file contents exactly, including all whitespace and indentation)
3. new_string: The edited text, which will replace the old_string in the file.
The tool will replace ONE occurrence of old_string with new_string in the specified file.
CRITICAL REQUIREMENTS FOR USING THIS TOOL:
1. UNIQUENESS: The old_string MUST uniquely identify the specific instance you want to change. This means:
- Include AT LEAST 3-5 lines of context BEFORE the change point
- Include AT LEAST 3-5 lines of context AFTER the change point
- Include all whitespace, indentation, and surrounding code exactly as it appears in the file
2. SINGLE INSTANCE: This tool can only change ONE instance at a time. If you need to change multiple instances:
- Make separate calls to this tool for each instance
- Each call must uniquely identify its specific instance using extensive context
3. VERIFICATION: Before using this tool:
- Check how many instances of the target text exist in the file
- If multiple instances exist, gather enough context to uniquely identify each one
- Plan separate tool calls for each instance
WARNING: If you do not follow these requirements:
- The tool will fail if old_string matches multiple locations
- The tool will fail if old_string doesn't match exactly (including whitespace)
- You may change the wrong instance if you don't include enough context
When making edits:
- Ensure the edit results in idiomatic, correct code
- Do not leave the code in a broken state
- Always use fully-qualified project paths (starting with the name of one of the project's root directories)
If you want to create a new file, use the `create_file` tool instead of this tool. Don't pass an empty `old_string`.
Remember: when making multiple file edits in a row to the same file, you should prefer to send all edits in a single message with multiple calls to this tool, rather than multiple messages with a single call each.

View File

@@ -9,7 +9,7 @@ use futures::AsyncReadExt as _;
use gpui::{AnyWindowHandle, App, AppContext as _, Entity, Task};
use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown};
use http_client::{AsyncBody, HttpClientWithUrl};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -142,10 +142,9 @@ impl Tool for FetchTool {
fn run(
self: Arc<Self>,
input: serde_json::Value,
_request: Arc<LanguageModelRequest>,
_messages: &[LanguageModelRequestMessage],
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
_model: Arc<dyn LanguageModel>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
@@ -167,7 +166,7 @@ impl Tool for FetchTool {
bail!("no textual content found");
}
Ok(text.into())
Ok(text)
})
.into()
}

View File

@@ -7,7 +7,7 @@ use gpui::{
AnyWindowHandle, App, AppContext, Context, Entity, IntoElement, Task, WeakEntity, Window,
};
use language;
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -73,10 +73,9 @@ impl Tool for FindPathTool {
fn run(
self: Arc<Self>,
input: serde_json::Value,
_request: Arc<LanguageModelRequest>,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
_model: Arc<dyn LanguageModel>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
@@ -99,7 +98,7 @@ impl Tool for FindPathTool {
sender.send(paginated_matches.to_vec()).log_err();
if matches.is_empty() {
Ok("No matches found".to_string().into())
Ok("No matches found".to_string())
} else {
let mut message = format!("Found {} total matches.", matches.len());
if matches.len() > RESULTS_PER_PAGE {
@@ -114,7 +113,7 @@ impl Tool for FindPathTool {
for mat in matches.into_iter().skip(offset).take(RESULTS_PER_PAGE) {
write!(&mut message, "\n{}", mat.display()).unwrap();
}
Ok(message.into())
Ok(message)
}
});

View File

@@ -4,7 +4,7 @@ use assistant_tool::{ActionLog, Tool, ToolResult};
use futures::StreamExt;
use gpui::{AnyWindowHandle, App, Entity, Task};
use language::{OffsetRangeExt, ParseStatus, Point};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::{
Project,
search::{SearchQuery, SearchResult},
@@ -96,10 +96,9 @@ impl Tool for GrepTool {
fn run(
self: Arc<Self>,
input: serde_json::Value,
_request: Arc<LanguageModelRequest>,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
_model: Arc<dyn LanguageModel>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
@@ -261,16 +260,16 @@ impl Tool for GrepTool {
}
if matches_found == 0 {
Ok("No matches found".to_string().into())
Ok("No matches found".to_string())
} else if has_more_matches {
Ok(format!(
"Showing matches {}-{} (there were more matches found; use offset: {} to see next page):\n{output}",
input.offset + 1,
input.offset + matches_found,
input.offset + RESULTS_PER_PAGE,
).into())
))
} else {
Ok(format!("Found {matches_found} matches:\n{output}").into())
Ok(format!("Found {matches_found} matches:\n{output}"))
}
}).into()
}
@@ -282,7 +281,6 @@ mod tests {
use assistant_tool::Tool;
use gpui::{AppContext, TestAppContext};
use language::{Language, LanguageConfig, LanguageMatcher};
use language_model::fake_provider::FakeLanguageModel;
use project::{FakeFs, Project};
use settings::SettingsStore;
use unindent::Unindent;
@@ -745,16 +743,14 @@ mod tests {
) -> String {
let tool = Arc::new(GrepTool);
let action_log = cx.new(|_cx| ActionLog::new(project.clone()));
let model = Arc::new(FakeLanguageModel::default());
let task =
cx.update(|cx| tool.run(input, Arc::default(), project, action_log, model, None, cx));
let task = cx.update(|cx| tool.run(input, &[], project, action_log, None, cx));
match task.output.await {
Ok(result) => {
if cfg!(windows) {
result.content.replace("root\\", "root/")
result.replace("root\\", "root/")
} else {
result.content
result
}
}
Err(e) => panic!("Failed to run grep tool: {}", e),

View File

@@ -2,7 +2,7 @@ use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{AnyWindowHandle, App, Entity, Task};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -73,10 +73,9 @@ impl Tool for ListDirectoryTool {
fn run(
self: Arc<Self>,
input: serde_json::Value,
_request: Arc<LanguageModelRequest>,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
_model: Arc<dyn LanguageModel>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
@@ -103,7 +102,7 @@ impl Tool for ListDirectoryTool {
.collect::<Vec<_>>()
.join("\n");
return Task::ready(Ok(output.into())).into();
return Task::ready(Ok(output)).into();
}
let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
@@ -135,8 +134,8 @@ impl Tool for ListDirectoryTool {
.unwrap();
}
if output.is_empty() {
return Task::ready(Ok(format!("{} is empty.", input.path).into())).into();
return Task::ready(Ok(format!("{} is empty.", input.path))).into();
}
Task::ready(Ok(output.into())).into()
Task::ready(Ok(output)).into()
}
}

View File

@@ -2,7 +2,7 @@ use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -86,10 +86,9 @@ impl Tool for MovePathTool {
fn run(
self: Arc<Self>,
input: serde_json::Value,
_request: Arc<LanguageModelRequest>,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
_model: Arc<dyn LanguageModel>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
@@ -118,9 +117,10 @@ impl Tool for MovePathTool {
cx.background_spawn(async move {
match rename_task.await {
Ok(_) => {
Ok(format!("Moved {} to {}", input.source_path, input.destination_path).into())
}
Ok(_) => Ok(format!(
"Moved {} to {}",
input.source_path, input.destination_path
)),
Err(err) => Err(anyhow!(
"Failed to move {} to {}: {}",
input.source_path,

View File

@@ -5,7 +5,7 @@ use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use chrono::{Local, Utc};
use gpui::{AnyWindowHandle, App, Entity, Task};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -56,10 +56,9 @@ impl Tool for NowTool {
fn run(
self: Arc<Self>,
input: serde_json::Value,
_request: Arc<LanguageModelRequest>,
_messages: &[LanguageModelRequestMessage],
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
_model: Arc<dyn LanguageModel>,
_window: Option<AnyWindowHandle>,
_cx: &mut App,
) -> ToolResult {
@@ -74,6 +73,6 @@ impl Tool for NowTool {
};
let text = format!("The current datetime is {now}.");
Task::ready(Ok(text.into())).into()
Task::ready(Ok(text)).into()
}
}

View File

@@ -2,7 +2,7 @@ use crate::schema::json_schema_for;
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -49,10 +49,9 @@ impl Tool for OpenTool {
fn run(
self: Arc<Self>,
input: serde_json::Value,
_request: Arc<LanguageModelRequest>,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
_model: Arc<dyn LanguageModel>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
@@ -71,7 +70,7 @@ impl Tool for OpenTool {
}
.context("Failed to open URL or file path")?;
Ok(format!("Successfully opened {}", input.path_or_url).into())
Ok(format!("Successfully opened {}", input.path_or_url))
})
.into()
}

View File

@@ -7,7 +7,7 @@ use gpui::{AnyWindowHandle, App, Entity, Task};
use indoc::formatdoc;
use itertools::Itertools;
use language::{Anchor, Point};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::{AgentLocation, Project};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -83,10 +83,9 @@ impl Tool for ReadFileTool {
fn run(
self: Arc<Self>,
input: serde_json::Value,
_request: Arc<LanguageModelRequest>,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
_model: Arc<dyn LanguageModel>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
@@ -98,22 +97,27 @@ impl Tool for ReadFileTool {
let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
return Task::ready(Err(anyhow!("Path {} not found in project", &input.path))).into();
};
let Some(worktree) = project
.read(cx)
.worktree_for_id(project_path.worktree_id, cx)
else {
return Task::ready(Err(anyhow!("Worktree not found for project path"))).into();
};
let exists = worktree.update(cx, |worktree, cx| {
worktree.file_exists(&project_path.path, cx)
});
let file_path = input.path.clone();
cx.spawn(async move |cx| {
if !exists.await? {
return Err(anyhow!("{} not found", file_path));
}
let buffer = cx
.update(|cx| {
project.update(cx, |project, cx| project.open_buffer(project_path, cx))
})?
.await?;
if buffer.read_with(cx, |buffer, _| {
buffer
.file()
.as_ref()
.map_or(true, |file| !file.disk_state().exists())
})? {
return Err(anyhow!("{} not found", file_path));
}
project.update(cx, |project, cx| {
project.set_agent_location(
@@ -141,13 +145,9 @@ impl Tool for ReadFileTool {
let lines = text.split('\n').skip(start_row as usize);
if let Some(end) = input.end_line {
let count = end.saturating_sub(start).saturating_add(1); // Ensure at least 1 line
Itertools::intersperse(lines.take(count as usize), "\n")
.collect::<String>()
.into()
Itertools::intersperse(lines.take(count as usize), "\n").collect()
} else {
Itertools::intersperse(lines, "\n")
.collect::<String>()
.into()
Itertools::intersperse(lines, "\n").collect()
}
})?;
@@ -180,24 +180,19 @@ impl Tool for ReadFileTool {
log.buffer_read(buffer, cx);
})?;
Ok(result.into())
Ok(result)
} else {
// File is too big, so return the outline
// and a suggestion to read again with line numbers.
let outline =
outline::file_outline(project, file_path, action_log, None, cx).await?;
let outline = outline::file_outline(project, file_path, action_log, None, cx).await?;
Ok(formatdoc! {"
This file was too big to read all at once.
Here is an outline of its symbols:
This file was too big to read all at once. Here is an outline of its symbols:
{outline}
Using the line numbers in this outline, you can call this tool again
while specifying the start_line and end_line fields to see the
implementations of symbols in the outline."
}
.into())
Using the line numbers in this outline, you can call this tool again while specifying
the start_line and end_line fields to see the implementations of symbols in the outline."
})
}
}
})
@@ -210,7 +205,6 @@ mod test {
use super::*;
use gpui::{AppContext, TestAppContext};
use language::{Language, LanguageConfig, LanguageMatcher};
use language_model::fake_provider::FakeLanguageModel;
use project::{FakeFs, Project};
use serde_json::json;
use settings::SettingsStore;
@@ -224,22 +218,13 @@ mod test {
fs.insert_tree("/root", json!({})).await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let model = Arc::new(FakeLanguageModel::default());
let result = cx
.update(|cx| {
let input = json!({
"path": "root/nonexistent_file.txt"
});
Arc::new(ReadFileTool)
.run(
input,
Arc::default(),
project.clone(),
action_log,
model,
None,
cx,
)
.run(input, &[], project.clone(), action_log, None, cx)
.output
})
.await;
@@ -263,26 +248,17 @@ mod test {
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let model = Arc::new(FakeLanguageModel::default());
let result = cx
.update(|cx| {
let input = json!({
"path": "root/small_file.txt"
});
Arc::new(ReadFileTool)
.run(
input,
Arc::default(),
project.clone(),
action_log,
model,
None,
cx,
)
.run(input, &[], project.clone(), action_log, None, cx)
.output
})
.await;
assert_eq!(result.unwrap().content, "This is a small file content");
assert_eq!(result.unwrap(), "This is a small file content");
}
#[gpui::test]
@@ -301,7 +277,6 @@ mod test {
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(Arc::new(rust_lang()));
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let model = Arc::new(FakeLanguageModel::default());
let result = cx
.update(|cx| {
@@ -309,21 +284,13 @@ mod test {
"path": "root/large_file.rs"
});
Arc::new(ReadFileTool)
.run(
input,
Arc::default(),
project.clone(),
action_log.clone(),
model.clone(),
None,
cx,
)
.run(input, &[], project.clone(), action_log.clone(), None, cx)
.output
})
.await;
let content = result.unwrap();
assert_eq!(
content.lines().skip(4).take(6).collect::<Vec<_>>(),
content.lines().skip(2).take(6).collect::<Vec<_>>(),
vec![
"struct Test0 [L1-4]",
" a [L2]",
@@ -341,15 +308,7 @@ mod test {
"offset": 1
});
Arc::new(ReadFileTool)
.run(
input,
Arc::default(),
project.clone(),
action_log,
model,
None,
cx,
)
.run(input, &[], project.clone(), action_log, None, cx)
.output
})
.await;
@@ -366,7 +325,7 @@ mod test {
pretty_assertions::assert_eq!(
content
.lines()
.skip(4)
.skip(2)
.take(expected_content.len())
.collect::<Vec<_>>(),
expected_content
@@ -387,7 +346,6 @@ mod test {
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let model = Arc::new(FakeLanguageModel::default());
let result = cx
.update(|cx| {
let input = json!({
@@ -396,19 +354,11 @@ mod test {
"end_line": 4
});
Arc::new(ReadFileTool)
.run(
input,
Arc::default(),
project.clone(),
action_log,
model,
None,
cx,
)
.run(input, &[], project.clone(), action_log, None, cx)
.output
})
.await;
assert_eq!(result.unwrap().content, "Line 2\nLine 3\nLine 4");
assert_eq!(result.unwrap(), "Line 2\nLine 3\nLine 4");
}
#[gpui::test]
@@ -425,7 +375,6 @@ mod test {
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let model = Arc::new(FakeLanguageModel::default());
// start_line of 0 should be treated as 1
let result = cx
@@ -436,19 +385,11 @@ mod test {
"end_line": 2
});
Arc::new(ReadFileTool)
.run(
input,
Arc::default(),
project.clone(),
action_log.clone(),
model.clone(),
None,
cx,
)
.run(input, &[], project.clone(), action_log.clone(), None, cx)
.output
})
.await;
assert_eq!(result.unwrap().content, "Line 1\nLine 2");
assert_eq!(result.unwrap(), "Line 1\nLine 2");
// end_line of 0 should result in at least 1 line
let result = cx
@@ -459,19 +400,11 @@ mod test {
"end_line": 0
});
Arc::new(ReadFileTool)
.run(
input,
Arc::default(),
project.clone(),
action_log.clone(),
model.clone(),
None,
cx,
)
.run(input, &[], project.clone(), action_log.clone(), None, cx)
.output
})
.await;
assert_eq!(result.unwrap().content, "Line 1");
assert_eq!(result.unwrap(), "Line 1");
// when start_line > end_line, should still return at least 1 line
let result = cx
@@ -482,19 +415,11 @@ mod test {
"end_line": 2
});
Arc::new(ReadFileTool)
.run(
input,
Arc::default(),
project.clone(),
action_log,
model,
None,
cx,
)
.run(input, &[], project.clone(), action_log, None, cx)
.output
})
.await;
assert_eq!(result.unwrap().content, "Line 3");
assert_eq!(result.unwrap(), "Line 3");
}
fn init_test(cx: &mut TestAppContext) {

View File

@@ -0,0 +1,872 @@
use language::{BufferSnapshot, Diff, Point, ToOffset};
use project::search::SearchQuery;
use std::iter;
use util::{ResultExt as _, paths::PathMatcher};
/// Performs an exact string replacement in a buffer, requiring precise character-for-character matching.
/// Uses the search functionality to locate the first occurrence of the exact string.
/// Returns None if no exact match is found in the buffer.
pub async fn replace_exact(old: &str, new: &str, snapshot: &BufferSnapshot) -> Option<Diff> {
let query = SearchQuery::text(
old,
false,
true,
true,
PathMatcher::new(iter::empty::<&str>()).ok()?,
PathMatcher::new(iter::empty::<&str>()).ok()?,
false,
None,
)
.log_err()?;
let matches = query.search(&snapshot, None).await;
if matches.is_empty() {
return None;
}
let edit_range = matches[0].clone();
let diff = language::text_diff(&old, &new);
let edits = diff
.into_iter()
.map(|(old_range, text)| {
let start = edit_range.start + old_range.start;
let end = edit_range.start + old_range.end;
(start..end, text)
})
.collect::<Vec<_>>();
let diff = language::Diff {
base_version: snapshot.version().clone(),
line_ending: snapshot.line_ending(),
edits,
};
Some(diff)
}
/// Performs a replacement that's indentation-aware - matches text content ignoring leading whitespace differences.
/// When replacing, preserves the indentation level found in the buffer at each matching line.
/// Returns None if no match found or if indentation is offset inconsistently across matched lines.
pub fn replace_with_flexible_indent(old: &str, new: &str, buffer: &BufferSnapshot) -> Option<Diff> {
let (old_lines, old_min_indent) = lines_with_min_indent(old);
let (new_lines, new_min_indent) = lines_with_min_indent(new);
let min_indent = old_min_indent.min(new_min_indent);
let old_lines = drop_lines_prefix(&old_lines, min_indent);
let new_lines = drop_lines_prefix(&new_lines, min_indent);
let max_row = buffer.max_point().row;
'windows: for start_row in 0..max_row + 1 {
let end_row = start_row + old_lines.len().saturating_sub(1) as u32;
if end_row > max_row {
// The buffer ends before fully matching the pattern
return None;
}
let start_point = Point::new(start_row, 0);
let end_point = Point::new(end_row, buffer.line_len(end_row));
let range = start_point.to_offset(buffer)..end_point.to_offset(buffer);
let window_text = buffer.text_for_range(range.clone());
let mut window_lines = window_text.lines();
let mut old_lines_iter = old_lines.iter();
let mut common_mismatch = None;
#[derive(Eq, PartialEq)]
enum Mismatch {
OverIndented(String),
UnderIndented(String),
}
while let (Some(window_line), Some(old_line)) = (window_lines.next(), old_lines_iter.next())
{
let line_trimmed = window_line.trim_start();
if line_trimmed != old_line.trim_start() {
continue 'windows;
}
if line_trimmed.is_empty() {
continue;
}
let line_mismatch = if window_line.len() > old_line.len() {
let prefix = window_line[..window_line.len() - old_line.len()].to_string();
Mismatch::UnderIndented(prefix)
} else {
let prefix = old_line[..old_line.len() - window_line.len()].to_string();
Mismatch::OverIndented(prefix)
};
match &common_mismatch {
Some(common_mismatch) if common_mismatch != &line_mismatch => {
continue 'windows;
}
Some(_) => (),
None => common_mismatch = Some(line_mismatch),
}
}
if let Some(common_mismatch) = &common_mismatch {
let line_ending = buffer.line_ending();
let replacement = new_lines
.iter()
.map(|new_line| {
if new_line.trim().is_empty() {
new_line.to_string()
} else {
match common_mismatch {
Mismatch::UnderIndented(prefix) => prefix.to_string() + new_line,
Mismatch::OverIndented(prefix) => new_line
.strip_prefix(prefix)
.unwrap_or(new_line)
.to_string(),
}
}
})
.collect::<Vec<_>>()
.join(line_ending.as_str());
let diff = Diff {
base_version: buffer.version().clone(),
line_ending,
edits: vec![(range, replacement.into())],
};
return Some(diff);
}
}
None
}
fn drop_lines_prefix<'a>(lines: &'a [&str], prefix_len: usize) -> Vec<&'a str> {
lines
.iter()
.map(|line| line.get(prefix_len..).unwrap_or(""))
.collect()
}
fn lines_with_min_indent(input: &str) -> (Vec<&str>, usize) {
let mut lines = Vec::new();
let mut min_indent: Option<usize> = None;
for line in input.lines() {
lines.push(line);
if !line.trim().is_empty() {
let indent = line.len() - line.trim_start().len();
min_indent = Some(min_indent.map_or(indent, |m| m.min(indent)));
}
}
(lines, min_indent.unwrap_or(0))
}
#[cfg(test)]
mod replace_exact_tests {
use super::*;
use gpui::TestAppContext;
use gpui::prelude::*;
#[gpui::test]
async fn basic(cx: &mut TestAppContext) {
let result = test_replace_exact(cx, "let x = 41;", "let x = 41;", "let x = 42;").await;
assert_eq!(result, Some("let x = 42;".to_string()));
}
#[gpui::test]
async fn no_match(cx: &mut TestAppContext) {
let result = test_replace_exact(cx, "let x = 41;", "let y = 42;", "let y = 43;").await;
assert_eq!(result, None);
}
#[gpui::test]
async fn multi_line(cx: &mut TestAppContext) {
let whole = "fn example() {\n let x = 41;\n println!(\"x = {}\", x);\n}";
let old_text = " let x = 41;\n println!(\"x = {}\", x);";
let new_text = " let x = 42;\n println!(\"x = {}\", x);";
let result = test_replace_exact(cx, whole, old_text, new_text).await;
assert_eq!(
result,
Some("fn example() {\n let x = 42;\n println!(\"x = {}\", x);\n}".to_string())
);
}
#[gpui::test]
async fn multiple_occurrences(cx: &mut TestAppContext) {
let whole = "let x = 41;\nlet y = 41;\nlet z = 41;";
let result = test_replace_exact(cx, whole, "let x = 41;", "let x = 42;").await;
assert_eq!(
result,
Some("let x = 42;\nlet y = 41;\nlet z = 41;".to_string())
);
}
#[gpui::test]
async fn empty_buffer(cx: &mut TestAppContext) {
let result = test_replace_exact(cx, "", "let x = 41;", "let x = 42;").await;
assert_eq!(result, None);
}
#[gpui::test]
async fn partial_match(cx: &mut TestAppContext) {
let whole = "let x = 41; let y = 42;";
let result = test_replace_exact(cx, whole, "let x = 41", "let x = 42").await;
assert_eq!(result, Some("let x = 42; let y = 42;".to_string()));
}
#[gpui::test]
async fn whitespace_sensitive(cx: &mut TestAppContext) {
let result = test_replace_exact(cx, "let x = 41;", " let x = 41;", "let x = 42;").await;
assert_eq!(result, None);
}
#[gpui::test]
async fn entire_buffer(cx: &mut TestAppContext) {
let result = test_replace_exact(cx, "let x = 41;", "let x = 41;", "let x = 42;").await;
assert_eq!(result, Some("let x = 42;".to_string()));
}
async fn test_replace_exact(
cx: &mut TestAppContext,
whole: &str,
old: &str,
new: &str,
) -> Option<String> {
let buffer = cx.new(|cx| language::Buffer::local(whole, cx));
let buffer_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
let diff = replace_exact(old, new, &buffer_snapshot).await;
diff.map(|diff| {
buffer.update(cx, |buffer, cx| {
let _ = buffer.apply_diff(diff, cx);
buffer.text()
})
})
}
}
#[cfg(test)]
mod flexible_indent_tests {
use super::*;
use gpui::TestAppContext;
use gpui::prelude::*;
use unindent::Unindent;
#[gpui::test]
fn test_underindented_single_line(cx: &mut TestAppContext) {
let cur = " let a = 41;".to_string();
let old = " let a = 41;".to_string();
let new = " let a = 42;".to_string();
let exp = " let a = 42;".to_string();
let result = test_replace_with_flexible_indent(cx, &cur, &old, &new);
assert_eq!(result, Some(exp.to_string()))
}
#[gpui::test]
fn test_overindented_single_line(cx: &mut TestAppContext) {
let cur = " let a = 41;".to_string();
let old = " let a = 41;".to_string();
let new = " let a = 42;".to_string();
let exp = " let a = 42;".to_string();
let result = test_replace_with_flexible_indent(cx, &cur, &old, &new);
assert_eq!(result, Some(exp.to_string()))
}
#[gpui::test]
fn test_underindented_multi_line(cx: &mut TestAppContext) {
let whole = r#"
fn test() {
let x = 5;
println!("x = {}", x);
let y = 10;
}
"#
.unindent();
let old = r#"
let x = 5;
println!("x = {}", x);
"#
.unindent();
let new = r#"
let x = 42;
println!("New value: {}", x);
"#
.unindent();
let expected = r#"
fn test() {
let x = 42;
println!("New value: {}", x);
let y = 10;
}
"#
.unindent();
assert_eq!(
test_replace_with_flexible_indent(cx, &whole, &old, &new),
Some(expected.to_string())
);
}
#[gpui::test]
fn test_overindented_multi_line(cx: &mut TestAppContext) {
let cur = r#"
fn foo() {
let a = 41;
let b = 3.13;
}
"#
.unindent();
// 6 space indent instead of 4
let old = " let a = 41;\n let b = 3.13;";
let new = " let a = 42;\n let b = 3.14;";
let expected = r#"
fn foo() {
let a = 42;
let b = 3.14;
}
"#
.unindent();
let result = test_replace_with_flexible_indent(cx, &cur, &old, &new);
assert_eq!(result, Some(expected.to_string()))
}
#[gpui::test]
fn test_replace_inconsistent_indentation(cx: &mut TestAppContext) {
let whole = r#"
fn test() {
if condition {
println!("{}", 43);
}
}
"#
.unindent();
let old = r#"
if condition {
println!("{}", 43);
"#
.unindent();
let new = r#"
if condition {
println!("{}", 42);
"#
.unindent();
assert_eq!(
test_replace_with_flexible_indent(cx, &whole, &old, &new),
None
);
}
#[gpui::test]
fn test_replace_with_empty_lines(cx: &mut TestAppContext) {
// Test with empty lines
let whole = r#"
fn test() {
let x = 5;
println!("x = {}", x);
}
"#
.unindent();
let old = r#"
let x = 5;
println!("x = {}", x);
"#
.unindent();
let new = r#"
let x = 10;
println!("New x: {}", x);
"#
.unindent();
let expected = r#"
fn test() {
let x = 10;
println!("New x: {}", x);
}
"#
.unindent();
assert_eq!(
test_replace_with_flexible_indent(cx, &whole, &old, &new),
Some(expected.to_string())
);
}
#[gpui::test]
fn test_replace_no_match(cx: &mut TestAppContext) {
let whole = r#"
fn test() {
let x = 5;
}
"#
.unindent();
let old = r#"
let y = 10;
"#
.unindent();
let new = r#"
let y = 20;
"#
.unindent();
assert_eq!(
test_replace_with_flexible_indent(cx, &whole, &old, &new),
None
);
}
#[gpui::test]
fn test_replace_whole_ends_before_matching_old(cx: &mut TestAppContext) {
let whole = r#"
fn test() {
let x = 5;
"#
.unindent();
let old = r#"
let x = 5;
println!("x = {}", x);
"#
.unindent();
let new = r#"
let x = 10;
println!("x = {}", x);
"#
.unindent();
// Should return None because whole doesn't fully contain the old text
assert_eq!(
test_replace_with_flexible_indent(cx, &whole, &old, &new),
None
);
}
#[gpui::test]
fn test_replace_whole_is_shorter_than_old(cx: &mut TestAppContext) {
let whole = r#"
let x = 5;
"#
.unindent();
let old = r#"
let x = 5;
let y = 10;
"#
.unindent();
let new = r#"
let x = 5;
let y = 20;
"#
.unindent();
assert_eq!(
test_replace_with_flexible_indent(cx, &whole, &old, &new),
None
);
}
#[gpui::test]
fn test_replace_old_is_empty(cx: &mut TestAppContext) {
let whole = r#"
fn test() {
let x = 5;
}
"#
.unindent();
let old = "";
let new = r#"
let y = 10;
"#
.unindent();
assert_eq!(
test_replace_with_flexible_indent(cx, &whole, &old, &new),
None
);
}
#[gpui::test]
fn test_replace_whole_is_empty(cx: &mut TestAppContext) {
let whole = "";
let old = r#"
let x = 5;
"#
.unindent();
let new = r#"
let x = 10;
"#
.unindent();
assert_eq!(
test_replace_with_flexible_indent(cx, &whole, &old, &new),
None
);
}
#[test]
fn test_lines_with_min_indent() {
// Empty string
assert_eq!(lines_with_min_indent(""), (vec![], 0));
// Single line without indentation
assert_eq!(lines_with_min_indent("hello"), (vec!["hello"], 0));
// Multiple lines with no indentation
assert_eq!(
lines_with_min_indent("line1\nline2\nline3"),
(vec!["line1", "line2", "line3"], 0)
);
// Multiple lines with consistent indentation
assert_eq!(
lines_with_min_indent(" line1\n line2\n line3"),
(vec![" line1", " line2", " line3"], 2)
);
// Multiple lines with varying indentation
assert_eq!(
lines_with_min_indent(" line1\n line2\n line3"),
(vec![" line1", " line2", " line3"], 2)
);
// Lines with mixed indentation and empty lines
assert_eq!(
lines_with_min_indent(" line1\n\n line2"),
(vec![" line1", "", " line2"], 2)
);
}
#[gpui::test]
fn test_replace_with_missing_indent_uneven_match(cx: &mut TestAppContext) {
let whole = r#"
fn test() {
if true {
let x = 5;
println!("x = {}", x);
}
}
"#
.unindent();
let old = r#"
let x = 5;
println!("x = {}", x);
"#
.unindent();
let new = r#"
let x = 42;
println!("x = {}", x);
"#
.unindent();
let expected = r#"
fn test() {
if true {
let x = 42;
println!("x = {}", x);
}
}
"#
.unindent();
assert_eq!(
test_replace_with_flexible_indent(cx, &whole, &old, &new),
Some(expected.to_string())
);
}
#[gpui::test]
fn test_replace_big_example(cx: &mut TestAppContext) {
let whole = r#"
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_valid_age() {
assert!(is_valid_age(0));
assert!(!is_valid_age(151));
}
}
"#
.unindent();
let old = r#"
#[test]
fn test_is_valid_age() {
assert!(is_valid_age(0));
assert!(!is_valid_age(151));
}
"#
.unindent();
let new = r#"
#[test]
fn test_is_valid_age() {
assert!(is_valid_age(0));
assert!(!is_valid_age(151));
}
#[test]
fn test_group_people_by_age() {
let people = vec![
Person::new("Young One", 5, "young@example.com").unwrap(),
Person::new("Teen One", 15, "teen@example.com").unwrap(),
Person::new("Teen Two", 18, "teen2@example.com").unwrap(),
Person::new("Adult One", 25, "adult@example.com").unwrap(),
];
let groups = group_people_by_age(&people);
assert_eq!(groups.get(&0).unwrap().len(), 1); // One person in 0-9
assert_eq!(groups.get(&10).unwrap().len(), 2); // Two people in 10-19
assert_eq!(groups.get(&20).unwrap().len(), 1); // One person in 20-29
}
"#
.unindent();
let expected = r#"
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_valid_age() {
assert!(is_valid_age(0));
assert!(!is_valid_age(151));
}
#[test]
fn test_group_people_by_age() {
let people = vec![
Person::new("Young One", 5, "young@example.com").unwrap(),
Person::new("Teen One", 15, "teen@example.com").unwrap(),
Person::new("Teen Two", 18, "teen2@example.com").unwrap(),
Person::new("Adult One", 25, "adult@example.com").unwrap(),
];
let groups = group_people_by_age(&people);
assert_eq!(groups.get(&0).unwrap().len(), 1); // One person in 0-9
assert_eq!(groups.get(&10).unwrap().len(), 2); // Two people in 10-19
assert_eq!(groups.get(&20).unwrap().len(), 1); // One person in 20-29
}
}
"#
.unindent();
assert_eq!(
test_replace_with_flexible_indent(cx, &whole, &old, &new),
Some(expected.to_string())
);
}
#[test]
fn test_drop_lines_prefix() {
// Empty array
assert_eq!(drop_lines_prefix(&[], 2), Vec::<&str>::new());
// Zero prefix length
assert_eq!(
drop_lines_prefix(&["line1", "line2"], 0),
vec!["line1", "line2"]
);
// Normal prefix drop
assert_eq!(
drop_lines_prefix(&[" line1", " line2"], 2),
vec!["line1", "line2"]
);
// Prefix longer than some lines
assert_eq!(drop_lines_prefix(&[" line1", "a"], 2), vec!["line1", ""]);
// Prefix longer than all lines
assert_eq!(drop_lines_prefix(&["a", "b"], 5), vec!["", ""]);
// Mixed length lines
assert_eq!(
drop_lines_prefix(&[" line1", " line2", " line3"], 2),
vec![" line1", "line2", " line3"]
);
}
#[gpui::test]
async fn test_replace_exact_basic(cx: &mut TestAppContext) {
let buffer = cx.new(|cx| language::Buffer::local("let x = 41;", cx));
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
let diff = replace_exact("let x = 41;", "let x = 42;", &snapshot).await;
assert!(diff.is_some());
let diff = diff.unwrap();
assert_eq!(diff.edits.len(), 1);
let result = buffer.update(cx, |buffer, cx| {
let _ = buffer.apply_diff(diff, cx);
buffer.text()
});
assert_eq!(result, "let x = 42;");
}
#[gpui::test]
async fn test_replace_exact_no_match(cx: &mut TestAppContext) {
let buffer = cx.new(|cx| language::Buffer::local("let x = 41;", cx));
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
let diff = replace_exact("let y = 42;", "let y = 43;", &snapshot).await;
assert!(diff.is_none());
}
#[gpui::test]
async fn test_replace_exact_multi_line(cx: &mut TestAppContext) {
let buffer = cx.new(|cx| {
language::Buffer::local(
"fn example() {\n let x = 41;\n println!(\"x = {}\", x);\n}",
cx,
)
});
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
let old_text = " let x = 41;\n println!(\"x = {}\", x);";
let new_text = " let x = 42;\n println!(\"x = {}\", x);";
let diff = replace_exact(old_text, new_text, &snapshot).await;
assert!(diff.is_some());
let diff = diff.unwrap();
let result = buffer.update(cx, |buffer, cx| {
let _ = buffer.apply_diff(diff, cx);
buffer.text()
});
assert_eq!(
result,
"fn example() {\n let x = 42;\n println!(\"x = {}\", x);\n}"
);
}
#[gpui::test]
async fn test_replace_exact_multiple_occurrences(cx: &mut TestAppContext) {
let buffer =
cx.new(|cx| language::Buffer::local("let x = 41;\nlet y = 41;\nlet z = 41;", cx));
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
// Should replace only the first occurrence
let diff = replace_exact("let x = 41;", "let x = 42;", &snapshot).await;
assert!(diff.is_some());
let diff = diff.unwrap();
let result = buffer.update(cx, |buffer, cx| {
let _ = buffer.apply_diff(diff, cx);
buffer.text()
});
assert_eq!(result, "let x = 42;\nlet y = 41;\nlet z = 41;");
}
#[gpui::test]
async fn test_replace_exact_empty_buffer(cx: &mut TestAppContext) {
let buffer = cx.new(|cx| language::Buffer::local("", cx));
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
let diff = replace_exact("let x = 41;", "let x = 42;", &snapshot).await;
assert!(diff.is_none());
}
#[gpui::test]
async fn test_replace_exact_partial_match(cx: &mut TestAppContext) {
let buffer = cx.new(|cx| language::Buffer::local("let x = 41; let y = 42;", cx));
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
// Verify substring replacement actually works
let diff = replace_exact("let x = 41", "let x = 42", &snapshot).await;
assert!(diff.is_some());
let diff = diff.unwrap();
let result = buffer.update(cx, |buffer, cx| {
let _ = buffer.apply_diff(diff, cx);
buffer.text()
});
assert_eq!(result, "let x = 42; let y = 42;");
}
#[gpui::test]
async fn test_replace_exact_whitespace_sensitive(cx: &mut TestAppContext) {
let buffer = cx.new(|cx| language::Buffer::local("let x = 41;", cx));
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
let diff = replace_exact(" let x = 41;", "let x = 42;", &snapshot).await;
assert!(diff.is_none());
}
#[gpui::test]
async fn test_replace_exact_entire_buffer(cx: &mut TestAppContext) {
let buffer = cx.new(|cx| language::Buffer::local("let x = 41;", cx));
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
let diff = replace_exact("let x = 41;", "let x = 42;", &snapshot).await;
assert!(diff.is_some());
let diff = diff.unwrap();
let result = buffer.update(cx, |buffer, cx| {
let _ = buffer.apply_diff(diff, cx);
buffer.text()
});
assert_eq!(result, "let x = 42;");
}
fn test_replace_with_flexible_indent(
cx: &mut TestAppContext,
whole: &str,
old: &str,
new: &str,
) -> Option<String> {
// Create a local buffer with the test content
let buffer = cx.new(|cx| language::Buffer::local(whole, cx));
// Get the buffer snapshot
let buffer_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
// Call replace_flexible and transform the result
replace_with_flexible_indent(old, new, &buffer_snapshot).map(|diff| {
buffer.update(cx, |buffer, cx| {
let _ = buffer.apply_diff(diff, cx);
buffer.text()
})
})
}
}

View File

@@ -0,0 +1,355 @@
use crate::{
Templates,
edit_agent::{EditAgent, EditAgentOutputEvent},
edit_file_tool::EditFileToolCard,
schema::json_schema_for,
};
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolResult};
use futures::StreamExt;
use gpui::{AnyWindowHandle, App, AppContext, AsyncApp, Entity, Task};
use indoc::formatdoc;
use language_model::{
LanguageModelRegistry, LanguageModelRequestMessage, LanguageModelToolSchemaFormat,
};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{path::PathBuf, sync::Arc};
use ui::prelude::*;
use util::ResultExt;
pub struct StreamingEditFileTool;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct StreamingEditFileToolInput {
/// A one-line, user-friendly markdown description of the edit. This will be
/// shown in the UI and also passed to another model to perform the edit.
///
/// Be terse, but also descriptive in what you want to achieve with this
/// edit. Avoid generic instructions.
///
/// NEVER mention the file path in this description.
///
/// <example>Fix API endpoint URLs</example>
/// <example>Update copyright year in `page_footer`</example>
///
/// Make sure to include this field before all the others in the input object
/// so that we can display it immediately.
pub display_description: String,
/// The full path of the file to create or modify in the project.
///
/// WARNING: When specifying which file path need changing, you MUST
/// start each path with one of the project's root directories.
///
/// The following examples assume we have two root directories in the project:
/// - backend
/// - frontend
///
/// <example>
/// `backend/src/main.rs`
///
/// Notice how the file path starts with root-1. Without that, the path
/// would be ambiguous and the call would fail!
/// </example>
///
/// <example>
/// `frontend/db.js`
/// </example>
pub path: PathBuf,
/// If true, this tool will recreate the file from scratch.
/// If false, this tool will produce granular edits to an existing file.
///
/// When a file already exists or you just created it, always prefer editing
/// it as opposed to recreating it from scratch.
pub create_or_overwrite: bool,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct PartialInput {
#[serde(default)]
path: String,
#[serde(default)]
display_description: String,
}
const DEFAULT_UI_TEXT: &str = "Editing file";
impl Tool for StreamingEditFileTool {
fn name(&self) -> String {
"edit_file".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
false
}
fn description(&self) -> String {
include_str!("streaming_edit_file_tool/description.md").to_string()
}
fn icon(&self) -> IconName {
IconName::Pencil
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
json_schema_for::<StreamingEditFileToolInput>(format)
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<StreamingEditFileToolInput>(input.clone()) {
Ok(input) => input.display_description,
Err(_) => "Editing file".to_string(),
}
}
fn still_streaming_ui_text(&self, input: &serde_json::Value) -> String {
if let Some(input) = serde_json::from_value::<PartialInput>(input.clone()).ok() {
let description = input.display_description.trim();
if !description.is_empty() {
return description.to_string();
}
let path = input.path.trim();
if !path.is_empty() {
return path.to_string();
}
}
DEFAULT_UI_TEXT.to_string()
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<StreamingEditFileToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
return Task::ready(Err(anyhow!(
"Path {} not found in project",
input.path.display()
)))
.into();
};
let Some(worktree) = project
.read(cx)
.worktree_for_id(project_path.worktree_id, cx)
else {
return Task::ready(Err(anyhow!("Worktree not found for project path"))).into();
};
let exists = worktree.update(cx, |worktree, cx| {
worktree.file_exists(&project_path.path, cx)
});
let card = window.and_then(|window| {
window
.update(cx, |_, window, cx| {
cx.new(|cx| {
EditFileToolCard::new(input.path.clone(), project.clone(), window, cx)
})
})
.ok()
});
let card_clone = card.clone();
let messages = messages.to_vec();
let task = cx.spawn(async move |cx: &mut AsyncApp| {
if !input.create_or_overwrite && !exists.await? {
return Err(anyhow!("{} not found", input.path.display()));
}
let model = cx
.update(|cx| LanguageModelRegistry::read_global(cx).default_model())?
.context("default model not set")?
.model;
let edit_agent = EditAgent::new(model, project.clone(), action_log, Templates::new());
let buffer = project
.update(cx, |project, cx| {
project.open_buffer(project_path.clone(), cx)
})?
.await?;
let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
let old_text = cx
.background_spawn({
let old_snapshot = old_snapshot.clone();
async move { old_snapshot.text() }
})
.await;
let (output, mut events) = if input.create_or_overwrite {
edit_agent.overwrite(
buffer.clone(),
input.display_description.clone(),
messages,
cx,
)
} else {
edit_agent.edit(
buffer.clone(),
input.display_description.clone(),
messages,
cx,
)
};
let mut hallucinated_old_text = false;
while let Some(event) = events.next().await {
match event {
EditAgentOutputEvent::Edited => {
if let Some(card) = card_clone.as_ref() {
let new_snapshot =
buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
let new_text = cx
.background_spawn({
let new_snapshot = new_snapshot.clone();
async move { new_snapshot.text() }
})
.await;
card.update(cx, |card, cx| {
card.set_diff(
project_path.path.clone(),
old_text.clone(),
new_text,
cx,
);
})
.log_err();
}
}
EditAgentOutputEvent::OldTextNotFound(_) => hallucinated_old_text = true,
}
}
output.await?;
project
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
.await?;
let new_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
let new_text = cx.background_spawn({
let new_snapshot = new_snapshot.clone();
async move { new_snapshot.text() }
});
let diff = cx.background_spawn(async move {
language::unified_diff(&old_snapshot.text(), &new_snapshot.text())
});
let (new_text, diff) = futures::join!(new_text, diff);
if let Some(card) = card_clone {
card.update(cx, |card, cx| {
card.set_diff(project_path.path.clone(), old_text, new_text, cx);
})
.log_err();
}
let input_path = input.path.display();
if diff.is_empty() {
if hallucinated_old_text {
Err(anyhow!(formatdoc! {"
Some edits were produced but none of them could be applied.
Read the relevant sections of {input_path} again so that
I can perform the requested edits.
"}))
} else {
Ok("No edits were made.".to_string())
}
} else {
Ok(format!("Edited {}:\n\n```diff\n{}\n```", input_path, diff))
}
});
ToolResult {
output: task,
card: card.map(AnyToolCard::from),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn still_streaming_ui_text_with_path() {
let input = json!({
"path": "src/main.rs",
"display_description": "",
"old_string": "old code",
"new_string": "new code"
});
assert_eq!(
StreamingEditFileTool.still_streaming_ui_text(&input),
"src/main.rs"
);
}
#[test]
fn still_streaming_ui_text_with_description() {
let input = json!({
"path": "",
"display_description": "Fix error handling",
"old_string": "old code",
"new_string": "new code"
});
assert_eq!(
StreamingEditFileTool.still_streaming_ui_text(&input),
"Fix error handling",
);
}
#[test]
fn still_streaming_ui_text_with_path_and_description() {
let input = json!({
"path": "src/main.rs",
"display_description": "Fix error handling",
"old_string": "old code",
"new_string": "new code"
});
assert_eq!(
StreamingEditFileTool.still_streaming_ui_text(&input),
"Fix error handling",
);
}
#[test]
fn still_streaming_ui_text_no_path_or_description() {
let input = json!({
"path": "",
"display_description": "",
"old_string": "old code",
"new_string": "new code"
});
assert_eq!(
StreamingEditFileTool.still_streaming_ui_text(&input),
DEFAULT_UI_TEXT,
);
}
#[test]
fn still_streaming_ui_text_with_null() {
let input = serde_json::Value::Null;
assert_eq!(
StreamingEditFileTool.still_streaming_ui_text(&input),
DEFAULT_UI_TEXT,
);
}
}

View File

@@ -0,0 +1,8 @@
This is a tool for creating a new file or editing an existing file. For moving or renaming files, you should generally use the `terminal` tool with the 'mv' command instead.
Before using this tool:
1. Use the `read_file` tool to understand the file's contents and context
2. Verify the directory path is correct (only applicable when creating new files):
- Use the `list_directory` tool to verify the parent directory exists and is the correct location

View File

@@ -1,4 +1,6 @@
You MUST respond with a series of edits to a file, using the following format:
You are an expert text editor and your task is to produce a series of edits to a file given a description of the changes you need to make.
You MUST respond with a series of edits to that one file in the following format:
```
<edits>
@@ -49,5 +51,3 @@ Rules for editing:
<edit_description>
{{edit_description}}
</edit_description>
Tool calls have been disabled. You MUST start your response with <edits>.

View File

@@ -2,18 +2,13 @@ use crate::schema::json_schema_for;
use anyhow::{Context as _, Result, anyhow, bail};
use assistant_tool::{ActionLog, Tool, ToolCard, ToolResult, ToolUseStatus};
use futures::{FutureExt as _, future::Shared};
use gpui::{
AnyWindowHandle, App, AppContext, Empty, Entity, EntityId, Task, TextStyleRefinement,
WeakEntity, Window,
};
use gpui::{AnyWindowHandle, App, AppContext, Empty, Entity, EntityId, Task, WeakEntity, Window};
use language::LineEnding;
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use markdown::{Markdown, MarkdownElement, MarkdownStyle};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use portable_pty::{CommandBuilder, PtySize, native_pty_system};
use project::{Project, terminals::TerminalKind};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
use std::{
env,
path::{Path, PathBuf},
@@ -22,7 +17,6 @@ use std::{
time::{Duration, Instant},
};
use terminal_view::TerminalView;
use theme::ThemeSettings;
use ui::{Disclosure, Tooltip, prelude::*};
use util::{
get_system_shell, markdown::MarkdownInlineCode, size::format_file_size,
@@ -113,10 +107,9 @@ impl Tool for TerminalTool {
fn run(
self: Arc<Self>,
input: serde_json::Value,
_request: Arc<LanguageModelRequest>,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
_model: Arc<dyn LanguageModel>,
window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
@@ -185,7 +178,7 @@ impl Tool for TerminalTool {
let exit_status = child.wait()?;
let (processed_content, _) =
process_content(content, &input.command, Some(exit_status));
Ok(processed_content.into())
Ok(processed_content)
});
return ToolResult {
output: task,
@@ -217,21 +210,8 @@ impl Tool for TerminalTool {
}
});
let command_markdown = cx.new(|cx| {
Markdown::new(
format!("```bash\n{}\n```", input.command).into(),
None,
None,
cx,
)
});
let card = cx.new(|cx| {
TerminalToolCard::new(
command_markdown.clone(),
working_dir.clone(),
cx.entity_id(),
)
TerminalToolCard::new(input.command.clone(), working_dir.clone(), cx.entity_id())
});
let output = cx.spawn({
@@ -286,7 +266,7 @@ impl Tool for TerminalTool {
card.elapsed_time = Some(card.start_instant.elapsed());
});
Ok(processed_content.into())
Ok(processed_content)
}
});
@@ -407,7 +387,7 @@ fn working_dir(
}
struct TerminalToolCard {
input_command: Entity<Markdown>,
input_command: String,
working_dir: Option<PathBuf>,
entity_id: EntityId,
exit_status: Option<ExitStatus>,
@@ -423,11 +403,7 @@ struct TerminalToolCard {
}
impl TerminalToolCard {
pub fn new(
input_command: Entity<Markdown>,
working_dir: Option<PathBuf>,
entity_id: EntityId,
) -> Self {
pub fn new(input_command: String, working_dir: Option<PathBuf>, entity_id: EntityId) -> Self {
Self {
input_command,
working_dir,
@@ -450,7 +426,7 @@ impl ToolCard for TerminalToolCard {
fn render(
&mut self,
status: &ToolUseStatus,
window: &mut Window,
_window: &mut Window,
_workspace: WeakEntity<Workspace>,
cx: &mut Context<Self>,
) -> impl IntoElement {
@@ -594,25 +570,11 @@ impl ToolCard for TerminalToolCard {
.rounded_lg()
.overflow_hidden()
.child(
v_flex()
.p_2()
.gap_0p5()
.bg(header_bg)
.text_xs()
.child(header)
.child(
MarkdownElement::new(
self.input_command.clone(),
markdown_style(window, cx),
)
.code_block_renderer(
markdown::CodeBlockRenderer::Default {
copy_button: false,
copy_button_on_hover: true,
border: false,
},
),
),
v_flex().p_2().gap_0p5().bg(header_bg).child(header).child(
Label::new(self.input_command.clone())
.buffer_font(cx)
.size(LabelSize::Small),
),
)
.when(self.preview_expanded && !should_hide_terminal, |this| {
this.child(
@@ -631,33 +593,11 @@ impl ToolCard for TerminalToolCard {
}
}
fn markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
let theme_settings = ThemeSettings::get_global(cx);
let buffer_font_size = TextSize::Default.rems(cx);
let mut text_style = window.text_style();
text_style.refine(&TextStyleRefinement {
font_family: Some(theme_settings.buffer_font.family.clone()),
font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
font_features: Some(theme_settings.buffer_font.features.clone()),
font_size: Some(buffer_font_size.into()),
color: Some(cx.theme().colors().text),
..Default::default()
});
MarkdownStyle {
base_text_style: text_style.clone(),
selection_background_color: cx.theme().players().local().selection,
..Default::default()
}
}
#[cfg(test)]
mod tests {
use editor::EditorSettings;
use fs::RealFs;
use gpui::{BackgroundExecutor, TestAppContext};
use language_model::fake_provider::FakeLanguageModel;
use pretty_assertions::assert_eq;
use serde_json::json;
use settings::{Settings, SettingsStore};
@@ -699,7 +639,6 @@ mod tests {
let project: Entity<Project> =
Project::test(fs, [tree.path().join("project").as_path()], cx).await;
let action_log = cx.update(|cx| cx.new(|_| ActionLog::new(project.clone())));
let model = Arc::new(FakeLanguageModel::default());
let input = TerminalToolInput {
command: "cat".to_owned(),
@@ -714,16 +653,15 @@ mod tests {
TerminalTool::run(
Arc::new(TerminalTool::new(cx)),
serde_json::to_value(input).unwrap(),
Arc::default(),
&[],
project.clone(),
action_log.clone(),
model,
None,
cx,
)
});
let output = result.output.await.log_err().map(|output| output.content);
let output = result.output.await.log_err();
assert_eq!(output, Some("Command executed successfully.".into()));
}
@@ -743,25 +681,19 @@ mod tests {
let project: Entity<Project> =
Project::test(fs, [tree.path().join("project").as_path()], cx).await;
let action_log = cx.update(|cx| cx.new(|_| ActionLog::new(project.clone())));
let model = Arc::new(FakeLanguageModel::default());
let check = |input, expected, cx: &mut App| {
let headless_result = TerminalTool::run(
Arc::new(TerminalTool::new(cx)),
serde_json::to_value(input).unwrap(),
Arc::default(),
&[],
project.clone(),
action_log.clone(),
model.clone(),
None,
cx,
);
cx.spawn(async move |_| {
let output = headless_result
.output
.await
.log_err()
.map(|output| output.content);
let output = headless_result.output.await.log_err();
assert_eq!(output, expected);
})
};

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