Compare commits
85 Commits
test-branc
...
fix-termin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd76b3b64d | ||
|
|
d1db6d6782 | ||
|
|
5509e0141a | ||
|
|
8be5ed22f6 | ||
|
|
5343f1cdaf | ||
|
|
8075c2458f | ||
|
|
d0276e6666 | ||
|
|
c729842804 | ||
|
|
715e23a491 | ||
|
|
63f0fda350 | ||
|
|
7984f0f11c | ||
|
|
37ebb47238 | ||
|
|
8546dc101d | ||
|
|
33912011b7 | ||
|
|
dce824f095 | ||
|
|
a1bef28da3 | ||
|
|
8a212be0b1 | ||
|
|
9bbb1e5476 | ||
|
|
50ad71a630 | ||
|
|
76c46c5bab | ||
|
|
8f0bacddd8 | ||
|
|
051483200d | ||
|
|
27cafe5567 | ||
|
|
ddc102c7e0 | ||
|
|
d517a212dc | ||
|
|
dc64ec9cc8 | ||
|
|
d50905e000 | ||
|
|
0729d24d77 | ||
|
|
d51aa2ffb0 | ||
|
|
d40b49ceb9 | ||
|
|
edf712d45b | ||
|
|
627ae7af6f | ||
|
|
17aecfde6f | ||
|
|
01a2c8eb01 | ||
|
|
fc269dfaf9 | ||
|
|
e1e8c1786e | ||
|
|
c8a9a74e6a | ||
|
|
f6d58f76e4 | ||
|
|
e6c64ebf7e | ||
|
|
12c58d01bb | ||
|
|
75689c1c88 | ||
|
|
ca6be249dc | ||
|
|
9b44bacc28 | ||
|
|
9b40770e9f | ||
|
|
5b2adfbb50 | ||
|
|
078b241223 | ||
|
|
e42406f9d5 | ||
|
|
4ee20dda23 | ||
|
|
74dd32d52c | ||
|
|
342acdd080 | ||
|
|
9f8776d1af | ||
|
|
548a8d75e6 | ||
|
|
73f77a7fbb | ||
|
|
4970fe2d56 | ||
|
|
7fe6188f8e | ||
|
|
8add90d7cb | ||
|
|
141a6c3915 | ||
|
|
b4254a33e0 | ||
|
|
f86977e2a7 | ||
|
|
8ecf553279 | ||
|
|
e171d16ae3 | ||
|
|
35da9c0cdc | ||
|
|
8b3eb98d86 | ||
|
|
d912b0dd36 | ||
|
|
044508ef77 | ||
|
|
d63658cee2 | ||
|
|
790b56f52c | ||
|
|
43142000a6 | ||
|
|
3e400a4969 | ||
|
|
e11e7df724 | ||
|
|
bbd1e628f0 | ||
|
|
01b400ea29 | ||
|
|
9445005bff | ||
|
|
5c0adde7bb | ||
|
|
4a5c492188 | ||
|
|
4da987dad4 | ||
|
|
be657aefa3 | ||
|
|
d0ae604eda | ||
|
|
08bb17a7eb | ||
|
|
55c1f9d26c | ||
|
|
28f0ba3381 | ||
|
|
b5dc09c0ca | ||
|
|
e90411efa2 | ||
|
|
fcadcbb510 | ||
|
|
94ed0b7767 |
298
Cargo.lock
generated
298
Cargo.lock
generated
@@ -451,6 +451,7 @@ dependencies = [
|
||||
"assistant_slash_command",
|
||||
"assistant_tool",
|
||||
"async-watch",
|
||||
"buffer_diff",
|
||||
"chrono",
|
||||
"client",
|
||||
"clock",
|
||||
@@ -466,7 +467,6 @@ dependencies = [
|
||||
"futures 0.3.31",
|
||||
"fuzzy",
|
||||
"git",
|
||||
"git_ui",
|
||||
"gpui",
|
||||
"heed",
|
||||
"html_to_markdown",
|
||||
@@ -493,6 +493,7 @@ dependencies = [
|
||||
"rand 0.8.5",
|
||||
"release_channel",
|
||||
"rope",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
@@ -692,6 +693,8 @@ name = "assistant_tool"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-watch",
|
||||
"buffer_diff",
|
||||
"clock",
|
||||
"collections",
|
||||
"derive_more",
|
||||
@@ -703,6 +706,9 @@ dependencies = [
|
||||
"project",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"text",
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -712,6 +718,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"assistant_tool",
|
||||
"chrono",
|
||||
"clock",
|
||||
"collections",
|
||||
"feature_flags",
|
||||
"futures 0.3.31",
|
||||
@@ -721,9 +728,12 @@ dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"language",
|
||||
"language_model",
|
||||
"log",
|
||||
"lsp",
|
||||
"open",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"release_channel",
|
||||
"schemars",
|
||||
"serde",
|
||||
@@ -1179,6 +1189,18 @@ dependencies = [
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "auditable-serde"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c7bf8143dfc3c0258df908843e169b5cc5fcf76c7718bd66135ef4a9cd558c5"
|
||||
dependencies = [
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"topological-sort",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "auto_update"
|
||||
version = "0.1.0"
|
||||
@@ -1900,6 +1922,24 @@ name = "bindgen"
|
||||
version = "0.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools 0.12.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"rustc-hash 1.1.0",
|
||||
"shlex",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bindgen"
|
||||
version = "0.71.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"cexpr",
|
||||
@@ -1910,7 +1950,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"rustc-hash 1.1.0",
|
||||
"rustc-hash 2.1.1",
|
||||
"shlex",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
@@ -1993,7 +2033,7 @@ dependencies = [
|
||||
"ash-window",
|
||||
"bitflags 2.9.0",
|
||||
"bytemuck",
|
||||
"codespan-reporting 0.11.1",
|
||||
"codespan-reporting",
|
||||
"glow",
|
||||
"gpu-alloc",
|
||||
"gpu-alloc-ash",
|
||||
@@ -2279,11 +2319,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bzip2-sys"
|
||||
version = "0.1.13+1.0.8"
|
||||
version = "0.1.11+1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14"
|
||||
checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
@@ -2437,7 +2478,8 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "cargo_metadata"
|
||||
version = "0.19.2"
|
||||
source = "git+https://github.com/zed-industries/cargo_metadata?rev=ce8171bad673923d61a77b6761d0dc4aff63398a#ce8171bad673923d61a77b6761d0dc4aff63398a"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba"
|
||||
dependencies = [
|
||||
"camino",
|
||||
"cargo-platform",
|
||||
@@ -2881,17 +2923,6 @@ dependencies = [
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codespan-reporting"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"termcolor",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "collab"
|
||||
version = "0.44.0"
|
||||
@@ -3838,9 +3869,9 @@ checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991"
|
||||
|
||||
[[package]]
|
||||
name = "cxx"
|
||||
version = "1.0.151"
|
||||
version = "1.0.134"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fdb3e596b379180315d2f934231e233a2fc745041f88231807774093d8de45f2"
|
||||
checksum = "a5a32d755fe20281b46118ee4b507233311fb7a48a0cfd42f554b93640521a2f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cxxbridge-cmd",
|
||||
@@ -3852,12 +3883,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cxx-build"
|
||||
version = "1.0.151"
|
||||
version = "1.0.134"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3743fae7f47620cd34ec23bab819db9ee52da93166a058f87ab0ad99d777dc9b"
|
||||
checksum = "11645536ada5d1c8804312cbffc9ab950f2216154de431de930da47ca6955199"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"codespan-reporting 0.12.0",
|
||||
"codespan-reporting",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"scratch",
|
||||
@@ -3866,12 +3897,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cxxbridge-cmd"
|
||||
version = "1.0.151"
|
||||
version = "1.0.134"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aaea0273c049b126a3918df88a1670c9c0168e0738df9370a988ff69070d4fff"
|
||||
checksum = "ebcc9c78e3c7289665aab921a2b394eaffe8bdb369aa18d81ffc0f534fd49385"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"codespan-reporting 0.12.0",
|
||||
"codespan-reporting",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.100",
|
||||
@@ -3879,15 +3910,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cxxbridge-flags"
|
||||
version = "1.0.151"
|
||||
version = "1.0.134"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "020a9a3d6b792aab7f30f6e323893ad7f45052e572cde5d014c47fe67c89495f"
|
||||
checksum = "3a22a87bd9e78d7204d793261470a4c9d585154fddd251828d8aefbb5f74c3bf"
|
||||
|
||||
[[package]]
|
||||
name = "cxxbridge-macro"
|
||||
version = "1.0.151"
|
||||
version = "1.0.134"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee54cd01f94db0328c4c73036d38bd8c3bb88927e953d05ffefe743edbf4eb68"
|
||||
checksum = "1dfdb020ff8787c5daf6e0dca743005cc8782868faeadfbabb8824ede5cb1c72"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -4371,7 +4402,6 @@ dependencies = [
|
||||
"anyhow",
|
||||
"assets",
|
||||
"buffer_diff",
|
||||
"chrono",
|
||||
"client",
|
||||
"clock",
|
||||
"collections",
|
||||
@@ -4420,7 +4450,6 @@ dependencies = [
|
||||
"text",
|
||||
"theme",
|
||||
"time",
|
||||
"time_format",
|
||||
"tree-sitter-html",
|
||||
"tree-sitter-rust",
|
||||
"tree-sitter-typescript",
|
||||
@@ -5065,9 +5094,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.0.35"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c"
|
||||
checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
@@ -5684,6 +5713,7 @@ dependencies = [
|
||||
"askpass",
|
||||
"assistant_settings",
|
||||
"buffer_diff",
|
||||
"chrono",
|
||||
"collections",
|
||||
"command_palette_hooks",
|
||||
"component",
|
||||
@@ -5701,6 +5731,7 @@ dependencies = [
|
||||
"linkify",
|
||||
"linkme",
|
||||
"log",
|
||||
"markdown",
|
||||
"menu",
|
||||
"multi_buffer",
|
||||
"notifications",
|
||||
@@ -5847,7 +5878,7 @@ dependencies = [
|
||||
"ashpd",
|
||||
"async-task",
|
||||
"backtrace",
|
||||
"bindgen 0.70.1",
|
||||
"bindgen 0.71.1",
|
||||
"blade-graphics",
|
||||
"blade-macros",
|
||||
"blade-util",
|
||||
@@ -7578,6 +7609,12 @@ version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67"
|
||||
|
||||
[[package]]
|
||||
name = "leb128fmt"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "lebe"
|
||||
version = "0.5.2"
|
||||
@@ -7629,7 +7666,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-targets 0.48.5",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7707,9 +7744,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "link-cplusplus"
|
||||
version = "1.0.10"
|
||||
version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a6f6da007f968f9def0d65a05b187e2960183de70c160204ecfccf0ee330212"
|
||||
checksum = "9d240c6f7e1ba3a28b0249f774e6a9dd0175054b52dfbb61b16eb8505c3785c9"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
@@ -8219,7 +8256,7 @@ name = "media"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bindgen 0.70.1",
|
||||
"bindgen 0.71.1",
|
||||
"core-foundation 0.10.0",
|
||||
"core-video",
|
||||
"ctor",
|
||||
@@ -8331,9 +8368,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.0"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1"
|
||||
checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
"simd-adler32",
|
||||
@@ -8426,6 +8463,12 @@ version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a"
|
||||
|
||||
[[package]]
|
||||
name = "multimap"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03"
|
||||
|
||||
[[package]]
|
||||
name = "naga"
|
||||
version = "23.1.0"
|
||||
@@ -8436,7 +8479,7 @@ dependencies = [
|
||||
"bit-set 0.8.0",
|
||||
"bitflags 2.9.0",
|
||||
"cfg_aliases 0.1.1",
|
||||
"codespan-reporting 0.11.1",
|
||||
"codespan-reporting",
|
||||
"hexf-parse",
|
||||
"indexmap",
|
||||
"log",
|
||||
@@ -10744,7 +10787,7 @@ dependencies = [
|
||||
"itertools 0.10.5",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"multimap",
|
||||
"multimap 0.8.3",
|
||||
"petgraph",
|
||||
"prost 0.9.0",
|
||||
"prost-types 0.9.0",
|
||||
@@ -10760,10 +10803,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4"
|
||||
dependencies = [
|
||||
"bytes 1.10.1",
|
||||
"heck 0.4.1",
|
||||
"itertools 0.10.5",
|
||||
"heck 0.5.0",
|
||||
"itertools 0.12.1",
|
||||
"log",
|
||||
"multimap",
|
||||
"multimap 0.10.0",
|
||||
"once_cell",
|
||||
"petgraph",
|
||||
"prettyplease",
|
||||
@@ -10794,7 +10837,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.10.5",
|
||||
"itertools 0.12.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.100",
|
||||
@@ -12182,9 +12225,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "scratch"
|
||||
version = "1.0.8"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f6280af86e5f559536da57a45ebc84948833b3bee313a7dd25232e09c878a52"
|
||||
checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152"
|
||||
|
||||
[[package]]
|
||||
name = "scrypt"
|
||||
@@ -13791,10 +13834,8 @@ dependencies = [
|
||||
"menu",
|
||||
"picker",
|
||||
"project",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"task",
|
||||
"tree-sitter-rust",
|
||||
"tree-sitter-typescript",
|
||||
@@ -15620,6 +15661,16 @@ dependencies = [
|
||||
"wasmparser 0.221.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-encoder"
|
||||
version = "0.227.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80bb72f02e7fbf07183443b27b0f3d4144abf8c114189f2e088ed95b696a7822"
|
||||
dependencies = [
|
||||
"leb128fmt",
|
||||
"wasmparser 0.227.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-metadata"
|
||||
version = "0.201.0"
|
||||
@@ -15652,6 +15703,25 @@ dependencies = [
|
||||
"wasmparser 0.221.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-metadata"
|
||||
version = "0.227.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce1ef0faabbbba6674e97a56bee857ccddf942785a336c8b47b42373c922a91d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"auditable-serde",
|
||||
"flate2",
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"spdx",
|
||||
"url",
|
||||
"wasm-encoder 0.227.1",
|
||||
"wasmparser 0.227.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-streams"
|
||||
version = "0.4.2"
|
||||
@@ -15689,6 +15759,18 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmparser"
|
||||
version = "0.227.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f51cad774fb3c9461ab9bccc9c62dfb7388397b5deda31bf40e8108ccd678b2"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"hashbrown 0.15.2",
|
||||
"indexmap",
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmprinter"
|
||||
version = "0.221.3"
|
||||
@@ -16253,7 +16335,7 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -16833,7 +16915,17 @@ checksum = "288f992ea30e6b5c531b52cdd5f3be81c148554b09ea416f058d16556ba92c27"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"wit-bindgen-rt 0.22.0",
|
||||
"wit-bindgen-rust-macro",
|
||||
"wit-bindgen-rust-macro 0.22.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.41.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10fb6648689b3929d56bbc7eb1acf70c9a42a29eb5358c67c10f54dbd5d695de"
|
||||
dependencies = [
|
||||
"wit-bindgen-rt 0.41.0",
|
||||
"wit-bindgen-rust-macro 0.41.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -16846,6 +16938,17 @@ dependencies = [
|
||||
"wit-parser 0.201.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-core"
|
||||
version = "0.41.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92fa781d4f2ff6d3f27f3cc9b74a73327b31ca0dc4a3ef25a0ce2983e0e5af9b"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck 0.5.0",
|
||||
"wit-parser 0.227.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rt"
|
||||
version = "0.22.0"
|
||||
@@ -16861,6 +16964,17 @@ dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rt"
|
||||
version = "0.41.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4db52a11d4dfb0a59f194c064055794ee6564eb1ced88c25da2cf76e50c5621"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"futures 0.3.31",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust"
|
||||
version = "0.22.0"
|
||||
@@ -16871,10 +16985,26 @@ dependencies = [
|
||||
"heck 0.4.1",
|
||||
"indexmap",
|
||||
"wasm-metadata 0.201.0",
|
||||
"wit-bindgen-core",
|
||||
"wit-bindgen-core 0.22.0",
|
||||
"wit-component 0.201.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust"
|
||||
version = "0.41.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d0809dc5ba19e2e98661bf32fc0addc5a3ca5bf3a6a7083aa6ba484085ff3ce"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck 0.5.0",
|
||||
"indexmap",
|
||||
"prettyplease",
|
||||
"syn 2.0.100",
|
||||
"wasm-metadata 0.227.1",
|
||||
"wit-bindgen-core 0.41.0",
|
||||
"wit-component 0.227.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust-macro"
|
||||
version = "0.22.0"
|
||||
@@ -16885,8 +17015,23 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.100",
|
||||
"wit-bindgen-core",
|
||||
"wit-bindgen-rust",
|
||||
"wit-bindgen-core 0.22.0",
|
||||
"wit-bindgen-rust 0.22.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust-macro"
|
||||
version = "0.41.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad19eec017904e04c60719592a803ee5da76cb51c81e3f6fbf9457f59db49799"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.100",
|
||||
"wit-bindgen-core 0.41.0",
|
||||
"wit-bindgen-rust 0.41.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -16927,6 +17072,25 @@ dependencies = [
|
||||
"wit-parser 0.221.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-component"
|
||||
version = "0.227.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "635c3adc595422cbf2341a17fb73a319669cc8d33deed3a48368a841df86b676"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.9.0",
|
||||
"indexmap",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"wasm-encoder 0.227.1",
|
||||
"wasm-metadata 0.227.1",
|
||||
"wasmparser 0.227.1",
|
||||
"wit-parser 0.227.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-parser"
|
||||
version = "0.201.0"
|
||||
@@ -16963,6 +17127,24 @@ dependencies = [
|
||||
"wasmparser 0.221.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-parser"
|
||||
version = "0.227.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddf445ed5157046e4baf56f9138c124a0824d4d1657e7204d71886ad8ce2fc11"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"id-arena",
|
||||
"indexmap",
|
||||
"log",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"unicode-xid",
|
||||
"wasmparser 0.227.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "witx"
|
||||
version = "0.9.1"
|
||||
@@ -17493,7 +17675,7 @@ checksum = "594fd10dd0f2f853eb243e2425e7c95938cef49adb81d9602921d002c5e6d9d9"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"wit-bindgen",
|
||||
"wit-bindgen 0.22.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -17502,7 +17684,7 @@ version = "0.4.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"wit-bindgen",
|
||||
"wit-bindgen 0.41.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -17532,7 +17714,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_proto"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.1.0",
|
||||
]
|
||||
|
||||
@@ -198,7 +198,7 @@ default-members = ["crates/zed"]
|
||||
|
||||
[workspace.package]
|
||||
publish = false
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
|
||||
[workspace.dependencies]
|
||||
|
||||
@@ -411,7 +411,7 @@ blade-util = { git = "https://github.com/kvark/blade", rev = "b16f5c7bd873c7126f
|
||||
naga = { version = "23.1.0", features = ["wgsl-in"] }
|
||||
blake3 = "1.5.3"
|
||||
bytes = "1.0"
|
||||
cargo_metadata = { git = "https://github.com/zed-industries/cargo_metadata", rev = "ce8171bad673923d61a77b6761d0dc4aff63398a"}
|
||||
cargo_metadata = "0.19"
|
||||
cargo_toml = "0.21"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
circular-buffer = "1.0"
|
||||
|
||||
1
assets/icons/debug_disabled_breakpoint.svg
Normal file
1
assets/icons/debug_disabled_breakpoint.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke="currentColor" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle"><circle cx="12" cy="12" r="10"/></svg>
|
||||
|
After Width: | Height: | Size: 249 B |
1
assets/icons/debug_disabled_log_breakpoint.svg
Normal file
1
assets/icons/debug_disabled_log_breakpoint.svg
Normal file
@@ -0,0 +1 @@
|
||||
<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-message-circle"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/></svg>
|
||||
|
After Width: | Height: | Size: 267 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-message-circle-more"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/><path d="M8 12h.01"/><path d="M12 12h.01"/><path d="M16 12h.01"/></svg>
|
||||
|
Before Width: | Height: | Size: 337 B |
1
assets/icons/user_round_pen.svg
Normal file
1
assets/icons/user_round_pen.svg
Normal file
@@ -0,0 +1 @@
|
||||
<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-user-round-pen-icon lucide-user-round-pen"><path d="M2 21a8 8 0 0 1 10.821-7.487"/><path d="M21.378 16.626a1 1 0 0 0-3.004-3.004l-4.01 4.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"/><circle cx="10" cy="8" r="5"/></svg>
|
||||
|
After Width: | Height: | Size: 461 B |
@@ -126,7 +126,6 @@
|
||||
// "alt-v": ["editor::MovePageUp", { "center_cursor": true }],
|
||||
"ctrl-alt-space": "editor::ShowCharacterPalette",
|
||||
"ctrl-;": "editor::ToggleLineNumbers",
|
||||
"ctrl-k ctrl-r": "git::Restore",
|
||||
"ctrl-'": "editor::ToggleSelectedDiffHunks",
|
||||
"ctrl-\"": "editor::ExpandAllDiffHunks",
|
||||
"ctrl-i": "editor::ShowSignatureHelp",
|
||||
@@ -138,6 +137,22 @@
|
||||
"shift-f9": "editor::EditLogBreakpoint"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && !assistant_diff",
|
||||
"bindings": {
|
||||
"ctrl-k ctrl-r": "git::Restore",
|
||||
"ctrl-alt-y": "git::ToggleStaged",
|
||||
"alt-y": "git::StageAndNext",
|
||||
"alt-shift-y": "git::UnstageAndNext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AssistantDiff",
|
||||
"bindings": {
|
||||
"ctrl-y": "assistant2::ToggleKeep",
|
||||
"ctrl-k ctrl-r": "assistant2::Reject"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
@@ -382,9 +397,6 @@
|
||||
"ctrl-k v": "markdown::OpenPreviewToTheSide",
|
||||
"ctrl-shift-v": "markdown::OpenPreview",
|
||||
"ctrl-alt-shift-c": "editor::DisplayCursorNames",
|
||||
"ctrl-alt-y": "git::ToggleStaged",
|
||||
"alt-y": "git::StageAndNext",
|
||||
"alt-shift-y": "git::UnstageAndNext",
|
||||
"alt-.": "editor::GoToHunk",
|
||||
"alt-,": "editor::GoToPreviousHunk"
|
||||
}
|
||||
@@ -617,7 +629,9 @@
|
||||
"bindings": {
|
||||
"ctrl-n": "assistant2::NewThread",
|
||||
"new": "assistant2::NewThread",
|
||||
"ctrl-alt-n": "assistant2::NewPromptEditor",
|
||||
"ctrl-shift-h": "assistant2::OpenHistory",
|
||||
"ctrl-alt-c": "assistant2::OpenConfiguration",
|
||||
"ctrl-i": "assistant2::ToggleProfileSelector",
|
||||
"ctrl-alt-/": "assistant::ToggleModelSelector",
|
||||
"ctrl-shift-a": "assistant2::ToggleContextPicker",
|
||||
@@ -637,7 +651,8 @@
|
||||
"context": "MessageEditor > Editor",
|
||||
"bindings": {
|
||||
"enter": "assistant2::Chat",
|
||||
"ctrl-i": "assistant2::ToggleProfileSelector"
|
||||
"ctrl-i": "assistant2::ToggleProfileSelector",
|
||||
"shift-ctrl-r": "assistant2::OpenAssistantDiff"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -147,10 +147,6 @@
|
||||
"ctrl-shift-v": ["editor::MovePageUp", { "center_cursor": true }],
|
||||
"ctrl-cmd-space": "editor::ShowCharacterPalette",
|
||||
"cmd-;": "editor::ToggleLineNumbers",
|
||||
"cmd-alt-z": "git::Restore",
|
||||
"cmd-alt-y": "git::ToggleStaged",
|
||||
"cmd-y": "git::StageAndNext",
|
||||
"cmd-shift-y": "git::UnstageAndNext",
|
||||
"cmd-'": "editor::ToggleSelectedDiffHunks",
|
||||
"cmd-\"": "editor::ExpandAllDiffHunks",
|
||||
"cmd-alt-g b": "editor::ToggleGitBlame",
|
||||
@@ -231,6 +227,24 @@
|
||||
"ctrl-alt-enter": "repl::RunInPlace"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && !assistant_diff",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-alt-z": "git::Restore",
|
||||
"cmd-alt-y": "git::ToggleStaged",
|
||||
"cmd-y": "git::StageAndNext",
|
||||
"cmd-shift-y": "git::UnstageAndNext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AssistantDiff",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-y": "assistant2::ToggleKeep",
|
||||
"cmd-alt-z": "assistant2::Reject"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AssistantPanel",
|
||||
"use_key_equivalents": true,
|
||||
@@ -267,8 +281,9 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-n": "assistant2::NewThread",
|
||||
"cmd-alt-p": "assistant2::NewPromptEditor",
|
||||
"cmd-alt-n": "assistant2::NewPromptEditor",
|
||||
"cmd-shift-h": "assistant2::OpenHistory",
|
||||
"cmd-alt-c": "assistant2::OpenConfiguration",
|
||||
"cmd-i": "assistant2::ToggleProfileSelector",
|
||||
"cmd-alt-/": "assistant::ToggleModelSelector",
|
||||
"cmd-shift-a": "assistant2::ToggleContextPicker",
|
||||
@@ -290,8 +305,7 @@
|
||||
"bindings": {
|
||||
"enter": "assistant2::Chat",
|
||||
"cmd-i": "assistant2::ToggleProfileSelector",
|
||||
"cmd-g d": "git::Diff",
|
||||
"shift-escape": "git::ExpandCommitEditor"
|
||||
"shift-ctrl-r": "assistant2::OpenAssistantDiff"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -258,9 +258,10 @@
|
||||
"u": "vim::ConvertToLowerCase",
|
||||
"shift-u": "vim::ConvertToUpperCase",
|
||||
"shift-o": "vim::OtherEnd",
|
||||
"o": "vim::OtherEnd",
|
||||
"o": "vim::OtherEndRowAware",
|
||||
"d": "vim::VisualDelete",
|
||||
"x": "vim::VisualDelete",
|
||||
"delete": "vim::VisualDelete",
|
||||
"shift-d": "vim::VisualDeleteLine",
|
||||
"shift-x": "vim::VisualDeleteLine",
|
||||
"y": "vim::VisualYank",
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
You are an AI assistant integrated into a text editor. Your goal is to do one of the following two things:
|
||||
You are an AI assistant integrated into a code editor. You have the programming ability of an expert programmer who takes pride in writing high-quality code and is driven to the point of obsession about solving problems effectively. Your goal is to do one of the following two things:
|
||||
|
||||
1. Help users answer questions and perform tasks related to their codebase.
|
||||
2. Answer general-purpose questions unrelated to their particular codebase.
|
||||
|
||||
It will be up to you to decide which of these you are doing based on what the user has told you. When unclear, ask clarifying questions to understand the user's intent before proceeding.
|
||||
|
||||
You should only perform actions that modify the user’s system if explicitly requested by the user:
|
||||
You should only perform actions that modify the user's system if explicitly requested by the user:
|
||||
- If the user asks a question about how to accomplish a task, provide guidance or information, and use read-only tools (e.g., search) to assist. You may suggest potential actions, but do not directly modify the user’s system without explicit instruction.
|
||||
- If the user clearly requests that you perform an action, carry out the action directly without explaining why you are doing so.
|
||||
- The editing actions you perform might produce errors or warnings. At the end of your changes, check whether you introduced any problems, and fix them before providing a summary of the changes you made.
|
||||
- The editing actions you perform might produce errors or warnings. At the end of your changes, check whether you introduced any problems, and fix them before providing a summary of the changes you made. You may only attempt to fix these up to 3 times. If you have tried 3 times to fix them, and there are still problems remaining, you must not continue trying to fix them, and must instead tell the user that there are problems remaining - and ask if the user would like you to attempt to solve them further.
|
||||
- Do not fix errors unrelated to your changes unless the user explicitly asks you to do so.
|
||||
|
||||
Be concise and direct in your responses.
|
||||
Be concise and direct in your responses. Never apologize or thank the user. Don't comment that you have just realized or understood something. When you are going to make a tool call, tersely explain your reasoning for choosing to use that tool, with no flourishes or commentary beyond that information. For example, rather than saying "You're absolutely right! Thank you for providing that context. Now I understand that we're missing a dependency, and I need to add it:" say "I'll add that missing dependency:" instead. Also, don't restate what a tool call is about to do (or just did). For example, don't say "Now I'm going to check diagnostics to see if there are any warnings or errors," followed by running a tool which checks diagnostics and reports warnings or errors; instead, just request the tool call without saying anything.
|
||||
|
||||
The user has opened a project that contains the following root directories/files. Whenever you specify a path in the project, it must be a relative path which begins with one of these root directories/files:
|
||||
|
||||
|
||||
@@ -115,6 +115,15 @@
|
||||
"confirm_quit": false,
|
||||
// Whether to restore last closed project when fresh Zed instance is opened.
|
||||
"restore_on_startup": "last_session",
|
||||
// Whether to attempt to restore previous file's state when opening it again.
|
||||
// The state is stored per pane.
|
||||
// When disabled, defaults are applied instead of the state restoration.
|
||||
//
|
||||
// E.g. for editors, selections, folds and scroll positions are restored, if the same file is closed and, later, opened again in the same pane.
|
||||
// When disabled, a single selection in the very beginning of the file, zero scroll position and no folds state is used as a default.
|
||||
//
|
||||
// Default: true
|
||||
"restore_on_file_reopen": true,
|
||||
// Size of the drop target in the editor.
|
||||
"drop_target_size": 0.2,
|
||||
// Whether the window should be closed when using 'close active item' on a window with no tabs.
|
||||
@@ -155,8 +164,8 @@
|
||||
//
|
||||
// Default: not set, defaults to "bar"
|
||||
"cursor_shape": null,
|
||||
// Determines whether the mouse cursor is hidden when typing in an editor or input box.
|
||||
"hide_mouse_while_typing": true,
|
||||
// Determines when the mouse cursor should be hidden in an editor or input box.
|
||||
"hide_mouse": "on_typing_and_movement",
|
||||
// How to highlight the current line in the editor.
|
||||
//
|
||||
// 1. Don't highlight the current line:
|
||||
@@ -624,10 +633,10 @@
|
||||
// The model to use.
|
||||
"model": "claude-3-5-sonnet-latest"
|
||||
},
|
||||
"default_profile": "code-writer",
|
||||
"default_profile": "write",
|
||||
"profiles": {
|
||||
"read-only": {
|
||||
"name": "Read-only",
|
||||
"ask": {
|
||||
"name": "Ask",
|
||||
"tools": {
|
||||
"diagnostics": true,
|
||||
"fetch": true,
|
||||
@@ -639,11 +648,12 @@
|
||||
"thinking": true
|
||||
}
|
||||
},
|
||||
"code-writer": {
|
||||
"name": "Code Writer",
|
||||
"write": {
|
||||
"name": "Write",
|
||||
"tools": {
|
||||
"bash": true,
|
||||
"batch-tool": true,
|
||||
"code-symbols": true,
|
||||
"copy-path": true,
|
||||
"create-file": true,
|
||||
"delete-path": true,
|
||||
@@ -657,12 +667,18 @@
|
||||
"path-search": true,
|
||||
"read-file": true,
|
||||
"regex-search": true,
|
||||
"symbol-info": true,
|
||||
"thinking": true
|
||||
}
|
||||
}
|
||||
},
|
||||
// Shows a notification when the agent needs confirmation before running an edit tool call or when that's concluded.
|
||||
"notify_when_agent_waiting": true
|
||||
// Where to show notifications when an agent has either completed
|
||||
// its response, or else needs confirmation before it can run a
|
||||
// tool action.
|
||||
// "primary_screen" - Show the notification only on your primary screen (default)
|
||||
// "all_screens" - Show these notifications on all screens
|
||||
// "never" - Never show these notifications
|
||||
"notify_when_agent_waiting": "primary_screen"
|
||||
},
|
||||
// The settings for slash commands.
|
||||
"slash_commands": {
|
||||
@@ -1466,11 +1482,6 @@
|
||||
"dev": {
|
||||
// "theme": "Andromeda"
|
||||
},
|
||||
// Task-related settings.
|
||||
"task": {
|
||||
// Whether to show task status indicator in the status bar. Default: true
|
||||
"show_status_indicator": true
|
||||
},
|
||||
// Whether to show full labels in line indicator or short ones
|
||||
//
|
||||
// Values:
|
||||
|
||||
Binary file not shown.
@@ -3,9 +3,9 @@ use editor::Editor;
|
||||
use extension_host::ExtensionStore;
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
actions, percentage, Animation, AnimationExt as _, App, Context, CursorStyle, Entity,
|
||||
EventEmitter, InteractiveElement as _, ParentElement as _, Render, SharedString,
|
||||
StatefulInteractiveElement, Styled, Transformation, Window,
|
||||
Animation, AnimationExt as _, App, Context, CursorStyle, Entity, EventEmitter,
|
||||
InteractiveElement as _, ParentElement as _, Render, SharedString, StatefulInteractiveElement,
|
||||
Styled, Transformation, Window, actions, percentage,
|
||||
};
|
||||
use language::{BinaryStatus, LanguageRegistry, LanguageServerId};
|
||||
use project::{
|
||||
@@ -14,9 +14,9 @@ use project::{
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use std::{cmp::Reverse, fmt::Write, sync::Arc, time::Duration};
|
||||
use ui::{prelude::*, ButtonLike, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip};
|
||||
use ui::{ButtonLike, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
|
||||
use util::truncate_and_trailoff;
|
||||
use workspace::{item::ItemHandle, StatusItemView, Workspace};
|
||||
use workspace::{StatusItemView, Workspace, item::ItemHandle};
|
||||
|
||||
actions!(activity_indicator, [ShowErrorMessage]);
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@ mod supported_countries;
|
||||
|
||||
use std::{pin::Pin, str::FromStr};
|
||||
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use chrono::{DateTime, Utc};
|
||||
use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt};
|
||||
use futures::{AsyncBufReadExt, AsyncReadExt, Stream, StreamExt, io::BufReader, stream::BoxStream};
|
||||
use http_client::http::{HeaderMap, HeaderValue};
|
||||
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -5,9 +5,9 @@ use std::time::Duration;
|
||||
use anyhow::Context as _;
|
||||
use futures::channel::{mpsc, oneshot};
|
||||
#[cfg(unix)]
|
||||
use futures::{io::BufReader, AsyncBufReadExt as _};
|
||||
use futures::{AsyncBufReadExt as _, io::BufReader};
|
||||
#[cfg(unix)]
|
||||
use futures::{select_biased, AsyncWriteExt as _, FutureExt as _};
|
||||
use futures::{AsyncWriteExt as _, FutureExt as _, select_biased};
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use gpui::{AsyncApp, BackgroundExecutor, Task};
|
||||
#[cfg(unix)]
|
||||
|
||||
@@ -57,10 +57,11 @@ impl Assets {
|
||||
|
||||
pub fn load_test_fonts(&self, cx: &App) {
|
||||
cx.text_system()
|
||||
.add_fonts(vec![self
|
||||
.load("fonts/plex-mono/ZedPlexMono-Regular.ttf")
|
||||
.unwrap()
|
||||
.unwrap()])
|
||||
.add_fonts(vec![
|
||||
self.load("fonts/plex-mono/ZedPlexMono-Regular.ttf")
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
])
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ use client::Client;
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
use fs::Fs;
|
||||
use gpui::{actions, App, Global, ReadGlobal, UpdateGlobal};
|
||||
use gpui::{App, Global, ReadGlobal, UpdateGlobal, actions};
|
||||
use language_model::{
|
||||
LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, LanguageModelResponseMessage,
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use collections::HashMap;
|
||||
use gpui::{canvas, AnyView, App, EventEmitter, FocusHandle, Focusable, Subscription};
|
||||
use gpui::{AnyView, App, EventEmitter, FocusHandle, Focusable, Subscription, canvas};
|
||||
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
|
||||
use ui::{prelude::*, ElevationIndex};
|
||||
use ui::{ElevationIndex, prelude::*};
|
||||
use workspace::Item;
|
||||
|
||||
pub struct ConfigurationView {
|
||||
|
||||
@@ -1,43 +1,44 @@
|
||||
use crate::assistant_configuration::{ConfigurationView, ConfigurationViewEvent};
|
||||
use crate::Assistant;
|
||||
use crate::assistant_configuration::{ConfigurationView, ConfigurationViewEvent};
|
||||
use crate::{
|
||||
terminal_inline_assistant::TerminalInlineAssistant, DeployHistory, InlineAssistant, NewChat,
|
||||
DeployHistory, InlineAssistant, NewChat, terminal_inline_assistant::TerminalInlineAssistant,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_context_editor::{
|
||||
make_lsp_adapter_delegate, AssistantContext, AssistantPanelDelegate, ContextEditor,
|
||||
ContextEditorToolbarItem, ContextEditorToolbarItemEvent, ContextHistory, ContextId,
|
||||
ContextStore, ContextStoreEvent, InsertDraggedFiles, SlashCommandCompletionProvider,
|
||||
DEFAULT_TAB_TITLE,
|
||||
AssistantContext, AssistantPanelDelegate, ContextEditor, ContextEditorToolbarItem,
|
||||
ContextEditorToolbarItemEvent, ContextHistory, ContextId, ContextStore, ContextStoreEvent,
|
||||
DEFAULT_TAB_TITLE, InsertDraggedFiles, SlashCommandCompletionProvider,
|
||||
make_lsp_adapter_delegate,
|
||||
};
|
||||
use assistant_settings::{AssistantDockPosition, AssistantSettings};
|
||||
use assistant_slash_command::SlashCommandWorkingSet;
|
||||
use client::{proto, Client, Status};
|
||||
use client::{Client, Status, proto};
|
||||
use editor::{Editor, EditorEvent};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
prelude::*, Action, App, AsyncWindowContext, Entity, EventEmitter, ExternalPaths, FocusHandle,
|
||||
Focusable, InteractiveElement, IntoElement, ParentElement, Pixels, Render, Styled,
|
||||
Subscription, Task, UpdateGlobal, WeakEntity,
|
||||
Action, App, AsyncWindowContext, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable,
|
||||
InteractiveElement, IntoElement, ParentElement, Pixels, Render, Styled, Subscription, Task,
|
||||
UpdateGlobal, WeakEntity, prelude::*,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use language_model::{
|
||||
AuthenticateError, LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID,
|
||||
};
|
||||
use project::Project;
|
||||
use prompt_library::{open_prompt_library, PromptLibrary};
|
||||
use prompt_library::{PromptLibrary, open_prompt_library};
|
||||
use prompt_store::PromptBuilder;
|
||||
use search::{buffer_search::DivRegistrar, BufferSearchBar};
|
||||
use settings::{update_settings_file, Settings};
|
||||
use search::{BufferSearchBar, buffer_search::DivRegistrar};
|
||||
use settings::{Settings, update_settings_file};
|
||||
use smol::stream::StreamExt;
|
||||
use std::{ops::ControlFlow, path::PathBuf, sync::Arc};
|
||||
use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
|
||||
use ui::{prelude::*, ContextMenu, PopoverMenu, Tooltip};
|
||||
use util::{maybe, ResultExt};
|
||||
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
|
||||
use ui::{ContextMenu, PopoverMenu, Tooltip, prelude::*};
|
||||
use util::{ResultExt, maybe};
|
||||
use workspace::DraggedTab;
|
||||
use workspace::{
|
||||
DraggedSelection, Pane, ShowConfiguration, ToggleZoom, Workspace,
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
pane, DraggedSelection, Pane, ShowConfiguration, ToggleZoom, Workspace,
|
||||
pane,
|
||||
};
|
||||
use zed_actions::assistant::{InlineAssist, OpenPromptLibrary, ToggleFocus};
|
||||
|
||||
|
||||
@@ -2,39 +2,40 @@ use crate::{
|
||||
Assistant, AssistantPanel, AssistantPanelEvent, CycleNextInlineAssist,
|
||||
CyclePreviousInlineAssist,
|
||||
};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use assistant_context_editor::{humanize_token_count, RequestType};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_context_editor::{RequestType, humanize_token_count};
|
||||
use assistant_settings::AssistantSettings;
|
||||
use client::{telemetry::Telemetry, ErrorExt};
|
||||
use collections::{hash_map, HashMap, HashSet, VecDeque};
|
||||
use client::{ErrorExt, telemetry::Telemetry};
|
||||
use collections::{HashMap, HashSet, VecDeque, hash_map};
|
||||
use editor::{
|
||||
Anchor, AnchorRangeExt, CodeActionProvider, Editor, EditorElement, EditorEvent, EditorMode,
|
||||
EditorStyle, ExcerptId, ExcerptRange, GutterDimensions, MultiBuffer, MultiBufferSnapshot,
|
||||
ToOffset as _, ToPoint,
|
||||
actions::{MoveDown, MoveUp, SelectAll},
|
||||
display_map::{
|
||||
BlockContext, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, RenderBlock,
|
||||
ToDisplayPoint,
|
||||
},
|
||||
Anchor, AnchorRangeExt, CodeActionProvider, Editor, EditorElement, EditorEvent, EditorMode,
|
||||
EditorStyle, ExcerptId, ExcerptRange, GutterDimensions, MultiBuffer, MultiBufferSnapshot,
|
||||
ToOffset as _, ToPoint,
|
||||
};
|
||||
use feature_flags::{
|
||||
Assistant2FeatureFlag, FeatureFlagAppExt as _, FeatureFlagViewExt as _, ZedPro,
|
||||
};
|
||||
use fs::Fs;
|
||||
use futures::{
|
||||
SinkExt, Stream, StreamExt,
|
||||
channel::mpsc,
|
||||
future::{BoxFuture, LocalBoxFuture},
|
||||
join, SinkExt, Stream, StreamExt,
|
||||
join,
|
||||
};
|
||||
use gpui::{
|
||||
anchored, deferred, point, AnyElement, App, ClickEvent, Context, CursorStyle, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, FontWeight, Global, HighlightStyle, Subscription, Task,
|
||||
TextStyle, UpdateGlobal, WeakEntity, Window,
|
||||
AnyElement, App, ClickEvent, Context, CursorStyle, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, FontWeight, Global, HighlightStyle, Subscription, Task, TextStyle, UpdateGlobal,
|
||||
WeakEntity, Window, anchored, deferred, point,
|
||||
};
|
||||
use language::{line_diff, Buffer, IndentKind, Point, Selection, TransactionId};
|
||||
use language::{Buffer, IndentKind, Point, Selection, TransactionId, line_diff};
|
||||
use language_model::{
|
||||
report_assistant_event, LanguageModel, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, LanguageModelTextStream, Role,
|
||||
LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||
LanguageModelTextStream, Role, report_assistant_event,
|
||||
};
|
||||
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
@@ -42,7 +43,7 @@ use parking_lot::Mutex;
|
||||
use project::{CodeAction, LspAction, ProjectTransaction};
|
||||
use prompt_store::PromptBuilder;
|
||||
use rope::Rope;
|
||||
use settings::{update_settings_file, Settings, SettingsStore};
|
||||
use settings::{Settings, SettingsStore, update_settings_file};
|
||||
use smol::future::FutureExt;
|
||||
use std::{
|
||||
cmp,
|
||||
@@ -61,10 +62,10 @@ use terminal_view::terminal_panel::TerminalPanel;
|
||||
use text::{OffsetRangeExt, ToPoint as _};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
prelude::*, text_for_action, CheckboxWithLabel, IconButtonShape, KeyBinding, Popover, Tooltip,
|
||||
CheckboxWithLabel, IconButtonShape, KeyBinding, Popover, Tooltip, prelude::*, text_for_action,
|
||||
};
|
||||
use util::{RangeExt, ResultExt};
|
||||
use workspace::{notifications::NotificationId, ItemHandle, Toast, Workspace};
|
||||
use workspace::{ItemHandle, Toast, Workspace, notifications::NotificationId};
|
||||
|
||||
pub fn init(
|
||||
fs: Arc<dyn Fs>,
|
||||
@@ -3710,8 +3711,8 @@ mod tests {
|
||||
use gpui::TestAppContext;
|
||||
use indoc::indoc;
|
||||
use language::{
|
||||
language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, LanguageMatcher,
|
||||
Point,
|
||||
Buffer, Language, LanguageConfig, LanguageMatcher, Point, language_settings,
|
||||
tree_sitter_rust,
|
||||
};
|
||||
use language_model::{LanguageModelRegistry, TokenUsage};
|
||||
use rand::prelude::*;
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
use crate::{AssistantPanel, AssistantPanelEvent, DEFAULT_CONTEXT_LINES};
|
||||
use anyhow::{Context as _, Result};
|
||||
use assistant_context_editor::{humanize_token_count, RequestType};
|
||||
use assistant_context_editor::{RequestType, humanize_token_count};
|
||||
use assistant_settings::AssistantSettings;
|
||||
use client::telemetry::Telemetry;
|
||||
use collections::{HashMap, VecDeque};
|
||||
use editor::{
|
||||
actions::{MoveDown, MoveUp, SelectAll},
|
||||
Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
|
||||
actions::{MoveDown, MoveUp, SelectAll},
|
||||
};
|
||||
use fs::Fs;
|
||||
use futures::{channel::mpsc, SinkExt, StreamExt};
|
||||
use futures::{SinkExt, StreamExt, channel::mpsc};
|
||||
use gpui::{
|
||||
App, Context, Entity, EventEmitter, FocusHandle, Focusable, Global, Subscription, Task,
|
||||
TextStyle, UpdateGlobal, WeakEntity,
|
||||
};
|
||||
use language::Buffer;
|
||||
use language_model::{
|
||||
report_assistant_event, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, Role,
|
||||
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
|
||||
report_assistant_event,
|
||||
};
|
||||
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
|
||||
use prompt_store::PromptBuilder;
|
||||
use settings::{update_settings_file, Settings};
|
||||
use settings::{Settings, update_settings_file};
|
||||
use std::{
|
||||
cmp,
|
||||
sync::Arc,
|
||||
@@ -31,9 +31,9 @@ use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
|
||||
use terminal::Terminal;
|
||||
use terminal_view::TerminalView;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{prelude::*, text_for_action, IconButtonShape, Tooltip};
|
||||
use ui::{IconButtonShape, Tooltip, prelude::*, text_for_action};
|
||||
use util::ResultExt;
|
||||
use workspace::{notifications::NotificationId, Toast, Workspace};
|
||||
use workspace::{Toast, Workspace, notifications::NotificationId};
|
||||
|
||||
pub fn init(
|
||||
fs: Arc<dyn Fs>,
|
||||
|
||||
@@ -25,6 +25,7 @@ assistant_settings.workspace = true
|
||||
assistant_slash_command.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
async-watch.workspace = true
|
||||
buffer_diff.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
clock.workspace = true
|
||||
@@ -40,7 +41,6 @@ fs.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
git.workspace = true
|
||||
git_ui.workspace = true
|
||||
gpui.workspace = true
|
||||
heed.workspace = true
|
||||
html_to_markdown.workspace = true
|
||||
@@ -65,6 +65,7 @@ prompt_store.workspace = true
|
||||
proto.workspace = true
|
||||
release_channel.workspace = true
|
||||
rope.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
@@ -87,6 +88,7 @@ workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
buffer_diff = { workspace = true, features = ["test-support"] }
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, "features" = ["test-support"] }
|
||||
indoc.workspace = true
|
||||
|
||||
@@ -1,33 +1,37 @@
|
||||
use crate::AssistantPanel;
|
||||
use crate::context::{AssistantContext, ContextId};
|
||||
use crate::thread::{
|
||||
LastRestoreCheckpoint, MessageId, MessageSegment, RequestKind, Thread, ThreadError,
|
||||
ThreadEvent, ThreadFeedback,
|
||||
};
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::tool_use::{PendingToolUseStatus, ToolUse, ToolUseStatus};
|
||||
use crate::ui::{AgentNotification, AgentNotificationEvent, ContextPill};
|
||||
use crate::AssistantPanel;
|
||||
use assistant_settings::AssistantSettings;
|
||||
use crate::ui::{AddedContext, AgentNotification, AgentNotificationEvent, ContextPill};
|
||||
use assistant_settings::{AssistantSettings, NotifyWhenAgentWaiting};
|
||||
use collections::HashMap;
|
||||
use editor::{Editor, MultiBuffer};
|
||||
use gpui::{
|
||||
linear_color_stop, linear_gradient, list, percentage, pulsating_between, AbsoluteLength,
|
||||
Animation, AnimationExt, AnyElement, App, ClickEvent, DefiniteLength, EdgesRefinement, Empty,
|
||||
Entity, Focusable, Hsla, Length, ListAlignment, ListState, MouseButton, ScrollHandle, Stateful,
|
||||
StyleRefinement, Subscription, Task, TextStyleRefinement, Transformation, UnderlineStyle,
|
||||
WeakEntity, WindowHandle,
|
||||
AbsoluteLength, Animation, AnimationExt, AnyElement, App, ClickEvent, DefiniteLength,
|
||||
EdgesRefinement, Empty, Entity, Focusable, Hsla, Length, ListAlignment, ListState, MouseButton,
|
||||
PlatformDisplay, ScrollHandle, Stateful, StyleRefinement, Subscription, Task,
|
||||
TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, WindowHandle,
|
||||
linear_color_stop, linear_gradient, list, percentage, pulsating_between,
|
||||
};
|
||||
use language::{Buffer, LanguageRegistry};
|
||||
use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role};
|
||||
use markdown::{Markdown, MarkdownStyle};
|
||||
use project::ProjectItem as _;
|
||||
use settings::Settings as _;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use text::ToPoint;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{prelude::*, Disclosure, IconButton, KeyBinding, Scrollbar, ScrollbarState, Tooltip};
|
||||
use ui::{Disclosure, IconButton, KeyBinding, Scrollbar, ScrollbarState, Tooltip, prelude::*};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{OpenOptions, Workspace};
|
||||
|
||||
use crate::context_store::{refresh_context_store_text, ContextStore};
|
||||
use crate::context_store::{ContextStore, refresh_context_store_text};
|
||||
|
||||
pub struct ActiveThread {
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
@@ -483,14 +487,14 @@ impl ActiveThread {
|
||||
let updated_context_ids = refresh_task.await;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.context_store.read_with(cx, |context_store, cx| {
|
||||
this.context_store.read_with(cx, |context_store, _cx| {
|
||||
context_store
|
||||
.context()
|
||||
.iter()
|
||||
.filter(|context| {
|
||||
updated_context_ids.contains(&context.id())
|
||||
})
|
||||
.flat_map(|context| context.snapshot(cx))
|
||||
.cloned()
|
||||
.collect()
|
||||
})
|
||||
})
|
||||
@@ -526,90 +530,108 @@ impl ActiveThread {
|
||||
caption: impl Into<SharedString>,
|
||||
icon: IconName,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<'_, ActiveThread>,
|
||||
cx: &mut Context<ActiveThread>,
|
||||
) {
|
||||
if window.is_window_active()
|
||||
|| !self.notifications.is_empty()
|
||||
|| !AssistantSettings::get_global(cx).notify_when_agent_waiting
|
||||
{
|
||||
if window.is_window_active() || !self.notifications.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let caption = caption.into();
|
||||
|
||||
let title = self
|
||||
.thread
|
||||
.read(cx)
|
||||
.summary()
|
||||
.unwrap_or("Agent Panel".into());
|
||||
|
||||
for screen in cx.displays() {
|
||||
let options = AgentNotification::window_options(screen, cx);
|
||||
match AssistantSettings::get_global(cx).notify_when_agent_waiting {
|
||||
NotifyWhenAgentWaiting::PrimaryScreen => {
|
||||
if let Some(primary) = cx.primary_display() {
|
||||
self.pop_up(icon, caption.into(), title.clone(), window, primary, cx);
|
||||
}
|
||||
}
|
||||
NotifyWhenAgentWaiting::AllScreens => {
|
||||
let caption = caption.into();
|
||||
for screen in cx.displays() {
|
||||
self.pop_up(icon, caption.clone(), title.clone(), window, screen, cx);
|
||||
}
|
||||
}
|
||||
NotifyWhenAgentWaiting::Never => {
|
||||
// Don't show anything
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(screen_window) = cx
|
||||
.open_window(options, |_, cx| {
|
||||
cx.new(|_| AgentNotification::new(title.clone(), caption.clone(), icon))
|
||||
})
|
||||
.log_err()
|
||||
{
|
||||
if let Some(pop_up) = screen_window.entity(cx).log_err() {
|
||||
self.notification_subscriptions
|
||||
.entry(screen_window)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(cx.subscribe_in(&pop_up, window, {
|
||||
|this, _, event, window, cx| match event {
|
||||
AgentNotificationEvent::Accepted => {
|
||||
let handle = window.window_handle();
|
||||
cx.activate(true); // Switch back to the Zed application
|
||||
fn pop_up(
|
||||
&mut self,
|
||||
icon: IconName,
|
||||
caption: SharedString,
|
||||
title: SharedString,
|
||||
window: &mut Window,
|
||||
screen: Rc<dyn PlatformDisplay>,
|
||||
cx: &mut Context<'_, ActiveThread>,
|
||||
) {
|
||||
let options = AgentNotification::window_options(screen, cx);
|
||||
|
||||
let workspace_handle = this.workspace.clone();
|
||||
if let Some(screen_window) = cx
|
||||
.open_window(options, |_, cx| {
|
||||
cx.new(|_| AgentNotification::new(title.clone(), caption.clone(), icon))
|
||||
})
|
||||
.log_err()
|
||||
{
|
||||
if let Some(pop_up) = screen_window.entity(cx).log_err() {
|
||||
self.notification_subscriptions
|
||||
.entry(screen_window)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(cx.subscribe_in(&pop_up, window, {
|
||||
|this, _, event, window, cx| match event {
|
||||
AgentNotificationEvent::Accepted => {
|
||||
let handle = window.window_handle();
|
||||
cx.activate(true); // Switch back to the Zed application
|
||||
|
||||
// If there are multiple Zed windows, activate the correct one.
|
||||
cx.defer(move |cx| {
|
||||
handle
|
||||
.update(cx, |_view, window, _cx| {
|
||||
window.activate_window();
|
||||
let workspace_handle = this.workspace.clone();
|
||||
|
||||
if let Some(workspace) = workspace_handle.upgrade()
|
||||
{
|
||||
workspace.update(_cx, |workspace, cx| {
|
||||
workspace.focus_panel::<AssistantPanel>(
|
||||
window, cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
// If there are multiple Zed windows, activate the correct one.
|
||||
cx.defer(move |cx| {
|
||||
handle
|
||||
.update(cx, |_view, window, _cx| {
|
||||
window.activate_window();
|
||||
|
||||
if let Some(workspace) = workspace_handle.upgrade() {
|
||||
workspace.update(_cx, |workspace, cx| {
|
||||
workspace
|
||||
.focus_panel::<AssistantPanel>(window, cx);
|
||||
});
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
});
|
||||
|
||||
this.dismiss_notifications(cx);
|
||||
}
|
||||
AgentNotificationEvent::Dismissed => {
|
||||
this.dismiss_notifications(cx);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
self.notifications.push(screen_window);
|
||||
|
||||
// If the user manually refocuses the original window, dismiss the popup.
|
||||
self.notification_subscriptions
|
||||
.entry(screen_window)
|
||||
.or_insert_with(Vec::new)
|
||||
.push({
|
||||
let pop_up_weak = pop_up.downgrade();
|
||||
|
||||
cx.observe_window_activation(window, move |_, window, cx| {
|
||||
if window.is_window_active() {
|
||||
if let Some(pop_up) = pop_up_weak.upgrade() {
|
||||
pop_up.update(cx, |_, cx| {
|
||||
cx.emit(AgentNotificationEvent::Dismissed);
|
||||
});
|
||||
|
||||
this.dismiss_notifications(cx);
|
||||
}
|
||||
AgentNotificationEvent::Dismissed => {
|
||||
this.dismiss_notifications(cx);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
self.notifications.push(screen_window);
|
||||
|
||||
// If the user manually refocuses the original window, dismiss the popup.
|
||||
self.notification_subscriptions
|
||||
.entry(screen_window)
|
||||
.or_insert_with(Vec::new)
|
||||
.push({
|
||||
let pop_up_weak = pop_up.downgrade();
|
||||
|
||||
cx.observe_window_activation(window, move |_, window, cx| {
|
||||
if window.is_window_active() {
|
||||
if let Some(pop_up) = pop_up_weak.upgrade() {
|
||||
pop_up.update(cx, |_, cx| {
|
||||
cx.emit(AgentNotificationEvent::Dismissed);
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -778,10 +800,13 @@ impl ActiveThread {
|
||||
return Empty.into_any();
|
||||
};
|
||||
|
||||
let context_store = self.context_store.clone();
|
||||
let workspace = self.workspace.clone();
|
||||
|
||||
let thread = self.thread.read(cx);
|
||||
// Get all the data we need from thread before we start using it in closures
|
||||
let checkpoint = thread.checkpoint_for_message(message_id);
|
||||
let context = thread.context_for_message(message_id);
|
||||
let context = thread.context_for_message(message_id).collect::<Vec<_>>();
|
||||
let tool_uses = thread.tool_uses_for_message(message_id, cx);
|
||||
|
||||
// Don't render user messages that are just there for returning tool results.
|
||||
@@ -918,18 +943,32 @@ impl ActiveThread {
|
||||
.child(self.render_message_content(message_id, rendered_message, cx))
|
||||
},
|
||||
)
|
||||
.when_some(context, |parent, context| {
|
||||
if !context.is_empty() {
|
||||
parent.child(
|
||||
h_flex().flex_wrap().gap_1().children(
|
||||
context
|
||||
.into_iter()
|
||||
.map(|context| ContextPill::added(context, false, false, None)),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
parent
|
||||
}
|
||||
.when(!context.is_empty(), |parent| {
|
||||
parent.child(
|
||||
h_flex()
|
||||
.flex_wrap()
|
||||
.gap_1()
|
||||
.children(context.into_iter().map(|context| {
|
||||
let context_id = context.id();
|
||||
ContextPill::added(AddedContext::new(context, cx), false, false, None)
|
||||
.on_click(Rc::new(cx.listener({
|
||||
let workspace = workspace.clone();
|
||||
let context_store = context_store.clone();
|
||||
move |_, _, window, cx| {
|
||||
if let Some(workspace) = workspace.upgrade() {
|
||||
open_context(
|
||||
context_id,
|
||||
context_store.clone(),
|
||||
workspace,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
})))
|
||||
})),
|
||||
)
|
||||
});
|
||||
|
||||
let styled_message = match message.role {
|
||||
@@ -1226,7 +1265,7 @@ impl ActiveThread {
|
||||
|
||||
let editor_bg = cx.theme().colors().editor_background;
|
||||
|
||||
div().py_2().child(
|
||||
div().pt_0p5().pb_2().child(
|
||||
v_flex()
|
||||
.rounded_lg()
|
||||
.border_1()
|
||||
@@ -1370,216 +1409,298 @@ impl ActiveThread {
|
||||
)
|
||||
}
|
||||
|
||||
fn render_tool_use(&self, tool_use: ToolUse, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn render_tool_use(
|
||||
&self,
|
||||
tool_use: ToolUse,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement + use<> {
|
||||
let is_open = self
|
||||
.expanded_tool_uses
|
||||
.get(&tool_use.id)
|
||||
.copied()
|
||||
.unwrap_or_default();
|
||||
|
||||
div().py_2().child(
|
||||
v_flex()
|
||||
.rounded_lg()
|
||||
.border_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
h_flex()
|
||||
.group("disclosure-header")
|
||||
.relative()
|
||||
.gap_1p5()
|
||||
.justify_between()
|
||||
.py_1()
|
||||
.px_2()
|
||||
.bg(self.tool_card_header_bg(cx))
|
||||
.map(|element| {
|
||||
if is_open {
|
||||
element.border_b_1().rounded_t_md()
|
||||
} else {
|
||||
element.rounded_md()
|
||||
}
|
||||
})
|
||||
let status_icons = div().child({
|
||||
let (icon_name, color, animated) = match &tool_use.status {
|
||||
ToolUseStatus::Pending | ToolUseStatus::NeedsConfirmation => {
|
||||
(IconName::Warning, Color::Warning, false)
|
||||
}
|
||||
ToolUseStatus::Running => (IconName::ArrowCircle, Color::Accent, true),
|
||||
ToolUseStatus::Finished(_) => (IconName::Check, Color::Success, false),
|
||||
ToolUseStatus::Error(_) => (IconName::Close, Color::Error, false),
|
||||
};
|
||||
|
||||
let icon = Icon::new(icon_name).color(color).size(IconSize::Small);
|
||||
|
||||
if animated {
|
||||
icon.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||
)
|
||||
.into_any_element()
|
||||
} else {
|
||||
icon.into_any_element()
|
||||
}
|
||||
});
|
||||
|
||||
let content_container = || v_flex().py_1().gap_0p5().px_2p5();
|
||||
let results_content = v_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
content_container()
|
||||
.child(
|
||||
Label::new("Input")
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
)
|
||||
.child(
|
||||
Label::new(
|
||||
serde_json::to_string_pretty(&tool_use.input).unwrap_or_default(),
|
||||
)
|
||||
.size(LabelSize::Small)
|
||||
.buffer_font(cx),
|
||||
),
|
||||
)
|
||||
.map(|container| match tool_use.status {
|
||||
ToolUseStatus::Finished(output) => container.child(
|
||||
content_container()
|
||||
.border_t_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.child(
|
||||
h_flex()
|
||||
.id("tool-label-container")
|
||||
.relative()
|
||||
.gap_1p5()
|
||||
.max_w_full()
|
||||
.overflow_x_scroll()
|
||||
.child(
|
||||
Icon::new(tool_use.icon)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(h_flex().pr_8().text_ui_sm(cx).children(
|
||||
self.rendered_tool_use_labels.get(&tool_use.id).cloned(),
|
||||
)),
|
||||
Label::new("Result")
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
div().visible_on_hover("disclosure-header").child(
|
||||
Disclosure::new("tool-use-disclosure", is_open)
|
||||
.opened_icon(IconName::ChevronUp)
|
||||
.closed_icon(IconName::ChevronDown)
|
||||
.on_click(cx.listener({
|
||||
let tool_use_id = tool_use.id.clone();
|
||||
move |this, _event, _window, _cx| {
|
||||
let is_open = this
|
||||
.expanded_tool_uses
|
||||
.entry(tool_use_id.clone())
|
||||
.or_insert(false);
|
||||
|
||||
*is_open = !*is_open;
|
||||
}
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child({
|
||||
let (icon_name, color, animated) = match &tool_use.status {
|
||||
ToolUseStatus::Pending
|
||||
| ToolUseStatus::NeedsConfirmation => {
|
||||
(IconName::Warning, Color::Warning, false)
|
||||
}
|
||||
ToolUseStatus::Running => {
|
||||
(IconName::ArrowCircle, Color::Accent, true)
|
||||
}
|
||||
ToolUseStatus::Finished(_) => {
|
||||
(IconName::Check, Color::Success, false)
|
||||
}
|
||||
ToolUseStatus::Error(_) => {
|
||||
(IconName::Close, Color::Error, false)
|
||||
}
|
||||
};
|
||||
|
||||
let icon =
|
||||
Icon::new(icon_name).color(color).size(IconSize::Small);
|
||||
|
||||
if animated {
|
||||
icon.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| {
|
||||
icon.transform(Transformation::rotate(percentage(
|
||||
delta,
|
||||
)))
|
||||
},
|
||||
)
|
||||
.into_any_element()
|
||||
} else {
|
||||
icon.into_any_element()
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(div().h_full().absolute().w_8().bottom_0().right_12().bg(
|
||||
linear_gradient(
|
||||
90.,
|
||||
linear_color_stop(self.tool_card_header_bg(cx), 1.),
|
||||
linear_color_stop(self.tool_card_header_bg(cx).opacity(0.2), 0.),
|
||||
),
|
||||
)),
|
||||
)
|
||||
.map(|parent| {
|
||||
if !is_open {
|
||||
return parent;
|
||||
}
|
||||
|
||||
let content_container = || v_flex().py_1().gap_0p5().px_2p5();
|
||||
|
||||
parent.child(
|
||||
v_flex()
|
||||
.child(Label::new(output).size(LabelSize::Small).buffer_font(cx)),
|
||||
),
|
||||
ToolUseStatus::Running => container.child(
|
||||
content_container().child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.rounded_b_lg()
|
||||
.pb_1()
|
||||
.border_t_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.child(
|
||||
content_container()
|
||||
.border_b_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.child(
|
||||
Label::new("Input")
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
)
|
||||
.child(
|
||||
Label::new(
|
||||
serde_json::to_string_pretty(&tool_use.input)
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.size(LabelSize::Small)
|
||||
.buffer_font(cx),
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Accent)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| {
|
||||
icon.transform(Transformation::rotate(percentage(
|
||||
delta,
|
||||
)))
|
||||
},
|
||||
),
|
||||
)
|
||||
.map(|container| match tool_use.status {
|
||||
ToolUseStatus::Finished(output) => container.child(
|
||||
content_container()
|
||||
.child(
|
||||
Label::new("Running…")
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
),
|
||||
),
|
||||
),
|
||||
ToolUseStatus::Error(err) => container.child(
|
||||
content_container()
|
||||
.border_t_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.child(
|
||||
Label::new("Error")
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
)
|
||||
.child(Label::new(err).size(LabelSize::Small).buffer_font(cx)),
|
||||
),
|
||||
ToolUseStatus::Pending => container,
|
||||
ToolUseStatus::NeedsConfirmation => container.child(
|
||||
content_container()
|
||||
.border_t_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.child(
|
||||
Label::new("Asking Permission")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
),
|
||||
),
|
||||
});
|
||||
|
||||
fn gradient_overlay(color: Hsla) -> impl IntoElement {
|
||||
div()
|
||||
.h_full()
|
||||
.absolute()
|
||||
.w_8()
|
||||
.bottom_0()
|
||||
.right_12()
|
||||
.bg(linear_gradient(
|
||||
90.,
|
||||
linear_color_stop(color, 1.),
|
||||
linear_color_stop(color.opacity(0.2), 0.),
|
||||
))
|
||||
}
|
||||
|
||||
div().map(|this| {
|
||||
if !tool_use.needs_confirmation {
|
||||
this.py_2p5().child(
|
||||
v_flex()
|
||||
.child(
|
||||
h_flex()
|
||||
.group("disclosure-header")
|
||||
.relative()
|
||||
.gap_1p5()
|
||||
.justify_between()
|
||||
.opacity(0.8)
|
||||
.hover(|style| style.opacity(1.))
|
||||
.pr_2()
|
||||
.child(
|
||||
h_flex()
|
||||
.id("tool-label-container")
|
||||
.gap_1p5()
|
||||
.max_w_full()
|
||||
.overflow_x_scroll()
|
||||
.child(
|
||||
Label::new("Result")
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
Icon::new(tool_use.icon)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Label::new(output)
|
||||
.size(LabelSize::Small)
|
||||
.buffer_font(cx),
|
||||
),
|
||||
),
|
||||
ToolUseStatus::Running => container.child(
|
||||
content_container().child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.pb_1()
|
||||
.child(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Accent)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat(),
|
||||
|icon, delta| {
|
||||
icon.transform(Transformation::rotate(
|
||||
percentage(delta),
|
||||
))
|
||||
},
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Label::new("Running…")
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
h_flex().pr_8().text_ui_sm(cx).children(
|
||||
self.rendered_tool_use_labels
|
||||
.get(&tool_use.id)
|
||||
.cloned(),
|
||||
),
|
||||
),
|
||||
),
|
||||
ToolUseStatus::Error(err) => container.child(
|
||||
content_container()
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new("Error")
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
div().visible_on_hover("disclosure-header").child(
|
||||
Disclosure::new("tool-use-disclosure", is_open)
|
||||
.opened_icon(IconName::ChevronUp)
|
||||
.closed_icon(IconName::ChevronDown)
|
||||
.on_click(cx.listener({
|
||||
let tool_use_id = tool_use.id.clone();
|
||||
move |this, _event, _window, _cx| {
|
||||
let is_open = this
|
||||
.expanded_tool_uses
|
||||
.entry(tool_use_id.clone())
|
||||
.or_insert(false);
|
||||
|
||||
*is_open = !*is_open;
|
||||
}
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(status_icons),
|
||||
)
|
||||
.child(gradient_overlay(cx.theme().colors().panel_background)),
|
||||
)
|
||||
.map(|parent| {
|
||||
if !is_open {
|
||||
return parent;
|
||||
}
|
||||
|
||||
parent.child(
|
||||
v_flex()
|
||||
.mt_1()
|
||||
.border_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.rounded_lg()
|
||||
.child(results_content),
|
||||
)
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
this.py_2().child(
|
||||
v_flex()
|
||||
.rounded_lg()
|
||||
.border_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
h_flex()
|
||||
.group("disclosure-header")
|
||||
.relative()
|
||||
.gap_1p5()
|
||||
.justify_between()
|
||||
.py_1()
|
||||
.px_2()
|
||||
.bg(self.tool_card_header_bg(cx))
|
||||
.map(|element| {
|
||||
if is_open {
|
||||
element.border_b_1().rounded_t_md()
|
||||
} else {
|
||||
element.rounded_md()
|
||||
}
|
||||
})
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.child(
|
||||
h_flex()
|
||||
.id("tool-label-container")
|
||||
.gap_1p5()
|
||||
.max_w_full()
|
||||
.overflow_x_scroll()
|
||||
.child(
|
||||
Icon::new(tool_use.icon)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Label::new(err).size(LabelSize::Small).buffer_font(cx),
|
||||
h_flex().pr_8().text_ui_sm(cx).children(
|
||||
self.rendered_tool_use_labels
|
||||
.get(&tool_use.id)
|
||||
.cloned(),
|
||||
),
|
||||
),
|
||||
),
|
||||
ToolUseStatus::Pending => container,
|
||||
ToolUseStatus::NeedsConfirmation => container.child(
|
||||
content_container().child(
|
||||
Label::new("Asking Permission")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
),
|
||||
),
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
div().visible_on_hover("disclosure-header").child(
|
||||
Disclosure::new("tool-use-disclosure", is_open)
|
||||
.opened_icon(IconName::ChevronUp)
|
||||
.closed_icon(IconName::ChevronDown)
|
||||
.on_click(cx.listener({
|
||||
let tool_use_id = tool_use.id.clone();
|
||||
move |this, _event, _window, _cx| {
|
||||
let is_open = this
|
||||
.expanded_tool_uses
|
||||
.entry(tool_use_id.clone())
|
||||
.or_insert(false);
|
||||
|
||||
*is_open = !*is_open;
|
||||
}
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(status_icons),
|
||||
)
|
||||
.child(gradient_overlay(self.tool_card_header_bg(cx))),
|
||||
)
|
||||
.map(|parent| {
|
||||
if !is_open {
|
||||
return parent;
|
||||
}
|
||||
|
||||
parent.child(
|
||||
v_flex()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.rounded_b_lg()
|
||||
.child(results_content),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn render_rules_item(&self, cx: &Context<Self>) -> AnyElement {
|
||||
@@ -1667,12 +1788,13 @@ impl ActiveThread {
|
||||
fn handle_deny_tool(
|
||||
&mut self,
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
tool_name: Arc<str>,
|
||||
_: &ClickEvent,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.deny_tool_use(tool_use_id, cx);
|
||||
thread.deny_tool_use(tool_use_id, tool_name, cx);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1745,10 +1867,12 @@ impl ActiveThread {
|
||||
})
|
||||
.child({
|
||||
let tool_id = tool.id.clone();
|
||||
let tool_name = tool.name.clone();
|
||||
Button::new("deny-tool", "Deny").on_click(cx.listener(
|
||||
move |this, event, window, cx| {
|
||||
this.handle_deny_tool(
|
||||
tool_id.clone(),
|
||||
tool_name.clone(),
|
||||
event,
|
||||
window,
|
||||
cx,
|
||||
@@ -1767,7 +1891,7 @@ impl ActiveThread {
|
||||
})
|
||||
}
|
||||
|
||||
fn dismiss_notifications(&mut self, cx: &mut Context<'_, ActiveThread>) {
|
||||
fn dismiss_notifications(&mut self, cx: &mut Context<ActiveThread>) {
|
||||
for window in self.notifications.drain(..) {
|
||||
window
|
||||
.update(cx, |_, window, _| {
|
||||
@@ -1823,3 +1947,93 @@ impl Render for ActiveThread {
|
||||
.child(self.render_vertical_scrollbar(cx))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn open_context(
|
||||
id: ContextId,
|
||||
context_store: Entity<ContextStore>,
|
||||
workspace: Entity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let Some(context) = context_store.read(cx).context_for_id(id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
match context {
|
||||
AssistantContext::File(file_context) => {
|
||||
if let Some(project_path) = file_context.context_buffer.buffer.read(cx).project_path(cx)
|
||||
{
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.open_path(project_path, None, true, window, cx)
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
AssistantContext::Directory(directory_context) => {
|
||||
let path = directory_context.project_path.clone();
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.project().update(cx, |project, cx| {
|
||||
if let Some(entry) = project.entry_for_path(&path, cx) {
|
||||
cx.emit(project::Event::RevealInProjectPanel(entry.id));
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
AssistantContext::Symbol(symbol_context) => {
|
||||
if let Some(project_path) = symbol_context
|
||||
.context_symbol
|
||||
.buffer
|
||||
.read(cx)
|
||||
.project_path(cx)
|
||||
{
|
||||
let snapshot = symbol_context.context_symbol.buffer.read(cx).snapshot();
|
||||
let target_position = symbol_context
|
||||
.context_symbol
|
||||
.id
|
||||
.range
|
||||
.start
|
||||
.to_point(&snapshot);
|
||||
|
||||
let open_task = workspace.update(cx, |workspace, cx| {
|
||||
workspace.open_path(project_path, None, true, window, cx)
|
||||
});
|
||||
window
|
||||
.spawn(cx, async move |cx| {
|
||||
if let Some(active_editor) = open_task
|
||||
.await
|
||||
.log_err()
|
||||
.and_then(|item| item.downcast::<Editor>())
|
||||
{
|
||||
active_editor
|
||||
.downgrade()
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
editor.go_to_singleton_buffer_point(
|
||||
target_position,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
AssistantContext::FetchedUrl(fetched_url_context) => {
|
||||
cx.open_url(&fetched_url_context.url);
|
||||
}
|
||||
AssistantContext::Thread(thread_context) => {
|
||||
let thread_id = thread_context.thread.read(cx).id().clone();
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel
|
||||
.open_thread(&thread_id, window, cx)
|
||||
.detach_and_log_err(cx)
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
mod active_thread;
|
||||
mod assistant_configuration;
|
||||
mod assistant_diff;
|
||||
mod assistant_model_selector;
|
||||
mod assistant_panel;
|
||||
mod buffer_codegen;
|
||||
@@ -27,8 +28,10 @@ use client::Client;
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt};
|
||||
use fs::Fs;
|
||||
use gpui::{actions, App};
|
||||
use gpui::{App, actions, impl_actions};
|
||||
use prompt_store::PromptBuilder;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use settings::Settings as _;
|
||||
|
||||
pub use crate::active_thread::ActiveThread;
|
||||
@@ -37,6 +40,7 @@ pub use crate::assistant_panel::{AssistantPanel, ConcreteAssistantPanelDelegate}
|
||||
pub use crate::inline_assistant::InlineAssistant;
|
||||
pub use crate::thread::{Message, RequestKind, Thread, ThreadEvent};
|
||||
pub use crate::thread_store::ThreadStore;
|
||||
pub use assistant_diff::{AssistantDiff, AssistantDiffToolbar};
|
||||
|
||||
actions!(
|
||||
assistant2,
|
||||
@@ -48,7 +52,6 @@ actions!(
|
||||
RemoveAllContext,
|
||||
OpenHistory,
|
||||
OpenConfiguration,
|
||||
ManageProfiles,
|
||||
AddContextServer,
|
||||
RemoveSelectedThread,
|
||||
Chat,
|
||||
@@ -61,10 +64,31 @@ actions!(
|
||||
FocusRight,
|
||||
RemoveFocusedContext,
|
||||
AcceptSuggestedContext,
|
||||
OpenActiveThreadAsMarkdown
|
||||
OpenActiveThreadAsMarkdown,
|
||||
OpenAssistantDiff,
|
||||
ToggleKeep,
|
||||
Reject,
|
||||
RejectAll,
|
||||
KeepAll
|
||||
]
|
||||
);
|
||||
|
||||
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema)]
|
||||
pub struct ManageProfiles {
|
||||
#[serde(default)]
|
||||
pub customize_tools: Option<Arc<str>>,
|
||||
}
|
||||
|
||||
impl ManageProfiles {
|
||||
pub fn customize_tools(profile_id: Arc<str>) -> Self {
|
||||
Self {
|
||||
customize_tools: Some(profile_id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl_actions!(assistant, [ManageProfiles]);
|
||||
|
||||
const NAMESPACE: &str = "assistant2";
|
||||
|
||||
/// Initializes the `assistant2` crate.
|
||||
|
||||
@@ -9,7 +9,7 @@ use collections::HashMap;
|
||||
use context_server::manager::ContextServerManager;
|
||||
use gpui::{Action, AnyView, App, Entity, EventEmitter, FocusHandle, Focusable, Subscription};
|
||||
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
|
||||
use ui::{prelude::*, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, Switch};
|
||||
use ui::{Disclosure, Divider, DividerColor, ElevationIndex, Indicator, Switch, prelude::*};
|
||||
use util::ResultExt as _;
|
||||
use zed_actions::ExtensionCategoryFilter;
|
||||
|
||||
@@ -105,7 +105,7 @@ impl AssistantConfiguration {
|
||||
&mut self,
|
||||
provider: &Arc<dyn LanguageModelProvider>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
) -> impl IntoElement + use<> {
|
||||
let provider_id = provider.id().0.clone();
|
||||
let provider_name = provider.name().0.clone();
|
||||
let configuration_view = self
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use context_server::{ContextServerSettings, ServerCommand, ServerConfig};
|
||||
use editor::Editor;
|
||||
use gpui::{prelude::*, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity};
|
||||
use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity, prelude::*};
|
||||
use serde_json::json;
|
||||
use settings::update_settings_file;
|
||||
use ui::{prelude::*, Modal, ModalFooter, ModalHeader, Section, Tooltip};
|
||||
use ui::{Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*};
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
use crate::AddContextServer;
|
||||
|
||||
@@ -2,22 +2,20 @@ mod profile_modal_header;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_settings::{
|
||||
AgentProfile, AgentProfileContent, AssistantSettings, AssistantSettingsContent,
|
||||
ContextServerPresetContent, VersionedAssistantSettingsContent,
|
||||
};
|
||||
use assistant_settings::{AgentProfile, AssistantSettings};
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use convert_case::{Case, Casing as _};
|
||||
use editor::Editor;
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
prelude::*, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription,
|
||||
WeakEntity,
|
||||
DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription, WeakEntity,
|
||||
prelude::*,
|
||||
};
|
||||
use settings::{update_settings_file, Settings as _};
|
||||
use settings::{Settings as _, update_settings_file};
|
||||
use ui::{
|
||||
prelude::*, KeyBinding, ListItem, ListItemSpacing, ListSeparator, Navigable, NavigableEntry,
|
||||
KeyBinding, ListItem, ListItemSpacing, ListSeparator, Navigable, NavigableEntry, prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
use crate::assistant_configuration::manage_profiles_modal::profile_modal_header::ProfileModalHeader;
|
||||
@@ -98,14 +96,20 @@ impl ManageProfilesModal {
|
||||
_window: Option<&mut Window>,
|
||||
_cx: &mut Context<Workspace>,
|
||||
) {
|
||||
workspace.register_action(|workspace, _: &ManageProfiles, window, cx| {
|
||||
workspace.register_action(|workspace, action: &ManageProfiles, window, 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();
|
||||
let thread_store = thread_store.downgrade();
|
||||
workspace.toggle_modal(window, cx, |window, cx| {
|
||||
Self::new(fs, tools, thread_store, window, cx)
|
||||
let mut this = Self::new(fs, tools, thread_store, window, cx);
|
||||
|
||||
if let Some(profile_id) = action.customize_tools.clone() {
|
||||
this.configure_tools(profile_id, window, cx);
|
||||
}
|
||||
|
||||
this
|
||||
})
|
||||
}
|
||||
});
|
||||
@@ -255,37 +259,8 @@ impl ManageProfilesModal {
|
||||
|
||||
fn create_profile(&self, profile_id: Arc<str>, profile: AgentProfile, cx: &mut Context<Self>) {
|
||||
update_settings_file::<AssistantSettings>(self.fs.clone(), cx, {
|
||||
move |settings, _cx| match settings {
|
||||
AssistantSettingsContent::Versioned(VersionedAssistantSettingsContent::V2(
|
||||
settings,
|
||||
)) => {
|
||||
let profiles = settings.profiles.get_or_insert_default();
|
||||
if profiles.contains_key(&profile_id) {
|
||||
log::error!("profile with ID '{profile_id}' already exists");
|
||||
return;
|
||||
}
|
||||
|
||||
profiles.insert(
|
||||
profile_id,
|
||||
AgentProfileContent {
|
||||
name: profile.name.into(),
|
||||
tools: profile.tools,
|
||||
context_servers: profile
|
||||
.context_servers
|
||||
.into_iter()
|
||||
.map(|(server_id, preset)| {
|
||||
(
|
||||
server_id,
|
||||
ContextServerPresetContent {
|
||||
tools: preset.tools,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
move |settings, _cx| {
|
||||
settings.create_profile(profile_id, profile).log_err();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -363,7 +338,7 @@ impl ManageProfilesModal {
|
||||
.on_click({
|
||||
let profile_id = profile.id.clone();
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
this.new_profile(Some(profile_id.clone()), window, cx);
|
||||
this.view_profile(profile_id.clone(), window, cx);
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -6,11 +6,11 @@ use assistant_settings::{
|
||||
};
|
||||
use assistant_tool::{ToolSource, ToolWorkingSet};
|
||||
use fs::Fs;
|
||||
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
|
||||
use gpui::{App, Context, DismissEvent, Entity, EventEmitter, Focusable, Task, WeakEntity, Window};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use settings::{update_settings_file, Settings as _};
|
||||
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
|
||||
use settings::{Settings as _, update_settings_file};
|
||||
use ui::{HighlightedLabel, ListItem, ListItemSpacing, prelude::*};
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::ThreadStore;
|
||||
|
||||
772
crates/assistant2/src/assistant_diff.rs
Normal file
772
crates/assistant2/src/assistant_diff.rs
Normal file
@@ -0,0 +1,772 @@
|
||||
use crate::{Thread, ThreadEvent, ToggleKeep};
|
||||
use anyhow::Result;
|
||||
use buffer_diff::DiffHunkStatus;
|
||||
use collections::HashSet;
|
||||
use editor::{
|
||||
Direction, Editor, EditorEvent, MultiBuffer, ToPoint,
|
||||
actions::{GoToHunk, GoToPreviousHunk},
|
||||
};
|
||||
use gpui::{
|
||||
Action, AnyElement, AnyView, App, Entity, EventEmitter, FocusHandle, Focusable, SharedString,
|
||||
Subscription, Task, WeakEntity, Window, prelude::*,
|
||||
};
|
||||
use language::{Capability, DiskState, OffsetRangeExt, Point};
|
||||
use multi_buffer::PathKey;
|
||||
use project::{Project, ProjectPath};
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
ops::Range,
|
||||
sync::Arc,
|
||||
};
|
||||
use ui::{IconButtonShape, KeyBinding, Tooltip, prelude::*};
|
||||
use workspace::{
|
||||
Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
|
||||
Workspace,
|
||||
item::{BreadcrumbText, ItemEvent, TabContentParams},
|
||||
searchable::SearchableItemHandle,
|
||||
};
|
||||
|
||||
pub struct AssistantDiff {
|
||||
multibuffer: Entity<MultiBuffer>,
|
||||
editor: Entity<Editor>,
|
||||
thread: Entity<Thread>,
|
||||
focus_handle: FocusHandle,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
title: SharedString,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl AssistantDiff {
|
||||
pub fn deploy(
|
||||
thread: Entity<Thread>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Result<()> {
|
||||
let existing_diff = workspace.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.items_of_type::<AssistantDiff>(cx)
|
||||
.find(|diff| diff.read(cx).thread == thread)
|
||||
})?;
|
||||
if let Some(existing_diff) = existing_diff {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.activate_item(&existing_diff, true, true, window, cx);
|
||||
})
|
||||
} else {
|
||||
let assistant_diff =
|
||||
cx.new(|cx| AssistantDiff::new(thread.clone(), workspace.clone(), window, cx));
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.add_item_to_center(Box::new(assistant_diff), window, cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
thread: Entity<Thread>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let focus_handle = cx.focus_handle();
|
||||
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
|
||||
|
||||
let project = thread.read(cx).project().clone();
|
||||
let render_diff_hunk_controls = Arc::new({
|
||||
let assistant_diff = cx.entity();
|
||||
move |row,
|
||||
status: &DiffHunkStatus,
|
||||
hunk_range,
|
||||
is_created_file,
|
||||
line_height,
|
||||
_editor: &Entity<Editor>,
|
||||
window: &mut Window,
|
||||
cx: &mut App| {
|
||||
render_diff_hunk_controls(
|
||||
row,
|
||||
status,
|
||||
hunk_range,
|
||||
is_created_file,
|
||||
line_height,
|
||||
&assistant_diff,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
});
|
||||
let editor = cx.new(|cx| {
|
||||
let mut editor =
|
||||
Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);
|
||||
editor.disable_inline_diagnostics();
|
||||
editor.set_expand_all_diff_hunks(cx);
|
||||
editor.set_render_diff_hunk_controls(render_diff_hunk_controls, cx);
|
||||
editor.register_addon(AssistantDiffAddon);
|
||||
editor
|
||||
});
|
||||
|
||||
let action_log = thread.read(cx).action_log().clone();
|
||||
let mut this = Self {
|
||||
_subscriptions: vec![
|
||||
cx.observe_in(&action_log, window, |this, _action_log, window, cx| {
|
||||
this.update_excerpts(window, cx)
|
||||
}),
|
||||
cx.subscribe(&thread, |this, _thread, event, cx| {
|
||||
this.handle_thread_event(event, cx)
|
||||
}),
|
||||
],
|
||||
title: SharedString::default(),
|
||||
multibuffer,
|
||||
editor,
|
||||
thread,
|
||||
focus_handle,
|
||||
workspace,
|
||||
};
|
||||
this.update_excerpts(window, cx);
|
||||
this.update_title(cx);
|
||||
this
|
||||
}
|
||||
|
||||
fn update_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let thread = self.thread.read(cx);
|
||||
let changed_buffers = thread.action_log().read(cx).changed_buffers(cx);
|
||||
let mut paths_to_delete = self.multibuffer.read(cx).paths().collect::<HashSet<_>>();
|
||||
|
||||
for (buffer, changed) in changed_buffers {
|
||||
let Some(file) = buffer.read(cx).file().cloned() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let path_key = PathKey::namespaced("", file.full_path(cx).into());
|
||||
paths_to_delete.remove(&path_key);
|
||||
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let diff = changed.diff.read(cx);
|
||||
let diff_hunk_ranges = diff
|
||||
.hunks_intersecting_range(
|
||||
language::Anchor::MIN..language::Anchor::MAX,
|
||||
&snapshot,
|
||||
cx,
|
||||
)
|
||||
.map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let (was_empty, is_excerpt_newly_added) =
|
||||
self.multibuffer.update(cx, |multibuffer, cx| {
|
||||
let was_empty = multibuffer.is_empty();
|
||||
let is_excerpt_newly_added = multibuffer.set_excerpts_for_path(
|
||||
path_key.clone(),
|
||||
buffer.clone(),
|
||||
diff_hunk_ranges,
|
||||
editor::DEFAULT_MULTIBUFFER_CONTEXT,
|
||||
cx,
|
||||
);
|
||||
multibuffer.add_diff(changed.diff.clone(), cx);
|
||||
(was_empty, is_excerpt_newly_added)
|
||||
});
|
||||
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
if was_empty {
|
||||
editor.change_selections(None, window, cx, |selections| {
|
||||
selections.select_ranges([0..0])
|
||||
});
|
||||
}
|
||||
|
||||
if is_excerpt_newly_added
|
||||
&& buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.map_or(false, |file| file.disk_state() == DiskState::Deleted)
|
||||
{
|
||||
editor.fold_buffer(snapshot.text.remote_id(), cx)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
self.multibuffer.update(cx, |multibuffer, cx| {
|
||||
for path in paths_to_delete {
|
||||
multibuffer.remove_excerpts_for_path(path, cx);
|
||||
}
|
||||
});
|
||||
|
||||
if self.multibuffer.read(cx).is_empty()
|
||||
&& self
|
||||
.editor
|
||||
.read(cx)
|
||||
.focus_handle(cx)
|
||||
.contains_focused(window, cx)
|
||||
{
|
||||
self.focus_handle.focus(window);
|
||||
} else if self.focus_handle.is_focused(window) && !self.multibuffer.read(cx).is_empty() {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.focus_handle(cx).focus(window);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn update_title(&mut self, cx: &mut Context<Self>) {
|
||||
let new_title = self
|
||||
.thread
|
||||
.read(cx)
|
||||
.summary()
|
||||
.unwrap_or("Assistant Changes".into());
|
||||
if new_title != self.title {
|
||||
self.title = new_title;
|
||||
cx.emit(EditorEvent::TitleChanged);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_thread_event(&mut self, event: &ThreadEvent, cx: &mut Context<Self>) {
|
||||
match event {
|
||||
ThreadEvent::SummaryChanged => self.update_title(cx),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_keep(&mut self, _: &crate::ToggleKeep, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
let ranges = self
|
||||
.editor
|
||||
.read(cx)
|
||||
.selections
|
||||
.disjoint_anchor_ranges()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let snapshot = self.multibuffer.read(cx).snapshot(cx);
|
||||
let diff_hunks_in_ranges = self
|
||||
.editor
|
||||
.read(cx)
|
||||
.diff_hunks_in_ranges(&ranges, &snapshot)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for hunk in diff_hunks_in_ranges {
|
||||
let buffer = self.multibuffer.read(cx).buffer(hunk.buffer_id);
|
||||
if let Some(buffer) = buffer {
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
let accept = hunk.status().has_secondary_hunk();
|
||||
thread.review_edits_in_range(buffer, hunk.buffer_range, accept, cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn reject(&mut self, _: &crate::Reject, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let ranges = self
|
||||
.editor
|
||||
.update(cx, |editor, cx| editor.selections.ranges(cx));
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.restore_hunks_in_ranges(ranges, window, cx)
|
||||
})
|
||||
}
|
||||
|
||||
fn reject_all(&mut self, _: &crate::RejectAll, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let max_point = editor.buffer().read(cx).read(cx).max_point();
|
||||
editor.restore_hunks_in_ranges(vec![Point::zero()..max_point], window, cx)
|
||||
})
|
||||
}
|
||||
|
||||
fn keep_all(&mut self, _: &crate::KeepAll, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.thread
|
||||
.update(cx, |thread, cx| thread.keep_all_edits(cx));
|
||||
}
|
||||
|
||||
fn review_diff_hunks(
|
||||
&mut self,
|
||||
hunk_ranges: Vec<Range<editor::Anchor>>,
|
||||
accept: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let snapshot = self.multibuffer.read(cx).snapshot(cx);
|
||||
let diff_hunks_in_ranges = self
|
||||
.editor
|
||||
.read(cx)
|
||||
.diff_hunks_in_ranges(&hunk_ranges, &snapshot)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for hunk in diff_hunks_in_ranges {
|
||||
let buffer = self.multibuffer.read(cx).buffer(hunk.buffer_id);
|
||||
if let Some(buffer) = buffer {
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.review_edits_in_range(buffer, hunk.buffer_range, accept, cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<EditorEvent> for AssistantDiff {}
|
||||
|
||||
impl Focusable for AssistantDiff {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
if self.multibuffer.read(cx).is_empty() {
|
||||
self.focus_handle.clone()
|
||||
} else {
|
||||
self.editor.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Item for AssistantDiff {
|
||||
type Event = EditorEvent;
|
||||
|
||||
fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
|
||||
Some(Icon::new(IconName::ZedAssistant).color(Color::Muted))
|
||||
}
|
||||
|
||||
fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
|
||||
Editor::to_item_events(event, f)
|
||||
}
|
||||
|
||||
fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| editor.deactivated(window, cx));
|
||||
}
|
||||
|
||||
fn navigate(
|
||||
&mut self,
|
||||
data: Box<dyn Any>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| editor.navigate(data, window, cx))
|
||||
}
|
||||
|
||||
fn tab_tooltip_text(&self, _: &App) -> Option<SharedString> {
|
||||
Some("Assistant Diff".into())
|
||||
}
|
||||
|
||||
fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
|
||||
let summary = self
|
||||
.thread
|
||||
.read(cx)
|
||||
.summary()
|
||||
.unwrap_or("Assistant Changes".into());
|
||||
Label::new(format!("Review: {}", summary))
|
||||
.color(if params.selected {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn telemetry_event_text(&self) -> Option<&'static str> {
|
||||
Some("Assistant Diff Opened")
|
||||
}
|
||||
|
||||
fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
|
||||
Some(Box::new(self.editor.clone()))
|
||||
}
|
||||
|
||||
fn for_each_project_item(
|
||||
&self,
|
||||
cx: &App,
|
||||
f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
|
||||
) {
|
||||
self.editor.for_each_project_item(cx, f)
|
||||
}
|
||||
|
||||
fn is_singleton(&self, _: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn set_nav_history(
|
||||
&mut self,
|
||||
nav_history: ItemNavHistory,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.editor.update(cx, |editor, _| {
|
||||
editor.set_nav_history(Some(nav_history));
|
||||
});
|
||||
}
|
||||
|
||||
fn clone_on_split(
|
||||
&self,
|
||||
_workspace_id: Option<workspace::WorkspaceId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Entity<Self>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Some(cx.new(|cx| Self::new(self.thread.clone(), self.workspace.clone(), window, cx)))
|
||||
}
|
||||
|
||||
fn is_dirty(&self, cx: &App) -> bool {
|
||||
self.multibuffer.read(cx).is_dirty(cx)
|
||||
}
|
||||
|
||||
fn has_conflict(&self, cx: &App) -> bool {
|
||||
self.multibuffer.read(cx).has_conflict(cx)
|
||||
}
|
||||
|
||||
fn can_save(&self, _: &App) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn save(
|
||||
&mut self,
|
||||
format: bool,
|
||||
project: Entity<Project>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.editor.save(format, project, window, cx)
|
||||
}
|
||||
|
||||
fn save_as(
|
||||
&mut self,
|
||||
_: Entity<Project>,
|
||||
_: ProjectPath,
|
||||
_window: &mut Window,
|
||||
_: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn reload(
|
||||
&mut self,
|
||||
project: Entity<Project>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.editor.reload(project, window, cx)
|
||||
}
|
||||
|
||||
fn act_as_type<'a>(
|
||||
&'a self,
|
||||
type_id: TypeId,
|
||||
self_handle: &'a Entity<Self>,
|
||||
_: &'a App,
|
||||
) -> Option<AnyView> {
|
||||
if type_id == TypeId::of::<Self>() {
|
||||
Some(self_handle.to_any())
|
||||
} else if type_id == TypeId::of::<Editor>() {
|
||||
Some(self.editor.to_any())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
|
||||
ToolbarItemLocation::PrimaryLeft
|
||||
}
|
||||
|
||||
fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
|
||||
self.editor.breadcrumbs(theme, cx)
|
||||
}
|
||||
|
||||
fn added_to_workspace(
|
||||
&mut self,
|
||||
workspace: &mut Workspace,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.added_to_workspace(workspace, window, cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AssistantDiff {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let is_empty = self.multibuffer.read(cx).is_empty();
|
||||
|
||||
div()
|
||||
.track_focus(&self.focus_handle)
|
||||
.key_context(if is_empty {
|
||||
"EmptyPane"
|
||||
} else {
|
||||
"AssistantDiff"
|
||||
})
|
||||
.on_action(cx.listener(Self::toggle_keep))
|
||||
.on_action(cx.listener(Self::reject))
|
||||
.on_action(cx.listener(Self::reject_all))
|
||||
.on_action(cx.listener(Self::keep_all))
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.size_full()
|
||||
.when(is_empty, |el| el.child("No changes to review"))
|
||||
.when(!is_empty, |el| el.child(self.editor.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
fn render_diff_hunk_controls(
|
||||
row: u32,
|
||||
status: &DiffHunkStatus,
|
||||
hunk_range: Range<editor::Anchor>,
|
||||
is_created_file: bool,
|
||||
line_height: Pixels,
|
||||
assistant_diff: &Entity<AssistantDiff>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> AnyElement {
|
||||
let editor = assistant_diff.read(cx).editor.clone();
|
||||
|
||||
h_flex()
|
||||
.h(line_height)
|
||||
.mr_0p5()
|
||||
.gap_1()
|
||||
.px_0p5()
|
||||
.pb_1()
|
||||
.border_x_1()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_b_md()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.gap_1()
|
||||
.occlude()
|
||||
.shadow_md()
|
||||
.children(if status.has_secondary_hunk() {
|
||||
vec![
|
||||
Button::new("reject", "Reject")
|
||||
.disabled(is_created_file)
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(
|
||||
&crate::Reject,
|
||||
&editor.read(cx).focus_handle(cx),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(12.))),
|
||||
)
|
||||
.on_click({
|
||||
let editor = editor.clone();
|
||||
move |_event, window, cx| {
|
||||
editor.update(cx, |editor, cx| {
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
let point = hunk_range.start.to_point(&snapshot.buffer_snapshot);
|
||||
editor.restore_hunks_in_ranges(vec![point..point], window, cx);
|
||||
});
|
||||
}
|
||||
}),
|
||||
Button::new(("keep", row as u64), "Keep")
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(
|
||||
&crate::ToggleKeep,
|
||||
&editor.read(cx).focus_handle(cx),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(12.))),
|
||||
)
|
||||
.on_click({
|
||||
let assistant_diff = assistant_diff.clone();
|
||||
move |_event, _window, cx| {
|
||||
assistant_diff.update(cx, |diff, cx| {
|
||||
diff.review_diff_hunks(
|
||||
vec![hunk_range.start..hunk_range.start],
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}),
|
||||
]
|
||||
} else {
|
||||
vec![
|
||||
Button::new(("review", row as u64), "Review")
|
||||
.key_binding(KeyBinding::for_action_in(
|
||||
&ToggleKeep,
|
||||
&editor.read(cx).focus_handle(cx),
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.on_click({
|
||||
let assistant_diff = assistant_diff.clone();
|
||||
move |_event, _window, cx| {
|
||||
assistant_diff.update(cx, |diff, cx| {
|
||||
diff.review_diff_hunks(
|
||||
vec![hunk_range.start..hunk_range.start],
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}),
|
||||
]
|
||||
})
|
||||
.when(
|
||||
!editor.read(cx).buffer().read(cx).all_diff_hunks_expanded(),
|
||||
|el| {
|
||||
el.child(
|
||||
IconButton::new(("next-hunk", row as u64), IconName::ArrowDown)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
// .disabled(!has_multiple_hunks)
|
||||
.tooltip({
|
||||
let focus_handle = editor.focus_handle(cx);
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Next Hunk",
|
||||
&GoToHunk,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click({
|
||||
let editor = editor.clone();
|
||||
move |_event, window, cx| {
|
||||
editor.update(cx, |editor, cx| {
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
let position =
|
||||
hunk_range.end.to_point(&snapshot.buffer_snapshot);
|
||||
editor.go_to_hunk_before_or_after_position(
|
||||
&snapshot,
|
||||
position,
|
||||
Direction::Next,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
editor.expand_selected_diff_hunks(cx);
|
||||
});
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
IconButton::new(("prev-hunk", row as u64), IconName::ArrowUp)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
// .disabled(!has_multiple_hunks)
|
||||
.tooltip({
|
||||
let focus_handle = editor.focus_handle(cx);
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Previous Hunk",
|
||||
&GoToPreviousHunk,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click({
|
||||
let editor = editor.clone();
|
||||
move |_event, window, cx| {
|
||||
editor.update(cx, |editor, cx| {
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
let point =
|
||||
hunk_range.start.to_point(&snapshot.buffer_snapshot);
|
||||
editor.go_to_hunk_before_or_after_position(
|
||||
&snapshot,
|
||||
point,
|
||||
Direction::Prev,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
editor.expand_selected_diff_hunks(cx);
|
||||
});
|
||||
}
|
||||
}),
|
||||
)
|
||||
},
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
struct AssistantDiffAddon;
|
||||
|
||||
impl editor::Addon for AssistantDiffAddon {
|
||||
fn to_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn extend_key_context(&self, key_context: &mut gpui::KeyContext, _: &App) {
|
||||
key_context.add("assistant_diff");
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AssistantDiffToolbar {
|
||||
assistant_diff: Option<WeakEntity<AssistantDiff>>,
|
||||
_workspace: WeakEntity<Workspace>,
|
||||
}
|
||||
|
||||
impl AssistantDiffToolbar {
|
||||
pub fn new(workspace: &Workspace, _: &mut Context<Self>) -> Self {
|
||||
Self {
|
||||
assistant_diff: None,
|
||||
_workspace: workspace.weak_handle(),
|
||||
}
|
||||
}
|
||||
|
||||
fn assistant_diff(&self, _: &App) -> Option<Entity<AssistantDiff>> {
|
||||
self.assistant_diff.as_ref()?.upgrade()
|
||||
}
|
||||
|
||||
fn dispatch_action(&self, action: &dyn Action, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(assistant_diff) = self.assistant_diff(cx) {
|
||||
assistant_diff.focus_handle(cx).focus(window);
|
||||
}
|
||||
let action = action.boxed_clone();
|
||||
cx.defer(move |cx| {
|
||||
cx.dispatch_action(action.as_ref());
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<ToolbarItemEvent> for AssistantDiffToolbar {}
|
||||
|
||||
impl ToolbarItemView for AssistantDiffToolbar {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
active_pane_item: Option<&dyn ItemHandle>,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> ToolbarItemLocation {
|
||||
self.assistant_diff = active_pane_item
|
||||
.and_then(|item| item.act_as::<AssistantDiff>(cx))
|
||||
.map(|entity| entity.downgrade());
|
||||
if self.assistant_diff.is_some() {
|
||||
ToolbarItemLocation::PrimaryRight
|
||||
} else {
|
||||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
}
|
||||
|
||||
fn pane_focus_update(
|
||||
&mut self,
|
||||
_pane_focused: bool,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AssistantDiffToolbar {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let assistant_diff = match self.assistant_diff(cx) {
|
||||
Some(ad) => ad,
|
||||
None => return div(),
|
||||
};
|
||||
|
||||
let is_empty = assistant_diff.read(cx).multibuffer.read(cx).is_empty();
|
||||
|
||||
if is_empty {
|
||||
return div();
|
||||
}
|
||||
|
||||
h_group_xl()
|
||||
.my_neg_1()
|
||||
.items_center()
|
||||
.p_1()
|
||||
.flex_wrap()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_group_sm()
|
||||
.child(
|
||||
Button::new("reject-all", "Reject All").on_click(cx.listener(
|
||||
|this, _, window, cx| {
|
||||
this.dispatch_action(&crate::RejectAll, window, cx)
|
||||
},
|
||||
)),
|
||||
)
|
||||
.child(Button::new("keep-all", "Keep All").on_click(cx.listener(
|
||||
|this, _, window, cx| this.dispatch_action(&crate::KeepAll, window, cx),
|
||||
))),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ use language_model_selector::{
|
||||
};
|
||||
use settings::update_settings_file;
|
||||
use std::sync::Arc;
|
||||
use ui::{prelude::*, ButtonLike, PopoverMenuHandle, Tooltip};
|
||||
use ui::{ButtonLike, PopoverMenuHandle, Tooltip, prelude::*};
|
||||
|
||||
pub struct AssistantModelSelector {
|
||||
selector: Entity<LanguageModelSelector>,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_context_editor::{
|
||||
make_lsp_adapter_delegate, render_remaining_tokens, AssistantPanelDelegate, ConfigurationError,
|
||||
ContextEditor, SlashCommandCompletionProvider,
|
||||
AssistantPanelDelegate, ConfigurationError, ContextEditor, SlashCommandCompletionProvider,
|
||||
make_lsp_adapter_delegate, render_remaining_tokens,
|
||||
};
|
||||
use assistant_settings::{AssistantDockPosition, AssistantSettings};
|
||||
use assistant_slash_command::SlashCommandWorkingSet;
|
||||
@@ -14,21 +14,22 @@ use client::zed_urls;
|
||||
use editor::{Editor, MultiBuffer};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
action_with_deprecated_aliases, prelude::*, Action, AnyElement, App, AsyncWindowContext,
|
||||
Corner, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext, Pixels,
|
||||
Subscription, Task, UpdateGlobal, WeakEntity,
|
||||
Action, AnyElement, App, AsyncWindowContext, Corner, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, FontWeight, KeyContext, Pixels, Subscription, Task, UpdateGlobal, WeakEntity,
|
||||
action_with_deprecated_aliases, prelude::*,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use language_model::{LanguageModelProviderTosView, LanguageModelRegistry};
|
||||
use language_model_selector::ToggleModelSelector;
|
||||
use project::Project;
|
||||
use prompt_library::{open_prompt_library, PromptLibrary};
|
||||
use prompt_library::{PromptLibrary, open_prompt_library};
|
||||
use prompt_store::PromptBuilder;
|
||||
use settings::{update_settings_file, Settings};
|
||||
use settings::{Settings, update_settings_file};
|
||||
use time::UtcOffset;
|
||||
use ui::{prelude::*, ContextMenu, KeyBinding, PopoverMenu, PopoverMenuHandle, Tab, Tooltip};
|
||||
use ui::{ContextMenu, KeyBinding, PopoverMenu, PopoverMenuHandle, Tab, Tooltip, prelude::*};
|
||||
use util::ResultExt as _;
|
||||
use workspace::dock::{DockPosition, Panel, PanelEvent};
|
||||
use workspace::Workspace;
|
||||
use workspace::dock::{DockPosition, Panel, PanelEvent};
|
||||
use zed_actions::assistant::ToggleFocus;
|
||||
|
||||
use crate::active_thread::ActiveThread;
|
||||
@@ -39,8 +40,8 @@ use crate::thread::{Thread, ThreadError, ThreadId};
|
||||
use crate::thread_history::{PastContext, PastThread, ThreadHistory};
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::{
|
||||
InlineAssistant, NewPromptEditor, NewThread, OpenActiveThreadAsMarkdown, OpenConfiguration,
|
||||
OpenHistory,
|
||||
AssistantDiff, InlineAssistant, NewPromptEditor, NewThread, OpenActiveThreadAsMarkdown,
|
||||
OpenAssistantDiff, OpenConfiguration, OpenHistory, ToggleContextPicker,
|
||||
};
|
||||
|
||||
action_with_deprecated_aliases!(
|
||||
@@ -65,6 +66,12 @@ pub fn init(cx: &mut App) {
|
||||
panel.update(cx, |panel, cx| panel.open_history(window, cx));
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &OpenConfiguration, 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, _: &NewPromptEditor, window, cx| {
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
workspace.focus_panel::<AssistantPanel>(window, cx);
|
||||
@@ -84,6 +91,14 @@ pub fn init(cx: &mut App) {
|
||||
workspace.focus_panel::<AssistantPanel>(window, cx);
|
||||
panel.update(cx, |panel, cx| panel.open_configuration(window, cx));
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &OpenAssistantDiff, window, cx| {
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
workspace.focus_panel::<AssistantPanel>(window, cx);
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.open_assistant_diff(&OpenAssistantDiff, window, cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
)
|
||||
@@ -113,7 +128,7 @@ pub struct AssistantPanel {
|
||||
active_view: ActiveView,
|
||||
history_store: Entity<HistoryStore>,
|
||||
history: Entity<ThreadHistory>,
|
||||
new_item_context_menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
assistant_dropdown_menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
width: Option<Pixels>,
|
||||
height: Option<Pixels>,
|
||||
}
|
||||
@@ -213,7 +228,7 @@ impl AssistantPanel {
|
||||
.unwrap(),
|
||||
history_store: history_store.clone(),
|
||||
history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, cx)),
|
||||
new_item_context_menu_handle: PopoverMenuHandle::default(),
|
||||
assistant_dropdown_menu_handle: PopoverMenuHandle::default(),
|
||||
width: None,
|
||||
height: None,
|
||||
}
|
||||
@@ -426,6 +441,16 @@ impl AssistantPanel {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn open_assistant_diff(
|
||||
&mut self,
|
||||
_: &OpenAssistantDiff,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let thread = self.thread.read(cx).thread().clone();
|
||||
AssistantDiff::deploy(thread, self.workspace.clone(), window, cx).log_err();
|
||||
}
|
||||
|
||||
pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let context_server_manager = self.thread_store.read(cx).context_server_manager();
|
||||
let tools = self.thread_store.read(cx).tools();
|
||||
@@ -659,8 +684,9 @@ impl Panel for AssistantPanel {
|
||||
}
|
||||
|
||||
impl AssistantPanel {
|
||||
fn render_toolbar(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn render_toolbar(&self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let thread = self.thread.read(cx);
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
|
||||
let title = match self.active_view {
|
||||
ActiveView::Thread => {
|
||||
@@ -680,7 +706,7 @@ impl AssistantPanel {
|
||||
})
|
||||
.unwrap_or_else(|| SharedString::from("Loading Summary…")),
|
||||
ActiveView::History => "History".into(),
|
||||
ActiveView::Configuration => "Assistant Settings".into(),
|
||||
ActiveView::Configuration => "Settings".into(),
|
||||
};
|
||||
|
||||
h_flex()
|
||||
@@ -720,57 +746,47 @@ impl AssistantPanel {
|
||||
.border_color(cx.theme().colors().border)
|
||||
.gap(DynamicSpacing::Base02.rems(cx))
|
||||
.child(
|
||||
PopoverMenu::new("assistant-toolbar-new-popover-menu")
|
||||
IconButton::new("new", IconName::Plus)
|
||||
.icon_size(IconSize::Small)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"New Thread",
|
||||
&NewThread,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(move |_event, window, cx| {
|
||||
window.dispatch_action(NewThread.boxed_clone(), cx);
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
PopoverMenu::new("assistant-menu")
|
||||
.trigger_with_tooltip(
|
||||
IconButton::new("new", IconName::Plus)
|
||||
IconButton::new("new", IconName::Ellipsis)
|
||||
.icon_size(IconSize::Small)
|
||||
.style(ButtonStyle::Subtle),
|
||||
Tooltip::text("New…"),
|
||||
Tooltip::text("Toggle Assistant Menu"),
|
||||
)
|
||||
.anchor(Corner::TopRight)
|
||||
.with_handle(self.new_item_context_menu_handle.clone())
|
||||
.with_handle(self.assistant_dropdown_menu_handle.clone())
|
||||
.menu(move |window, cx| {
|
||||
Some(ContextMenu::build(
|
||||
window,
|
||||
cx,
|
||||
|menu, _window, _cx| {
|
||||
menu.action("New Thread", NewThread.boxed_clone())
|
||||
.action(
|
||||
"New Prompt Editor",
|
||||
NewPromptEditor.boxed_clone(),
|
||||
)
|
||||
menu.action(
|
||||
"New Prompt Editor",
|
||||
NewPromptEditor.boxed_clone(),
|
||||
)
|
||||
.separator()
|
||||
.action("History", OpenHistory.boxed_clone())
|
||||
.action("Settings", OpenConfiguration.boxed_clone())
|
||||
},
|
||||
))
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("open-history", IconName::HistoryRerun)
|
||||
.icon_size(IconSize::Small)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip({
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"History",
|
||||
&OpenHistory,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(move |_event, window, cx| {
|
||||
window.dispatch_action(OpenHistory.boxed_clone(), cx);
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("configure-assistant", IconName::Settings)
|
||||
.icon_size(IconSize::Small)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(Tooltip::text("Assistant Settings"))
|
||||
.on_click(move |_event, window, cx| {
|
||||
window.dispatch_action(OpenConfiguration.boxed_clone(), cx);
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -815,66 +831,150 @@ impl AssistantPanel {
|
||||
.history_store
|
||||
.update(cx, |this, cx| this.recent_entries(6, cx));
|
||||
|
||||
let create_welcome_heading = || {
|
||||
h_flex()
|
||||
.w_full()
|
||||
.child(Headline::new("Welcome to the Assistant Panel").size(HeadlineSize::Small))
|
||||
};
|
||||
|
||||
let configuration_error = self.configuration_error(cx);
|
||||
let no_error = configuration_error.is_none();
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
|
||||
v_flex()
|
||||
.p_1p5()
|
||||
.size_full()
|
||||
.justify_end()
|
||||
.gap_1()
|
||||
.map(|parent| {
|
||||
match configuration_error {
|
||||
Some(ConfigurationError::ProviderNotAuthenticated)
|
||||
| Some(ConfigurationError::NoProvider) => {
|
||||
parent.child(
|
||||
v_flex()
|
||||
.px_1p5()
|
||||
.gap_0p5()
|
||||
.child(create_welcome_heading())
|
||||
.child(
|
||||
Label::new(
|
||||
"To start using the assistant, configure at least one LLM provider.",
|
||||
)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
h_flex().mt_1().w_full().child(
|
||||
Button::new("open-configuration", "Configure a Provider")
|
||||
.size(ButtonSize::Compact)
|
||||
.icon(Some(IconName::Sliders))
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.open_configuration(window, cx);
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => parent
|
||||
.child(v_flex().px_1p5().gap_0p5().child(create_welcome_heading()).children(
|
||||
provider.render_accept_terms(
|
||||
LanguageModelProviderTosView::ThreadEmptyState,
|
||||
cx,
|
||||
.when(recent_history.is_empty(), |this| {
|
||||
this.child(
|
||||
v_flex()
|
||||
.size_full()
|
||||
.max_w_80()
|
||||
.mx_auto()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.child(
|
||||
h_flex().child(
|
||||
Headline::new("Welcome to the Assistant Panel")
|
||||
),
|
||||
)),
|
||||
None => parent,
|
||||
}
|
||||
})
|
||||
.when(recent_history.is_empty() && no_error, |parent| {
|
||||
parent.child(v_flex().gap_0p5().child(create_welcome_heading()).child(
|
||||
Label::new("Start typing to chat with your codebase").color(Color::Muted),
|
||||
))
|
||||
)
|
||||
.when(no_error, |parent| {
|
||||
parent.child(
|
||||
h_flex().child(
|
||||
Label::new("Ask and build anything.")
|
||||
.color(Color::Muted)
|
||||
.mb_2p5(),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Button::new("new-thread", "Start New Thread")
|
||||
.icon(IconName::Plus)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.full_width()
|
||||
.key_binding(KeyBinding::for_action_in(
|
||||
&NewThread,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.on_click(|_event, window, cx| {
|
||||
window.dispatch_action(NewThread.boxed_clone(), cx)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("context", "Add Context")
|
||||
.icon(IconName::FileCode)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.full_width()
|
||||
.key_binding(KeyBinding::for_action_in(
|
||||
&ToggleContextPicker,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.on_click(|_event, window, cx| {
|
||||
window.dispatch_action(ToggleContextPicker.boxed_clone(), cx)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("mode", "Switch Model")
|
||||
.icon(IconName::DatabaseZap)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.full_width()
|
||||
.key_binding(KeyBinding::for_action_in(
|
||||
&ToggleModelSelector,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.on_click(|_event, window, cx| {
|
||||
window.dispatch_action(ToggleModelSelector.boxed_clone(), cx)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("settings", "View Settings")
|
||||
.icon(IconName::Settings)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.full_width()
|
||||
.key_binding(KeyBinding::for_action_in(
|
||||
&OpenConfiguration,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.on_click(|_event, window, cx| {
|
||||
window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
|
||||
}),
|
||||
)
|
||||
})
|
||||
.map(|parent| {
|
||||
match configuration_error {
|
||||
Some(ConfigurationError::ProviderNotAuthenticated)
|
||||
| Some(ConfigurationError::NoProvider) => {
|
||||
parent
|
||||
.child(
|
||||
h_flex().child(
|
||||
Label::new("To start using the assistant, configure at least one LLM provider.")
|
||||
.color(Color::Muted)
|
||||
.mb_2p5()
|
||||
)
|
||||
)
|
||||
.child(
|
||||
Button::new("settings", "Configure a Provider")
|
||||
.icon(IconName::Settings)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.full_width()
|
||||
.key_binding(KeyBinding::for_action_in(
|
||||
&OpenConfiguration,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.on_click(|_event, window, cx| {
|
||||
window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
|
||||
}),
|
||||
)
|
||||
}
|
||||
Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => parent
|
||||
.children(
|
||||
provider.render_accept_terms(
|
||||
LanguageModelProviderTosView::ThreadEmptyState,
|
||||
cx,
|
||||
),
|
||||
),
|
||||
None => parent,
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
.when(!recent_history.is_empty(), |parent| {
|
||||
parent
|
||||
.p_1p5()
|
||||
.justify_end()
|
||||
.gap_1()
|
||||
.child(
|
||||
h_flex()
|
||||
.pl_1p5()
|
||||
@@ -897,7 +997,7 @@ impl AssistantPanel {
|
||||
&self.focus_handle(cx),
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
).map(|kb| kb.size(rems_from_px(12.))),)
|
||||
.on_click(move |_event, window, cx| {
|
||||
window.dispatch_action(OpenHistory.boxed_clone(), cx);
|
||||
}),
|
||||
@@ -905,7 +1005,7 @@ impl AssistantPanel {
|
||||
)
|
||||
.child(v_flex().gap_1().children(
|
||||
recent_history.into_iter().map(|entry| {
|
||||
// TODO: Add keyboard navigation.
|
||||
// TODO: Add keyboard navigation.
|
||||
match entry {
|
||||
HistoryEntry::Thread(thread) => {
|
||||
PastThread::new(thread, cx.entity().downgrade(), false)
|
||||
@@ -1103,9 +1203,13 @@ impl Render for AssistantPanel {
|
||||
.on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
|
||||
this.open_history(window, cx);
|
||||
}))
|
||||
.on_action(cx.listener(|this, _: &OpenConfiguration, window, cx| {
|
||||
this.open_configuration(window, cx);
|
||||
}))
|
||||
.on_action(cx.listener(Self::open_active_thread_as_markdown))
|
||||
.on_action(cx.listener(Self::deploy_prompt_library))
|
||||
.child(self.render_toolbar(cx))
|
||||
.on_action(cx.listener(Self::open_assistant_diff))
|
||||
.child(self.render_toolbar(window, cx))
|
||||
.map(|parent| match self.active_view {
|
||||
ActiveView::Thread => parent
|
||||
.child(self.render_active_thread_or_empty_state(window, cx))
|
||||
|
||||
@@ -5,12 +5,12 @@ use anyhow::{Context as _, Result};
|
||||
use client::telemetry::Telemetry;
|
||||
use collections::HashSet;
|
||||
use editor::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint};
|
||||
use futures::{channel::mpsc, future::LocalBoxFuture, join, SinkExt, Stream, StreamExt};
|
||||
use futures::{SinkExt, Stream, StreamExt, channel::mpsc, future::LocalBoxFuture, join};
|
||||
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Subscription, Task};
|
||||
use language::{line_diff, Buffer, IndentKind, Point, TransactionId};
|
||||
use language::{Buffer, IndentKind, Point, TransactionId, line_diff};
|
||||
use language_model::{
|
||||
report_assistant_event, LanguageModel, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, LanguageModelTextStream, Role,
|
||||
LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||
LanguageModelTextStream, Role, report_assistant_event,
|
||||
};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use parking_lot::Mutex;
|
||||
@@ -414,7 +414,11 @@ impl CodegenAlternative {
|
||||
};
|
||||
|
||||
if let Some(context_store) = &self.context_store {
|
||||
attach_context_to_message(&mut request_message, context_store.read(cx).snapshot(cx));
|
||||
attach_context_to_message(
|
||||
&mut request_message,
|
||||
context_store.read(cx).context().iter(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
request_message.content.push(prompt.into());
|
||||
@@ -1028,14 +1032,14 @@ impl Diff {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use futures::{
|
||||
stream::{self},
|
||||
Stream,
|
||||
stream::{self},
|
||||
};
|
||||
use gpui::TestAppContext;
|
||||
use indoc::indoc;
|
||||
use language::{
|
||||
language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, LanguageMatcher,
|
||||
Point,
|
||||
Buffer, Language, LanguageConfig, LanguageMatcher, Point, language_settings,
|
||||
tree_sitter_rust,
|
||||
};
|
||||
use language_model::{LanguageModelRegistry, TokenUsage};
|
||||
use rand::prelude::*;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
use std::rc::Rc;
|
||||
use std::{ops::Range, path::Path};
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
use file_icons::FileIcons;
|
||||
use gpui::{App, Entity, SharedString};
|
||||
use language::Buffer;
|
||||
use language::{Buffer, File};
|
||||
use language_model::{LanguageModelRequestMessage, MessageContent};
|
||||
use project::ProjectPath;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -11,7 +9,7 @@ use text::{Anchor, BufferId};
|
||||
use ui::IconName;
|
||||
use util::post_inc;
|
||||
|
||||
use crate::{context_store::buffer_path_log_err, thread::Thread};
|
||||
use crate::thread::Thread;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
|
||||
pub struct ContextId(pub(crate) usize);
|
||||
@@ -22,19 +20,6 @@ impl ContextId {
|
||||
}
|
||||
}
|
||||
|
||||
/// Some context attached to a message in a thread.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ContextSnapshot {
|
||||
pub id: ContextId,
|
||||
pub name: SharedString,
|
||||
pub parent: Option<SharedString>,
|
||||
pub tooltip: Option<SharedString>,
|
||||
pub icon_path: Option<SharedString>,
|
||||
pub kind: ContextKind,
|
||||
/// Joining these strings separated by \n yields text for model. Not refreshed by `snapshot`.
|
||||
pub text: Box<[SharedString]>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ContextKind {
|
||||
File,
|
||||
@@ -51,12 +36,12 @@ impl ContextKind {
|
||||
ContextKind::Directory => IconName::Folder,
|
||||
ContextKind::Symbol => IconName::Code,
|
||||
ContextKind::FetchedUrl => IconName::Globe,
|
||||
ContextKind::Thread => IconName::MessageCircle,
|
||||
ContextKind::Thread => IconName::MessageBubbles,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AssistantContext {
|
||||
File(FileContext),
|
||||
Directory(DirectoryContext),
|
||||
@@ -69,7 +54,7 @@ impl AssistantContext {
|
||||
pub fn id(&self) -> ContextId {
|
||||
match self {
|
||||
Self::File(file) => file.id,
|
||||
Self::Directory(directory) => directory.snapshot.id,
|
||||
Self::Directory(directory) => directory.id,
|
||||
Self::Symbol(symbol) => symbol.id,
|
||||
Self::FetchedUrl(url) => url.id,
|
||||
Self::Thread(thread) => thread.id,
|
||||
@@ -77,26 +62,26 @@ impl AssistantContext {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FileContext {
|
||||
pub id: ContextId,
|
||||
pub context_buffer: ContextBuffer,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DirectoryContext {
|
||||
pub path: Rc<Path>,
|
||||
pub id: ContextId,
|
||||
pub project_path: ProjectPath,
|
||||
pub context_buffers: Vec<ContextBuffer>,
|
||||
pub snapshot: ContextSnapshot,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SymbolContext {
|
||||
pub id: ContextId,
|
||||
pub context_symbol: ContextSymbol,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FetchedUrlContext {
|
||||
pub id: ContextId,
|
||||
pub url: SharedString,
|
||||
@@ -106,24 +91,45 @@ pub struct FetchedUrlContext {
|
||||
// TODO: Model<Thread> holds onto the thread even if the thread is deleted. Can either handle this
|
||||
// explicitly or have a WeakModel<Thread> and remove during snapshot.
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ThreadContext {
|
||||
pub id: ContextId,
|
||||
pub thread: Entity<Thread>,
|
||||
pub text: SharedString,
|
||||
}
|
||||
|
||||
impl ThreadContext {
|
||||
pub fn summary(&self, cx: &App) -> SharedString {
|
||||
self.thread
|
||||
.read(cx)
|
||||
.summary()
|
||||
.unwrap_or("New thread".into())
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Model<Buffer> holds onto the buffer even if the file is deleted and closed. Should remove
|
||||
// the context from the message editor in this case.
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Clone)]
|
||||
pub struct ContextBuffer {
|
||||
pub id: BufferId,
|
||||
pub buffer: Entity<Buffer>,
|
||||
pub file: Arc<dyn File>,
|
||||
pub version: clock::Global,
|
||||
pub text: SharedString,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ContextBuffer {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ContextBuffer")
|
||||
.field("id", &self.id)
|
||||
.field("buffer", &self.buffer)
|
||||
.field("version", &self.version)
|
||||
.field("text", &self.text)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ContextSymbol {
|
||||
pub id: ContextSymbolId,
|
||||
@@ -142,144 +148,10 @@ pub struct ContextSymbolId {
|
||||
pub range: Range<Anchor>,
|
||||
}
|
||||
|
||||
impl AssistantContext {
|
||||
pub fn snapshot(&self, cx: &App) -> Option<ContextSnapshot> {
|
||||
match &self {
|
||||
Self::File(file_context) => file_context.snapshot(cx),
|
||||
Self::Directory(directory_context) => Some(directory_context.snapshot()),
|
||||
Self::Symbol(symbol_context) => symbol_context.snapshot(cx),
|
||||
Self::FetchedUrl(fetched_url_context) => Some(fetched_url_context.snapshot()),
|
||||
Self::Thread(thread_context) => Some(thread_context.snapshot(cx)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FileContext {
|
||||
pub fn snapshot(&self, cx: &App) -> Option<ContextSnapshot> {
|
||||
let buffer = self.context_buffer.buffer.read(cx);
|
||||
let path = buffer_path_log_err(buffer)?;
|
||||
let full_path: SharedString = path.to_string_lossy().into_owned().into();
|
||||
let name = match path.file_name() {
|
||||
Some(name) => name.to_string_lossy().into_owned().into(),
|
||||
None => full_path.clone(),
|
||||
};
|
||||
let parent = path
|
||||
.parent()
|
||||
.and_then(|p| p.file_name())
|
||||
.map(|p| p.to_string_lossy().into_owned().into());
|
||||
|
||||
let icon_path = FileIcons::get_icon(&path, cx);
|
||||
|
||||
Some(ContextSnapshot {
|
||||
id: self.id,
|
||||
name,
|
||||
parent,
|
||||
tooltip: Some(full_path),
|
||||
icon_path,
|
||||
kind: ContextKind::File,
|
||||
text: Box::new([self.context_buffer.text.clone()]),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl DirectoryContext {
|
||||
pub fn new(
|
||||
id: ContextId,
|
||||
path: &Path,
|
||||
context_buffers: Vec<ContextBuffer>,
|
||||
) -> DirectoryContext {
|
||||
let full_path: SharedString = path.to_string_lossy().into_owned().into();
|
||||
|
||||
let name = match path.file_name() {
|
||||
Some(name) => name.to_string_lossy().into_owned().into(),
|
||||
None => full_path.clone(),
|
||||
};
|
||||
|
||||
let parent = path
|
||||
.parent()
|
||||
.and_then(|p| p.file_name())
|
||||
.map(|p| p.to_string_lossy().into_owned().into());
|
||||
|
||||
// TODO: include directory path in text?
|
||||
let text = context_buffers
|
||||
.iter()
|
||||
.map(|b| b.text.clone())
|
||||
.collect::<Vec<_>>()
|
||||
.into();
|
||||
|
||||
DirectoryContext {
|
||||
path: path.into(),
|
||||
context_buffers,
|
||||
snapshot: ContextSnapshot {
|
||||
id,
|
||||
name,
|
||||
parent,
|
||||
tooltip: Some(full_path),
|
||||
icon_path: None,
|
||||
kind: ContextKind::Directory,
|
||||
text,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn snapshot(&self) -> ContextSnapshot {
|
||||
self.snapshot.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl SymbolContext {
|
||||
pub fn snapshot(&self, cx: &App) -> Option<ContextSnapshot> {
|
||||
let buffer = self.context_symbol.buffer.read(cx);
|
||||
let name = self.context_symbol.id.name.clone();
|
||||
let path = buffer_path_log_err(buffer)?
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
.into();
|
||||
|
||||
Some(ContextSnapshot {
|
||||
id: self.id,
|
||||
name,
|
||||
parent: Some(path),
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
kind: ContextKind::Symbol,
|
||||
text: Box::new([self.context_symbol.text.clone()]),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl FetchedUrlContext {
|
||||
pub fn snapshot(&self) -> ContextSnapshot {
|
||||
ContextSnapshot {
|
||||
id: self.id,
|
||||
name: self.url.clone(),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
kind: ContextKind::FetchedUrl,
|
||||
text: Box::new([self.text.clone()]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ThreadContext {
|
||||
pub fn snapshot(&self, cx: &App) -> ContextSnapshot {
|
||||
let thread = self.thread.read(cx);
|
||||
ContextSnapshot {
|
||||
id: self.id,
|
||||
name: thread.summary().unwrap_or("New thread".into()),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
kind: ContextKind::Thread,
|
||||
text: Box::new([self.text.clone()]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn attach_context_to_message(
|
||||
pub fn attach_context_to_message<'a>(
|
||||
message: &mut LanguageModelRequestMessage,
|
||||
contexts: impl Iterator<Item = ContextSnapshot>,
|
||||
contexts: impl Iterator<Item = &'a AssistantContext>,
|
||||
cx: &App,
|
||||
) {
|
||||
let mut file_context = Vec::new();
|
||||
let mut directory_context = Vec::new();
|
||||
@@ -287,91 +159,61 @@ pub fn attach_context_to_message(
|
||||
let mut fetch_context = Vec::new();
|
||||
let mut thread_context = Vec::new();
|
||||
|
||||
let mut capacity = 0;
|
||||
for context in contexts {
|
||||
capacity += context.text.len();
|
||||
match context.kind {
|
||||
ContextKind::File => file_context.push(context),
|
||||
ContextKind::Directory => directory_context.push(context),
|
||||
ContextKind::Symbol => symbol_context.push(context),
|
||||
ContextKind::FetchedUrl => fetch_context.push(context),
|
||||
ContextKind::Thread => thread_context.push(context),
|
||||
match context {
|
||||
AssistantContext::File(context) => file_context.push(context),
|
||||
AssistantContext::Directory(context) => directory_context.push(context),
|
||||
AssistantContext::Symbol(context) => symbol_context.push(context),
|
||||
AssistantContext::FetchedUrl(context) => fetch_context.push(context),
|
||||
AssistantContext::Thread(context) => thread_context.push(context),
|
||||
}
|
||||
}
|
||||
if !file_context.is_empty() {
|
||||
capacity += 1;
|
||||
}
|
||||
if !directory_context.is_empty() {
|
||||
capacity += 1;
|
||||
}
|
||||
if !symbol_context.is_empty() {
|
||||
capacity += 1;
|
||||
}
|
||||
if !fetch_context.is_empty() {
|
||||
capacity += 1 + fetch_context.len();
|
||||
}
|
||||
if !thread_context.is_empty() {
|
||||
capacity += 1 + thread_context.len();
|
||||
}
|
||||
if capacity == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut context_chunks = Vec::with_capacity(capacity);
|
||||
let mut context_chunks = Vec::new();
|
||||
|
||||
if !file_context.is_empty() {
|
||||
context_chunks.push("The following files are available:\n");
|
||||
for context in &file_context {
|
||||
for chunk in &context.text {
|
||||
context_chunks.push(&chunk);
|
||||
}
|
||||
for context in file_context {
|
||||
context_chunks.push(&context.context_buffer.text);
|
||||
}
|
||||
}
|
||||
|
||||
if !directory_context.is_empty() {
|
||||
context_chunks.push("The following directories are available:\n");
|
||||
for context in &directory_context {
|
||||
for chunk in &context.text {
|
||||
context_chunks.push(&chunk);
|
||||
for context in directory_context {
|
||||
for context_buffer in &context.context_buffers {
|
||||
context_chunks.push(&context_buffer.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !symbol_context.is_empty() {
|
||||
context_chunks.push("The following symbols are available:\n");
|
||||
for context in &symbol_context {
|
||||
for chunk in &context.text {
|
||||
context_chunks.push(&chunk);
|
||||
}
|
||||
for context in symbol_context {
|
||||
context_chunks.push(&context.context_symbol.text);
|
||||
}
|
||||
}
|
||||
|
||||
if !fetch_context.is_empty() {
|
||||
context_chunks.push("The following fetched results are available:\n");
|
||||
for context in &fetch_context {
|
||||
context_chunks.push(&context.name);
|
||||
for chunk in &context.text {
|
||||
context_chunks.push(&chunk);
|
||||
}
|
||||
context_chunks.push(&context.url);
|
||||
context_chunks.push(&context.text);
|
||||
}
|
||||
}
|
||||
|
||||
// Need to own the SharedString for summary so that it can be referenced.
|
||||
let mut thread_context_chunks = Vec::new();
|
||||
if !thread_context.is_empty() {
|
||||
context_chunks.push("The following previous conversation threads are available:\n");
|
||||
for context in &thread_context {
|
||||
context_chunks.push(&context.name);
|
||||
for chunk in &context.text {
|
||||
context_chunks.push(&chunk);
|
||||
}
|
||||
thread_context_chunks.push(context.summary(cx));
|
||||
thread_context_chunks.push(context.text.clone());
|
||||
}
|
||||
}
|
||||
|
||||
debug_assert!(
|
||||
context_chunks.len() == capacity,
|
||||
"attach_context_message calculated capacity of {}, but length was {}",
|
||||
capacity,
|
||||
context_chunks.len()
|
||||
);
|
||||
for chunk in &thread_context_chunks {
|
||||
context_chunks.push(chunk);
|
||||
}
|
||||
|
||||
if !context_chunks.is_empty() {
|
||||
message
|
||||
|
||||
@@ -8,7 +8,7 @@ use std::ops::Range;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::{Result, anyhow};
|
||||
use editor::display_map::{Crease, FoldId};
|
||||
use editor::{Anchor, AnchorRangeExt as _, Editor, ExcerptId, FoldPlaceholder, ToOffset};
|
||||
use file_context_picker::render_file_context_entry;
|
||||
@@ -18,19 +18,19 @@ use gpui::{
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use project::ProjectPath;
|
||||
use symbol_context_picker::SymbolContextPicker;
|
||||
use thread_context_picker::{render_thread_context_entry, ThreadContextEntry};
|
||||
use thread_context_picker::{ThreadContextEntry, render_thread_context_entry};
|
||||
use ui::{
|
||||
prelude::*, ButtonLike, ContextMenu, ContextMenuEntry, ContextMenuItem, Disclosure, TintColor,
|
||||
ButtonLike, ContextMenu, ContextMenuEntry, ContextMenuItem, Disclosure, TintColor, prelude::*,
|
||||
};
|
||||
use workspace::{notifications::NotifyResultExt, Workspace};
|
||||
use workspace::{Workspace, notifications::NotifyResultExt};
|
||||
|
||||
use crate::AssistantPanel;
|
||||
pub use crate::context_picker::completion_provider::ContextPickerCompletionProvider;
|
||||
use crate::context_picker::fetch_context_picker::FetchContextPicker;
|
||||
use crate::context_picker::file_context_picker::FileContextPicker;
|
||||
use crate::context_picker::thread_context_picker::ThreadContextPicker;
|
||||
use crate::context_store::ContextStore;
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::AssistantPanel;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ConfirmBehavior {
|
||||
@@ -84,7 +84,7 @@ impl ContextPickerMode {
|
||||
Self::File => IconName::File,
|
||||
Self::Symbol => IconName::Code,
|
||||
Self::Fetch => IconName::Globe,
|
||||
Self::Thread => IconName::MessageCircle,
|
||||
Self::Thread => IconName::MessageBubbles,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ use std::cell::RefCell;
|
||||
use std::ops::Range;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
use anyhow::Result;
|
||||
use editor::{CompletionProvider, Editor, ExcerptId};
|
||||
@@ -24,7 +24,7 @@ use crate::thread_store::ThreadStore;
|
||||
|
||||
use super::fetch_context_picker::fetch_url_content;
|
||||
use super::thread_context_picker::ThreadContextEntry;
|
||||
use super::{recent_context_picker_entries, supported_context_picker_modes, ContextPickerMode};
|
||||
use super::{ContextPickerMode, recent_context_picker_entries, supported_context_picker_modes};
|
||||
|
||||
pub struct ContextPickerCompletionProvider {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
@@ -152,7 +152,7 @@ impl ContextPickerCompletionProvider {
|
||||
let icon_for_completion = if recent {
|
||||
IconName::HistoryRerun
|
||||
} else {
|
||||
IconName::MessageCircle
|
||||
IconName::MessageBubbles
|
||||
};
|
||||
let new_text = format!("@thread {}", thread_entry.summary);
|
||||
let new_text_len = new_text.len();
|
||||
@@ -164,7 +164,7 @@ impl ContextPickerCompletionProvider {
|
||||
source: project::CompletionSource::Custom,
|
||||
icon_path: Some(icon_for_completion.path().into()),
|
||||
confirm: Some(confirm_completion_callback(
|
||||
IconName::MessageCircle.path().into(),
|
||||
IconName::MessageBubbles.path().into(),
|
||||
thread_entry.summary.clone(),
|
||||
excerpt_id,
|
||||
source_range.start,
|
||||
|
||||
@@ -2,13 +2,13 @@ use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{bail, Context as _, Result};
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
use futures::AsyncReadExt as _;
|
||||
use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
|
||||
use html_to_markdown::{convert_html_to_markdown, markdown, TagHandler};
|
||||
use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown};
|
||||
use http_client::{AsyncBody, HttpClientWithUrl};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use ui::{prelude::*, Context, ListItem, Window};
|
||||
use ui::{Context, ListItem, Window, prelude::*};
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
||||
@@ -163,11 +163,7 @@ impl PickerDelegate for FetchContextPickerDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
if self.url.is_empty() {
|
||||
0
|
||||
} else {
|
||||
1
|
||||
}
|
||||
if self.url.is_empty() { 0 } else { 1 }
|
||||
}
|
||||
|
||||
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
use file_icons::FileIcons;
|
||||
use fuzzy::PathMatch;
|
||||
@@ -9,9 +9,9 @@ use gpui::{
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::{PathMatchCandidateSet, ProjectPath, WorktreeId};
|
||||
use ui::{prelude::*, ListItem, Tooltip};
|
||||
use ui::{ListItem, Tooltip, prelude::*};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{notifications::NotifyResultExt, Workspace};
|
||||
use workspace::{Workspace, notifications::NotifyResultExt};
|
||||
|
||||
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
||||
use crate::context_store::{ContextStore, FileInclusion};
|
||||
@@ -321,10 +321,7 @@ pub fn render_file_context_entry(
|
||||
|
||||
let added = context_store.upgrade().and_then(|context_store| {
|
||||
if is_directory {
|
||||
context_store
|
||||
.read(cx)
|
||||
.includes_directory(path)
|
||||
.map(FileInclusion::Direct)
|
||||
context_store.read(cx).includes_directory(path)
|
||||
} else {
|
||||
context_store.read(cx).will_include_file_path(path, cx)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::cmp::Reverse;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
@@ -11,7 +11,7 @@ use ordered_float::OrderedFloat;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::{DocumentSymbol, Symbol};
|
||||
use text::OffsetRangeExt;
|
||||
use ui::{prelude::*, ListItem};
|
||||
use ui::{ListItem, prelude::*};
|
||||
use util::ResultExt as _;
|
||||
use workspace::Workspace;
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use ui::{prelude::*, ListItem};
|
||||
use ui::{ListItem, prelude::*};
|
||||
|
||||
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
||||
use crate::context_store::{self, ContextStore};
|
||||
@@ -197,7 +197,7 @@ pub fn render_thread_context_entry(
|
||||
.gap_1p5()
|
||||
.max_w_72()
|
||||
.child(
|
||||
Icon::new(IconName::MessageCircle)
|
||||
Icon::new(IconName::MessageBubbles)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
|
||||
@@ -2,20 +2,20 @@ use std::ops::Range;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use collections::{BTreeMap, HashMap, HashSet};
|
||||
use futures::{self, future, Future, FutureExt};
|
||||
use futures::{self, Future, FutureExt, future};
|
||||
use gpui::{App, AppContext as _, AsyncApp, Context, Entity, SharedString, Task, WeakEntity};
|
||||
use language::Buffer;
|
||||
use language::{Buffer, File};
|
||||
use project::{ProjectItem, ProjectPath, Worktree};
|
||||
use rope::Rope;
|
||||
use text::{Anchor, BufferId, OffsetRangeExt};
|
||||
use util::maybe;
|
||||
use util::{ResultExt, maybe};
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::context::{
|
||||
AssistantContext, ContextBuffer, ContextId, ContextSnapshot, ContextSymbol, ContextSymbolId,
|
||||
DirectoryContext, FetchedUrlContext, FileContext, SymbolContext, ThreadContext,
|
||||
AssistantContext, ContextBuffer, ContextId, ContextSymbol, ContextSymbolId, DirectoryContext,
|
||||
FetchedUrlContext, FileContext, SymbolContext, ThreadContext,
|
||||
};
|
||||
use crate::context_strip::SuggestedContext;
|
||||
use crate::thread::{Thread, ThreadId};
|
||||
@@ -50,16 +50,14 @@ impl ContextStore {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn snapshot<'a>(&'a self, cx: &'a App) -> impl Iterator<Item = ContextSnapshot> + 'a {
|
||||
self.context()
|
||||
.iter()
|
||||
.flat_map(|context| context.snapshot(cx))
|
||||
}
|
||||
|
||||
pub fn context(&self) -> &Vec<AssistantContext> {
|
||||
&self.context
|
||||
}
|
||||
|
||||
pub fn context_for_id(&self, id: ContextId) -> Option<&AssistantContext> {
|
||||
self.context().iter().find(|context| context.id() == id)
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.context.clear();
|
||||
self.files.clear();
|
||||
@@ -117,7 +115,7 @@ impl ContextStore {
|
||||
None,
|
||||
cx.to_async(),
|
||||
)
|
||||
})?;
|
||||
})??;
|
||||
|
||||
let text = text_task.await;
|
||||
|
||||
@@ -140,13 +138,13 @@ impl ContextStore {
|
||||
let Some(file) = buffer.file() else {
|
||||
return Err(anyhow!("Buffer has no path."));
|
||||
};
|
||||
Ok(collect_buffer_info_and_text(
|
||||
collect_buffer_info_and_text(
|
||||
file.path().clone(),
|
||||
buffer_entity,
|
||||
buffer,
|
||||
None,
|
||||
cx.to_async(),
|
||||
))
|
||||
)
|
||||
})??;
|
||||
|
||||
let text = text_task.await;
|
||||
@@ -162,8 +160,10 @@ impl ContextStore {
|
||||
fn insert_file(&mut self, context_buffer: ContextBuffer) {
|
||||
let id = self.next_context_id.post_inc();
|
||||
self.files.insert(context_buffer.id, id);
|
||||
self.context
|
||||
.push(AssistantContext::File(FileContext { id, context_buffer }));
|
||||
self.context.push(AssistantContext::File(FileContext {
|
||||
id,
|
||||
context_buffer: context_buffer,
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn add_directory(
|
||||
@@ -180,14 +180,15 @@ impl ContextStore {
|
||||
return Task::ready(Err(anyhow!("failed to read project")));
|
||||
};
|
||||
|
||||
let already_included = if let Some(context_id) = self.includes_directory(&project_path.path)
|
||||
{
|
||||
if remove_if_exists {
|
||||
self.remove_context(context_id);
|
||||
let already_included = match self.includes_directory(&project_path.path) {
|
||||
Some(FileInclusion::Direct(context_id)) => {
|
||||
if remove_if_exists {
|
||||
self.remove_context(context_id);
|
||||
}
|
||||
true
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
Some(FileInclusion::InDirectory(_)) => true,
|
||||
None => false,
|
||||
};
|
||||
if already_included {
|
||||
return Task::ready(Ok(()));
|
||||
@@ -227,15 +228,18 @@ impl ContextStore {
|
||||
// Skip all binary files and other non-UTF8 files
|
||||
if let Ok(buffer_entity) = buffer_entity {
|
||||
let buffer = buffer_entity.read(cx);
|
||||
let (buffer_info, text_task) = collect_buffer_info_and_text(
|
||||
if let Some((buffer_info, text_task)) = collect_buffer_info_and_text(
|
||||
path,
|
||||
buffer_entity,
|
||||
buffer,
|
||||
None,
|
||||
cx.to_async(),
|
||||
);
|
||||
buffer_infos.push(buffer_info);
|
||||
text_tasks.push(text_task);
|
||||
)
|
||||
.log_err()
|
||||
{
|
||||
buffer_infos.push(buffer_info);
|
||||
text_tasks.push(text_task);
|
||||
}
|
||||
}
|
||||
}
|
||||
anyhow::Ok(())
|
||||
@@ -249,27 +253,30 @@ impl ContextStore {
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if context_buffers.is_empty() {
|
||||
bail!("No text files found in {}", &project_path.path.display());
|
||||
return Err(anyhow!(
|
||||
"No text files found in {}",
|
||||
&project_path.path.display()
|
||||
));
|
||||
}
|
||||
|
||||
this.update(cx, |this, _| {
|
||||
this.insert_directory(&project_path.path, context_buffers);
|
||||
this.insert_directory(project_path, context_buffers);
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn insert_directory(&mut self, path: &Path, context_buffers: Vec<ContextBuffer>) {
|
||||
fn insert_directory(&mut self, project_path: ProjectPath, context_buffers: Vec<ContextBuffer>) {
|
||||
let id = self.next_context_id.post_inc();
|
||||
self.directories.insert(path.to_path_buf(), id);
|
||||
self.directories.insert(project_path.path.to_path_buf(), id);
|
||||
|
||||
self.context
|
||||
.push(AssistantContext::Directory(DirectoryContext::new(
|
||||
.push(AssistantContext::Directory(DirectoryContext {
|
||||
id,
|
||||
path,
|
||||
project_path,
|
||||
context_buffers,
|
||||
)));
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn add_symbol(
|
||||
@@ -310,13 +317,16 @@ impl ContextStore {
|
||||
}
|
||||
}
|
||||
|
||||
let (buffer_info, collect_content_task) = collect_buffer_info_and_text(
|
||||
let (buffer_info, collect_content_task) = match collect_buffer_info_and_text(
|
||||
file.path().clone(),
|
||||
buffer,
|
||||
buffer_ref,
|
||||
Some(symbol_enclosing_range.clone()),
|
||||
cx.to_async(),
|
||||
);
|
||||
) {
|
||||
Ok((buffer_info, collect_context_task)) => (buffer_info, collect_context_task),
|
||||
Err(err) => return Task::ready(Err(err)),
|
||||
};
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let content = collect_content_task.await;
|
||||
@@ -468,7 +478,7 @@ impl ContextStore {
|
||||
let found_file_context = self.context.iter().find(|context| match &context {
|
||||
AssistantContext::File(file_context) => {
|
||||
let buffer = file_context.context_buffer.buffer.read(cx);
|
||||
if let Some(file_path) = buffer_path_log_err(buffer) {
|
||||
if let Some(file_path) = buffer_path_log_err(buffer, cx) {
|
||||
*file_path == *path
|
||||
} else {
|
||||
false
|
||||
@@ -500,8 +510,12 @@ impl ContextStore {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn includes_directory(&self, path: &Path) -> Option<ContextId> {
|
||||
self.directories.get(path).copied()
|
||||
pub fn includes_directory(&self, path: &Path) -> Option<FileInclusion> {
|
||||
if let Some(context_id) = self.directories.get(path) {
|
||||
return Some(FileInclusion::Direct(*context_id));
|
||||
}
|
||||
|
||||
self.will_include_file_path_via_directory(path)
|
||||
}
|
||||
|
||||
pub fn included_symbol(&self, symbol_id: &ContextSymbolId) -> Option<ContextId> {
|
||||
@@ -541,7 +555,7 @@ impl ContextStore {
|
||||
.filter_map(|context| match context {
|
||||
AssistantContext::File(file) => {
|
||||
let buffer = file.context_buffer.buffer.read(cx);
|
||||
buffer_path_log_err(buffer).map(|p| p.to_path_buf())
|
||||
buffer_path_log_err(buffer, cx).map(|p| p.to_path_buf())
|
||||
}
|
||||
AssistantContext::Directory(_)
|
||||
| AssistantContext::Symbol(_)
|
||||
@@ -564,6 +578,7 @@ pub enum FileInclusion {
|
||||
// ContextBuffer without text.
|
||||
struct BufferInfo {
|
||||
buffer_entity: Entity<Buffer>,
|
||||
file: Arc<dyn File>,
|
||||
id: BufferId,
|
||||
version: clock::Global,
|
||||
}
|
||||
@@ -572,6 +587,7 @@ fn make_context_buffer(info: BufferInfo, text: SharedString) -> ContextBuffer {
|
||||
ContextBuffer {
|
||||
id: info.id,
|
||||
buffer: info.buffer_entity,
|
||||
file: info.file,
|
||||
version: info.version,
|
||||
text,
|
||||
}
|
||||
@@ -600,10 +616,14 @@ fn collect_buffer_info_and_text(
|
||||
buffer: &Buffer,
|
||||
range: Option<Range<Anchor>>,
|
||||
cx: AsyncApp,
|
||||
) -> (BufferInfo, Task<SharedString>) {
|
||||
) -> Result<(BufferInfo, Task<SharedString>)> {
|
||||
let buffer_info = BufferInfo {
|
||||
id: buffer.remote_id(),
|
||||
buffer_entity,
|
||||
file: buffer
|
||||
.file()
|
||||
.context("buffer context must have a file")?
|
||||
.clone(),
|
||||
version: buffer.version(),
|
||||
};
|
||||
// Important to collect version at the same time as content so that staleness logic is correct.
|
||||
@@ -613,12 +633,16 @@ fn collect_buffer_info_and_text(
|
||||
buffer.as_rope().clone()
|
||||
};
|
||||
let text_task = cx.background_spawn(async move { to_fenced_codeblock(&path, content) });
|
||||
(buffer_info, text_task)
|
||||
Ok((buffer_info, text_task))
|
||||
}
|
||||
|
||||
pub fn buffer_path_log_err(buffer: &Buffer) -> Option<Arc<Path>> {
|
||||
pub fn buffer_path_log_err(buffer: &Buffer, cx: &App) -> Option<Arc<Path>> {
|
||||
if let Some(file) = buffer.file() {
|
||||
Some(file.path().clone())
|
||||
let mut path = file.path().clone();
|
||||
if path.as_os_str().is_empty() {
|
||||
path = file.full_path(cx).into();
|
||||
}
|
||||
Some(path)
|
||||
} else {
|
||||
log::error!("Buffer that had a path unexpectedly no longer has a path.");
|
||||
None
|
||||
@@ -683,7 +707,7 @@ pub fn refresh_context_store_text(
|
||||
context_store: Entity<ContextStore>,
|
||||
changed_buffers: &HashSet<Entity<Buffer>>,
|
||||
cx: &App,
|
||||
) -> impl Future<Output = Vec<ContextId>> {
|
||||
) -> impl Future<Output = Vec<ContextId>> + use<> {
|
||||
let mut tasks = Vec::new();
|
||||
|
||||
for context in &context_store.read(cx).context {
|
||||
@@ -704,8 +728,9 @@ pub fn refresh_context_store_text(
|
||||
|| changed_buffers.iter().any(|buffer| {
|
||||
let buffer = buffer.read(cx);
|
||||
|
||||
buffer_path_log_err(&buffer)
|
||||
.map_or(false, |path| path.starts_with(&directory_context.path))
|
||||
buffer_path_log_err(&buffer, cx).map_or(false, |path| {
|
||||
path.starts_with(&directory_context.project_path.path)
|
||||
})
|
||||
});
|
||||
|
||||
if should_refresh {
|
||||
@@ -791,13 +816,17 @@ fn refresh_directory_text(
|
||||
|
||||
let context_buffers = future::join_all(futures);
|
||||
|
||||
let id = directory_context.snapshot.id;
|
||||
let path = directory_context.path.clone();
|
||||
let id = directory_context.id;
|
||||
let project_path = directory_context.project_path.clone();
|
||||
Some(cx.spawn(async move |cx| {
|
||||
let context_buffers = context_buffers.await;
|
||||
context_store
|
||||
.update(cx, |context_store, _| {
|
||||
let new_directory_context = DirectoryContext::new(id, &path, context_buffers);
|
||||
let new_directory_context = DirectoryContext {
|
||||
id,
|
||||
project_path,
|
||||
context_buffers,
|
||||
};
|
||||
context_store.replace_context(AssistantContext::Directory(new_directory_context));
|
||||
})
|
||||
.ok();
|
||||
@@ -850,9 +879,9 @@ fn refresh_thread_text(
|
||||
fn refresh_context_buffer(
|
||||
context_buffer: &ContextBuffer,
|
||||
cx: &App,
|
||||
) -> Option<impl Future<Output = ContextBuffer>> {
|
||||
) -> Option<impl Future<Output = ContextBuffer> + use<>> {
|
||||
let buffer = context_buffer.buffer.read(cx);
|
||||
let path = buffer_path_log_err(buffer)?;
|
||||
let path = buffer_path_log_err(buffer, cx)?;
|
||||
if buffer.version.changed_since(&context_buffer.version) {
|
||||
let (buffer_info, text_task) = collect_buffer_info_and_text(
|
||||
path,
|
||||
@@ -860,7 +889,8 @@ fn refresh_context_buffer(
|
||||
buffer,
|
||||
None,
|
||||
cx.to_async(),
|
||||
);
|
||||
)
|
||||
.log_err()?;
|
||||
Some(text_task.map(move |text| make_context_buffer(buffer_info, text)))
|
||||
} else {
|
||||
None
|
||||
@@ -870,9 +900,9 @@ fn refresh_context_buffer(
|
||||
fn refresh_context_symbol(
|
||||
context_symbol: &ContextSymbol,
|
||||
cx: &App,
|
||||
) -> Option<impl Future<Output = ContextSymbol>> {
|
||||
) -> Option<impl Future<Output = ContextSymbol> + use<>> {
|
||||
let buffer = context_symbol.buffer.read(cx);
|
||||
let path = buffer_path_log_err(buffer)?;
|
||||
let path = buffer_path_log_err(buffer, cx)?;
|
||||
let project_path = buffer.project_path(cx)?;
|
||||
if buffer.version.changed_since(&context_symbol.buffer_version) {
|
||||
let (buffer_info, text_task) = collect_buffer_info_and_text(
|
||||
@@ -881,7 +911,8 @@ fn refresh_context_symbol(
|
||||
buffer,
|
||||
Some(context_symbol.enclosing_range.clone()),
|
||||
cx.to_async(),
|
||||
);
|
||||
)
|
||||
.log_err()?;
|
||||
let name = context_symbol.id.name.clone();
|
||||
let range = context_symbol.id.range.clone();
|
||||
let enclosing_range = context_symbol.enclosing_range.clone();
|
||||
|
||||
@@ -4,20 +4,20 @@ use collections::HashSet;
|
||||
use editor::Editor;
|
||||
use file_icons::FileIcons;
|
||||
use gpui::{
|
||||
App, Bounds, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription,
|
||||
WeakEntity,
|
||||
App, Bounds, ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
Subscription, WeakEntity,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::Buffer;
|
||||
use ui::{prelude::*, KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip};
|
||||
use workspace::{notifications::NotifyResultExt, Workspace};
|
||||
use ui::{KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
|
||||
use workspace::{Workspace, notifications::NotifyResultExt};
|
||||
|
||||
use crate::context::ContextKind;
|
||||
use crate::context::{ContextId, ContextKind};
|
||||
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
||||
use crate::context_store::ContextStore;
|
||||
use crate::thread::Thread;
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::ui::ContextPill;
|
||||
use crate::ui::{AddedContext, ContextPill};
|
||||
use crate::{
|
||||
AcceptSuggestedContext, AssistantPanel, FocusDown, FocusLeft, FocusRight, FocusUp,
|
||||
RemoveAllContext, RemoveFocusedContext, ToggleContextPicker,
|
||||
@@ -92,12 +92,12 @@ impl ContextStrip {
|
||||
let active_buffer_entity = editor.buffer().read(cx).as_singleton()?;
|
||||
let active_buffer = active_buffer_entity.read(cx);
|
||||
|
||||
let path = active_buffer.file()?.path();
|
||||
let path = active_buffer.file()?.full_path(cx);
|
||||
|
||||
if self
|
||||
.context_store
|
||||
.read(cx)
|
||||
.will_include_buffer(active_buffer.remote_id(), path)
|
||||
.will_include_buffer(active_buffer.remote_id(), &path)
|
||||
.is_some()
|
||||
{
|
||||
return None;
|
||||
@@ -108,7 +108,7 @@ impl ContextStrip {
|
||||
None => path.to_string_lossy().into_owned().into(),
|
||||
};
|
||||
|
||||
let icon_path = FileIcons::get_icon(path, cx);
|
||||
let icon_path = FileIcons::get_icon(&path, cx);
|
||||
|
||||
Some(SuggestedContext::File {
|
||||
name,
|
||||
@@ -239,11 +239,7 @@ impl ContextStrip {
|
||||
let eraser = if bounds.len() < 3 { 0 } else { 1 };
|
||||
let pills = &bounds[1..bounds.len() - eraser];
|
||||
|
||||
if pills.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(pills)
|
||||
}
|
||||
if pills.is_empty() { None } else { Some(pills) }
|
||||
}
|
||||
|
||||
fn last_pill_index(&self) -> Option<usize> {
|
||||
@@ -277,6 +273,14 @@ impl ContextStrip {
|
||||
best.map(|(index, _, _)| index)
|
||||
}
|
||||
|
||||
fn open_context(&mut self, id: ContextId, window: &mut Window, cx: &mut App) {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
crate::active_thread::open_context(id, self.context_store.clone(), workspace, window, cx);
|
||||
}
|
||||
|
||||
fn remove_focused_context(
|
||||
&mut self,
|
||||
_: &RemoveFocusedContext,
|
||||
@@ -359,19 +363,19 @@ impl Focusable for ContextStrip {
|
||||
impl Render for ContextStrip {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let context_store = self.context_store.read(cx);
|
||||
let context = context_store
|
||||
.context()
|
||||
.iter()
|
||||
.flat_map(|context| context.snapshot(cx))
|
||||
.collect::<Vec<_>>();
|
||||
let context = context_store.context();
|
||||
let context_picker = self.context_picker.clone();
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
let suggested_context = self.suggested_context(cx);
|
||||
|
||||
let dupe_names = context
|
||||
let added_contexts = context
|
||||
.iter()
|
||||
.map(|context| context.name.clone())
|
||||
.map(|c| AddedContext::new(c, cx))
|
||||
.collect::<Vec<_>>();
|
||||
let dupe_names = added_contexts
|
||||
.iter()
|
||||
.map(|c| c.name.clone())
|
||||
.sorted()
|
||||
.tuple_windows()
|
||||
.filter(|(a, b)| a == b)
|
||||
@@ -457,27 +461,39 @@ impl Render for ContextStrip {
|
||||
)
|
||||
}
|
||||
})
|
||||
.children(context.iter().enumerate().map(|(i, context)| {
|
||||
ContextPill::added(
|
||||
context.clone(),
|
||||
dupe_names.contains(&context.name),
|
||||
self.focused_index == Some(i),
|
||||
Some({
|
||||
let id = context.id;
|
||||
let context_store = self.context_store.clone();
|
||||
Rc::new(cx.listener(move |_this, _event, _window, cx| {
|
||||
context_store.update(cx, |this, _cx| {
|
||||
this.remove_context(id);
|
||||
});
|
||||
cx.notify();
|
||||
}))
|
||||
.children(
|
||||
added_contexts
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, added_context)| {
|
||||
let name = added_context.name.clone();
|
||||
let id = added_context.id;
|
||||
ContextPill::added(
|
||||
added_context,
|
||||
dupe_names.contains(&name),
|
||||
self.focused_index == Some(i),
|
||||
Some({
|
||||
let context_store = self.context_store.clone();
|
||||
Rc::new(cx.listener(move |_this, _event, _window, cx| {
|
||||
context_store.update(cx, |this, _cx| {
|
||||
this.remove_context(id);
|
||||
});
|
||||
cx.notify();
|
||||
}))
|
||||
}),
|
||||
)
|
||||
.on_click({
|
||||
Rc::new(cx.listener(move |this, event: &ClickEvent, window, cx| {
|
||||
if event.down.click_count > 1 {
|
||||
this.open_context(id, window, cx);
|
||||
} else {
|
||||
this.focused_index = Some(i);
|
||||
}
|
||||
cx.notify();
|
||||
}))
|
||||
})
|
||||
}),
|
||||
)
|
||||
.on_click(Rc::new(cx.listener(move |this, _, _window, cx| {
|
||||
this.focused_index = Some(i);
|
||||
cx.notify();
|
||||
})))
|
||||
}))
|
||||
)
|
||||
.when_some(suggested_context, |el, suggested| {
|
||||
el.child(
|
||||
ContextPill::suggested(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use assistant_context_editor::SavedContextMetadata;
|
||||
use chrono::{DateTime, Utc};
|
||||
use gpui::{prelude::*, Entity};
|
||||
use gpui::{Entity, prelude::*};
|
||||
|
||||
use crate::thread_store::{SerializedThreadMetadata, ThreadStore};
|
||||
|
||||
@@ -43,6 +43,11 @@ impl HistoryStore {
|
||||
pub fn entries(&self, cx: &mut Context<Self>) -> Vec<HistoryEntry> {
|
||||
let mut history_entries = Vec::new();
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
|
||||
return history_entries;
|
||||
}
|
||||
|
||||
for thread in self.thread_store.update(cx, |this, _cx| this.threads()) {
|
||||
history_entries.push(HistoryEntry::Thread(thread));
|
||||
}
|
||||
|
||||
@@ -7,24 +7,24 @@ use std::sync::Arc;
|
||||
use anyhow::{Context as _, Result};
|
||||
use assistant_settings::AssistantSettings;
|
||||
use client::telemetry::Telemetry;
|
||||
use collections::{hash_map, HashMap, HashSet, VecDeque};
|
||||
use collections::{HashMap, HashSet, VecDeque, hash_map};
|
||||
use editor::{
|
||||
Anchor, AnchorRangeExt, CodeActionProvider, Editor, EditorEvent, ExcerptId, ExcerptRange,
|
||||
GutterDimensions, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint,
|
||||
actions::SelectAll,
|
||||
display_map::{
|
||||
BlockContext, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, RenderBlock,
|
||||
ToDisplayPoint,
|
||||
},
|
||||
Anchor, AnchorRangeExt, CodeActionProvider, Editor, EditorEvent, ExcerptId, ExcerptRange,
|
||||
GutterDimensions, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint,
|
||||
};
|
||||
use feature_flags::{Assistant2FeatureFlag, FeatureFlagViewExt as _};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
point, App, Context, Entity, Focusable, Global, HighlightStyle, Subscription, Task,
|
||||
UpdateGlobal, WeakEntity, Window,
|
||||
App, Context, Entity, Focusable, Global, HighlightStyle, Subscription, Task, UpdateGlobal,
|
||||
WeakEntity, Window, point,
|
||||
};
|
||||
use language::{Buffer, Point, Selection, TransactionId};
|
||||
use language_model::{report_assistant_event, LanguageModelRegistry};
|
||||
use language_model::{LanguageModelRegistry, report_assistant_event};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use parking_lot::Mutex;
|
||||
use project::LspAction;
|
||||
@@ -32,20 +32,20 @@ use project::{CodeAction, ProjectTransaction};
|
||||
use prompt_store::PromptBuilder;
|
||||
use settings::{Settings, SettingsStore};
|
||||
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
|
||||
use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
|
||||
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
|
||||
use text::{OffsetRangeExt, ToPoint as _};
|
||||
use ui::prelude::*;
|
||||
use util::RangeExt;
|
||||
use util::ResultExt;
|
||||
use workspace::{dock::Panel, ShowConfiguration};
|
||||
use workspace::{notifications::NotificationId, ItemHandle, Toast, Workspace};
|
||||
use workspace::{ItemHandle, Toast, Workspace, notifications::NotificationId};
|
||||
use workspace::{ShowConfiguration, dock::Panel};
|
||||
|
||||
use crate::AssistantPanel;
|
||||
use crate::buffer_codegen::{BufferCodegen, CodegenAlternative, CodegenEvent};
|
||||
use crate::context_store::ContextStore;
|
||||
use crate::inline_prompt_editor::{CodegenStatus, InlineAssistId, PromptEditor, PromptEditorEvent};
|
||||
use crate::terminal_inline_assistant::TerminalInlineAssistant;
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::AssistantPanel;
|
||||
|
||||
pub fn init(
|
||||
fs: Arc<dyn Fs>,
|
||||
|
||||
@@ -10,14 +10,14 @@ use crate::{RemoveAllContext, ToggleContextPicker};
|
||||
use client::ErrorExt;
|
||||
use collections::VecDeque;
|
||||
use editor::{
|
||||
actions::{MoveDown, MoveUp},
|
||||
Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, GutterDimensions, MultiBuffer,
|
||||
actions::{MoveDown, MoveUp},
|
||||
};
|
||||
use feature_flags::{FeatureFlagAppExt as _, ZedPro};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
anchored, deferred, point, AnyElement, App, ClickEvent, Context, CursorStyle, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, FontWeight, Subscription, TextStyle, WeakEntity, Window,
|
||||
AnyElement, App, ClickEvent, Context, CursorStyle, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, FontWeight, Subscription, TextStyle, WeakEntity, Window, anchored, deferred, point,
|
||||
};
|
||||
use language_model::{LanguageModel, LanguageModelRegistry};
|
||||
use language_model_selector::ToggleModelSelector;
|
||||
@@ -28,7 +28,7 @@ use std::sync::Arc;
|
||||
use theme::ThemeSettings;
|
||||
use ui::utils::WithRemSize;
|
||||
use ui::{
|
||||
prelude::*, CheckboxWithLabel, IconButtonShape, KeyBinding, Popover, PopoverMenuHandle, Tooltip,
|
||||
CheckboxWithLabel, IconButtonShape, KeyBinding, Popover, PopoverMenuHandle, Tooltip, prelude::*,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
@@ -455,47 +455,55 @@ impl<T: 'static> PromptEditor<T> {
|
||||
|
||||
match codegen_status {
|
||||
CodegenStatus::Idle => {
|
||||
vec![Button::new("start", mode.start_label())
|
||||
.label_size(LabelSize::Small)
|
||||
.icon(IconName::Return)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StartRequested)))
|
||||
.into_any_element()]
|
||||
vec![
|
||||
Button::new("start", mode.start_label())
|
||||
.label_size(LabelSize::Small)
|
||||
.icon(IconName::Return)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.on_click(
|
||||
cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StartRequested)),
|
||||
)
|
||||
.into_any_element(),
|
||||
]
|
||||
}
|
||||
CodegenStatus::Pending => vec![IconButton::new("stop", IconName::Stop)
|
||||
.icon_color(Color::Error)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::with_meta(
|
||||
mode.tooltip_interrupt(),
|
||||
Some(&menu::Cancel),
|
||||
"Changes won't be discarded",
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StopRequested)))
|
||||
.into_any_element()],
|
||||
CodegenStatus::Pending => vec![
|
||||
IconButton::new("stop", IconName::Stop)
|
||||
.icon_color(Color::Error)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::with_meta(
|
||||
mode.tooltip_interrupt(),
|
||||
Some(&menu::Cancel),
|
||||
"Changes won't be discarded",
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StopRequested)))
|
||||
.into_any_element(),
|
||||
],
|
||||
CodegenStatus::Done | CodegenStatus::Error(_) => {
|
||||
let has_error = matches!(codegen_status, CodegenStatus::Error(_));
|
||||
if has_error || self.edited_since_done {
|
||||
vec![IconButton::new("restart", IconName::RotateCw)
|
||||
.icon_color(Color::Info)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::with_meta(
|
||||
mode.tooltip_restart(),
|
||||
Some(&menu::Confirm),
|
||||
"Changes will be discarded",
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(|_, _, _, cx| {
|
||||
cx.emit(PromptEditorEvent::StartRequested);
|
||||
}))
|
||||
.into_any_element()]
|
||||
vec![
|
||||
IconButton::new("restart", IconName::RotateCw)
|
||||
.icon_color(Color::Info)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::with_meta(
|
||||
mode.tooltip_restart(),
|
||||
Some(&menu::Confirm),
|
||||
"Changes will be discarded",
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(|_, _, _, cx| {
|
||||
cx.emit(PromptEditorEvent::StartRequested);
|
||||
}))
|
||||
.into_any_element(),
|
||||
]
|
||||
} else {
|
||||
let accept = IconButton::new("accept", IconName::Check)
|
||||
.icon_color(Color::Info)
|
||||
|
||||
@@ -3,12 +3,11 @@ use std::sync::Arc;
|
||||
use collections::HashSet;
|
||||
use editor::actions::MoveUp;
|
||||
use editor::{ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorStyle};
|
||||
use file_icons::FileIcons;
|
||||
use fs::Fs;
|
||||
use git::ExpandCommitEditor;
|
||||
use git_ui::git_panel;
|
||||
use gpui::{
|
||||
point, Animation, AnimationExt, App, DismissEvent, Entity, Focusable, Subscription, TextStyle,
|
||||
WeakEntity,
|
||||
Animation, AnimationExt, App, DismissEvent, Entity, Focusable, Subscription, TextStyle,
|
||||
WeakEntity, linear_color_stop, linear_gradient, point,
|
||||
};
|
||||
use language_model::LanguageModelRegistry;
|
||||
use language_model_selector::ToggleModelSelector;
|
||||
@@ -17,20 +16,23 @@ use settings::Settings;
|
||||
use std::time::Duration;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
prelude::*, ButtonLike, KeyBinding, PlatformStyle, PopoverMenu, PopoverMenuHandle, Tooltip,
|
||||
ButtonLike, Disclosure, KeyBinding, PlatformStyle, PopoverMenu, PopoverMenuHandle, Tooltip,
|
||||
prelude::*,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use vim_mode_setting::VimModeSetting;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::assistant_model_selector::AssistantModelSelector;
|
||||
use crate::context_picker::{ConfirmBehavior, ContextPicker, ContextPickerCompletionProvider};
|
||||
use crate::context_store::{refresh_context_store_text, ContextStore};
|
||||
use crate::context_store::{ContextStore, refresh_context_store_text};
|
||||
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
|
||||
use crate::profile_selector::ProfileSelector;
|
||||
use crate::thread::{RequestKind, Thread};
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::{
|
||||
Chat, ChatMode, RemoveAllContext, ThreadEvent, ToggleContextPicker, ToggleProfileSelector,
|
||||
AssistantDiff, Chat, ChatMode, OpenAssistantDiff, RemoveAllContext, ThreadEvent,
|
||||
ToggleContextPicker, ToggleProfileSelector,
|
||||
};
|
||||
|
||||
pub struct MessageEditor {
|
||||
@@ -46,6 +48,7 @@ pub struct MessageEditor {
|
||||
inline_context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||
model_selector: Entity<AssistantModelSelector>,
|
||||
profile_selector: Entity<ProfileSelector>,
|
||||
edits_expanded: bool,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
@@ -137,6 +140,7 @@ impl MessageEditor {
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
edits_expanded: false,
|
||||
profile_selector: cx
|
||||
.new(|cx| ProfileSelector::new(fs, thread_store, editor.focus_handle(cx), cx)),
|
||||
_subscriptions: subscriptions,
|
||||
@@ -235,7 +239,10 @@ impl MessageEditor {
|
||||
.ok();
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
let context = context_store.read(cx).snapshot(cx).collect::<Vec<_>>();
|
||||
let context = context_store.read(cx).context().clone();
|
||||
thread.action_log().update(cx, |action_log, cx| {
|
||||
action_log.clear_reviewed_changes(cx);
|
||||
});
|
||||
thread.insert_user_message(user_message, context, checkpoint, cx);
|
||||
thread.send_to_model(model, request_kind, cx);
|
||||
})
|
||||
@@ -282,6 +289,10 @@ impl MessageEditor {
|
||||
self.context_strip.focus_handle(cx).focus(window);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_review_click(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
AssistantDiff::deploy(self.thread.clone(), self.workspace.clone(), window, cx).log_err();
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for MessageEditor {
|
||||
@@ -298,7 +309,6 @@ impl Render for MessageEditor {
|
||||
let focus_handle = self.editor.focus_handle(cx);
|
||||
let inline_context_picker = self.inline_context_picker.clone();
|
||||
|
||||
let empty_thread = self.thread.read(cx).is_empty();
|
||||
let is_generating = self.thread.read(cx).is_generating();
|
||||
let is_model_selected = self.is_model_selected(cx);
|
||||
let is_editor_empty = self.is_editor_empty(cx);
|
||||
@@ -318,31 +328,15 @@ impl Render for MessageEditor {
|
||||
px(64.)
|
||||
};
|
||||
|
||||
let project = self.thread.read(cx).project();
|
||||
let changed_files = if let Some(repository) = project.read(cx).active_repository(cx) {
|
||||
repository.read(cx).cached_status().count()
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let action_log = self.thread.read(cx).action_log();
|
||||
let changed_buffers = action_log.read(cx).changed_buffers(cx);
|
||||
let changed_buffers_count = changed_buffers.len();
|
||||
|
||||
let editor_bg_color = cx.theme().colors().editor_background;
|
||||
let border_color = cx.theme().colors().border;
|
||||
let active_color = cx.theme().colors().element_selected;
|
||||
let editor_bg_color = cx.theme().colors().editor_background;
|
||||
let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
|
||||
|
||||
let edit_files_container = || {
|
||||
h_flex()
|
||||
.mx_2()
|
||||
.py_1()
|
||||
.pl_2p5()
|
||||
.pr_1()
|
||||
.bg(bg_edit_files_disclosure)
|
||||
.border_1()
|
||||
.border_color(border_color)
|
||||
.justify_between()
|
||||
.flex_wrap()
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.size_full()
|
||||
.when(is_generating, |parent| {
|
||||
@@ -403,169 +397,212 @@ impl Render for MessageEditor {
|
||||
),
|
||||
)
|
||||
})
|
||||
.when(
|
||||
changed_files > 0 && !is_generating && !empty_thread,
|
||||
|parent| {
|
||||
parent.child(
|
||||
edit_files_container()
|
||||
.border_b_0()
|
||||
.rounded_t_md()
|
||||
.shadow(smallvec::smallvec![gpui::BoxShadow {
|
||||
color: gpui::black().opacity(0.15),
|
||||
offset: point(px(1.), px(-1.)),
|
||||
blur_radius: px(3.),
|
||||
spread_radius: px(0.),
|
||||
}])
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Label::new("Edits").size(LabelSize::XSmall))
|
||||
.child(div().size_1().rounded_full().bg(border_color))
|
||||
.child(
|
||||
Label::new(format!(
|
||||
"{} {}",
|
||||
changed_files,
|
||||
if changed_files == 1 { "file" } else { "files" }
|
||||
))
|
||||
.size(LabelSize::XSmall),
|
||||
.when(changed_buffers_count > 0, |parent| {
|
||||
parent.child(
|
||||
v_flex()
|
||||
.mx_2()
|
||||
.bg(bg_edit_files_disclosure)
|
||||
.border_1()
|
||||
.border_b_0()
|
||||
.border_color(border_color)
|
||||
.rounded_t_md()
|
||||
.shadow(smallvec::smallvec![gpui::BoxShadow {
|
||||
color: gpui::black().opacity(0.15),
|
||||
offset: point(px(1.), px(-1.)),
|
||||
blur_radius: px(3.),
|
||||
spread_radius: px(0.),
|
||||
}])
|
||||
.child(
|
||||
h_flex()
|
||||
.p_1p5()
|
||||
.justify_between()
|
||||
.when(self.edits_expanded, |this| {
|
||||
this.border_b_1().border_color(border_color)
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Disclosure::new(
|
||||
"edits-disclosure",
|
||||
self.edits_expanded,
|
||||
)
|
||||
.on_click(
|
||||
cx.listener(|this, _ev, _window, cx| {
|
||||
this.edits_expanded = !this.edits_expanded;
|
||||
cx.notify();
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Label::new("Edits")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Label::new("•")
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Label::new(format!(
|
||||
"{} {}",
|
||||
changed_buffers_count,
|
||||
if changed_buffers_count == 1 {
|
||||
"file"
|
||||
} else {
|
||||
"files"
|
||||
}
|
||||
))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Button::new("review", "Review Changes")
|
||||
.label_size(LabelSize::Small)
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(
|
||||
&OpenAssistantDiff,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(12.))),
|
||||
)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.handle_review_click(window, cx)
|
||||
})),
|
||||
),
|
||||
)
|
||||
.when(self.edits_expanded, |parent| {
|
||||
parent.child(
|
||||
v_flex().bg(cx.theme().colors().editor_background).children(
|
||||
changed_buffers.into_iter().enumerate().flat_map(
|
||||
|(index, (buffer, changed))| {
|
||||
let file = buffer.read(cx).file()?;
|
||||
let path = file.path();
|
||||
|
||||
let parent_label = path.parent().and_then(|parent| {
|
||||
let parent_str = parent.to_string_lossy();
|
||||
|
||||
if parent_str.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
Label::new(format!(
|
||||
"{}{}",
|
||||
parent_str,
|
||||
std::path::MAIN_SEPARATOR_STR
|
||||
))
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::XSmall)
|
||||
.buffer_font(cx),
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
let name_label = path.file_name().map(|name| {
|
||||
Label::new(name.to_string_lossy().to_string())
|
||||
.size(LabelSize::XSmall)
|
||||
.buffer_font(cx)
|
||||
});
|
||||
|
||||
let file_icon = FileIcons::get_icon(&path, cx)
|
||||
.map(Icon::from_path)
|
||||
.map(|icon| {
|
||||
icon.color(Color::Muted).size(IconSize::Small)
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
Icon::new(IconName::File)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::Small)
|
||||
});
|
||||
|
||||
let element = div()
|
||||
.relative()
|
||||
.py_1()
|
||||
.px_2()
|
||||
.when(index + 1 < changed_buffers_count, |parent| {
|
||||
parent.border_color(border_color).border_b_1()
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.id("file-container")
|
||||
.pr_8()
|
||||
.gap_1p5()
|
||||
.max_w_full()
|
||||
.overflow_x_scroll()
|
||||
.child(file_icon)
|
||||
.child(
|
||||
h_flex()
|
||||
.children(parent_label)
|
||||
.children(name_label),
|
||||
) // TODO: show lines changed
|
||||
.child(
|
||||
Label::new("+")
|
||||
.color(Color::Created),
|
||||
)
|
||||
.child(
|
||||
Label::new("-")
|
||||
.color(Color::Deleted),
|
||||
),
|
||||
)
|
||||
.when(!changed.needs_review, |parent| {
|
||||
parent.child(
|
||||
Icon::new(IconName::Check)
|
||||
.color(Color::Success),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.h_full()
|
||||
.absolute()
|
||||
.w_8()
|
||||
.bottom_0()
|
||||
.map(|this| {
|
||||
if !changed.needs_review {
|
||||
this.right_4()
|
||||
} else {
|
||||
this.right_0()
|
||||
}
|
||||
})
|
||||
.bg(linear_gradient(
|
||||
90.,
|
||||
linear_color_stop(
|
||||
editor_bg_color,
|
||||
1.,
|
||||
),
|
||||
linear_color_stop(
|
||||
editor_bg_color
|
||||
.opacity(0.2),
|
||||
0.,
|
||||
),
|
||||
)),
|
||||
),
|
||||
);
|
||||
|
||||
Some(element)
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Button::new("panel", "Open Git Panel")
|
||||
.label_size(LabelSize::XSmall)
|
||||
.key_binding({
|
||||
let focus_handle = focus_handle.clone();
|
||||
KeyBinding::for_action_in(
|
||||
&git_panel::ToggleFocus,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(10.)))
|
||||
})
|
||||
.on_click(|_ev, _window, cx| {
|
||||
cx.defer(|cx| {
|
||||
cx.dispatch_action(&git_panel::ToggleFocus)
|
||||
});
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("review", "Review Diff")
|
||||
.label_size(LabelSize::XSmall)
|
||||
.key_binding({
|
||||
let focus_handle = focus_handle.clone();
|
||||
KeyBinding::for_action_in(
|
||||
&git_ui::project_diff::Diff,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(10.)))
|
||||
})
|
||||
.on_click(|_event, _window, cx| {
|
||||
cx.defer(|cx| {
|
||||
cx.dispatch_action(&git_ui::project_diff::Diff)
|
||||
});
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("commit", "Commit Changes")
|
||||
.label_size(LabelSize::XSmall)
|
||||
.key_binding({
|
||||
let focus_handle = focus_handle.clone();
|
||||
KeyBinding::for_action_in(
|
||||
&ExpandCommitEditor,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(10.)))
|
||||
})
|
||||
.on_click(|_event, _window, cx| {
|
||||
cx.defer(|cx| {
|
||||
cx.dispatch_action(&ExpandCommitEditor)
|
||||
});
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
.when(
|
||||
changed_files > 0 && !is_generating && empty_thread,
|
||||
|parent| {
|
||||
parent.child(
|
||||
edit_files_container()
|
||||
.mb_2()
|
||||
.rounded_md()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Label::new("Consider committing your changes before starting a fresh thread").size(LabelSize::XSmall))
|
||||
.child(div().size_1().rounded_full().bg(border_color))
|
||||
.child(
|
||||
Label::new(format!(
|
||||
"{} {}",
|
||||
changed_files,
|
||||
if changed_files == 1 { "file" } else { "files" }
|
||||
))
|
||||
.size(LabelSize::XSmall),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Button::new("review", "Review Diff")
|
||||
.label_size(LabelSize::XSmall)
|
||||
.key_binding({
|
||||
let focus_handle = focus_handle.clone();
|
||||
KeyBinding::for_action_in(
|
||||
&git_ui::project_diff::Diff,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(10.)))
|
||||
})
|
||||
.on_click(|_event, _window, cx| {
|
||||
cx.defer(|cx| {
|
||||
cx.dispatch_action(&git_ui::project_diff::Diff)
|
||||
});
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("commit", "Commit Changes")
|
||||
.label_size(LabelSize::XSmall)
|
||||
.key_binding({
|
||||
let focus_handle = focus_handle.clone();
|
||||
KeyBinding::for_action_in(
|
||||
&ExpandCommitEditor,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(10.)))
|
||||
})
|
||||
.on_click(|_event, _window, cx| {
|
||||
cx.defer(|cx| {
|
||||
cx.dispatch_action(&ExpandCommitEditor)
|
||||
});
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
v_flex()
|
||||
.key_context("MessageEditor")
|
||||
.on_action(cx.listener(Self::chat))
|
||||
.on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| {
|
||||
this.profile_selector.read(cx).menu_handle().toggle(window, cx);
|
||||
this.profile_selector
|
||||
.read(cx)
|
||||
.menu_handle()
|
||||
.toggle(window, cx);
|
||||
}))
|
||||
.on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
|
||||
this.model_selector
|
||||
|
||||
@@ -2,10 +2,13 @@ use std::sync::Arc;
|
||||
|
||||
use assistant_settings::{AgentProfile, AssistantSettings};
|
||||
use fs::Fs;
|
||||
use gpui::{prelude::*, Action, Entity, FocusHandle, Subscription, WeakEntity};
|
||||
use gpui::{Action, Entity, FocusHandle, Subscription, WeakEntity, prelude::*};
|
||||
use indexmap::IndexMap;
|
||||
use settings::{update_settings_file, Settings as _, SettingsStore};
|
||||
use ui::{prelude::*, ContextMenu, ContextMenuEntry, PopoverMenu, PopoverMenuHandle, Tooltip};
|
||||
use settings::{Settings as _, SettingsStore, update_settings_file};
|
||||
use ui::{
|
||||
ButtonLike, ContextMenu, ContextMenuEntry, KeyBinding, PopoverMenu, PopoverMenuHandle,
|
||||
prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::{ManageProfiles, ThreadStore, ToggleProfileSelector};
|
||||
@@ -60,7 +63,7 @@ impl ProfileSelector {
|
||||
) -> Entity<ContextMenu> {
|
||||
ContextMenu::build(window, cx, |mut menu, _window, cx| {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
let icon_position = IconPosition::Start;
|
||||
let icon_position = IconPosition::End;
|
||||
|
||||
menu = menu.header("Profiles");
|
||||
for (profile_id, profile) in self.profiles.clone() {
|
||||
@@ -91,14 +94,23 @@ impl ProfileSelector {
|
||||
}
|
||||
|
||||
menu = menu.separator();
|
||||
menu = menu.item(
|
||||
ContextMenuEntry::new("Configure Profiles")
|
||||
.icon(IconName::Pencil)
|
||||
.icon_color(Color::Muted)
|
||||
.handler(move |window, cx| {
|
||||
window.dispatch_action(ManageProfiles.boxed_clone(), cx);
|
||||
}),
|
||||
);
|
||||
menu = menu.header("Customize Current Profile");
|
||||
menu = menu.item(ContextMenuEntry::new("Tools…").handler({
|
||||
let profile_id = settings.default_profile.clone();
|
||||
move |window, cx| {
|
||||
window.dispatch_action(
|
||||
ManageProfiles::customize_tools(profile_id.clone()).boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}));
|
||||
|
||||
menu = menu.separator();
|
||||
menu = menu.item(ContextMenuEntry::new("Configure Profiles…").handler(
|
||||
move |window, cx| {
|
||||
window.dispatch_action(ManageProfiles::default().boxed_clone(), cx);
|
||||
},
|
||||
));
|
||||
|
||||
menu
|
||||
})
|
||||
@@ -106,33 +118,53 @@ impl ProfileSelector {
|
||||
}
|
||||
|
||||
impl Render for ProfileSelector {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
let profile = settings
|
||||
.profiles
|
||||
.get(&settings.default_profile)
|
||||
let profile_id = &settings.default_profile;
|
||||
let profile = settings.profiles.get(profile_id);
|
||||
|
||||
let selected_profile = profile
|
||||
.map(|profile| profile.name.clone())
|
||||
.unwrap_or_else(|| "Unknown".into());
|
||||
|
||||
let icon = match profile_id.as_ref() {
|
||||
"write" => IconName::Pencil,
|
||||
"ask" => IconName::MessageBubbles,
|
||||
_ => IconName::UserRoundPen,
|
||||
};
|
||||
|
||||
let this = cx.entity().clone();
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
PopoverMenu::new("profile-selector")
|
||||
.menu(move |window, cx| {
|
||||
Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
|
||||
})
|
||||
.trigger_with_tooltip(
|
||||
Button::new("profile-selector-button", profile)
|
||||
.style(ButtonStyle::Filled)
|
||||
.label_size(LabelSize::Small),
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Change Profile",
|
||||
&ToggleProfileSelector,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
},
|
||||
.trigger(
|
||||
ButtonLike::new("profile-selector-button").child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted))
|
||||
.child(
|
||||
Label::new(selected_profile)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(div().opacity(0.5).children({
|
||||
let focus_handle = focus_handle.clone();
|
||||
KeyBinding::for_action_in(
|
||||
&ToggleProfileSelector,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(10.)))
|
||||
})),
|
||||
),
|
||||
)
|
||||
.anchor(gpui::Corner::BottomLeft)
|
||||
.with_handle(self.menu_handle.clone())
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::inline_prompt_editor::CodegenStatus;
|
||||
use client::telemetry::Telemetry;
|
||||
use futures::{channel::mpsc, SinkExt, StreamExt};
|
||||
use futures::{SinkExt, StreamExt, channel::mpsc};
|
||||
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Task};
|
||||
use language_model::{report_assistant_event, LanguageModelRegistry, LanguageModelRequest};
|
||||
use language_model::{LanguageModelRegistry, LanguageModelRequest, report_assistant_event};
|
||||
use std::{sync::Arc, time::Instant};
|
||||
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
|
||||
use terminal::Terminal;
|
||||
|
||||
@@ -3,18 +3,18 @@ use crate::context_store::ContextStore;
|
||||
use crate::inline_prompt_editor::{
|
||||
CodegenStatus, PromptEditor, PromptEditorEvent, TerminalInlineAssistId,
|
||||
};
|
||||
use crate::terminal_codegen::{CodegenEvent, TerminalCodegen, CLEAR_INPUT};
|
||||
use crate::terminal_codegen::{CLEAR_INPUT, CodegenEvent, TerminalCodegen};
|
||||
use crate::thread_store::ThreadStore;
|
||||
use anyhow::{Context as _, Result};
|
||||
use client::telemetry::Telemetry;
|
||||
use collections::{HashMap, VecDeque};
|
||||
use editor::{actions::SelectAll, MultiBuffer};
|
||||
use editor::{MultiBuffer, actions::SelectAll};
|
||||
use fs::Fs;
|
||||
use gpui::{App, Entity, Focusable, Global, Subscription, UpdateGlobal, WeakEntity};
|
||||
use language::Buffer;
|
||||
use language_model::{
|
||||
report_assistant_event, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, Role,
|
||||
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
|
||||
report_assistant_event,
|
||||
};
|
||||
use prompt_store::PromptBuilder;
|
||||
use std::sync::Arc;
|
||||
@@ -22,7 +22,7 @@ use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
|
||||
use terminal_view::TerminalView;
|
||||
use ui::prelude::*;
|
||||
use util::ResultExt;
|
||||
use workspace::{notifications::NotificationId, Toast, Workspace};
|
||||
use workspace::{Toast, Workspace, notifications::NotificationId};
|
||||
|
||||
pub fn init(
|
||||
fs: Arc<dyn Fs>,
|
||||
@@ -252,7 +252,8 @@ impl TerminalInlineAssistant {
|
||||
|
||||
attach_context_to_message(
|
||||
&mut request_message,
|
||||
assist.context_store.read(cx).snapshot(cx),
|
||||
assist.context_store.read(cx).context().iter(),
|
||||
cx,
|
||||
);
|
||||
|
||||
request_message.content.push(prompt.into());
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::fmt::Write as _;
|
||||
use std::io::Write;
|
||||
use std::ops::Range;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
@@ -25,10 +26,10 @@ use prompt_store::{
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
use util::{maybe, post_inc, ResultExt as _, TryFutureExt as _};
|
||||
use util::{ResultExt as _, TryFutureExt as _, maybe, post_inc};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::context::{attach_context_to_message, ContextId, ContextSnapshot};
|
||||
use crate::context::{AssistantContext, ContextId, attach_context_to_message};
|
||||
use crate::thread_store::{
|
||||
SerializedMessage, SerializedMessageSegment, SerializedThread, SerializedToolResult,
|
||||
SerializedToolUse,
|
||||
@@ -174,7 +175,7 @@ pub struct Thread {
|
||||
pending_summary: Task<Option<()>>,
|
||||
messages: Vec<Message>,
|
||||
next_message_id: MessageId,
|
||||
context: BTreeMap<ContextId, ContextSnapshot>,
|
||||
context: BTreeMap<ContextId, AssistantContext>,
|
||||
context_by_message: HashMap<MessageId, Vec<ContextId>>,
|
||||
system_prompt_context: Option<AssistantSystemPromptContext>,
|
||||
checkpoints_by_message: HashMap<MessageId, ThreadCheckpoint>,
|
||||
@@ -472,15 +473,15 @@ impl Thread {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn context_for_message(&self, id: MessageId) -> Option<Vec<ContextSnapshot>> {
|
||||
let context = self.context_by_message.get(&id)?;
|
||||
Some(
|
||||
context
|
||||
.into_iter()
|
||||
.filter_map(|context_id| self.context.get(&context_id))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
pub fn context_for_message(&self, id: MessageId) -> impl Iterator<Item = &AssistantContext> {
|
||||
self.context_by_message
|
||||
.get(&id)
|
||||
.into_iter()
|
||||
.flat_map(|context| {
|
||||
context
|
||||
.iter()
|
||||
.filter_map(|context_id| self.context.get(&context_id))
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns whether all of the tool uses have finished running.
|
||||
@@ -512,15 +513,18 @@ impl Thread {
|
||||
pub fn insert_user_message(
|
||||
&mut self,
|
||||
text: impl Into<String>,
|
||||
context: Vec<ContextSnapshot>,
|
||||
context: Vec<AssistantContext>,
|
||||
git_checkpoint: Option<GitStoreCheckpoint>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> MessageId {
|
||||
let message_id =
|
||||
self.insert_message(Role::User, vec![MessageSegment::Text(text.into())], cx);
|
||||
let context_ids = context.iter().map(|context| context.id).collect::<Vec<_>>();
|
||||
let context_ids = context
|
||||
.iter()
|
||||
.map(|context| context.id())
|
||||
.collect::<Vec<_>>();
|
||||
self.context
|
||||
.extend(context.into_iter().map(|context| (context.id, context)));
|
||||
.extend(context.into_iter().map(|context| (context.id(), context)));
|
||||
self.context_by_message.insert(message_id, context_ids);
|
||||
if let Some(git_checkpoint) = git_checkpoint {
|
||||
self.pending_checkpoint = Some(ThreadCheckpoint {
|
||||
@@ -776,7 +780,7 @@ impl Thread {
|
||||
LanguageModelRequestTool {
|
||||
name: tool.name(),
|
||||
description: tool.description(),
|
||||
input_schema: tool.input_schema(),
|
||||
input_schema: tool.input_schema(model.tool_input_format()),
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -888,9 +892,8 @@ impl Thread {
|
||||
|
||||
let referenced_context = referenced_context_ids
|
||||
.into_iter()
|
||||
.filter_map(|context_id| self.context.get(context_id))
|
||||
.cloned();
|
||||
attach_context_to_message(&mut context_message, referenced_context);
|
||||
.filter_map(|context_id| self.context.get(context_id));
|
||||
attach_context_to_message(&mut context_message, referenced_context, cx);
|
||||
|
||||
request.messages.push(context_message);
|
||||
}
|
||||
@@ -931,7 +934,10 @@ impl Thread {
|
||||
|
||||
if action_log.has_edited_files_since_project_diagnostics_check() {
|
||||
content.push(
|
||||
"When you're done making changes, make sure to check project diagnostics and fix all errors AND warnings you introduced!".into(),
|
||||
"\n\nWhen you're done making changes, make sure to check project diagnostics \
|
||||
and fix all errors AND warnings you introduced! \
|
||||
DO NOT mention you're going to do this until you're done."
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1029,17 +1035,23 @@ impl Thread {
|
||||
}
|
||||
}
|
||||
LanguageModelCompletionEvent::ToolUse(tool_use) => {
|
||||
if let Some(last_assistant_message) = thread
|
||||
let last_assistant_message_id = thread
|
||||
.messages
|
||||
.iter()
|
||||
.rfind(|message| message.role == Role::Assistant)
|
||||
{
|
||||
thread.tool_use.request_tool_use(
|
||||
last_assistant_message.id,
|
||||
tool_use,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
.map(|message| message.id)
|
||||
.unwrap_or_else(|| {
|
||||
thread.insert_message(
|
||||
Role::Assistant,
|
||||
vec![MessageSegment::Text("Using tool...".to_string())],
|
||||
cx,
|
||||
)
|
||||
});
|
||||
thread.tool_use.request_tool_use(
|
||||
last_assistant_message_id,
|
||||
tool_use,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1185,7 +1197,7 @@ impl Thread {
|
||||
pub fn use_pending_tools(
|
||||
&mut self,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoIterator<Item = PendingToolUse> {
|
||||
) -> impl IntoIterator<Item = PendingToolUse> + use<> {
|
||||
let request = self.to_completion_request(RequestKind::Chat, cx);
|
||||
let messages = Arc::new(request.messages);
|
||||
let pending_tool_uses = self
|
||||
@@ -1241,7 +1253,7 @@ impl Thread {
|
||||
input: serde_json::Value,
|
||||
messages: &[LanguageModelRequestMessage],
|
||||
tool: Arc<dyn Tool>,
|
||||
cx: &mut Context<'_, Thread>,
|
||||
cx: &mut Context<Thread>,
|
||||
) {
|
||||
let task = self.spawn_tool_use(tool_use_id.clone(), messages, input, tool, cx);
|
||||
self.tool_use
|
||||
@@ -1256,6 +1268,7 @@ impl Thread {
|
||||
tool: Arc<dyn Tool>,
|
||||
cx: &mut Context<Thread>,
|
||||
) -> Task<()> {
|
||||
let tool_name: Arc<str> = tool.name().into();
|
||||
let run_tool = tool.run(
|
||||
input,
|
||||
messages,
|
||||
@@ -1270,9 +1283,11 @@ impl Thread {
|
||||
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
let pending_tool_use = thread
|
||||
.tool_use
|
||||
.insert_tool_output(tool_use_id.clone(), output);
|
||||
let pending_tool_use = thread.tool_use.insert_tool_output(
|
||||
tool_use_id.clone(),
|
||||
tool_name,
|
||||
output,
|
||||
);
|
||||
|
||||
cx.emit(ThreadEvent::ToolFinished {
|
||||
tool_use_id,
|
||||
@@ -1287,13 +1302,13 @@ impl Thread {
|
||||
|
||||
pub fn attach_tool_results(
|
||||
&mut self,
|
||||
updated_context: Vec<ContextSnapshot>,
|
||||
updated_context: Vec<AssistantContext>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.context.extend(
|
||||
updated_context
|
||||
.into_iter()
|
||||
.map(|context| (context.id, context)),
|
||||
.map(|context| (context.id(), context)),
|
||||
);
|
||||
|
||||
// Insert a user message to contain the tool results.
|
||||
@@ -1529,6 +1544,25 @@ impl Thread {
|
||||
Ok(String::from_utf8_lossy(&markdown).to_string())
|
||||
}
|
||||
|
||||
pub fn review_edits_in_range(
|
||||
&mut self,
|
||||
buffer: Entity<language::Buffer>,
|
||||
buffer_range: Range<language::Anchor>,
|
||||
accept: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.action_log.update(cx, |action_log, cx| {
|
||||
action_log.review_edits_in_range(buffer, buffer_range, accept, cx)
|
||||
});
|
||||
}
|
||||
|
||||
/// Keeps all edits across all buffers at once.
|
||||
/// This provides a more performant alternative to calling review_edits_in_range for each buffer.
|
||||
pub fn keep_all_edits(&mut self, cx: &mut Context<Self>) {
|
||||
self.action_log
|
||||
.update(cx, |action_log, _cx| action_log.keep_all_edits());
|
||||
}
|
||||
|
||||
pub fn action_log(&self) -> &Entity<ActionLog> {
|
||||
&self.action_log
|
||||
}
|
||||
@@ -1541,12 +1575,18 @@ impl Thread {
|
||||
self.cumulative_token_usage.clone()
|
||||
}
|
||||
|
||||
pub fn deny_tool_use(&mut self, tool_use_id: LanguageModelToolUseId, cx: &mut Context<Self>) {
|
||||
pub fn deny_tool_use(
|
||||
&mut self,
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
tool_name: Arc<str>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let err = Err(anyhow::anyhow!(
|
||||
"Permission to run tool action denied by user"
|
||||
));
|
||||
|
||||
self.tool_use.insert_tool_output(tool_use_id.clone(), err);
|
||||
self.tool_use
|
||||
.insert_tool_output(tool_use_id.clone(), tool_name, err);
|
||||
|
||||
cx.emit(ThreadEvent::ToolFinished {
|
||||
tool_use_id,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use assistant_context_editor::SavedContextMetadata;
|
||||
use gpui::{
|
||||
uniform_list, App, Entity, FocusHandle, Focusable, ScrollStrategy, UniformListScrollHandle,
|
||||
WeakEntity,
|
||||
App, Entity, FocusHandle, Focusable, ScrollStrategy, UniformListScrollHandle, WeakEntity,
|
||||
uniform_list,
|
||||
};
|
||||
use time::{OffsetDateTime, UtcOffset};
|
||||
use ui::{prelude::*, IconButtonShape, ListItem, ListItemSpacing, Tooltip};
|
||||
use ui::{IconButtonShape, ListItem, ListItemSpacing, Tooltip, prelude::*};
|
||||
|
||||
use crate::history_store::{HistoryEntry, HistoryStore};
|
||||
use crate::thread_store::SerializedThreadMetadata;
|
||||
|
||||
@@ -2,20 +2,20 @@ use std::borrow::Cow;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_settings::{AgentProfile, AssistantSettings};
|
||||
use assistant_tool::{ToolId, ToolSource, ToolWorkingSet};
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::HashMap;
|
||||
use context_server::manager::ContextServerManager;
|
||||
use context_server::{ContextServerFactoryRegistry, ContextServerTool};
|
||||
use futures::future::{self, BoxFuture, Shared};
|
||||
use futures::FutureExt as _;
|
||||
use futures::future::{self, BoxFuture, Shared};
|
||||
use gpui::{
|
||||
prelude::*, App, BackgroundExecutor, Context, Entity, Global, ReadGlobal, SharedString, Task,
|
||||
App, BackgroundExecutor, Context, Entity, Global, ReadGlobal, SharedString, Task, prelude::*,
|
||||
};
|
||||
use heed::types::SerdeBincode;
|
||||
use heed::Database;
|
||||
use heed::types::SerdeBincode;
|
||||
use language_model::{LanguageModelToolUseId, Role, TokenUsage};
|
||||
use project::Project;
|
||||
use prompt_store::PromptBuilder;
|
||||
|
||||
@@ -3,8 +3,8 @@ use std::sync::Arc;
|
||||
use anyhow::Result;
|
||||
use assistant_tool::{Tool, ToolWorkingSet};
|
||||
use collections::HashMap;
|
||||
use futures::future::Shared;
|
||||
use futures::FutureExt as _;
|
||||
use futures::future::Shared;
|
||||
use gpui::{App, SharedString, Task};
|
||||
use language_model::{
|
||||
LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelToolUse,
|
||||
@@ -23,6 +23,7 @@ pub struct ToolUse {
|
||||
pub status: ToolUseStatus,
|
||||
pub input: serde_json::Value,
|
||||
pub icon: ui::IconName,
|
||||
pub needs_confirmation: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -112,6 +113,7 @@ impl ToolUseState {
|
||||
tool_use_id.clone(),
|
||||
LanguageModelToolResult {
|
||||
tool_use_id,
|
||||
tool_name: tool_use.clone(),
|
||||
is_error: tool_result.is_error,
|
||||
content: tool_result.content.clone(),
|
||||
},
|
||||
@@ -133,6 +135,7 @@ impl ToolUseState {
|
||||
tool_use_id.clone(),
|
||||
LanguageModelToolResult {
|
||||
tool_use_id,
|
||||
tool_name: tool_use.name.clone(),
|
||||
content: "Tool canceled by user".into(),
|
||||
is_error: true,
|
||||
},
|
||||
@@ -181,10 +184,11 @@ impl ToolUseState {
|
||||
}
|
||||
})();
|
||||
|
||||
let icon = if let Some(tool) = self.tools.tool(&tool_use.name, cx) {
|
||||
tool.icon()
|
||||
let (icon, needs_confirmation) = if let Some(tool) = self.tools.tool(&tool_use.name, cx)
|
||||
{
|
||||
(tool.icon(), tool.needs_confirmation())
|
||||
} else {
|
||||
IconName::Cog
|
||||
(IconName::Cog, false)
|
||||
};
|
||||
|
||||
tool_uses.push(ToolUse {
|
||||
@@ -194,6 +198,7 @@ impl ToolUseState {
|
||||
input: tool_use.input.clone(),
|
||||
status,
|
||||
icon,
|
||||
needs_confirmation,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -310,6 +315,7 @@ impl ToolUseState {
|
||||
pub fn insert_tool_output(
|
||||
&mut self,
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
tool_name: Arc<str>,
|
||||
output: Result<String>,
|
||||
) -> Option<PendingToolUse> {
|
||||
match output {
|
||||
@@ -318,6 +324,7 @@ impl ToolUseState {
|
||||
tool_use_id.clone(),
|
||||
LanguageModelToolResult {
|
||||
tool_use_id: tool_use_id.clone(),
|
||||
tool_name,
|
||||
content: tool_result.into(),
|
||||
is_error: false,
|
||||
},
|
||||
@@ -329,6 +336,7 @@ impl ToolUseState {
|
||||
tool_use_id.clone(),
|
||||
LanguageModelToolResult {
|
||||
tool_use_id: tool_use_id.clone(),
|
||||
tool_name,
|
||||
content: err.to_string().into(),
|
||||
is_error: true,
|
||||
},
|
||||
@@ -376,6 +384,7 @@ impl ToolUseState {
|
||||
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.
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use gpui::{
|
||||
linear_color_stop, linear_gradient, point, App, Context, EventEmitter, IntoElement,
|
||||
PlatformDisplay, Size, Window, WindowBackgroundAppearance, WindowBounds, WindowDecorations,
|
||||
WindowKind, WindowOptions,
|
||||
App, Context, EventEmitter, IntoElement, PlatformDisplay, Size, Window,
|
||||
WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, WindowOptions,
|
||||
linear_color_stop, linear_gradient, point,
|
||||
};
|
||||
use release_channel::ReleaseChannel;
|
||||
use std::rc::Rc;
|
||||
use theme;
|
||||
use ui::{prelude::*, Render};
|
||||
use ui::{Render, prelude::*};
|
||||
|
||||
pub struct AgentNotification {
|
||||
title: SharedString,
|
||||
@@ -76,6 +76,19 @@ impl Render for AgentNotification {
|
||||
let line_height = window.line_height();
|
||||
|
||||
let bg = cx.theme().colors().elevated_surface_background;
|
||||
let gradient_overflow = || {
|
||||
div()
|
||||
.h_full()
|
||||
.absolute()
|
||||
.w_8()
|
||||
.bottom_0()
|
||||
.right_0()
|
||||
.bg(linear_gradient(
|
||||
90.,
|
||||
linear_color_stop(bg, 1.),
|
||||
linear_color_stop(bg.opacity(0.2), 0.),
|
||||
))
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.id("agent-notification")
|
||||
@@ -107,27 +120,23 @@ impl Render for AgentNotification {
|
||||
v_flex()
|
||||
.child(
|
||||
div()
|
||||
.relative()
|
||||
.text_size(px(14.))
|
||||
.text_color(cx.theme().colors().text)
|
||||
.child(self.title.clone()),
|
||||
.max_w(px(300.))
|
||||
.truncate()
|
||||
.child(self.title.clone())
|
||||
.child(gradient_overflow()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.relative()
|
||||
.text_size(px(12.))
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.max_w(px(340.))
|
||||
.truncate()
|
||||
.child(self.caption.clone())
|
||||
.relative()
|
||||
.child(
|
||||
div().h_full().absolute().w_8().bottom_0().right_0().bg(
|
||||
linear_gradient(
|
||||
90.,
|
||||
linear_color_stop(bg, 1.),
|
||||
linear_color_stop(bg.opacity(0.2), 0.),
|
||||
),
|
||||
),
|
||||
),
|
||||
.child(gradient_overflow()),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use file_icons::FileIcons;
|
||||
use gpui::ClickEvent;
|
||||
use ui::{prelude::*, IconButtonShape, Tooltip};
|
||||
use ui::{IconButtonShape, Tooltip, prelude::*};
|
||||
|
||||
use crate::context::{ContextKind, ContextSnapshot};
|
||||
use crate::context::{AssistantContext, ContextId, ContextKind};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub enum ContextPill {
|
||||
Added {
|
||||
context: ContextSnapshot,
|
||||
context: AddedContext,
|
||||
dupe_name: bool,
|
||||
focused: bool,
|
||||
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
|
||||
@@ -25,7 +26,7 @@ pub enum ContextPill {
|
||||
|
||||
impl ContextPill {
|
||||
pub fn added(
|
||||
context: ContextSnapshot,
|
||||
context: AddedContext,
|
||||
dupe_name: bool,
|
||||
focused: bool,
|
||||
on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
|
||||
@@ -77,17 +78,21 @@ impl ContextPill {
|
||||
|
||||
pub fn icon(&self) -> Icon {
|
||||
match self {
|
||||
Self::Added { context, .. } => match &context.icon_path {
|
||||
Some(icon_path) => Icon::from_path(icon_path),
|
||||
None => Icon::new(context.kind.icon()),
|
||||
},
|
||||
Self::Suggested {
|
||||
icon_path: Some(icon_path),
|
||||
..
|
||||
}
|
||||
| Self::Added {
|
||||
context:
|
||||
AddedContext {
|
||||
icon_path: Some(icon_path),
|
||||
..
|
||||
},
|
||||
..
|
||||
} => Icon::from_path(icon_path),
|
||||
Self::Suggested {
|
||||
kind,
|
||||
icon_path: None,
|
||||
Self::Suggested { kind, .. }
|
||||
| Self::Added {
|
||||
context: AddedContext { kind, .. },
|
||||
..
|
||||
} => Icon::new(kind.icon()),
|
||||
}
|
||||
@@ -144,7 +149,7 @@ impl RenderOnce for ContextPill {
|
||||
element
|
||||
}
|
||||
})
|
||||
.when_some(context.tooltip.clone(), |element, tooltip| {
|
||||
.when_some(context.tooltip.as_ref(), |element, tooltip| {
|
||||
element.tooltip(Tooltip::text(tooltip.clone()))
|
||||
}),
|
||||
)
|
||||
@@ -162,7 +167,9 @@ impl RenderOnce for ContextPill {
|
||||
})
|
||||
.when_some(on_click.as_ref(), |element, on_click| {
|
||||
let on_click = on_click.clone();
|
||||
element.on_click(move |event, window, cx| on_click(event, window, cx))
|
||||
element
|
||||
.cursor_pointer()
|
||||
.on_click(move |event, window, cx| on_click(event, window, cx))
|
||||
}),
|
||||
ContextPill::Suggested {
|
||||
name,
|
||||
@@ -173,10 +180,14 @@ impl RenderOnce for ContextPill {
|
||||
} => base_pill
|
||||
.cursor_pointer()
|
||||
.pr_1()
|
||||
.when(*focused, |this| {
|
||||
this.bg(color.element_background.opacity(0.5))
|
||||
})
|
||||
.border_dashed()
|
||||
.border_color(if *focused {
|
||||
color.border_focused
|
||||
} else {
|
||||
color.border_variant.opacity(0.5)
|
||||
color.border
|
||||
})
|
||||
.hover(|style| style.bg(color.element_hover.opacity(0.5)))
|
||||
.child(
|
||||
@@ -213,3 +224,91 @@ impl RenderOnce for ContextPill {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AddedContext {
|
||||
pub id: ContextId,
|
||||
pub kind: ContextKind,
|
||||
pub name: SharedString,
|
||||
pub parent: Option<SharedString>,
|
||||
pub tooltip: Option<SharedString>,
|
||||
pub icon_path: Option<SharedString>,
|
||||
}
|
||||
|
||||
impl AddedContext {
|
||||
pub fn new(context: &AssistantContext, cx: &App) -> AddedContext {
|
||||
match context {
|
||||
AssistantContext::File(file_context) => {
|
||||
let full_path = file_context.context_buffer.file.full_path(cx);
|
||||
let full_path_string: SharedString =
|
||||
full_path.to_string_lossy().into_owned().into();
|
||||
let name = full_path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned().into())
|
||||
.unwrap_or_else(|| full_path_string.clone());
|
||||
let parent = full_path
|
||||
.parent()
|
||||
.and_then(|p| p.file_name())
|
||||
.map(|n| n.to_string_lossy().into_owned().into());
|
||||
AddedContext {
|
||||
id: file_context.id,
|
||||
kind: ContextKind::File,
|
||||
name,
|
||||
parent,
|
||||
tooltip: Some(full_path_string),
|
||||
icon_path: FileIcons::get_icon(&full_path, cx),
|
||||
}
|
||||
}
|
||||
|
||||
AssistantContext::Directory(directory_context) => {
|
||||
// TODO: handle worktree disambiguation. Maybe by storing an `Arc<dyn File>` to also
|
||||
// handle renames?
|
||||
let full_path = &directory_context.project_path.path;
|
||||
let full_path_string: SharedString =
|
||||
full_path.to_string_lossy().into_owned().into();
|
||||
let name = full_path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned().into())
|
||||
.unwrap_or_else(|| full_path_string.clone());
|
||||
let parent = full_path
|
||||
.parent()
|
||||
.and_then(|p| p.file_name())
|
||||
.map(|n| n.to_string_lossy().into_owned().into());
|
||||
AddedContext {
|
||||
id: directory_context.id,
|
||||
kind: ContextKind::Directory,
|
||||
name,
|
||||
parent,
|
||||
tooltip: Some(full_path_string),
|
||||
icon_path: None,
|
||||
}
|
||||
}
|
||||
|
||||
AssistantContext::Symbol(symbol_context) => AddedContext {
|
||||
id: symbol_context.id,
|
||||
kind: ContextKind::Symbol,
|
||||
name: symbol_context.context_symbol.id.name.clone(),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
},
|
||||
|
||||
AssistantContext::FetchedUrl(fetched_url_context) => AddedContext {
|
||||
id: fetched_url_context.id,
|
||||
kind: ContextKind::FetchedUrl,
|
||||
name: fetched_url_context.url.clone(),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
},
|
||||
|
||||
AssistantContext::Thread(thread_context) => AddedContext {
|
||||
id: thread_context.id,
|
||||
kind: ContextKind::Thread,
|
||||
name: thread_context.summary(cx),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
mod context_tests;
|
||||
|
||||
use crate::patch::{AssistantEdit, AssistantPatch, AssistantPatchStatus};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_slash_command::{
|
||||
SlashCommandContent, SlashCommandEvent, SlashCommandLine, SlashCommandOutputSection,
|
||||
SlashCommandResult, SlashCommandWorkingSet,
|
||||
@@ -12,17 +12,17 @@ use client::{self, proto, telemetry::Telemetry};
|
||||
use clock::ReplicaId;
|
||||
use collections::{HashMap, HashSet};
|
||||
use fs::{Fs, RemoveOptions};
|
||||
use futures::{future::Shared, FutureExt, StreamExt};
|
||||
use futures::{FutureExt, StreamExt, future::Shared};
|
||||
use gpui::{
|
||||
App, AppContext as _, Context, Entity, EventEmitter, RenderImage, SharedString, Subscription,
|
||||
Task,
|
||||
};
|
||||
use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, Point, ToOffset};
|
||||
use language_model::{
|
||||
report_assistant_event, LanguageModel, LanguageModelCacheConfiguration,
|
||||
LanguageModelCompletionEvent, LanguageModelImage, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, LanguageModelToolUseId, MaxMonthlySpendReachedError,
|
||||
MessageContent, PaymentRequiredError, Role, StopReason,
|
||||
LanguageModel, LanguageModelCacheConfiguration, LanguageModelCompletionEvent,
|
||||
LanguageModelImage, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||
LanguageModelToolUseId, MaxMonthlySpendReachedError, MessageContent, PaymentRequiredError,
|
||||
Role, StopReason, report_assistant_event,
|
||||
};
|
||||
use open_ai::Model as OpenAiModel;
|
||||
use paths::contexts_dir;
|
||||
@@ -31,7 +31,7 @@ use prompt_store::PromptBuilder;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
cmp::{max, Ordering},
|
||||
cmp::{Ordering, max},
|
||||
fmt::Debug,
|
||||
iter, mem,
|
||||
ops::Range,
|
||||
@@ -43,7 +43,7 @@ use std::{
|
||||
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
|
||||
use text::{BufferSnapshot, ToPoint};
|
||||
use ui::IconName;
|
||||
use util::{post_inc, ResultExt, TryFutureExt};
|
||||
use util::{ResultExt, TryFutureExt, post_inc};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
|
||||
@@ -14,7 +14,7 @@ use futures::{
|
||||
channel::mpsc,
|
||||
stream::{self, StreamExt},
|
||||
};
|
||||
use gpui::{prelude::*, App, Entity, SharedString, Task, TestAppContext, WeakEntity};
|
||||
use gpui::{App, Entity, SharedString, Task, TestAppContext, WeakEntity, prelude::*};
|
||||
use language::{Buffer, BufferSnapshot, LanguageRegistry, LspAdapterDelegate};
|
||||
use language_model::{LanguageModelCacheConfiguration, LanguageModelRegistry, Role};
|
||||
use parking_lot::Mutex;
|
||||
@@ -30,14 +30,14 @@ use std::{
|
||||
ops::Range,
|
||||
path::Path,
|
||||
rc::Rc,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
sync::{Arc, atomic::AtomicBool},
|
||||
};
|
||||
use text::{network::Network, OffsetRangeExt as _, ReplicaId, ToOffset};
|
||||
use text::{OffsetRangeExt as _, ReplicaId, ToOffset, network::Network};
|
||||
use ui::{IconName, Window};
|
||||
use unindent::Unindent;
|
||||
use util::{
|
||||
test::{generate_marked_text, marked_text_ranges},
|
||||
RandomCharIter,
|
||||
test::{generate_marked_text, marked_text_ranges},
|
||||
};
|
||||
use workspace::Workspace;
|
||||
|
||||
|
||||
@@ -2,36 +2,36 @@ use anyhow::Result;
|
||||
use assistant_settings::AssistantSettings;
|
||||
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection, SlashCommandWorkingSet};
|
||||
use assistant_slash_commands::{
|
||||
selections_creases, DefaultSlashCommand, DocsSlashCommand, DocsSlashCommandArgs,
|
||||
FileSlashCommand,
|
||||
DefaultSlashCommand, DocsSlashCommand, DocsSlashCommandArgs, FileSlashCommand,
|
||||
selections_creases,
|
||||
};
|
||||
use client::{proto, zed_urls};
|
||||
use collections::{hash_map, BTreeSet, HashMap, HashSet};
|
||||
use collections::{BTreeSet, HashMap, HashSet, hash_map};
|
||||
use editor::{
|
||||
Anchor, Editor, EditorEvent, MenuInlineCompletionsPolicy, ProposedChangeLocation,
|
||||
ProposedChangesEditor, RowExt, ToOffset as _, ToPoint,
|
||||
actions::{FoldAt, MoveToEndOfLine, Newline, ShowCompletions, UnfoldAt},
|
||||
display_map::{
|
||||
BlockContext, BlockId, BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata,
|
||||
CustomBlockId, FoldId, RenderBlock, ToDisplayPoint,
|
||||
},
|
||||
scroll::Autoscroll,
|
||||
Anchor, Editor, EditorEvent, MenuInlineCompletionsPolicy, ProposedChangeLocation,
|
||||
ProposedChangesEditor, RowExt, ToOffset as _, ToPoint,
|
||||
};
|
||||
use editor::{display_map::CreaseId, FoldPlaceholder};
|
||||
use editor::{FoldPlaceholder, display_map::CreaseId};
|
||||
use fs::Fs;
|
||||
use futures::FutureExt;
|
||||
use gpui::{
|
||||
actions, div, img, impl_internal_actions, percentage, point, prelude::*, pulsating_between,
|
||||
size, Animation, AnimationExt, AnyElement, AnyView, App, AsyncWindowContext, ClipboardEntry,
|
||||
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,
|
||||
WeakEntity, actions, div, img, impl_internal_actions, percentage, point, prelude::*,
|
||||
pulsating_between, size,
|
||||
};
|
||||
use indexed_docs::IndexedDocsStore;
|
||||
use language::{
|
||||
language_settings::{all_language_settings, SoftWrap},
|
||||
BufferSnapshot, LspAdapterDelegate, ToOffset,
|
||||
language_settings::{SoftWrap, all_language_settings},
|
||||
};
|
||||
use language_model::{
|
||||
LanguageModelImage, LanguageModelProvider, LanguageModelProviderTosView, LanguageModelRegistry,
|
||||
@@ -46,33 +46,33 @@ use project::lsp_store::LocalLspAdapterDelegate;
|
||||
use project::{Project, Worktree};
|
||||
use rope::Point;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{update_settings_file, Settings, SettingsStore};
|
||||
use settings::{Settings, SettingsStore, update_settings_file};
|
||||
use std::{any::TypeId, borrow::Cow, cmp, ops::Range, path::PathBuf, sync::Arc, time::Duration};
|
||||
use text::SelectionGoal;
|
||||
use ui::{
|
||||
prelude::*, ButtonLike, Disclosure, ElevationIndex, KeyBinding, PopoverMenuHandle, TintColor,
|
||||
Tooltip,
|
||||
ButtonLike, Disclosure, ElevationIndex, KeyBinding, PopoverMenuHandle, TintColor, Tooltip,
|
||||
prelude::*,
|
||||
};
|
||||
use util::{maybe, ResultExt};
|
||||
use util::{ResultExt, maybe};
|
||||
use workspace::searchable::{Direction, SearchableItemHandle};
|
||||
use workspace::{
|
||||
Save, ShowConfiguration, Toast, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
|
||||
Workspace,
|
||||
item::{self, FollowableItem, Item, ItemHandle},
|
||||
notifications::NotificationId,
|
||||
pane::{self, SaveIntent},
|
||||
searchable::{SearchEvent, SearchableItem},
|
||||
Save, ShowConfiguration, Toast, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
|
||||
Workspace,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
slash_command::SlashCommandCompletionProvider, slash_command_picker,
|
||||
ThoughtProcessOutputSection,
|
||||
};
|
||||
use crate::{
|
||||
AssistantContext, AssistantPatch, AssistantPatchStatus, CacheStatus, Content, ContextEvent,
|
||||
ContextId, InvokedSlashCommandId, InvokedSlashCommandStatus, Message, MessageId,
|
||||
MessageMetadata, MessageStatus, ParsedSlashCommand, PendingSlashCommandStatus, RequestType,
|
||||
};
|
||||
use crate::{
|
||||
ThoughtProcessOutputSection, slash_command::SlashCommandCompletionProvider,
|
||||
slash_command_picker,
|
||||
};
|
||||
|
||||
actions!(
|
||||
assistant,
|
||||
@@ -3413,7 +3413,7 @@ impl ContextEditorToolbarItem {
|
||||
pub fn render_remaining_tokens(
|
||||
context_editor: &Entity<ContextEditor>,
|
||||
cx: &App,
|
||||
) -> Option<impl IntoElement> {
|
||||
) -> Option<impl IntoElement + use<>> {
|
||||
let context = &context_editor.read(cx).context;
|
||||
|
||||
let (token_count_color, token_count, max_token_count, tooltip) = match token_state(context, cx)?
|
||||
|
||||
@@ -3,13 +3,13 @@ use std::sync::Arc;
|
||||
use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::Project;
|
||||
use ui::utils::{format_distance_from_now, DateTimeType};
|
||||
use ui::{prelude::*, Avatar, ListItem, ListItemSpacing};
|
||||
use ui::utils::{DateTimeType, format_distance_from_now};
|
||||
use ui::{Avatar, ListItem, ListItemSpacing, prelude::*};
|
||||
use workspace::{Item, Workspace};
|
||||
|
||||
use crate::{
|
||||
AssistantPanelDelegate, ContextStore, RemoteContextMetadata, SavedContextMetadata,
|
||||
DEFAULT_TAB_TITLE,
|
||||
AssistantPanelDelegate, ContextStore, DEFAULT_TAB_TITLE, RemoteContextMetadata,
|
||||
SavedContextMetadata,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -229,10 +229,12 @@ impl PickerDelegate for SavedContextPickerDelegate {
|
||||
.into_any_element(),
|
||||
]
|
||||
} else {
|
||||
vec![Label::new("Shared by host")
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small)
|
||||
.into_any_element()]
|
||||
vec![
|
||||
Label::new("Shared by host")
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small)
|
||||
.into_any_element(),
|
||||
]
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@ use crate::{
|
||||
AssistantContext, ContextEvent, ContextId, ContextOperation, ContextVersion, SavedContext,
|
||||
SavedContextMetadata,
|
||||
};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_slash_command::{SlashCommandId, SlashCommandWorkingSet};
|
||||
use client::{proto, telemetry::Telemetry, Client, TypedEnvelope};
|
||||
use client::{Client, TypedEnvelope, proto, telemetry::Telemetry};
|
||||
use clock::ReplicaId;
|
||||
use collections::HashMap;
|
||||
use context_server::manager::ContextServerManager;
|
||||
use context_server::ContextServerFactoryRegistry;
|
||||
use context_server::manager::ContextServerManager;
|
||||
use fs::{Fs, RemoveOptions};
|
||||
use futures::StreamExt;
|
||||
use fuzzy::StringMatchCandidate;
|
||||
@@ -104,52 +104,47 @@ impl ContextStore {
|
||||
const CONTEXT_WATCH_DURATION: Duration = Duration::from_millis(100);
|
||||
let (mut events, _) = fs.watch(contexts_dir(), CONTEXT_WATCH_DURATION).await;
|
||||
|
||||
let this =
|
||||
cx.new(|cx: &mut Context<Self>| {
|
||||
let context_server_factory_registry =
|
||||
ContextServerFactoryRegistry::default_global(cx);
|
||||
let context_server_manager = cx.new(|cx| {
|
||||
ContextServerManager::new(
|
||||
context_server_factory_registry,
|
||||
project.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let mut this = Self {
|
||||
contexts: Vec::new(),
|
||||
contexts_metadata: Vec::new(),
|
||||
context_server_manager,
|
||||
context_server_slash_command_ids: HashMap::default(),
|
||||
host_contexts: Vec::new(),
|
||||
fs,
|
||||
languages,
|
||||
slash_commands,
|
||||
telemetry,
|
||||
_watch_updates: cx.spawn(async move |this, cx| {
|
||||
async move {
|
||||
while events.next().await.is_some() {
|
||||
this.update(cx, |this, cx| this.reload(cx))?.await.log_err();
|
||||
}
|
||||
anyhow::Ok(())
|
||||
let this = cx.new(|cx: &mut Context<Self>| {
|
||||
let context_server_factory_registry =
|
||||
ContextServerFactoryRegistry::default_global(cx);
|
||||
let context_server_manager = cx.new(|cx| {
|
||||
ContextServerManager::new(context_server_factory_registry, project.clone(), cx)
|
||||
});
|
||||
let mut this = Self {
|
||||
contexts: Vec::new(),
|
||||
contexts_metadata: Vec::new(),
|
||||
context_server_manager,
|
||||
context_server_slash_command_ids: HashMap::default(),
|
||||
host_contexts: Vec::new(),
|
||||
fs,
|
||||
languages,
|
||||
slash_commands,
|
||||
telemetry,
|
||||
_watch_updates: cx.spawn(async move |this, cx| {
|
||||
async move {
|
||||
while events.next().await.is_some() {
|
||||
this.update(cx, |this, cx| this.reload(cx))?.await.log_err();
|
||||
}
|
||||
.log_err()
|
||||
.await
|
||||
}),
|
||||
client_subscription: None,
|
||||
_project_subscriptions: vec![
|
||||
cx.subscribe(&project, Self::handle_project_event)
|
||||
],
|
||||
project_is_shared: false,
|
||||
client: project.read(cx).client(),
|
||||
project: project.clone(),
|
||||
prompt_builder,
|
||||
};
|
||||
this.handle_project_shared(project.clone(), cx);
|
||||
this.synchronize_contexts(cx);
|
||||
this.register_context_server_handlers(cx);
|
||||
this.reload(cx).detach_and_log_err(cx);
|
||||
this
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err()
|
||||
.await
|
||||
}),
|
||||
client_subscription: None,
|
||||
_project_subscriptions: vec![
|
||||
cx.subscribe(&project, Self::handle_project_event),
|
||||
],
|
||||
project_is_shared: false,
|
||||
client: project.read(cx).client(),
|
||||
project: project.clone(),
|
||||
prompt_builder,
|
||||
};
|
||||
this.handle_project_shared(project.clone(), cx);
|
||||
this.synchronize_contexts(cx);
|
||||
this.register_context_server_handlers(cx);
|
||||
this.reload(cx).detach_and_log_err(cx);
|
||||
this
|
||||
})?;
|
||||
|
||||
Ok(this)
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use collections::HashMap;
|
||||
use editor::ProposedChangesEditor;
|
||||
use futures::{future, TryFutureExt as _};
|
||||
use futures::{TryFutureExt as _, future};
|
||||
use gpui::{App, AppContext as _, AsyncApp, Entity, SharedString};
|
||||
use language::{AutoindentMode, Buffer, BufferSnapshot};
|
||||
use project::{Project, ProjectPath};
|
||||
@@ -548,7 +548,7 @@ mod tests {
|
||||
use super::*;
|
||||
use gpui::App;
|
||||
use language::{
|
||||
language_settings::AllLanguageSettings, Language, LanguageConfig, LanguageMatcher,
|
||||
Language, LanguageConfig, LanguageMatcher, language_settings::AllLanguageSettings,
|
||||
};
|
||||
use settings::SettingsStore;
|
||||
use ui::BorrowAppContext;
|
||||
|
||||
@@ -3,19 +3,19 @@ use anyhow::Result;
|
||||
pub use assistant_slash_command::SlashCommand;
|
||||
use assistant_slash_command::{AfterCompletion, SlashCommandLine, SlashCommandWorkingSet};
|
||||
use editor::{CompletionProvider, Editor, ExcerptId};
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
use fuzzy::{StringMatchCandidate, match_strings};
|
||||
use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity, Window};
|
||||
use language::{Anchor, Buffer, ToPoint};
|
||||
use parking_lot::Mutex;
|
||||
use project::{lsp_store::CompletionDocumentation, CompletionIntent, CompletionSource};
|
||||
use project::{CompletionIntent, CompletionSource, lsp_store::CompletionDocumentation};
|
||||
use rope::Point;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
ops::Range,
|
||||
rc::Rc,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering::SeqCst},
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering::SeqCst},
|
||||
},
|
||||
};
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||
use assistant_slash_command::SlashCommandWorkingSet;
|
||||
use gpui::{AnyElement, AnyView, DismissEvent, SharedString, Task, WeakEntity};
|
||||
use picker::{Picker, PickerDelegate, PickerEditorPosition};
|
||||
use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger, Tooltip};
|
||||
use ui::{ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger, Tooltip, prelude::*};
|
||||
|
||||
use crate::context_editor::ContextEditor;
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use anyhow::anyhow;
|
||||
use assistant2::{RequestKind, Thread, ThreadEvent, ThreadStore};
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use assistant2::{RequestKind, Thread, ThreadEvent, ThreadStore};
|
||||
use client::{Client, UserStore};
|
||||
use collections::HashMap;
|
||||
use dap::DapRegistry;
|
||||
use futures::StreamExt;
|
||||
use gpui::{prelude::*, App, AsyncApp, Entity, SemanticVersion, Subscription, Task};
|
||||
use gpui::{App, AsyncApp, Entity, SemanticVersion, Subscription, Task, prelude::*};
|
||||
use language::LanguageRegistry;
|
||||
use language_model::{
|
||||
AuthenticateError, LanguageModel, LanguageModelProviderId, LanguageModelRegistry,
|
||||
|
||||
@@ -6,7 +6,7 @@ use clap::Parser;
|
||||
use eval::{Eval, EvalOutput};
|
||||
use futures::future;
|
||||
use gpui::{Application, AsyncApp};
|
||||
use headless_assistant::{authenticate_model_provider, find_model, HeadlessAppState};
|
||||
use headless_assistant::{HeadlessAppState, authenticate_model_provider, find_model};
|
||||
use itertools::Itertools;
|
||||
use judge::Judge;
|
||||
use language_model::{LanguageModel, LanguageModelRegistry};
|
||||
|
||||
@@ -4,14 +4,15 @@ use std::sync::Arc;
|
||||
|
||||
use ::open_ai::Model as OpenAiModel;
|
||||
use anthropic::Model as AnthropicModel;
|
||||
use anyhow::{Result, bail};
|
||||
use deepseek::Model as DeepseekModel;
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt};
|
||||
use gpui::{App, Pixels};
|
||||
use indexmap::IndexMap;
|
||||
use language_model::{CloudModel, LanguageModel};
|
||||
use lmstudio::Model as LmStudioModel;
|
||||
use ollama::Model as OllamaModel;
|
||||
use schemars::{schema::Schema, JsonSchema};
|
||||
use schemars::{JsonSchema, schema::Schema};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
|
||||
@@ -26,6 +27,15 @@ pub enum AssistantDockPosition {
|
||||
Bottom,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum NotifyWhenAgentWaiting {
|
||||
#[default]
|
||||
PrimaryScreen,
|
||||
AllScreens,
|
||||
Never,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
#[serde(tag = "name", rename_all = "snake_case")]
|
||||
pub enum AssistantProviderContentV1 {
|
||||
@@ -74,11 +84,15 @@ pub struct AssistantSettings {
|
||||
pub default_profile: Arc<str>,
|
||||
pub profiles: IndexMap<Arc<str>, AgentProfile>,
|
||||
pub always_allow_tool_actions: bool,
|
||||
pub notify_when_agent_waiting: bool,
|
||||
pub notify_when_agent_waiting: NotifyWhenAgentWaiting,
|
||||
}
|
||||
|
||||
impl AssistantSettings {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -96,8 +110,8 @@ impl JsonSchema for AssistantSettingsContent {
|
||||
VersionedAssistantSettingsContent::schema_name()
|
||||
}
|
||||
|
||||
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> Schema {
|
||||
VersionedAssistantSettingsContent::json_schema(gen)
|
||||
fn json_schema(r#gen: &mut schemars::r#gen::SchemaGenerator) -> Schema {
|
||||
VersionedAssistantSettingsContent::json_schema(r#gen)
|
||||
}
|
||||
|
||||
fn is_referenceable() -> bool {
|
||||
@@ -312,15 +326,48 @@ impl AssistantSettingsContent {
|
||||
}
|
||||
|
||||
pub fn set_profile(&mut self, profile_id: Arc<str>) {
|
||||
match self {
|
||||
AssistantSettingsContent::Versioned(settings) => match settings {
|
||||
VersionedAssistantSettingsContent::V2(settings) => {
|
||||
settings.default_profile = Some(profile_id);
|
||||
}
|
||||
VersionedAssistantSettingsContent::V1(_) => {}
|
||||
},
|
||||
AssistantSettingsContent::Legacy(_) => {}
|
||||
let AssistantSettingsContent::Versioned(VersionedAssistantSettingsContent::V2(settings)) =
|
||||
self
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
settings.default_profile = Some(profile_id);
|
||||
}
|
||||
|
||||
pub fn create_profile(&mut self, profile_id: Arc<str>, profile: AgentProfile) -> Result<()> {
|
||||
let AssistantSettingsContent::Versioned(VersionedAssistantSettingsContent::V2(settings)) =
|
||||
self
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let profiles = settings.profiles.get_or_insert_default();
|
||||
if profiles.contains_key(&profile_id) {
|
||||
bail!("profile with ID '{profile_id}' already exists");
|
||||
}
|
||||
|
||||
profiles.insert(
|
||||
profile_id,
|
||||
AgentProfileContent {
|
||||
name: profile.name.into(),
|
||||
tools: profile.tools,
|
||||
context_servers: profile
|
||||
.context_servers
|
||||
.into_iter()
|
||||
.map(|(server_id, preset)| {
|
||||
(
|
||||
server_id,
|
||||
ContextServerPresetContent {
|
||||
tools: preset.tools,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,10 +441,10 @@ pub struct AssistantSettingsContentV2 {
|
||||
///
|
||||
/// Default: false
|
||||
always_allow_tool_actions: Option<bool>,
|
||||
/// Whether to show a popup notification when the agent is waiting for user input.
|
||||
/// Where to show a popup notification when the agent is waiting for user input.
|
||||
///
|
||||
/// Default: true
|
||||
notify_when_agent_waiting: Option<bool>,
|
||||
/// Default: "primary_screen"
|
||||
notify_when_agent_waiting: Option<NotifyWhenAgentWaiting>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
@@ -407,7 +454,7 @@ pub struct LanguageModelSelection {
|
||||
pub model: String,
|
||||
}
|
||||
|
||||
fn providers_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
|
||||
fn providers_schema(_: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
|
||||
schemars::schema::SchemaObject {
|
||||
enum_values: Some(vec![
|
||||
"anthropic".into(),
|
||||
|
||||
@@ -6,17 +6,17 @@ pub use crate::extension_slash_command::*;
|
||||
pub use crate::slash_command_registry::*;
|
||||
pub use crate::slash_command_working_set::*;
|
||||
use anyhow::Result;
|
||||
use futures::stream::{self, BoxStream};
|
||||
use futures::StreamExt;
|
||||
use futures::stream::{self, BoxStream};
|
||||
use gpui::{App, SharedString, Task, WeakEntity, Window};
|
||||
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate, OffsetRangeExt};
|
||||
pub use language_model::Role;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
ops::Range,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
sync::{Arc, atomic::AtomicBool},
|
||||
};
|
||||
use workspace::{ui::IconName, Workspace};
|
||||
use workspace::{Workspace, ui::IconName};
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
SlashCommandRegistry::default_global(cx);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{atomic::AtomicBool, Arc};
|
||||
use std::sync::{Arc, atomic::AtomicBool};
|
||||
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||
SlashCommandResult,
|
||||
@@ -10,7 +10,7 @@ use project::{Project, ProjectPath};
|
||||
use std::{
|
||||
fmt::Write,
|
||||
path::Path,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
sync::{Arc, atomic::AtomicBool},
|
||||
};
|
||||
use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_slash_command::{
|
||||
AfterCompletion, ArgumentCompletion, SlashCommand, SlashCommandOutput,
|
||||
SlashCommandOutputSection, SlashCommandResult,
|
||||
@@ -10,8 +10,8 @@ use context_server::{
|
||||
};
|
||||
use gpui::{App, Entity, Task, WeakEntity, Window};
|
||||
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate};
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use text::LineEnding;
|
||||
use ui::{IconName, SharedString};
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||
SlashCommandResult,
|
||||
@@ -8,7 +8,7 @@ use language::{BufferSnapshot, LspAdapterDelegate};
|
||||
use prompt_store::PromptStore;
|
||||
use std::{
|
||||
fmt::Write,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
sync::{Arc, atomic::AtomicBool},
|
||||
};
|
||||
use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::file_command::{FileCommandMetadata, FileSlashCommand};
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||
SlashCommandResult,
|
||||
@@ -8,7 +8,7 @@ use collections::HashSet;
|
||||
use futures::future;
|
||||
use gpui::{App, Task, WeakEntity, Window};
|
||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||
use std::sync::{atomic::AtomicBool, Arc};
|
||||
use std::sync::{Arc, atomic::AtomicBool};
|
||||
use text::OffsetRangeExt;
|
||||
use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||
SlashCommandResult,
|
||||
@@ -14,11 +14,11 @@ use rope::Point;
|
||||
use std::{
|
||||
fmt::Write,
|
||||
path::{Path, PathBuf},
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
sync::{Arc, atomic::AtomicBool},
|
||||
};
|
||||
use ui::prelude::*;
|
||||
use util::paths::PathMatcher;
|
||||
use util::ResultExt;
|
||||
use util::paths::PathMatcher;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::create_label_for_command;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use anyhow::{Result, anyhow, bail};
|
||||
use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||
SlashCommandResult,
|
||||
@@ -16,7 +16,7 @@ use indexed_docs::{
|
||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||
use project::{Project, ProjectPath};
|
||||
use ui::prelude::*;
|
||||
use util::{maybe, ResultExt};
|
||||
use util::{ResultExt, maybe};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub struct DocsSlashCommand;
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use anyhow::{Context, Result, anyhow, bail};
|
||||
use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||
SlashCommandResult,
|
||||
};
|
||||
use futures::AsyncReadExt;
|
||||
use gpui::{Task, WeakEntity};
|
||||
use html_to_markdown::{convert_html_to_markdown, markdown, TagHandler};
|
||||
use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown};
|
||||
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
|
||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||
use ui::prelude::*;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_slash_command::{
|
||||
AfterCompletion, ArgumentCompletion, SlashCommand, SlashCommandContent, SlashCommandEvent,
|
||||
SlashCommandOutput, SlashCommandOutputSection, SlashCommandResult,
|
||||
};
|
||||
use futures::channel::mpsc;
|
||||
use futures::Stream;
|
||||
use futures::channel::mpsc;
|
||||
use fuzzy::PathMatch;
|
||||
use gpui::{App, Entity, Task, WeakEntity};
|
||||
use language::{BufferSnapshot, CodeLabel, HighlightId, LineEnding, LspAdapterDelegate};
|
||||
@@ -15,7 +15,7 @@ use std::{
|
||||
fmt::Write,
|
||||
ops::{Range, RangeInclusive},
|
||||
path::{Path, PathBuf},
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
sync::{Arc, atomic::AtomicBool},
|
||||
};
|
||||
use ui::prelude::*;
|
||||
use util::ResultExt;
|
||||
@@ -221,7 +221,7 @@ fn collect_files(
|
||||
project: Entity<Project>,
|
||||
glob_inputs: &[String],
|
||||
cx: &mut App,
|
||||
) -> impl Stream<Item = Result<SlashCommandEvent>> {
|
||||
) -> impl Stream<Item = Result<SlashCommandEvent>> + use<> {
|
||||
let Ok(matchers) = glob_inputs
|
||||
.into_iter()
|
||||
.map(|glob_input| {
|
||||
@@ -694,15 +694,21 @@ mod test {
|
||||
assert_eq!(result.sections.len(), 7);
|
||||
|
||||
// Ensure that full file paths are included in the real output
|
||||
assert!(result
|
||||
.text
|
||||
.contains(separator!("zed/assets/themes/andromeda/LICENSE")));
|
||||
assert!(result
|
||||
.text
|
||||
.contains(separator!("zed/assets/themes/ayu/LICENSE")));
|
||||
assert!(result
|
||||
.text
|
||||
.contains(separator!("zed/assets/themes/summercamp/LICENSE")));
|
||||
assert!(
|
||||
result
|
||||
.text
|
||||
.contains(separator!("zed/assets/themes/andromeda/LICENSE"))
|
||||
);
|
||||
assert!(
|
||||
result
|
||||
.text
|
||||
.contains(separator!("zed/assets/themes/ayu/LICENSE"))
|
||||
);
|
||||
assert!(
|
||||
result
|
||||
.text
|
||||
.contains(separator!("zed/assets/themes/summercamp/LICENSE"))
|
||||
);
|
||||
|
||||
assert_eq!(result.sections[5].label, "summercamp");
|
||||
|
||||
@@ -782,7 +788,12 @@ mod test {
|
||||
assert_eq!(result.sections[6].label, "summercamp");
|
||||
assert_eq!(result.sections[7].label, separator!("zed/assets/themes"));
|
||||
|
||||
assert_eq!(result.text, separator!("zed/assets/themes\n```zed/assets/themes/LICENSE\n1\n```\n\nsummercamp\n```zed/assets/themes/summercamp/LICENSE\n1\n```\n\nsubdir\n```zed/assets/themes/summercamp/subdir/LICENSE\n1\n```\n\nsubsubdir\n```zed/assets/themes/summercamp/subdir/subsubdir/LICENSE\n3\n```\n\n"));
|
||||
assert_eq!(
|
||||
result.text,
|
||||
separator!(
|
||||
"zed/assets/themes\n```zed/assets/themes/LICENSE\n1\n```\n\nsummercamp\n```zed/assets/themes/summercamp/LICENSE\n1\n```\n\nsubdir\n```zed/assets/themes/summercamp/subdir/LICENSE\n1\n```\n\nsubsubdir\n```zed/assets/themes/summercamp/subdir/subsubdir/LICENSE\n3\n```\n\n"
|
||||
)
|
||||
);
|
||||
|
||||
// Ensure that the project lasts until after the last await
|
||||
drop(project);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
use anyhow::Result;
|
||||
use assistant_slash_command::{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||
SlashCommandResult,
|
||||
@@ -6,7 +6,7 @@ use assistant_slash_command::{
|
||||
use gpui::{Task, WeakEntity};
|
||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||
use prompt_store::PromptStore;
|
||||
use std::sync::{atomic::AtomicBool, Arc};
|
||||
use std::sync::{Arc, atomic::AtomicBool};
|
||||
use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandContent, SlashCommandEvent,
|
||||
SlashCommandOutputSection, SlashCommandResult,
|
||||
@@ -7,8 +7,8 @@ use editor::Editor;
|
||||
use futures::StreamExt;
|
||||
use gpui::{App, Context, SharedString, Task, WeakEntity, Window};
|
||||
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate};
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use ui::IconName;
|
||||
use workspace::Workspace;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
@@ -11,8 +11,8 @@ use feature_flags::FeatureFlag;
|
||||
use futures::channel::mpsc;
|
||||
use gpui::{Task, WeakEntity};
|
||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||
use smol::stream::StreamExt;
|
||||
use smol::Timer;
|
||||
use smol::stream::StreamExt;
|
||||
use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||
SlashCommandResult,
|
||||
|
||||
@@ -10,9 +10,9 @@ use gpui::{Task, WeakEntity};
|
||||
use language::{BufferSnapshot, CodeLabel, HighlightId, LspAdapterDelegate};
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
sync::{Arc, atomic::AtomicBool},
|
||||
};
|
||||
use ui::{prelude::*, ActiveTheme, App, Window};
|
||||
use ui::{ActiveTheme, App, Window, prelude::*};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
use anyhow::Result;
|
||||
use assistant_slash_command::{
|
||||
@@ -8,9 +8,9 @@ use assistant_slash_command::{
|
||||
};
|
||||
use gpui::{App, Entity, Task, WeakEntity};
|
||||
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate};
|
||||
use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
|
||||
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
|
||||
use ui::prelude::*;
|
||||
use workspace::{dock::Panel, Workspace};
|
||||
use workspace::{Workspace, dock::Panel};
|
||||
|
||||
use super::create_label_for_command;
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ path = "src/assistant_tool.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
async-watch.workspace = true
|
||||
buffer_diff.workspace = true
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
derive_more.workspace = true
|
||||
@@ -24,3 +26,16 @@ parking_lot.workspace = true
|
||||
project.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
text.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
buffer_diff = { workspace = true, features = ["test-support"] }
|
||||
collections = { workspace = true, features = ["test-support"] }
|
||||
clock = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
language_model = { workspace = true, features = ["test-support"] }
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
text = { workspace = true, features = ["test-support"] }
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
|
||||
990
crates/assistant_tool/src/action_log.rs
Normal file
990
crates/assistant_tool/src/action_log.rs
Normal file
@@ -0,0 +1,990 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
use buffer_diff::BufferDiff;
|
||||
use collections::{BTreeMap, HashMap, HashSet};
|
||||
use gpui::{App, AppContext, AsyncApp, Context, Entity, Subscription, Task, WeakEntity};
|
||||
use language::{
|
||||
Buffer, BufferEvent, DiskState, OffsetRangeExt, Operation, TextBufferSnapshot, ToOffset,
|
||||
};
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
/// Tracks actions performed by tools in a thread
|
||||
pub struct ActionLog {
|
||||
/// Buffers that user manually added to the context, and whose content has
|
||||
/// changed since the model last saw them.
|
||||
stale_buffers_in_context: HashSet<Entity<Buffer>>,
|
||||
/// Buffers that we want to notify the model about when they change.
|
||||
tracked_buffers: BTreeMap<Entity<Buffer>, TrackedBuffer>,
|
||||
/// Has the model edited a file since it last checked diagnostics?
|
||||
edited_since_project_diagnostics_check: bool,
|
||||
}
|
||||
|
||||
impl ActionLog {
|
||||
/// Creates a new, empty action log.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
stale_buffers_in_context: HashSet::default(),
|
||||
tracked_buffers: BTreeMap::default(),
|
||||
edited_since_project_diagnostics_check: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_reviewed_changes(&mut self, cx: &mut Context<Self>) {
|
||||
self.tracked_buffers
|
||||
.retain(|_buffer, tracked_buffer| match &mut tracked_buffer.change {
|
||||
Change::Edited {
|
||||
accepted_edit_ids, ..
|
||||
} => {
|
||||
accepted_edit_ids.clear();
|
||||
tracked_buffer.schedule_diff_update();
|
||||
true
|
||||
}
|
||||
Change::Deleted { reviewed, .. } => !*reviewed,
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Notifies a diagnostics check
|
||||
pub fn checked_project_diagnostics(&mut self) {
|
||||
self.edited_since_project_diagnostics_check = false;
|
||||
}
|
||||
|
||||
/// Returns true if any files have been edited since the last project diagnostics check
|
||||
pub fn has_edited_files_since_project_diagnostics_check(&self) -> bool {
|
||||
self.edited_since_project_diagnostics_check
|
||||
}
|
||||
|
||||
fn track_buffer(
|
||||
&mut self,
|
||||
buffer: Entity<Buffer>,
|
||||
created: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> &mut TrackedBuffer {
|
||||
let tracked_buffer = self
|
||||
.tracked_buffers
|
||||
.entry(buffer.clone())
|
||||
.or_insert_with(|| {
|
||||
let text_snapshot = buffer.read(cx).text_snapshot();
|
||||
let unreviewed_diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
|
||||
let diff = cx.new(|cx| {
|
||||
let mut diff = BufferDiff::new(&text_snapshot, cx);
|
||||
diff.set_secondary_diff(unreviewed_diff.clone());
|
||||
diff
|
||||
});
|
||||
let (diff_update_tx, diff_update_rx) = async_watch::channel(());
|
||||
TrackedBuffer {
|
||||
buffer: buffer.clone(),
|
||||
change: Change::Edited {
|
||||
unreviewed_edit_ids: HashSet::default(),
|
||||
accepted_edit_ids: HashSet::default(),
|
||||
initial_content: if created {
|
||||
None
|
||||
} else {
|
||||
Some(text_snapshot.clone())
|
||||
},
|
||||
},
|
||||
version: buffer.read(cx).version(),
|
||||
diff,
|
||||
secondary_diff: unreviewed_diff,
|
||||
diff_update: diff_update_tx,
|
||||
_maintain_diff: cx.spawn({
|
||||
let buffer = buffer.clone();
|
||||
async move |this, cx| {
|
||||
Self::maintain_diff(this, buffer, diff_update_rx, cx)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}),
|
||||
_subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
|
||||
}
|
||||
});
|
||||
tracked_buffer.version = buffer.read(cx).version();
|
||||
tracked_buffer
|
||||
}
|
||||
|
||||
fn handle_buffer_event(
|
||||
&mut self,
|
||||
buffer: Entity<Buffer>,
|
||||
event: &BufferEvent,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
BufferEvent::Operation { operation, .. } => {
|
||||
self.handle_buffer_operation(buffer, operation, cx)
|
||||
}
|
||||
BufferEvent::FileHandleChanged => {
|
||||
self.handle_buffer_file_changed(buffer, cx);
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
fn handle_buffer_operation(
|
||||
&mut self,
|
||||
buffer: Entity<Buffer>,
|
||||
operation: &Operation,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
|
||||
return;
|
||||
};
|
||||
let Operation::Buffer(text::Operation::Edit(operation)) = operation else {
|
||||
return;
|
||||
};
|
||||
let Change::Edited {
|
||||
unreviewed_edit_ids,
|
||||
accepted_edit_ids,
|
||||
..
|
||||
} = &mut tracked_buffer.change
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if unreviewed_edit_ids.contains(&operation.timestamp)
|
||||
|| accepted_edit_ids.contains(&operation.timestamp)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let buffer = buffer.read(cx);
|
||||
let operation_edit_ranges = buffer
|
||||
.edited_ranges_for_edit_ids::<usize>([&operation.timestamp])
|
||||
.collect::<Vec<_>>();
|
||||
let intersects_unreviewed_edits = ranges_intersect(
|
||||
operation_edit_ranges.iter().cloned(),
|
||||
buffer.edited_ranges_for_edit_ids::<usize>(unreviewed_edit_ids.iter()),
|
||||
);
|
||||
let mut intersected_accepted_edits = HashSet::default();
|
||||
for accepted_edit_id in accepted_edit_ids.iter() {
|
||||
let intersects_accepted_edit = ranges_intersect(
|
||||
operation_edit_ranges.iter().cloned(),
|
||||
buffer.edited_ranges_for_edit_ids::<usize>([accepted_edit_id]),
|
||||
);
|
||||
if intersects_accepted_edit {
|
||||
intersected_accepted_edits.insert(*accepted_edit_id);
|
||||
}
|
||||
}
|
||||
|
||||
// If the buffer operation overlaps with any tracked edits, mark it as unreviewed.
|
||||
// If it intersects an already-accepted id, mark that edit as unreviewed again.
|
||||
if intersects_unreviewed_edits || !intersected_accepted_edits.is_empty() {
|
||||
unreviewed_edit_ids.insert(operation.timestamp);
|
||||
for accepted_edit_id in intersected_accepted_edits {
|
||||
unreviewed_edit_ids.insert(accepted_edit_id);
|
||||
accepted_edit_ids.remove(&accepted_edit_id);
|
||||
}
|
||||
tracked_buffer.schedule_diff_update();
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_buffer_file_changed(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
|
||||
let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
|
||||
return;
|
||||
};
|
||||
|
||||
match tracked_buffer.change {
|
||||
Change::Deleted { .. } => {
|
||||
if buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.map_or(false, |file| file.disk_state() != DiskState::Deleted)
|
||||
{
|
||||
// If the buffer had been deleted by a tool, but it got
|
||||
// resurrected externally, we want to clear the changes we
|
||||
// were tracking and reset the buffer's state.
|
||||
tracked_buffer.change = Change::Edited {
|
||||
unreviewed_edit_ids: HashSet::default(),
|
||||
accepted_edit_ids: HashSet::default(),
|
||||
initial_content: Some(buffer.read(cx).text_snapshot()),
|
||||
};
|
||||
}
|
||||
tracked_buffer.schedule_diff_update();
|
||||
}
|
||||
Change::Edited { .. } => {
|
||||
if buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.map_or(false, |file| file.disk_state() == DiskState::Deleted)
|
||||
{
|
||||
// If the buffer had been edited by a tool, but it got
|
||||
// deleted externally, we want to stop tracking it.
|
||||
self.tracked_buffers.remove(&buffer);
|
||||
} else {
|
||||
tracked_buffer.schedule_diff_update();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn maintain_diff(
|
||||
this: WeakEntity<Self>,
|
||||
buffer: Entity<Buffer>,
|
||||
mut diff_update: async_watch::Receiver<()>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<()> {
|
||||
while let Some(_) = diff_update.recv().await.ok() {
|
||||
let update = this.update(cx, |this, cx| {
|
||||
let tracked_buffer = this
|
||||
.tracked_buffers
|
||||
.get_mut(&buffer)
|
||||
.context("buffer not tracked")?;
|
||||
anyhow::Ok(tracked_buffer.update_diff(cx))
|
||||
})??;
|
||||
update.await;
|
||||
this.update(cx, |_this, cx| cx.notify())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Track a buffer as read, so we can notify the model about user edits.
|
||||
pub fn buffer_read(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
|
||||
self.track_buffer(buffer, false, cx);
|
||||
}
|
||||
|
||||
/// Track a buffer as read, so we can notify the model about user edits.
|
||||
pub fn will_create_buffer(
|
||||
&mut self,
|
||||
buffer: Entity<Buffer>,
|
||||
edit_id: Option<clock::Lamport>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.track_buffer(buffer.clone(), true, cx);
|
||||
self.buffer_edited(buffer, edit_id.into_iter().collect(), cx)
|
||||
}
|
||||
|
||||
/// Mark a buffer as edited, so we can refresh it in the context
|
||||
pub fn buffer_edited(
|
||||
&mut self,
|
||||
buffer: Entity<Buffer>,
|
||||
mut edit_ids: Vec<clock::Lamport>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.edited_since_project_diagnostics_check = true;
|
||||
self.stale_buffers_in_context.insert(buffer.clone());
|
||||
|
||||
let tracked_buffer = self.track_buffer(buffer.clone(), false, cx);
|
||||
|
||||
match &mut tracked_buffer.change {
|
||||
Change::Edited {
|
||||
unreviewed_edit_ids,
|
||||
..
|
||||
} => {
|
||||
unreviewed_edit_ids.extend(edit_ids.iter().copied());
|
||||
}
|
||||
Change::Deleted {
|
||||
deleted_content,
|
||||
deletion_id,
|
||||
..
|
||||
} => {
|
||||
edit_ids.extend(*deletion_id);
|
||||
tracked_buffer.change = Change::Edited {
|
||||
unreviewed_edit_ids: edit_ids.into_iter().collect(),
|
||||
accepted_edit_ids: HashSet::default(),
|
||||
initial_content: Some(deleted_content.clone()),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
tracked_buffer.schedule_diff_update();
|
||||
}
|
||||
|
||||
pub fn will_delete_buffer(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
|
||||
let tracked_buffer = self.track_buffer(buffer.clone(), false, cx);
|
||||
if let Change::Edited {
|
||||
initial_content, ..
|
||||
} = &tracked_buffer.change
|
||||
{
|
||||
if let Some(initial_content) = initial_content {
|
||||
let deletion_id = buffer.update(cx, |buffer, cx| buffer.set_text("", cx));
|
||||
tracked_buffer.change = Change::Deleted {
|
||||
reviewed: false,
|
||||
deleted_content: initial_content.clone(),
|
||||
deletion_id,
|
||||
};
|
||||
tracked_buffer.schedule_diff_update();
|
||||
} else {
|
||||
self.tracked_buffers.remove(&buffer);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Accepts edits in a given range within a buffer.
|
||||
pub fn review_edits_in_range<T: ToOffset>(
|
||||
&mut self,
|
||||
buffer: Entity<Buffer>,
|
||||
buffer_range: Range<T>,
|
||||
accept: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let buffer = buffer.read(cx);
|
||||
let buffer_range = buffer_range.to_offset(buffer);
|
||||
|
||||
match &mut tracked_buffer.change {
|
||||
Change::Deleted { reviewed, .. } => {
|
||||
*reviewed = accept;
|
||||
}
|
||||
Change::Edited {
|
||||
unreviewed_edit_ids,
|
||||
accepted_edit_ids,
|
||||
..
|
||||
} => {
|
||||
let (source, destination) = if accept {
|
||||
(unreviewed_edit_ids, accepted_edit_ids)
|
||||
} else {
|
||||
(accepted_edit_ids, unreviewed_edit_ids)
|
||||
};
|
||||
source.retain(|edit_id| {
|
||||
for range in buffer.edited_ranges_for_edit_ids::<usize>([edit_id]) {
|
||||
if buffer_range.end >= range.start && buffer_range.start <= range.end {
|
||||
destination.insert(*edit_id);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
tracked_buffer.schedule_diff_update();
|
||||
}
|
||||
|
||||
/// Keep all edits across all buffers.
|
||||
/// This is a more performant alternative to calling review_edits_in_range for each buffer.
|
||||
pub fn keep_all_edits(&mut self) {
|
||||
// Process all tracked buffers
|
||||
for (_, tracked_buffer) in self.tracked_buffers.iter_mut() {
|
||||
match &mut tracked_buffer.change {
|
||||
Change::Deleted { reviewed, .. } => {
|
||||
*reviewed = true;
|
||||
}
|
||||
Change::Edited {
|
||||
unreviewed_edit_ids,
|
||||
accepted_edit_ids,
|
||||
..
|
||||
} => {
|
||||
accepted_edit_ids.extend(unreviewed_edit_ids.drain());
|
||||
}
|
||||
}
|
||||
|
||||
tracked_buffer.schedule_diff_update();
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the set of buffers that contain changes that haven't been reviewed by the user.
|
||||
pub fn changed_buffers(&self, cx: &App) -> BTreeMap<Entity<Buffer>, ChangedBuffer> {
|
||||
self.tracked_buffers
|
||||
.iter()
|
||||
.filter(|(_, tracked)| tracked.has_changes(cx))
|
||||
.map(|(buffer, tracked)| {
|
||||
(
|
||||
buffer.clone(),
|
||||
ChangedBuffer {
|
||||
diff: tracked.diff.clone(),
|
||||
needs_review: match &tracked.change {
|
||||
Change::Edited {
|
||||
unreviewed_edit_ids,
|
||||
..
|
||||
} => !unreviewed_edit_ids.is_empty(),
|
||||
Change::Deleted { reviewed, .. } => !reviewed,
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Iterate over buffers changed since last read or edited by the model
|
||||
pub fn stale_buffers<'a>(&'a self, cx: &'a App) -> impl Iterator<Item = &'a Entity<Buffer>> {
|
||||
self.tracked_buffers
|
||||
.iter()
|
||||
.filter(|(buffer, tracked)| {
|
||||
let buffer = buffer.read(cx);
|
||||
|
||||
tracked.version != buffer.version
|
||||
&& buffer
|
||||
.file()
|
||||
.map_or(false, |file| file.disk_state() != DiskState::Deleted)
|
||||
})
|
||||
.map(|(buffer, _)| buffer)
|
||||
}
|
||||
|
||||
/// Takes and returns the set of buffers pending refresh, clearing internal state.
|
||||
pub fn take_stale_buffers_in_context(&mut self) -> HashSet<Entity<Buffer>> {
|
||||
std::mem::take(&mut self.stale_buffers_in_context)
|
||||
}
|
||||
}
|
||||
|
||||
fn ranges_intersect(
|
||||
ranges_a: impl IntoIterator<Item = Range<usize>>,
|
||||
ranges_b: impl IntoIterator<Item = Range<usize>>,
|
||||
) -> bool {
|
||||
let mut ranges_a_iter = ranges_a.into_iter().peekable();
|
||||
let mut ranges_b_iter = ranges_b.into_iter().peekable();
|
||||
while let (Some(range_a), Some(range_b)) = (ranges_a_iter.peek(), ranges_b_iter.peek()) {
|
||||
if range_a.end < range_b.start {
|
||||
ranges_a_iter.next();
|
||||
} else if range_b.end < range_a.start {
|
||||
ranges_b_iter.next();
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
struct TrackedBuffer {
|
||||
buffer: Entity<Buffer>,
|
||||
change: Change,
|
||||
version: clock::Global,
|
||||
diff: Entity<BufferDiff>,
|
||||
secondary_diff: Entity<BufferDiff>,
|
||||
diff_update: async_watch::Sender<()>,
|
||||
_maintain_diff: Task<()>,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
enum Change {
|
||||
Edited {
|
||||
unreviewed_edit_ids: HashSet<clock::Lamport>,
|
||||
accepted_edit_ids: HashSet<clock::Lamport>,
|
||||
initial_content: Option<TextBufferSnapshot>,
|
||||
},
|
||||
Deleted {
|
||||
reviewed: bool,
|
||||
deleted_content: TextBufferSnapshot,
|
||||
deletion_id: Option<clock::Lamport>,
|
||||
},
|
||||
}
|
||||
|
||||
impl TrackedBuffer {
|
||||
fn has_changes(&self, cx: &App) -> bool {
|
||||
self.diff
|
||||
.read(cx)
|
||||
.hunks(&self.buffer.read(cx), cx)
|
||||
.next()
|
||||
.is_some()
|
||||
}
|
||||
|
||||
fn schedule_diff_update(&self) {
|
||||
self.diff_update.send(()).ok();
|
||||
}
|
||||
|
||||
fn update_diff(&mut self, cx: &mut App) -> Task<()> {
|
||||
match &self.change {
|
||||
Change::Edited {
|
||||
unreviewed_edit_ids,
|
||||
accepted_edit_ids,
|
||||
..
|
||||
} => {
|
||||
let edits_to_undo = unreviewed_edit_ids
|
||||
.iter()
|
||||
.chain(accepted_edit_ids)
|
||||
.map(|edit_id| (*edit_id, u32::MAX))
|
||||
.collect::<HashMap<_, _>>();
|
||||
let buffer_without_edits = self.buffer.update(cx, |buffer, cx| buffer.branch(cx));
|
||||
buffer_without_edits
|
||||
.update(cx, |buffer, cx| buffer.undo_operations(edits_to_undo, cx));
|
||||
let primary_diff_update = self.diff.update(cx, |diff, cx| {
|
||||
diff.set_base_text_buffer(
|
||||
buffer_without_edits,
|
||||
self.buffer.read(cx).text_snapshot(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let unreviewed_edits_to_undo = unreviewed_edit_ids
|
||||
.iter()
|
||||
.map(|edit_id| (*edit_id, u32::MAX))
|
||||
.collect::<HashMap<_, _>>();
|
||||
let buffer_without_unreviewed_edits =
|
||||
self.buffer.update(cx, |buffer, cx| buffer.branch(cx));
|
||||
buffer_without_unreviewed_edits.update(cx, |buffer, cx| {
|
||||
buffer.undo_operations(unreviewed_edits_to_undo, cx)
|
||||
});
|
||||
let secondary_diff_update = self.secondary_diff.update(cx, |diff, cx| {
|
||||
diff.set_base_text_buffer(
|
||||
buffer_without_unreviewed_edits.clone(),
|
||||
self.buffer.read(cx).text_snapshot(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
cx.background_spawn(async move {
|
||||
_ = primary_diff_update.await;
|
||||
_ = secondary_diff_update.await;
|
||||
})
|
||||
}
|
||||
Change::Deleted {
|
||||
reviewed,
|
||||
deleted_content,
|
||||
..
|
||||
} => {
|
||||
let reviewed = *reviewed;
|
||||
let deleted_content = deleted_content.clone();
|
||||
|
||||
let primary_diff = self.diff.clone();
|
||||
let secondary_diff = self.secondary_diff.clone();
|
||||
let buffer_snapshot = self.buffer.read(cx).text_snapshot();
|
||||
let language = self.buffer.read(cx).language().cloned();
|
||||
let language_registry = self.buffer.read(cx).language_registry().clone();
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let base_text = Arc::new(deleted_content.text());
|
||||
|
||||
let primary_diff_snapshot = BufferDiff::update_diff(
|
||||
primary_diff.clone(),
|
||||
buffer_snapshot.clone(),
|
||||
Some(base_text.clone()),
|
||||
true,
|
||||
false,
|
||||
language.clone(),
|
||||
language_registry.clone(),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
let secondary_diff_snapshot = BufferDiff::update_diff(
|
||||
secondary_diff.clone(),
|
||||
buffer_snapshot.clone(),
|
||||
if reviewed {
|
||||
None
|
||||
} else {
|
||||
Some(base_text.clone())
|
||||
},
|
||||
true,
|
||||
false,
|
||||
language.clone(),
|
||||
language_registry.clone(),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Ok(primary_diff_snapshot) = primary_diff_snapshot {
|
||||
primary_diff
|
||||
.update(cx, |diff, cx| {
|
||||
diff.set_snapshot(primary_diff_snapshot, &buffer_snapshot, None, cx)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
if let Ok(secondary_diff_snapshot) = secondary_diff_snapshot {
|
||||
secondary_diff
|
||||
.update(cx, |diff, cx| {
|
||||
diff.set_snapshot(
|
||||
secondary_diff_snapshot,
|
||||
&buffer_snapshot,
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ChangedBuffer {
|
||||
pub diff: Entity<BufferDiff>,
|
||||
pub needs_review: bool,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use buffer_diff::DiffHunkStatusKind;
|
||||
use gpui::TestAppContext;
|
||||
use language::Point;
|
||||
use project::{FakeFs, Fs, Project, RemoveOptions};
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use util::path;
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_edit_review(cx: &mut TestAppContext) {
|
||||
let action_log = cx.new(|_| ActionLog::new());
|
||||
let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno", cx));
|
||||
|
||||
let edit1 = buffer.update(cx, |buffer, cx| {
|
||||
buffer
|
||||
.edit([(Point::new(1, 1)..Point::new(1, 2), "E")], None, cx)
|
||||
.unwrap()
|
||||
});
|
||||
let edit2 = buffer.update(cx, |buffer, cx| {
|
||||
buffer
|
||||
.edit([(Point::new(4, 2)..Point::new(4, 3), "O")], None, cx)
|
||||
.unwrap()
|
||||
});
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, _| buffer.text()),
|
||||
"abc\ndEf\nghi\njkl\nmnO"
|
||||
);
|
||||
|
||||
action_log.update(cx, |log, cx| {
|
||||
log.buffer_edited(buffer.clone(), vec![edit1, edit2], cx)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
unreviewed_hunks(&action_log, cx),
|
||||
vec![(
|
||||
buffer.clone(),
|
||||
vec![
|
||||
HunkStatus {
|
||||
range: Point::new(1, 0)..Point::new(2, 0),
|
||||
review_status: ReviewStatus::Unreviewed,
|
||||
diff_status: DiffHunkStatusKind::Modified,
|
||||
old_text: "def\n".into(),
|
||||
},
|
||||
HunkStatus {
|
||||
range: Point::new(4, 0)..Point::new(4, 3),
|
||||
review_status: ReviewStatus::Unreviewed,
|
||||
diff_status: DiffHunkStatusKind::Modified,
|
||||
old_text: "mno".into(),
|
||||
}
|
||||
],
|
||||
)]
|
||||
);
|
||||
|
||||
action_log.update(cx, |log, cx| {
|
||||
log.review_edits_in_range(buffer.clone(), Point::new(3, 0)..Point::new(4, 3), true, cx)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
unreviewed_hunks(&action_log, cx),
|
||||
vec![(
|
||||
buffer.clone(),
|
||||
vec![
|
||||
HunkStatus {
|
||||
range: Point::new(1, 0)..Point::new(2, 0),
|
||||
review_status: ReviewStatus::Unreviewed,
|
||||
diff_status: DiffHunkStatusKind::Modified,
|
||||
old_text: "def\n".into(),
|
||||
},
|
||||
HunkStatus {
|
||||
range: Point::new(4, 0)..Point::new(4, 3),
|
||||
review_status: ReviewStatus::Reviewed,
|
||||
diff_status: DiffHunkStatusKind::Modified,
|
||||
old_text: "mno".into(),
|
||||
}
|
||||
],
|
||||
)]
|
||||
);
|
||||
|
||||
action_log.update(cx, |log, cx| {
|
||||
log.review_edits_in_range(
|
||||
buffer.clone(),
|
||||
Point::new(3, 0)..Point::new(4, 3),
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
unreviewed_hunks(&action_log, cx),
|
||||
vec![(
|
||||
buffer.clone(),
|
||||
vec![
|
||||
HunkStatus {
|
||||
range: Point::new(1, 0)..Point::new(2, 0),
|
||||
review_status: ReviewStatus::Unreviewed,
|
||||
diff_status: DiffHunkStatusKind::Modified,
|
||||
old_text: "def\n".into(),
|
||||
},
|
||||
HunkStatus {
|
||||
range: Point::new(4, 0)..Point::new(4, 3),
|
||||
review_status: ReviewStatus::Unreviewed,
|
||||
diff_status: DiffHunkStatusKind::Modified,
|
||||
old_text: "mno".into(),
|
||||
}
|
||||
],
|
||||
)]
|
||||
);
|
||||
|
||||
action_log.update(cx, |log, cx| {
|
||||
log.review_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(4, 3), true, cx)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
unreviewed_hunks(&action_log, cx),
|
||||
vec![(
|
||||
buffer.clone(),
|
||||
vec![
|
||||
HunkStatus {
|
||||
range: Point::new(1, 0)..Point::new(2, 0),
|
||||
review_status: ReviewStatus::Reviewed,
|
||||
diff_status: DiffHunkStatusKind::Modified,
|
||||
old_text: "def\n".into(),
|
||||
},
|
||||
HunkStatus {
|
||||
range: Point::new(4, 0)..Point::new(4, 3),
|
||||
review_status: ReviewStatus::Reviewed,
|
||||
diff_status: DiffHunkStatusKind::Modified,
|
||||
old_text: "mno".into(),
|
||||
}
|
||||
],
|
||||
)]
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_overlapping_user_edits(cx: &mut TestAppContext) {
|
||||
let action_log = cx.new(|_| ActionLog::new());
|
||||
let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno", cx));
|
||||
|
||||
let tool_edit = buffer.update(cx, |buffer, cx| {
|
||||
buffer
|
||||
.edit(
|
||||
[(Point::new(0, 2)..Point::new(2, 3), "C\nDEF\nGHI")],
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, _| buffer.text()),
|
||||
"abC\nDEF\nGHI\njkl\nmno"
|
||||
);
|
||||
|
||||
action_log.update(cx, |log, cx| {
|
||||
log.buffer_edited(buffer.clone(), vec![tool_edit], cx)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
unreviewed_hunks(&action_log, cx),
|
||||
vec![(
|
||||
buffer.clone(),
|
||||
vec![HunkStatus {
|
||||
range: Point::new(0, 0)..Point::new(3, 0),
|
||||
review_status: ReviewStatus::Unreviewed,
|
||||
diff_status: DiffHunkStatusKind::Modified,
|
||||
old_text: "abc\ndef\nghi\n".into(),
|
||||
}],
|
||||
)]
|
||||
);
|
||||
|
||||
action_log.update(cx, |log, cx| {
|
||||
log.review_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(1, 0), true, cx)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
unreviewed_hunks(&action_log, cx),
|
||||
vec![(
|
||||
buffer.clone(),
|
||||
vec![HunkStatus {
|
||||
range: Point::new(0, 0)..Point::new(3, 0),
|
||||
review_status: ReviewStatus::Reviewed,
|
||||
diff_status: DiffHunkStatusKind::Modified,
|
||||
old_text: "abc\ndef\nghi\n".into(),
|
||||
}],
|
||||
)]
|
||||
);
|
||||
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit([(Point::new(0, 2)..Point::new(0, 2), "X")], None, cx)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
unreviewed_hunks(&action_log, cx),
|
||||
vec![(
|
||||
buffer.clone(),
|
||||
vec![HunkStatus {
|
||||
range: Point::new(0, 0)..Point::new(3, 0),
|
||||
review_status: ReviewStatus::Unreviewed,
|
||||
diff_status: DiffHunkStatusKind::Modified,
|
||||
old_text: "abc\ndef\nghi\n".into(),
|
||||
}],
|
||||
)]
|
||||
);
|
||||
|
||||
action_log.update(cx, |log, cx| log.clear_reviewed_changes(cx));
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
unreviewed_hunks(&action_log, cx),
|
||||
vec![(
|
||||
buffer.clone(),
|
||||
vec![HunkStatus {
|
||||
range: Point::new(0, 0)..Point::new(3, 0),
|
||||
review_status: ReviewStatus::Unreviewed,
|
||||
diff_status: DiffHunkStatusKind::Modified,
|
||||
old_text: "abc\ndef\nghi\n".into(),
|
||||
}],
|
||||
)]
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_deletion(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/dir"),
|
||||
json!({"file1": "lorem\n", "file2": "ipsum\n"}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
|
||||
let file1_path = project
|
||||
.read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
|
||||
.unwrap();
|
||||
let file2_path = project
|
||||
.read_with(cx, |project, cx| project.find_project_path("dir/file2", cx))
|
||||
.unwrap();
|
||||
|
||||
let action_log = cx.new(|_| ActionLog::new());
|
||||
let buffer1 = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_buffer(file1_path.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let buffer2 = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_buffer(file2_path.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
action_log.update(cx, |log, cx| log.will_delete_buffer(buffer1.clone(), cx));
|
||||
action_log.update(cx, |log, cx| log.will_delete_buffer(buffer2.clone(), cx));
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project.delete_file(file1_path.clone(), false, cx)
|
||||
})
|
||||
.unwrap()
|
||||
.await
|
||||
.unwrap();
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project.delete_file(file2_path.clone(), false, cx)
|
||||
})
|
||||
.unwrap()
|
||||
.await
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
unreviewed_hunks(&action_log, cx),
|
||||
vec![
|
||||
(
|
||||
buffer1.clone(),
|
||||
vec![HunkStatus {
|
||||
range: Point::new(0, 0)..Point::new(0, 0),
|
||||
review_status: ReviewStatus::Unreviewed,
|
||||
diff_status: DiffHunkStatusKind::Deleted,
|
||||
old_text: "lorem\n".into(),
|
||||
}]
|
||||
),
|
||||
(
|
||||
buffer2.clone(),
|
||||
vec![HunkStatus {
|
||||
range: Point::new(0, 0)..Point::new(0, 0),
|
||||
review_status: ReviewStatus::Unreviewed,
|
||||
diff_status: DiffHunkStatusKind::Deleted,
|
||||
old_text: "ipsum\n".into(),
|
||||
}],
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
// Simulate file1 being recreated externally.
|
||||
fs.insert_file(path!("/dir/file1"), "LOREM".as_bytes().to_vec())
|
||||
.await;
|
||||
let buffer2 = project
|
||||
.update(cx, |project, cx| project.open_buffer(file2_path, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
// Simulate file2 being recreated by a tool.
|
||||
let edit_id = buffer2.update(cx, |buffer, cx| buffer.set_text("IPSUM", cx));
|
||||
action_log.update(cx, |log, cx| {
|
||||
log.will_create_buffer(buffer2.clone(), edit_id, cx)
|
||||
});
|
||||
project
|
||||
.update(cx, |project, cx| project.save_buffer(buffer2.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
unreviewed_hunks(&action_log, cx),
|
||||
vec![(
|
||||
buffer2.clone(),
|
||||
vec![HunkStatus {
|
||||
range: Point::new(0, 0)..Point::new(0, 5),
|
||||
review_status: ReviewStatus::Unreviewed,
|
||||
diff_status: DiffHunkStatusKind::Modified,
|
||||
old_text: "ipsum\n".into(),
|
||||
}],
|
||||
)]
|
||||
);
|
||||
|
||||
// Simulate file2 being deleted externally.
|
||||
fs.remove_file(path!("/dir/file2").as_ref(), RemoveOptions::default())
|
||||
.await
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct HunkStatus {
|
||||
range: Range<Point>,
|
||||
review_status: ReviewStatus,
|
||||
diff_status: DiffHunkStatusKind,
|
||||
old_text: String,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
enum ReviewStatus {
|
||||
Unreviewed,
|
||||
Reviewed,
|
||||
}
|
||||
|
||||
fn unreviewed_hunks(
|
||||
action_log: &Entity<ActionLog>,
|
||||
cx: &TestAppContext,
|
||||
) -> Vec<(Entity<Buffer>, Vec<HunkStatus>)> {
|
||||
cx.read(|cx| {
|
||||
action_log
|
||||
.read(cx)
|
||||
.changed_buffers(cx)
|
||||
.into_iter()
|
||||
.map(|(buffer, tracked_buffer)| {
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
(
|
||||
buffer,
|
||||
tracked_buffer
|
||||
.diff
|
||||
.read(cx)
|
||||
.hunks(&snapshot, cx)
|
||||
.map(|hunk| HunkStatus {
|
||||
review_status: if hunk.status().has_secondary_hunk() {
|
||||
ReviewStatus::Unreviewed
|
||||
} else {
|
||||
ReviewStatus::Reviewed
|
||||
},
|
||||
diff_status: hunk.status().kind,
|
||||
range: hunk.range,
|
||||
old_text: tracked_buffer
|
||||
.diff
|
||||
.read(cx)
|
||||
.base_text()
|
||||
.text_for_range(hunk.diff_base_byte_range)
|
||||
.collect(),
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,20 @@
|
||||
mod action_log;
|
||||
mod tool_registry;
|
||||
mod tool_working_set;
|
||||
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
use std::fmt;
|
||||
use std::fmt::Debug;
|
||||
use std::fmt::Formatter;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use collections::{HashMap, HashSet};
|
||||
use gpui::{App, Context, Entity, SharedString, Task};
|
||||
use gpui::{App, Entity, SharedString, Task};
|
||||
use icons::IconName;
|
||||
use language::Buffer;
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use language_model::LanguageModelToolSchemaFormat;
|
||||
use project::Project;
|
||||
|
||||
pub use crate::action_log::*;
|
||||
pub use crate::tool_registry::*;
|
||||
pub use crate::tool_working_set::*;
|
||||
|
||||
@@ -48,7 +51,7 @@ pub trait Tool: 'static + Send + Sync {
|
||||
fn needs_confirmation(&self) -> bool;
|
||||
|
||||
/// Returns the JSON schema that describes the tool's input.
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
fn input_schema(&self, _: LanguageModelToolSchemaFormat) -> serde_json::Value {
|
||||
serde_json::Value::Object(serde_json::Map::default())
|
||||
}
|
||||
|
||||
@@ -71,71 +74,3 @@ impl Debug for dyn Tool {
|
||||
f.debug_struct("Tool").field("name", &self.name()).finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Tracks actions performed by tools in a thread
|
||||
#[derive(Debug)]
|
||||
pub struct ActionLog {
|
||||
/// Buffers that user manually added to the context, and whose content has
|
||||
/// changed since the model last saw them.
|
||||
stale_buffers_in_context: HashSet<Entity<Buffer>>,
|
||||
/// Buffers that we want to notify the model about when they change.
|
||||
tracked_buffers: HashMap<Entity<Buffer>, TrackedBuffer>,
|
||||
/// Has the model edited a file since it last checked diagnostics?
|
||||
edited_since_project_diagnostics_check: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct TrackedBuffer {
|
||||
version: clock::Global,
|
||||
}
|
||||
|
||||
impl ActionLog {
|
||||
/// Creates a new, empty action log.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
stale_buffers_in_context: HashSet::default(),
|
||||
tracked_buffers: HashMap::default(),
|
||||
edited_since_project_diagnostics_check: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Track a buffer as read, so we can notify the model about user edits.
|
||||
pub fn buffer_read(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
|
||||
let tracked_buffer = self.tracked_buffers.entry(buffer.clone()).or_default();
|
||||
tracked_buffer.version = buffer.read(cx).version();
|
||||
}
|
||||
|
||||
/// Mark a buffer as edited, so we can refresh it in the context
|
||||
pub fn buffer_edited(&mut self, buffers: HashSet<Entity<Buffer>>, cx: &mut Context<Self>) {
|
||||
for buffer in &buffers {
|
||||
let tracked_buffer = self.tracked_buffers.entry(buffer.clone()).or_default();
|
||||
tracked_buffer.version = buffer.read(cx).version();
|
||||
}
|
||||
|
||||
self.stale_buffers_in_context.extend(buffers);
|
||||
self.edited_since_project_diagnostics_check = true;
|
||||
}
|
||||
|
||||
/// Notifies a diagnostics check
|
||||
pub fn checked_project_diagnostics(&mut self) {
|
||||
self.edited_since_project_diagnostics_check = false;
|
||||
}
|
||||
|
||||
/// Iterate over buffers changed since last read or edited by the model
|
||||
pub fn stale_buffers<'a>(&'a self, cx: &'a App) -> impl Iterator<Item = &'a Entity<Buffer>> {
|
||||
self.tracked_buffers
|
||||
.iter()
|
||||
.filter(|(buffer, tracked)| tracked.version != buffer.read(cx).version)
|
||||
.map(|(buffer, _)| buffer)
|
||||
}
|
||||
|
||||
/// Returns true if any files have been edited since the last project diagnostics check
|
||||
pub fn has_edited_files_since_project_diagnostics_check(&self) -> bool {
|
||||
self.edited_since_project_diagnostics_check
|
||||
}
|
||||
|
||||
/// Takes and returns the set of buffers pending refresh, clearing internal state.
|
||||
pub fn take_stale_buffers_in_context(&mut self) -> HashSet<Entity<Buffer>> {
|
||||
std::mem::take(&mut self.stale_buffers_in_context)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ path = "src/assistant_tools.rs"
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
clock.workspace = true
|
||||
chrono.workspace = true
|
||||
collections.workspace = true
|
||||
feature_flags.workspace = true
|
||||
@@ -24,7 +25,10 @@ http_client.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
project.workspace = true
|
||||
regex.workspace = true
|
||||
release_channel.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
@@ -38,6 +42,7 @@ worktree.workspace = true
|
||||
open = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
clock = { workspace = true, features = ["test-support"] }
|
||||
collections = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
mod bash_tool;
|
||||
mod batch_tool;
|
||||
mod code_symbol_iter;
|
||||
mod code_symbols_tool;
|
||||
mod copy_path_tool;
|
||||
mod create_directory_tool;
|
||||
mod create_file_tool;
|
||||
@@ -16,6 +18,8 @@ mod path_search_tool;
|
||||
mod read_file_tool;
|
||||
mod regex_search_tool;
|
||||
mod replace;
|
||||
mod schema;
|
||||
mod symbol_info_tool;
|
||||
mod thinking_tool;
|
||||
|
||||
use std::sync::Arc;
|
||||
@@ -28,6 +32,7 @@ use move_path_tool::MovePathTool;
|
||||
|
||||
use crate::bash_tool::BashTool;
|
||||
use crate::batch_tool::BatchTool;
|
||||
use crate::code_symbols_tool::CodeSymbolsTool;
|
||||
use crate::create_directory_tool::CreateDirectoryTool;
|
||||
use crate::create_file_tool::CreateFileTool;
|
||||
use crate::delete_path_tool::DeletePathTool;
|
||||
@@ -41,6 +46,7 @@ use crate::open_tool::OpenTool;
|
||||
use crate::path_search_tool::PathSearchTool;
|
||||
use crate::read_file_tool::ReadFileTool;
|
||||
use crate::regex_search_tool::RegexSearchTool;
|
||||
use crate::symbol_info_tool::SymbolInfoTool;
|
||||
use crate::thinking_tool::ThinkingTool;
|
||||
|
||||
pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
|
||||
@@ -55,12 +61,14 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
|
||||
registry.register_tool(CopyPathTool);
|
||||
registry.register_tool(DeletePathTool);
|
||||
registry.register_tool(FindReplaceFileTool);
|
||||
registry.register_tool(SymbolInfoTool);
|
||||
registry.register_tool(MovePathTool);
|
||||
registry.register_tool(DiagnosticsTool);
|
||||
registry.register_tool(EditFilesTool);
|
||||
registry.register_tool(ListDirectoryTool);
|
||||
registry.register_tool(NowTool);
|
||||
registry.register_tool(OpenTool);
|
||||
registry.register_tool(CodeSymbolsTool);
|
||||
registry.register_tool(PathSearchTool);
|
||||
registry.register_tool(ReadFileTool);
|
||||
registry.register_tool(RegexSearchTool);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool};
|
||||
use gpui::{App, Entity, Task};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -38,9 +39,8 @@ impl Tool for BashTool {
|
||||
IconName::Terminal
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
let schema = schemars::schema_for!(BashToolInput);
|
||||
serde_json::to_value(&schema).unwrap()
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value {
|
||||
json_schema_for::<BashToolInput>(format)
|
||||
}
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
@@ -81,7 +81,9 @@ impl Tool for BashTool {
|
||||
};
|
||||
|
||||
if worktrees.next().is_some() {
|
||||
return Task::ready(Err(anyhow!("'.' is ambiguous in multi-root workspaces. Please specify a root directory explicitly.")));
|
||||
return Task::ready(Err(anyhow!(
|
||||
"'.' is ambiguous in multi-root workspaces. Please specify a root directory explicitly."
|
||||
)));
|
||||
}
|
||||
|
||||
only_worktree.read(cx).abs_path()
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolWorkingSet};
|
||||
use futures::future::join_all;
|
||||
use gpui::{App, AppContext, Entity, Task};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -162,9 +163,8 @@ impl Tool for BatchTool {
|
||||
IconName::Cog
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
let schema = schemars::schema_for!(BatchToolInput);
|
||||
serde_json::to_value(&schema).unwrap()
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value {
|
||||
json_schema_for::<BatchToolInput>(format)
|
||||
}
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
|
||||
88
crates/assistant_tools/src/code_symbol_iter.rs
Normal file
88
crates/assistant_tools/src/code_symbol_iter.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
use project::DocumentSymbol;
|
||||
use regex::Regex;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Entry {
|
||||
pub name: String,
|
||||
pub kind: lsp::SymbolKind,
|
||||
pub depth: u32,
|
||||
pub start_line: usize,
|
||||
pub end_line: usize,
|
||||
}
|
||||
|
||||
/// An iterator that filters document symbols based on a regex pattern.
|
||||
/// This iterator recursively traverses the document symbol tree, incrementing depth for child symbols.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CodeSymbolIterator<'a> {
|
||||
symbols: &'a [DocumentSymbol],
|
||||
regex: Option<Regex>,
|
||||
// Stack of (symbol, depth) pairs to process
|
||||
pending_symbols: Vec<(&'a DocumentSymbol, u32)>,
|
||||
current_index: usize,
|
||||
current_depth: u32,
|
||||
}
|
||||
|
||||
impl<'a> CodeSymbolIterator<'a> {
|
||||
pub fn new(symbols: &'a [DocumentSymbol], regex: Option<Regex>) -> Self {
|
||||
Self {
|
||||
symbols,
|
||||
regex,
|
||||
pending_symbols: Vec::new(),
|
||||
current_index: 0,
|
||||
current_depth: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for CodeSymbolIterator<'_> {
|
||||
type Item = Entry;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if let Some((symbol, depth)) = self.pending_symbols.pop() {
|
||||
for child in symbol.children.iter().rev() {
|
||||
self.pending_symbols.push((child, depth + 1));
|
||||
}
|
||||
|
||||
return Some(Entry {
|
||||
name: symbol.name.clone(),
|
||||
kind: symbol.kind,
|
||||
depth,
|
||||
start_line: symbol.range.start.0.row as usize,
|
||||
end_line: symbol.range.end.0.row as usize,
|
||||
});
|
||||
}
|
||||
|
||||
while self.current_index < self.symbols.len() {
|
||||
let regex = self.regex.as_ref();
|
||||
let symbol = &self.symbols[self.current_index];
|
||||
self.current_index += 1;
|
||||
|
||||
if regex.is_none_or(|regex| regex.is_match(&symbol.name)) {
|
||||
// Push in reverse order to maintain traversal order
|
||||
for child in symbol.children.iter().rev() {
|
||||
self.pending_symbols.push((child, self.current_depth + 1));
|
||||
}
|
||||
|
||||
return Some(Entry {
|
||||
name: symbol.name.clone(),
|
||||
kind: symbol.kind,
|
||||
depth: self.current_depth,
|
||||
start_line: symbol.range.start.0.row as usize,
|
||||
end_line: symbol.range.end.0.row as usize,
|
||||
});
|
||||
} else {
|
||||
// Even if parent doesn't match, push children to check them later
|
||||
for child in symbol.children.iter().rev() {
|
||||
self.pending_symbols.push((child, self.current_depth + 1));
|
||||
}
|
||||
|
||||
// Check if any pending children match our criteria
|
||||
if let Some(result) = self.next() {
|
||||
return Some(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
445
crates/assistant_tools/src/code_symbols_tool.rs
Normal file
445
crates/assistant_tools/src/code_symbols_tool.rs
Normal file
@@ -0,0 +1,445 @@
|
||||
use std::fmt::{self, Write};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool};
|
||||
use collections::IndexMap;
|
||||
use gpui::{App, AsyncApp, Entity, Task};
|
||||
use language::{CodeLabel, Language, LanguageRegistry};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use lsp::SymbolKind;
|
||||
use project::{DocumentSymbol, Project, Symbol};
|
||||
use regex::{Regex, RegexBuilder};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ui::IconName;
|
||||
use util::markdown::MarkdownString;
|
||||
|
||||
use crate::code_symbol_iter::{CodeSymbolIterator, Entry};
|
||||
use crate::schema::json_schema_for;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct CodeSymbolsInput {
|
||||
/// The relative path of the source code file to read and get the symbols for.
|
||||
/// This tool should only be used on source code files, never on any other type of file.
|
||||
///
|
||||
/// This path should never be absolute, and the first component
|
||||
/// of the path should always be a root directory in a project.
|
||||
///
|
||||
/// If no path is specified, this tool returns a flat list of all symbols in the project
|
||||
/// instead of a hierarchical outline of a specific file.
|
||||
///
|
||||
/// <example>
|
||||
/// If the project has the following root directories:
|
||||
///
|
||||
/// - directory1
|
||||
/// - directory2
|
||||
///
|
||||
/// If you want to access `file.md` in `directory1`, you should use the path `directory1/file.md`.
|
||||
/// If you want to access `file.md` in `directory2`, you should use the path `directory2/file.md`.
|
||||
/// </example>
|
||||
#[serde(default)]
|
||||
pub path: Option<String>,
|
||||
|
||||
/// Optional regex pattern to filter symbols by name.
|
||||
/// When provided, only symbols whose names match this pattern will be included in the results.
|
||||
///
|
||||
/// <example>
|
||||
/// To find only symbols that contain the word "test", use the regex pattern "test".
|
||||
/// To find methods that start with "get_", use the regex pattern "^get_".
|
||||
/// </example>
|
||||
#[serde(default)]
|
||||
pub regex: Option<String>,
|
||||
|
||||
/// Whether the regex is case-sensitive. Defaults to false (case-insensitive).
|
||||
///
|
||||
/// <example>
|
||||
/// Set to `true` to make regex matching case-sensitive.
|
||||
/// </example>
|
||||
#[serde(default)]
|
||||
pub case_sensitive: bool,
|
||||
|
||||
/// Optional starting position for paginated results (0-based).
|
||||
/// When not provided, starts from the beginning.
|
||||
#[serde(default)]
|
||||
pub offset: u32,
|
||||
}
|
||||
|
||||
impl CodeSymbolsInput {
|
||||
/// Which page of search results this is.
|
||||
pub fn page(&self) -> u32 {
|
||||
1 + (self.offset / RESULTS_PER_PAGE)
|
||||
}
|
||||
}
|
||||
|
||||
const RESULTS_PER_PAGE: u32 = 2000;
|
||||
|
||||
pub struct CodeSymbolsTool;
|
||||
|
||||
impl Tool for CodeSymbolsTool {
|
||||
fn name(&self) -> String {
|
||||
"code-symbols".into()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
include_str!("./code_symbols_tool/description.md").into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Code
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value {
|
||||
json_schema_for::<CodeSymbolsInput>(format)
|
||||
}
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<CodeSymbolsInput>(input.clone()) {
|
||||
Ok(input) => {
|
||||
let page = input.page();
|
||||
|
||||
match &input.path {
|
||||
Some(path) => {
|
||||
let path = MarkdownString::inline_code(path);
|
||||
if page > 1 {
|
||||
format!("List page {page} of code symbols for {path}")
|
||||
} else {
|
||||
format!("List code symbols for {path}")
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if page > 1 {
|
||||
format!("List page {page} of project symbols")
|
||||
} else {
|
||||
"List all project symbols".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => "List code symbols".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
let input = match serde_json::from_value::<CodeSymbolsInput>(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
};
|
||||
|
||||
let regex = match input.regex {
|
||||
Some(regex_str) => match RegexBuilder::new(®ex_str)
|
||||
.case_insensitive(!input.case_sensitive)
|
||||
.build()
|
||||
{
|
||||
Ok(regex) => Some(regex),
|
||||
Err(err) => return Task::ready(Err(anyhow!("Invalid regex: {err}"))),
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
|
||||
cx.spawn(async move |cx| match input.path {
|
||||
Some(path) => file_outline(project, path, action_log, regex, input.offset, cx).await,
|
||||
None => project_symbols(project, regex, input.offset, cx).await,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn file_outline(
|
||||
project: Entity<Project>,
|
||||
path: String,
|
||||
action_log: Entity<ActionLog>,
|
||||
regex: Option<Regex>,
|
||||
offset: u32,
|
||||
cx: &mut AsyncApp,
|
||||
) -> anyhow::Result<String> {
|
||||
let buffer = {
|
||||
let project_path = project.read_with(cx, |project, cx| {
|
||||
project
|
||||
.find_project_path(&path, cx)
|
||||
.ok_or_else(|| anyhow!("Path {path} not found in project"))
|
||||
})??;
|
||||
|
||||
project
|
||||
.update(cx, |project, cx| project.open_buffer(project_path, cx))?
|
||||
.await?
|
||||
};
|
||||
|
||||
action_log.update(cx, |action_log, cx| {
|
||||
action_log.buffer_read(buffer.clone(), cx);
|
||||
})?;
|
||||
|
||||
let symbols = project
|
||||
.update(cx, |project, cx| project.document_symbols(&buffer, cx))?
|
||||
.await?;
|
||||
|
||||
if symbols.is_empty() {
|
||||
return Err(
|
||||
if buffer.read_with(cx, |buffer, _| buffer.snapshot().is_empty())? {
|
||||
anyhow!("This file is empty.")
|
||||
} else {
|
||||
anyhow!("No outline information available for this file.")
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let language = buffer.read_with(cx, |buffer, _| buffer.language().cloned())?;
|
||||
let language_registry = project.read_with(cx, |project, _| project.languages().clone())?;
|
||||
|
||||
render_outline(&symbols, language, language_registry, regex, offset).await
|
||||
}
|
||||
|
||||
async fn project_symbols(
|
||||
project: Entity<Project>,
|
||||
regex: Option<Regex>,
|
||||
offset: u32,
|
||||
cx: &mut AsyncApp,
|
||||
) -> anyhow::Result<String> {
|
||||
let symbols = project
|
||||
.update(cx, |project, cx| project.symbols("", cx))?
|
||||
.await?;
|
||||
|
||||
if symbols.is_empty() {
|
||||
return Err(anyhow!("No symbols found in project."));
|
||||
}
|
||||
|
||||
let mut symbols_by_path: IndexMap<PathBuf, Vec<&Symbol>> = IndexMap::default();
|
||||
|
||||
for symbol in symbols
|
||||
.iter()
|
||||
.filter(|symbol| {
|
||||
if let Some(regex) = ®ex {
|
||||
regex.is_match(&symbol.name)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
.skip(offset as usize)
|
||||
// Take 1 more than RESULTS_PER_PAGE so we can tell if there are more results.
|
||||
.take((RESULTS_PER_PAGE as usize).saturating_add(1))
|
||||
{
|
||||
if let Some(worktree_path) = project.read_with(cx, |project, cx| {
|
||||
project
|
||||
.worktree_for_id(symbol.path.worktree_id, cx)
|
||||
.map(|worktree| PathBuf::from(worktree.read(cx).root_name()))
|
||||
})? {
|
||||
let path = worktree_path.join(&symbol.path.path);
|
||||
symbols_by_path.entry(path).or_default().push(symbol);
|
||||
}
|
||||
}
|
||||
|
||||
// If no symbols matched the filter, return early
|
||||
if symbols_by_path.is_empty() {
|
||||
return Err(anyhow!("No symbols found matching the criteria."));
|
||||
}
|
||||
|
||||
let mut symbols_rendered = 0;
|
||||
let mut has_more_symbols = false;
|
||||
let mut output = String::new();
|
||||
|
||||
'outer: for (file_path, file_symbols) in symbols_by_path {
|
||||
if symbols_rendered > 0 {
|
||||
output.push('\n');
|
||||
}
|
||||
|
||||
writeln!(&mut output, "{}", file_path.display()).ok();
|
||||
|
||||
for symbol in file_symbols {
|
||||
if symbols_rendered >= RESULTS_PER_PAGE {
|
||||
has_more_symbols = true;
|
||||
break 'outer;
|
||||
}
|
||||
|
||||
write!(&mut output, " {} ", symbol.label.text()).ok();
|
||||
|
||||
// Convert to 1-based line numbers for display
|
||||
let start_line = symbol.range.start.0.row as usize + 1;
|
||||
let end_line = symbol.range.end.0.row as usize + 1;
|
||||
|
||||
if start_line == end_line {
|
||||
writeln!(&mut output, "[L{}]", start_line).ok();
|
||||
} else {
|
||||
writeln!(&mut output, "[L{}-{}]", start_line, end_line).ok();
|
||||
}
|
||||
|
||||
symbols_rendered += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(if symbols_rendered == 0 {
|
||||
"No symbols found in the requested page.".to_string()
|
||||
} else if has_more_symbols {
|
||||
format!(
|
||||
"{output}\nShowing symbols {}-{} (more symbols were found; use offset: {} to see next page)",
|
||||
offset + 1,
|
||||
offset + symbols_rendered,
|
||||
offset + RESULTS_PER_PAGE,
|
||||
)
|
||||
} else {
|
||||
output
|
||||
})
|
||||
}
|
||||
|
||||
async fn render_outline(
|
||||
symbols: &[DocumentSymbol],
|
||||
language: Option<Arc<Language>>,
|
||||
registry: Arc<LanguageRegistry>,
|
||||
regex: Option<Regex>,
|
||||
offset: u32,
|
||||
) -> Result<String> {
|
||||
const RESULTS_PER_PAGE_USIZE: usize = RESULTS_PER_PAGE as usize;
|
||||
let entries = CodeSymbolIterator::new(symbols, regex.clone())
|
||||
.skip(offset as usize)
|
||||
// Take 1 more than RESULTS_PER_PAGE so we can tell if there are more results.
|
||||
.take(RESULTS_PER_PAGE_USIZE.saturating_add(1))
|
||||
.collect::<Vec<Entry>>();
|
||||
let has_more = entries.len() > RESULTS_PER_PAGE_USIZE;
|
||||
|
||||
// Get language-specific labels, if available
|
||||
let labels = match &language {
|
||||
Some(lang) => {
|
||||
let entries_for_labels: Vec<(String, SymbolKind)> = entries
|
||||
.iter()
|
||||
.take(RESULTS_PER_PAGE_USIZE)
|
||||
.map(|entry| (entry.name.clone(), entry.kind))
|
||||
.collect();
|
||||
|
||||
let lang_name = lang.name();
|
||||
if let Some(lsp_adapter) = registry.lsp_adapters(&lang_name).first().cloned() {
|
||||
lsp_adapter
|
||||
.labels_for_symbols(&entries_for_labels, lang)
|
||||
.await
|
||||
.ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
let mut output = String::new();
|
||||
|
||||
let entries_rendered = match &labels {
|
||||
Some(label_list) => render_entries(
|
||||
&mut output,
|
||||
entries
|
||||
.into_iter()
|
||||
.take(RESULTS_PER_PAGE_USIZE)
|
||||
.zip(label_list.iter())
|
||||
.map(|(entry, label)| (entry, label.as_ref())),
|
||||
),
|
||||
None => render_entries(
|
||||
&mut output,
|
||||
entries
|
||||
.into_iter()
|
||||
.take(RESULTS_PER_PAGE_USIZE)
|
||||
.map(|entry| (entry, None)),
|
||||
),
|
||||
};
|
||||
|
||||
// Calculate pagination information
|
||||
let page_start = offset + 1;
|
||||
let page_end = offset + entries_rendered;
|
||||
let total_symbols = if has_more {
|
||||
format!("more than {}", page_end)
|
||||
} else {
|
||||
page_end.to_string()
|
||||
};
|
||||
|
||||
// Add pagination information
|
||||
if has_more {
|
||||
writeln!(&mut output, "\nShowing symbols {page_start}-{page_end} (there were more symbols found; use offset: {page_end} to see next page)",
|
||||
)
|
||||
} else {
|
||||
writeln!(
|
||||
&mut output,
|
||||
"\nShowing symbols {page_start}-{page_end} (total symbols: {total_symbols})",
|
||||
)
|
||||
}
|
||||
.ok();
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn render_entries<'a>(
|
||||
output: &mut String,
|
||||
entries: impl IntoIterator<Item = (Entry, Option<&'a CodeLabel>)>,
|
||||
) -> u32 {
|
||||
let mut entries_rendered = 0;
|
||||
|
||||
for (entry, label) in entries {
|
||||
// Indent based on depth ("" for level 0, " " for level 1, etc.)
|
||||
for _ in 0..entry.depth {
|
||||
output.push_str(" ");
|
||||
}
|
||||
|
||||
match label {
|
||||
Some(label) => {
|
||||
output.push_str(label.text());
|
||||
}
|
||||
None => {
|
||||
write_symbol_kind(output, entry.kind).ok();
|
||||
output.push_str(&entry.name);
|
||||
}
|
||||
}
|
||||
|
||||
// Add position information - convert to 1-based line numbers for display
|
||||
let start_line = entry.start_line + 1;
|
||||
let end_line = entry.end_line + 1;
|
||||
|
||||
if start_line == end_line {
|
||||
writeln!(output, " [L{}]", start_line).ok();
|
||||
} else {
|
||||
writeln!(output, " [L{}-{}]", start_line, end_line).ok();
|
||||
}
|
||||
entries_rendered += 1;
|
||||
}
|
||||
|
||||
entries_rendered
|
||||
}
|
||||
|
||||
// We may not have a language server adapter to have language-specific
|
||||
// ways to translate SymbolKnd into a string. In that situation,
|
||||
// fall back on some reasonable default strings to render.
|
||||
fn write_symbol_kind(buf: &mut String, kind: SymbolKind) -> Result<(), fmt::Error> {
|
||||
match kind {
|
||||
SymbolKind::FILE => write!(buf, "file "),
|
||||
SymbolKind::MODULE => write!(buf, "module "),
|
||||
SymbolKind::NAMESPACE => write!(buf, "namespace "),
|
||||
SymbolKind::PACKAGE => write!(buf, "package "),
|
||||
SymbolKind::CLASS => write!(buf, "class "),
|
||||
SymbolKind::METHOD => write!(buf, "method "),
|
||||
SymbolKind::PROPERTY => write!(buf, "property "),
|
||||
SymbolKind::FIELD => write!(buf, "field "),
|
||||
SymbolKind::CONSTRUCTOR => write!(buf, "constructor "),
|
||||
SymbolKind::ENUM => write!(buf, "enum "),
|
||||
SymbolKind::INTERFACE => write!(buf, "interface "),
|
||||
SymbolKind::FUNCTION => write!(buf, "function "),
|
||||
SymbolKind::VARIABLE => write!(buf, "variable "),
|
||||
SymbolKind::CONSTANT => write!(buf, "constant "),
|
||||
SymbolKind::STRING => write!(buf, "string "),
|
||||
SymbolKind::NUMBER => write!(buf, "number "),
|
||||
SymbolKind::BOOLEAN => write!(buf, "boolean "),
|
||||
SymbolKind::ARRAY => write!(buf, "array "),
|
||||
SymbolKind::OBJECT => write!(buf, "object "),
|
||||
SymbolKind::KEY => write!(buf, "key "),
|
||||
SymbolKind::NULL => write!(buf, "null "),
|
||||
SymbolKind::ENUM_MEMBER => write!(buf, "enum member "),
|
||||
SymbolKind::STRUCT => write!(buf, "struct "),
|
||||
SymbolKind::EVENT => write!(buf, "event "),
|
||||
SymbolKind::OPERATOR => write!(buf, "operator "),
|
||||
SymbolKind::TYPE_PARAMETER => write!(buf, "type parameter "),
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
39
crates/assistant_tools/src/code_symbols_tool/description.md
Normal file
39
crates/assistant_tools/src/code_symbols_tool/description.md
Normal file
@@ -0,0 +1,39 @@
|
||||
Returns either an outline of the public code symbols in the entire project (grouped by file) or else an outline of both the public and private code symbols within a particular file.
|
||||
|
||||
When a path is provided, this tool returns a hierarchical outline of code symbols for that specific file.
|
||||
When no path is provided, it returns a list of all public code symbols in the project, organized by file.
|
||||
|
||||
You can also provide an optional regular expression which filters the output by only showing code symbols which match that regex.
|
||||
|
||||
Results are paginated with 2000 entries per page. Use the optional 'offset' parameter to request subsequent pages.
|
||||
|
||||
Markdown headings indicate the structure of the output; just like
|
||||
with markdown headings, the more # symbols there are at the beginning of a line,
|
||||
the deeper it is in the hierarchy.
|
||||
|
||||
Each code symbol entry ends with a line number or range, which tells you what portion of the
|
||||
underlying source code file corresponds to that part of the outline. You can use
|
||||
that line information with other tools, to strategically read portions of the source code.
|
||||
|
||||
For example, you can use this tool to find a relevant symbol in the project, then get the outline of the file which contains that symbol, then use the line number information from that file's outline to read different sections of that file, without having to read the entire file all at once (which can be slow, or use a lot of tokens).
|
||||
|
||||
<example>
|
||||
# class Foo [L123-136]
|
||||
## method do_something(arg1, arg2) [L124-126]
|
||||
## method process_data(data) [L128-135]
|
||||
# class Bar [L145-161]
|
||||
## method initialize() [L146-149]
|
||||
## method update_state(new_state) [L160]
|
||||
## private method _validate_state(state) [L161-162]
|
||||
</example>
|
||||
|
||||
This example shows how tree-sitter outlines the structure of source code:
|
||||
|
||||
1. `class Foo` is defined on lines 123-136
|
||||
- It contains a method `do_something` spanning lines 124-126
|
||||
- It also has a method `process_data` spanning lines 128-135
|
||||
|
||||
2. `class Bar` is defined on lines 145-161
|
||||
- It has an `initialize` method spanning lines 146-149
|
||||
- It has an `update_state` method on line 160
|
||||
- It has a private method `_validate_state` spanning lines 161-162
|
||||
@@ -1,7 +1,9 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool};
|
||||
use gpui::{App, AppContext, Entity, Task};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use language_model::LanguageModelToolSchemaFormat;
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -53,9 +55,8 @@ impl Tool for CopyPathTool {
|
||||
IconName::Clipboard
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
let schema = schemars::schema_for!(CopyPathToolInput);
|
||||
serde_json::to_value(&schema).unwrap()
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value {
|
||||
json_schema_for::<CopyPathToolInput>(format)
|
||||
}
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool};
|
||||
use gpui::{App, Entity, Task};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use language_model::LanguageModelToolSchemaFormat;
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -43,9 +45,8 @@ impl Tool for CreateDirectoryTool {
|
||||
IconName::Folder
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
let schema = schemars::schema_for!(CreateDirectoryToolInput);
|
||||
serde_json::to_value(&schema).unwrap()
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value {
|
||||
json_schema_for::<CreateDirectoryToolInput>(format)
|
||||
}
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool};
|
||||
use gpui::{App, Entity, Task};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use language_model::LanguageModelToolSchemaFormat;
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -50,9 +52,8 @@ impl Tool for CreateFileTool {
|
||||
IconName::FileCreate
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
let schema = schemars::schema_for!(CreateFileToolInput);
|
||||
serde_json::to_value(&schema).unwrap()
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value {
|
||||
json_schema_for::<CreateFileToolInput>(format)
|
||||
}
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
@@ -70,7 +71,7 @@ impl Tool for CreateFileTool {
|
||||
input: serde_json::Value,
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
action_log: Entity<ActionLog>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
let input = match serde_json::from_value::<CreateFileToolInput>(input) {
|
||||
@@ -85,24 +86,20 @@ impl Tool for CreateFileTool {
|
||||
let destination_path: Arc<str> = input.path.as_str().into();
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project.create_entry(project_path.clone(), false, cx)
|
||||
})?
|
||||
.await
|
||||
.map_err(|err| anyhow!("Unable to create {destination_path}: {err}"))?;
|
||||
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}"))?;
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.set_text(contents, cx);
|
||||
let edit_id = buffer.update(cx, |buffer, cx| buffer.set_text(contents, cx))?;
|
||||
|
||||
action_log.update(cx, |action_log, cx| {
|
||||
action_log.will_create_buffer(buffer.clone(), edit_id, cx)
|
||||
})?;
|
||||
|
||||
project
|
||||
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
|
||||
.update(cx, |project, cx| project.save_buffer(buffer, cx))?
|
||||
.await
|
||||
.map_err(|err| anyhow!("Unable to save buffer for {destination_path}: {err}"))?;
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool};
|
||||
use futures::{SinkExt, StreamExt, channel::mpsc};
|
||||
use gpui::{App, AppContext, Entity, Task};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use project::Project;
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::{Project, ProjectPath};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
@@ -43,9 +45,8 @@ impl Tool for DeletePathTool {
|
||||
IconName::FileDelete
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
let schema = schemars::schema_for!(DeletePathToolInput);
|
||||
serde_json::to_value(&schema).unwrap()
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value {
|
||||
json_schema_for::<DeletePathToolInput>(format)
|
||||
}
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
@@ -60,28 +61,76 @@ impl Tool for DeletePathTool {
|
||||
input: serde_json::Value,
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
action_log: Entity<ActionLog>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
let path_str = match serde_json::from_value::<DeletePathToolInput>(input) {
|
||||
Ok(input) => input.path,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
};
|
||||
let Some(project_path) = project.read(cx).find_project_path(&path_str, cx) else {
|
||||
return Task::ready(Err(anyhow!(
|
||||
"Couldn't delete {path_str} because that path isn't in this project."
|
||||
)));
|
||||
};
|
||||
|
||||
match project
|
||||
let Some(worktree) = project
|
||||
.read(cx)
|
||||
.find_project_path(&path_str, cx)
|
||||
.and_then(|path| project.update(cx, |project, cx| project.delete_file(path, false, cx)))
|
||||
{
|
||||
Some(deletion_task) => cx.background_spawn(async move {
|
||||
match deletion_task.await {
|
||||
.worktree_for_id(project_path.worktree_id, cx)
|
||||
else {
|
||||
return Task::ready(Err(anyhow!(
|
||||
"Couldn't delete {path_str} because that path isn't in this project."
|
||||
)));
|
||||
};
|
||||
|
||||
let worktree_snapshot = worktree.read(cx).snapshot();
|
||||
let (mut paths_tx, mut paths_rx) = mpsc::channel(256);
|
||||
cx.background_spawn({
|
||||
let project_path = project_path.clone();
|
||||
async move {
|
||||
for entry in
|
||||
worktree_snapshot.traverse_from_path(true, false, false, &project_path.path)
|
||||
{
|
||||
if !entry.path.starts_with(&project_path.path) {
|
||||
break;
|
||||
}
|
||||
paths_tx
|
||||
.send(ProjectPath {
|
||||
worktree_id: project_path.worktree_id,
|
||||
path: entry.path.clone(),
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
while let Some(path) = paths_rx.next().await {
|
||||
if let Ok(buffer) = project
|
||||
.update(cx, |project, cx| project.open_buffer(path, cx))?
|
||||
.await
|
||||
{
|
||||
action_log.update(cx, |action_log, cx| {
|
||||
action_log.will_delete_buffer(buffer.clone(), cx)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
let delete = project.update(cx, |project, cx| {
|
||||
project.delete_file(project_path, false, cx)
|
||||
})?;
|
||||
|
||||
match delete {
|
||||
Some(deletion_task) => match deletion_task.await {
|
||||
Ok(()) => Ok(format!("Deleted {path_str}")),
|
||||
Err(err) => Err(anyhow!("Failed to delete {path_str}: {err}")),
|
||||
}
|
||||
}),
|
||||
None => Task::ready(Err(anyhow!(
|
||||
"Couldn't delete {path_str} because that path isn't in this project."
|
||||
))),
|
||||
}
|
||||
},
|
||||
None => Err(anyhow!(
|
||||
"Couldn't delete {path_str} because that path isn't in this project."
|
||||
)),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool};
|
||||
use gpui::{App, Entity, Task};
|
||||
use language::{DiagnosticSeverity, OffsetRangeExt};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -54,12 +55,11 @@ impl Tool for DiagnosticsTool {
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Warning
|
||||
IconName::XCircle
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
let schema = schemars::schema_for!(DiagnosticsToolInput);
|
||||
serde_json::to_value(&schema).unwrap()
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value {
|
||||
json_schema_for::<DiagnosticsToolInput>(format)
|
||||
}
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
|
||||
@@ -16,3 +16,5 @@ To get a project-wide diagnostic summary:
|
||||
</example>
|
||||
|
||||
IMPORTANT: When you're done making changes, you **MUST** get the **project** diagnostics (input: `{}`) at the end of your edits so you can fix any problems you might have introduced. **DO NOT** tell the user you're done before doing this!
|
||||
|
||||
You may only attempt to fix these up to 3 times. If you have tried 3 times to fix them, and there are still problems remaining, you must not continue trying to fix them, and must instead tell the user that there are problems remaining - and ask if the user would like you to attempt to solve them further.
|
||||
|
||||
@@ -2,12 +2,14 @@ mod edit_action;
|
||||
pub mod log;
|
||||
|
||||
use crate::replace::{replace_exact, replace_with_flexible_indent};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool};
|
||||
use collections::HashSet;
|
||||
use edit_action::{EditAction, EditActionParser};
|
||||
use futures::{channel::mpsc, SinkExt, StreamExt};
|
||||
use edit_action::{EditAction, EditActionParser, edit_model_prompt};
|
||||
use futures::{SinkExt, StreamExt, channel::mpsc};
|
||||
use gpui::{App, AppContext, AsyncApp, Entity, Task};
|
||||
use language_model::LanguageModelToolSchemaFormat;
|
||||
use language_model::{
|
||||
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, MessageContent, Role,
|
||||
};
|
||||
@@ -91,9 +93,8 @@ impl Tool for EditFilesTool {
|
||||
IconName::Pencil
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
let schema = schemars::schema_for!(EditFilesToolInput);
|
||||
serde_json::to_value(&schema).unwrap()
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value {
|
||||
json_schema_for::<EditFilesToolInput>(format)
|
||||
}
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
@@ -173,6 +174,7 @@ enum EditorResponse {
|
||||
struct AppliedAction {
|
||||
source: String,
|
||||
buffer: Entity<language::Buffer>,
|
||||
edit_ids: Vec<clock::Lamport>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -228,10 +230,7 @@ impl EditToolRequest {
|
||||
|
||||
messages.push(LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![
|
||||
include_str!("./edit_files_tool/edit_prompt.md").into(),
|
||||
input.edit_instructions.into(),
|
||||
],
|
||||
content: vec![edit_model_prompt().into(), input.edit_instructions.into()],
|
||||
cache: false,
|
||||
});
|
||||
|
||||
@@ -340,9 +339,18 @@ impl EditToolRequest {
|
||||
self.push_search_error(error);
|
||||
}
|
||||
DiffResult::Diff(diff) => {
|
||||
let _clock = buffer.update(cx, |buffer, cx| buffer.apply_diff(diff, cx))?;
|
||||
let edit_ids = buffer.update(cx, |buffer, cx| {
|
||||
buffer.finalize_last_transaction();
|
||||
buffer.apply_diff(diff, false, cx);
|
||||
let transaction = buffer.finalize_last_transaction();
|
||||
transaction.map_or(Vec::new(), |transaction| transaction.edit_ids.clone())
|
||||
})?;
|
||||
|
||||
self.push_applied_action(AppliedAction { source, buffer });
|
||||
self.push_applied_action(AppliedAction {
|
||||
source,
|
||||
buffer,
|
||||
edit_ids,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,7 +472,10 @@ impl EditToolRequest {
|
||||
let mut changed_buffers = HashSet::default();
|
||||
|
||||
for action in applied {
|
||||
changed_buffers.insert(action.buffer);
|
||||
changed_buffers.insert(action.buffer.clone());
|
||||
self.action_log.update(cx, |log, cx| {
|
||||
log.buffer_edited(action.buffer, action.edit_ids, cx)
|
||||
})?;
|
||||
write!(&mut output, "\n\n{}", action.source)?;
|
||||
}
|
||||
|
||||
@@ -474,10 +485,6 @@ impl EditToolRequest {
|
||||
.await?;
|
||||
}
|
||||
|
||||
self.action_log
|
||||
.update(cx, |log, cx| log.buffer_edited(changed_buffers.clone(), cx))
|
||||
.log_err();
|
||||
|
||||
if !search_errors.is_empty() {
|
||||
writeln!(
|
||||
&mut output,
|
||||
@@ -519,7 +526,8 @@ impl EditToolRequest {
|
||||
}
|
||||
}
|
||||
|
||||
write!(&mut output,
|
||||
write!(
|
||||
&mut output,
|
||||
"The SEARCH section must exactly match an existing block of lines including all white \
|
||||
space, comments, indentation, docstrings, etc."
|
||||
)?;
|
||||
@@ -538,7 +546,8 @@ impl EditToolRequest {
|
||||
}
|
||||
|
||||
if has_errors {
|
||||
writeln!(&mut output,
|
||||
writeln!(
|
||||
&mut output,
|
||||
"\n\nYou can fix errors by running the tool again. You can include instructions, \
|
||||
but errors are part of the conversation so you don't need to repeat them.",
|
||||
)?;
|
||||
|
||||
@@ -59,6 +59,20 @@ enum State {
|
||||
CloseFence,
|
||||
}
|
||||
|
||||
/// used to avoid having source code that looks like git-conflict markers
|
||||
macro_rules! marker_sym {
|
||||
($char:expr) => {
|
||||
concat!($char, $char, $char, $char, $char, $char, $char)
|
||||
};
|
||||
}
|
||||
|
||||
const SEARCH_MARKER: &str = concat!(marker_sym!('<'), " SEARCH");
|
||||
const DIVIDER: &str = marker_sym!('=');
|
||||
const NL_DIVIDER: &str = concat!("\n", marker_sym!('='));
|
||||
const REPLACE_MARKER: &str = concat!(marker_sym!('>'), " REPLACE");
|
||||
const NL_REPLACE_MARKER: &str = concat!("\n", marker_sym!('>'), " REPLACE");
|
||||
const FENCE: &str = "```";
|
||||
|
||||
impl EditActionParser {
|
||||
/// Creates a new `EditActionParser`
|
||||
pub fn new() -> Self {
|
||||
@@ -87,13 +101,6 @@ impl EditActionParser {
|
||||
pub fn parse_chunk(&mut self, input: &str) -> Vec<(EditAction, String)> {
|
||||
use State::*;
|
||||
|
||||
const FENCE: &[u8] = b"```";
|
||||
const SEARCH_MARKER: &[u8] = b"<<<<<<< SEARCH";
|
||||
const DIVIDER: &[u8] = b"=======";
|
||||
const NL_DIVIDER: &[u8] = b"\n=======";
|
||||
const REPLACE_MARKER: &[u8] = b">>>>>>> REPLACE";
|
||||
const NL_REPLACE_MARKER: &[u8] = b"\n>>>>>>> REPLACE";
|
||||
|
||||
let mut actions = Vec::new();
|
||||
|
||||
for byte in input.bytes() {
|
||||
@@ -227,7 +234,7 @@ impl EditActionParser {
|
||||
self.to_state(State::Default);
|
||||
}
|
||||
|
||||
fn expect_marker(&mut self, byte: u8, marker: &'static [u8], trailing_newline: bool) -> bool {
|
||||
fn expect_marker(&mut self, byte: u8, marker: &'static str, trailing_newline: bool) -> bool {
|
||||
match self.match_marker(byte, marker, trailing_newline) {
|
||||
MarkerMatch::Complete => true,
|
||||
MarkerMatch::Partial => false,
|
||||
@@ -245,7 +252,7 @@ impl EditActionParser {
|
||||
}
|
||||
}
|
||||
|
||||
fn extend_block_range(&mut self, byte: u8, marker: &[u8], nl_marker: &[u8]) -> bool {
|
||||
fn extend_block_range(&mut self, byte: u8, marker: &str, nl_marker: &str) -> bool {
|
||||
let marker = if self.block_range.is_empty() {
|
||||
// do not require another newline if block is empty
|
||||
marker
|
||||
@@ -287,7 +294,7 @@ impl EditActionParser {
|
||||
}
|
||||
}
|
||||
|
||||
fn match_marker(&mut self, byte: u8, marker: &[u8], trailing_newline: bool) -> MarkerMatch {
|
||||
fn match_marker(&mut self, byte: u8, marker: &str, trailing_newline: bool) -> MarkerMatch {
|
||||
if trailing_newline && self.marker_ix >= marker.len() {
|
||||
if byte == b'\n' {
|
||||
MarkerMatch::Complete
|
||||
@@ -296,7 +303,7 @@ impl EditActionParser {
|
||||
} else {
|
||||
MarkerMatch::None
|
||||
}
|
||||
} else if byte == marker[self.marker_ix] {
|
||||
} else if byte == marker.as_bytes()[self.marker_ix] {
|
||||
self.marker_ix += 1;
|
||||
|
||||
if self.marker_ix < marker.len() || trailing_newline {
|
||||
@@ -321,7 +328,7 @@ enum MarkerMatch {
|
||||
pub struct ParseError {
|
||||
line: usize,
|
||||
column: usize,
|
||||
expected: &'static [u8],
|
||||
expected: &'static str,
|
||||
found: u8,
|
||||
}
|
||||
|
||||
@@ -330,34 +337,45 @@ impl std::fmt::Display for ParseError {
|
||||
write!(
|
||||
f,
|
||||
"input:{}:{}: Expected marker {:?}, found {:?}",
|
||||
self.line,
|
||||
self.column,
|
||||
String::from_utf8_lossy(self.expected),
|
||||
self.found as char
|
||||
self.line, self.column, self.expected, self.found as char
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn edit_model_prompt() -> String {
|
||||
include_str!("edit_prompt.md")
|
||||
.to_string()
|
||||
.replace("{{SEARCH_MARKER}}", SEARCH_MARKER)
|
||||
.replace("{{DIVIDER}}", DIVIDER)
|
||||
.replace("{{REPLACE_MARKER}}", REPLACE_MARKER)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rand::prelude::*;
|
||||
use util::line_endings;
|
||||
|
||||
const WRONG_MARKER: &str = concat!(marker_sym!('<'), " WRONG_MARKER");
|
||||
|
||||
#[test]
|
||||
fn test_simple_edit_action() {
|
||||
let input = r#"src/main.rs
|
||||
// Construct test input using format with multiline string literals
|
||||
let input = format!(
|
||||
r#"src/main.rs
|
||||
```
|
||||
<<<<<<< SEARCH
|
||||
fn original() {}
|
||||
=======
|
||||
fn replacement() {}
|
||||
>>>>>>> REPLACE
|
||||
{}
|
||||
fn original() {{}}
|
||||
{}
|
||||
fn replacement() {{}}
|
||||
{}
|
||||
```
|
||||
"#;
|
||||
"#,
|
||||
SEARCH_MARKER, DIVIDER, REPLACE_MARKER
|
||||
);
|
||||
|
||||
let mut parser = EditActionParser::new();
|
||||
let actions = parser.parse_chunk(input);
|
||||
let actions = parser.parse_chunk(&input);
|
||||
|
||||
assert_no_errors(&parser);
|
||||
assert_eq!(actions.len(), 1);
|
||||
@@ -373,18 +391,22 @@ fn replacement() {}
|
||||
|
||||
#[test]
|
||||
fn test_with_language_tag() {
|
||||
let input = r#"src/main.rs
|
||||
// Construct test input using format with multiline string literals
|
||||
let input = format!(
|
||||
r#"src/main.rs
|
||||
```rust
|
||||
<<<<<<< SEARCH
|
||||
fn original() {}
|
||||
=======
|
||||
fn replacement() {}
|
||||
>>>>>>> REPLACE
|
||||
{}
|
||||
fn original() {{}}
|
||||
{}
|
||||
fn replacement() {{}}
|
||||
{}
|
||||
```
|
||||
"#;
|
||||
"#,
|
||||
SEARCH_MARKER, DIVIDER, REPLACE_MARKER
|
||||
);
|
||||
|
||||
let mut parser = EditActionParser::new();
|
||||
let actions = parser.parse_chunk(input);
|
||||
let actions = parser.parse_chunk(&input);
|
||||
|
||||
assert_no_errors(&parser);
|
||||
assert_eq!(actions.len(), 1);
|
||||
@@ -400,22 +422,26 @@ fn replacement() {}
|
||||
|
||||
#[test]
|
||||
fn test_with_surrounding_text() {
|
||||
let input = r#"Here's a modification I'd like to make to the file:
|
||||
// Construct test input using format with multiline string literals
|
||||
let input = format!(
|
||||
r#"Here's a modification I'd like to make to the file:
|
||||
|
||||
src/main.rs
|
||||
```rust
|
||||
<<<<<<< SEARCH
|
||||
fn original() {}
|
||||
=======
|
||||
fn replacement() {}
|
||||
>>>>>>> REPLACE
|
||||
{}
|
||||
fn original() {{}}
|
||||
{}
|
||||
fn replacement() {{}}
|
||||
{}
|
||||
```
|
||||
|
||||
This change makes the function better.
|
||||
"#;
|
||||
"#,
|
||||
SEARCH_MARKER, DIVIDER, REPLACE_MARKER
|
||||
);
|
||||
|
||||
let mut parser = EditActionParser::new();
|
||||
let actions = parser.parse_chunk(input);
|
||||
let actions = parser.parse_chunk(&input);
|
||||
|
||||
assert_no_errors(&parser);
|
||||
assert_eq!(actions.len(), 1);
|
||||
@@ -431,29 +457,33 @@ This change makes the function better.
|
||||
|
||||
#[test]
|
||||
fn test_multiple_edit_actions() {
|
||||
let input = r#"First change:
|
||||
// Construct test input using format with multiline string literals
|
||||
let input = format!(
|
||||
r#"First change:
|
||||
src/main.rs
|
||||
```
|
||||
<<<<<<< SEARCH
|
||||
fn original() {}
|
||||
=======
|
||||
fn replacement() {}
|
||||
>>>>>>> REPLACE
|
||||
{}
|
||||
fn original() {{}}
|
||||
{}
|
||||
fn replacement() {{}}
|
||||
{}
|
||||
```
|
||||
|
||||
Second change:
|
||||
src/utils.rs
|
||||
```rust
|
||||
<<<<<<< SEARCH
|
||||
fn old_util() -> bool { false }
|
||||
=======
|
||||
fn new_util() -> bool { true }
|
||||
>>>>>>> REPLACE
|
||||
{}
|
||||
fn old_util() -> bool {{ false }}
|
||||
{}
|
||||
fn new_util() -> bool {{ true }}
|
||||
{}
|
||||
```
|
||||
"#;
|
||||
"#,
|
||||
SEARCH_MARKER, DIVIDER, REPLACE_MARKER, SEARCH_MARKER, DIVIDER, REPLACE_MARKER
|
||||
);
|
||||
|
||||
let mut parser = EditActionParser::new();
|
||||
let actions = parser.parse_chunk(input);
|
||||
let actions = parser.parse_chunk(&input);
|
||||
|
||||
assert_no_errors(&parser);
|
||||
assert_eq!(actions.len(), 2);
|
||||
@@ -480,32 +510,36 @@ fn new_util() -> bool { true }
|
||||
|
||||
#[test]
|
||||
fn test_multiline() {
|
||||
let input = r#"src/main.rs
|
||||
// Construct test input using format with multiline string literals
|
||||
let input = format!(
|
||||
r#"src/main.rs
|
||||
```rust
|
||||
<<<<<<< SEARCH
|
||||
fn original() {
|
||||
{}
|
||||
fn original() {{
|
||||
println!("This is the original function");
|
||||
let x = 42;
|
||||
if x > 0 {
|
||||
if x > 0 {{
|
||||
println!("Positive number");
|
||||
}
|
||||
}
|
||||
=======
|
||||
fn replacement() {
|
||||
}}
|
||||
}}
|
||||
{}
|
||||
fn replacement() {{
|
||||
println!("This is the replacement function");
|
||||
let x = 100;
|
||||
if x > 50 {
|
||||
if x > 50 {{
|
||||
println!("Large number");
|
||||
} else {
|
||||
}} else {{
|
||||
println!("Small number");
|
||||
}
|
||||
}
|
||||
>>>>>>> REPLACE
|
||||
}}
|
||||
}}
|
||||
{}
|
||||
```
|
||||
"#;
|
||||
"#,
|
||||
SEARCH_MARKER, DIVIDER, REPLACE_MARKER
|
||||
);
|
||||
|
||||
let mut parser = EditActionParser::new();
|
||||
let actions = parser.parse_chunk(input);
|
||||
let actions = parser.parse_chunk(&input);
|
||||
|
||||
assert_no_errors(&parser);
|
||||
assert_eq!(actions.len(), 1);
|
||||
@@ -523,21 +557,25 @@ fn replacement() {
|
||||
|
||||
#[test]
|
||||
fn test_write_action() {
|
||||
let input = r#"Create a new main.rs file:
|
||||
// Construct test input using format with multiline string literals
|
||||
let input = format!(
|
||||
r#"Create a new main.rs file:
|
||||
|
||||
src/main.rs
|
||||
```rust
|
||||
<<<<<<< SEARCH
|
||||
=======
|
||||
fn new_function() {
|
||||
{}
|
||||
{}
|
||||
fn new_function() {{
|
||||
println!("This function is being added");
|
||||
}
|
||||
>>>>>>> REPLACE
|
||||
}}
|
||||
{}
|
||||
```
|
||||
"#;
|
||||
"#,
|
||||
SEARCH_MARKER, DIVIDER, REPLACE_MARKER
|
||||
);
|
||||
|
||||
let mut parser = EditActionParser::new();
|
||||
let actions = parser.parse_chunk(input);
|
||||
let actions = parser.parse_chunk(&input);
|
||||
|
||||
assert_no_errors(&parser);
|
||||
assert_eq!(actions.len(), 1);
|
||||
@@ -553,16 +591,20 @@ fn new_function() {
|
||||
|
||||
#[test]
|
||||
fn test_empty_replace() {
|
||||
let input = r#"src/main.rs
|
||||
// Construct test input using format with multiline string literals
|
||||
let input = format!(
|
||||
r#"src/main.rs
|
||||
```rust
|
||||
<<<<<<< SEARCH
|
||||
fn this_will_be_deleted() {
|
||||
{}
|
||||
fn this_will_be_deleted() {{
|
||||
println!("Deleting this function");
|
||||
}
|
||||
=======
|
||||
>>>>>>> REPLACE
|
||||
}}
|
||||
{}
|
||||
{}
|
||||
```
|
||||
"#;
|
||||
"#,
|
||||
SEARCH_MARKER, DIVIDER, REPLACE_MARKER
|
||||
);
|
||||
|
||||
let mut parser = EditActionParser::new();
|
||||
let actions = parser.parse_chunk(&input);
|
||||
@@ -597,16 +639,20 @@ fn this_will_be_deleted() {
|
||||
|
||||
#[test]
|
||||
fn test_empty_both() {
|
||||
let input = r#"src/main.rs
|
||||
// Construct test input using format with multiline string literals
|
||||
let input = format!(
|
||||
r#"src/main.rs
|
||||
```rust
|
||||
<<<<<<< SEARCH
|
||||
=======
|
||||
>>>>>>> REPLACE
|
||||
{}
|
||||
{}
|
||||
{}
|
||||
```
|
||||
"#;
|
||||
"#,
|
||||
SEARCH_MARKER, DIVIDER, REPLACE_MARKER
|
||||
);
|
||||
|
||||
let mut parser = EditActionParser::new();
|
||||
let actions = parser.parse_chunk(input);
|
||||
let actions = parser.parse_chunk(&input);
|
||||
|
||||
assert_eq!(actions.len(), 1);
|
||||
assert_eq!(
|
||||
@@ -621,31 +667,24 @@ fn this_will_be_deleted() {
|
||||
|
||||
#[test]
|
||||
fn test_resumability() {
|
||||
let input_part1 = r#"src/main.rs
|
||||
```rust
|
||||
<<<<<<< SEARCH
|
||||
fn ori"#;
|
||||
// Construct test input using format with multiline string literals
|
||||
let input_part1 = format!("src/main.rs\n```rust\n{}\nfn ori", SEARCH_MARKER);
|
||||
|
||||
let input_part2 = r#"ginal() {}
|
||||
=======
|
||||
fn replacement() {}"#;
|
||||
let input_part2 = format!("ginal() {{}}\n{}\nfn replacement() {{}}", DIVIDER);
|
||||
|
||||
let input_part3 = r#"
|
||||
>>>>>>> REPLACE
|
||||
```
|
||||
"#;
|
||||
let input_part3 = format!("\n{}\n```\n", REPLACE_MARKER);
|
||||
|
||||
let mut parser = EditActionParser::new();
|
||||
let actions1 = parser.parse_chunk(input_part1);
|
||||
let actions1 = parser.parse_chunk(&input_part1);
|
||||
assert_no_errors(&parser);
|
||||
assert_eq!(actions1.len(), 0);
|
||||
|
||||
let actions2 = parser.parse_chunk(input_part2);
|
||||
let actions2 = parser.parse_chunk(&input_part2);
|
||||
// No actions should be complete yet
|
||||
assert_no_errors(&parser);
|
||||
assert_eq!(actions2.len(), 0);
|
||||
|
||||
let actions3 = parser.parse_chunk(input_part3);
|
||||
let actions3 = parser.parse_chunk(&input_part3);
|
||||
// The third chunk should complete the action
|
||||
assert_no_errors(&parser);
|
||||
assert_eq!(actions3.len(), 1);
|
||||
@@ -663,18 +702,17 @@ fn replacement() {}"#;
|
||||
#[test]
|
||||
fn test_parser_state_preservation() {
|
||||
let mut parser = EditActionParser::new();
|
||||
let actions1 = parser.parse_chunk("src/main.rs\n```rust\n<<<<<<< SEARCH\n");
|
||||
let first_chunk = format!("src/main.rs\n```rust\n{}\n", SEARCH_MARKER);
|
||||
let actions1 = parser.parse_chunk(&first_chunk);
|
||||
|
||||
// Check parser is in the correct state
|
||||
assert_no_errors(&parser);
|
||||
assert_eq!(parser.state, State::SearchBlock);
|
||||
assert_eq!(
|
||||
parser.action_source,
|
||||
b"src/main.rs\n```rust\n<<<<<<< SEARCH\n"
|
||||
);
|
||||
assert_eq!(parser.action_source, first_chunk.as_bytes());
|
||||
|
||||
// Continue parsing
|
||||
let actions2 = parser.parse_chunk("original code\n=======\n");
|
||||
let second_chunk = format!("original code\n{}\n", DIVIDER);
|
||||
let actions2 = parser.parse_chunk(&second_chunk);
|
||||
|
||||
assert_no_errors(&parser);
|
||||
assert_eq!(parser.state, State::ReplaceBlock);
|
||||
@@ -683,7 +721,8 @@ fn replacement() {}"#;
|
||||
b"original code"
|
||||
);
|
||||
|
||||
let actions3 = parser.parse_chunk("replacement code\n>>>>>>> REPLACE\n```\n");
|
||||
let third_chunk = format!("replacement code\n{}\n```\n", REPLACE_MARKER);
|
||||
let actions3 = parser.parse_chunk(&third_chunk);
|
||||
|
||||
// After complete parsing, state should reset
|
||||
assert_no_errors(&parser);
|
||||
@@ -699,18 +738,21 @@ fn replacement() {}"#;
|
||||
|
||||
#[test]
|
||||
fn test_invalid_search_marker() {
|
||||
let input = r#"src/main.rs
|
||||
let input = format!(
|
||||
r#"src/main.rs
|
||||
```rust
|
||||
<<<<<<< WRONG_MARKER
|
||||
fn original() {}
|
||||
=======
|
||||
fn replacement() {}
|
||||
>>>>>>> REPLACE
|
||||
{}
|
||||
fn original() {{}}
|
||||
{}
|
||||
fn replacement() {{}}
|
||||
{}
|
||||
```
|
||||
"#;
|
||||
"#,
|
||||
WRONG_MARKER, DIVIDER, REPLACE_MARKER
|
||||
);
|
||||
|
||||
let mut parser = EditActionParser::new();
|
||||
let actions = parser.parse_chunk(input);
|
||||
let actions = parser.parse_chunk(&input);
|
||||
assert_eq!(actions.len(), 0);
|
||||
|
||||
assert_eq!(parser.errors().len(), 1);
|
||||
@@ -718,33 +760,40 @@ fn replacement() {}
|
||||
|
||||
assert_eq!(
|
||||
error.to_string(),
|
||||
"input:3:9: Expected marker \"<<<<<<< SEARCH\", found 'W'"
|
||||
format!(
|
||||
"input:3:9: Expected marker \"{}\", found 'W'",
|
||||
SEARCH_MARKER
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_missing_closing_fence() {
|
||||
let input = r#"src/main.rs
|
||||
// Construct test input using format with multiline string literals
|
||||
let input = format!(
|
||||
r#"src/main.rs
|
||||
```rust
|
||||
<<<<<<< SEARCH
|
||||
fn original() {}
|
||||
=======
|
||||
fn replacement() {}
|
||||
>>>>>>> REPLACE
|
||||
{}
|
||||
fn original() {{}}
|
||||
{}
|
||||
fn replacement() {{}}
|
||||
{}
|
||||
<!-- Missing closing fence -->
|
||||
|
||||
src/utils.rs
|
||||
```rust
|
||||
<<<<<<< SEARCH
|
||||
fn utils_func() {}
|
||||
=======
|
||||
fn new_utils_func() {}
|
||||
>>>>>>> REPLACE
|
||||
{}
|
||||
fn utils_func() {{}}
|
||||
{}
|
||||
fn new_utils_func() {{}}
|
||||
{}
|
||||
```
|
||||
"#;
|
||||
"#,
|
||||
SEARCH_MARKER, DIVIDER, REPLACE_MARKER, SEARCH_MARKER, DIVIDER, REPLACE_MARKER
|
||||
);
|
||||
|
||||
let mut parser = EditActionParser::new();
|
||||
let actions = parser.parse_chunk(input);
|
||||
let actions = parser.parse_chunk(&input);
|
||||
|
||||
// Only the second block should be parsed
|
||||
assert_eq!(actions.len(), 1);
|
||||
@@ -767,19 +816,17 @@ fn new_utils_func() {}
|
||||
assert_eq!(parser.state, State::Default);
|
||||
}
|
||||
|
||||
const SYSTEM_PROMPT: &str = include_str!("./edit_prompt.md");
|
||||
|
||||
#[test]
|
||||
fn test_parse_examples_in_system_prompt() {
|
||||
fn test_parse_examples_in_edit_prompt() {
|
||||
let mut parser = EditActionParser::new();
|
||||
let actions = parser.parse_chunk(SYSTEM_PROMPT);
|
||||
assert_examples_in_system_prompt(&actions, parser.errors());
|
||||
let actions = parser.parse_chunk(&edit_model_prompt());
|
||||
assert_examples_in_edit_prompt(&actions, parser.errors());
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
fn test_random_chunking_of_system_prompt(mut rng: StdRng) {
|
||||
fn test_random_chunking_of_edit_prompt(mut rng: StdRng) {
|
||||
let mut parser = EditActionParser::new();
|
||||
let mut remaining = SYSTEM_PROMPT;
|
||||
let mut remaining: &str = &edit_model_prompt();
|
||||
let mut actions = Vec::with_capacity(5);
|
||||
|
||||
while !remaining.is_empty() {
|
||||
@@ -792,10 +839,10 @@ fn new_utils_func() {}
|
||||
remaining = rest;
|
||||
}
|
||||
|
||||
assert_examples_in_system_prompt(&actions, parser.errors());
|
||||
assert_examples_in_edit_prompt(&actions, parser.errors());
|
||||
}
|
||||
|
||||
fn assert_examples_in_system_prompt(actions: &[(EditAction, String)], errors: &[ParseError]) {
|
||||
fn assert_examples_in_edit_prompt(actions: &[(EditAction, String)], errors: &[ParseError]) {
|
||||
assert_eq!(actions.len(), 5);
|
||||
|
||||
assert_eq!(
|
||||
@@ -851,38 +898,53 @@ fn new_utils_func() {}
|
||||
// The system prompt includes some text that would produce errors
|
||||
assert_eq!(
|
||||
errors[0].to_string(),
|
||||
"input:102:1: Expected marker \"<<<<<<< SEARCH\", found '3'"
|
||||
format!(
|
||||
"input:102:1: Expected marker \"{}\", found '3'",
|
||||
SEARCH_MARKER
|
||||
)
|
||||
);
|
||||
#[cfg(not(windows))]
|
||||
assert_eq!(
|
||||
errors[1].to_string(),
|
||||
"input:109:0: Expected marker \"<<<<<<< SEARCH\", found '\\n'"
|
||||
format!(
|
||||
"input:109:0: Expected marker \"{}\", found '\\n'",
|
||||
SEARCH_MARKER
|
||||
)
|
||||
);
|
||||
#[cfg(windows)]
|
||||
assert_eq!(
|
||||
errors[1].to_string(),
|
||||
"input:108:1: Expected marker \"<<<<<<< SEARCH\", found '\\r'"
|
||||
format!(
|
||||
"input:108:1: Expected marker \"{}\", found '\\r'",
|
||||
SEARCH_MARKER
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_print_error() {
|
||||
let input = r#"src/main.rs
|
||||
let input = format!(
|
||||
r#"src/main.rs
|
||||
```rust
|
||||
<<<<<<< WRONG_MARKER
|
||||
fn original() {}
|
||||
=======
|
||||
fn replacement() {}
|
||||
>>>>>>> REPLACE
|
||||
{}
|
||||
fn original() {{}}
|
||||
{}
|
||||
fn replacement() {{}}
|
||||
{}
|
||||
```
|
||||
"#;
|
||||
"#,
|
||||
WRONG_MARKER, DIVIDER, REPLACE_MARKER
|
||||
);
|
||||
|
||||
let mut parser = EditActionParser::new();
|
||||
parser.parse_chunk(input);
|
||||
parser.parse_chunk(&input);
|
||||
|
||||
assert_eq!(parser.errors().len(), 1);
|
||||
let error = &parser.errors()[0];
|
||||
let expected_error = r#"input:3:9: Expected marker "<<<<<<< SEARCH", found 'W'"#;
|
||||
let expected_error = format!(
|
||||
r#"input:3:9: Expected marker "{}", found 'W'"#,
|
||||
SEARCH_MARKER
|
||||
);
|
||||
|
||||
assert_eq!(format!("{}", error), expected_error);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user