Compare commits
527 Commits
ask-profil
...
sqlite-ove
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef60a60e11 | ||
|
|
5d2ac968d8 | ||
|
|
e9d4b8766f | ||
|
|
6812872d1a | ||
|
|
2aebeb067c | ||
|
|
7dfd5d1963 | ||
|
|
a677b891a1 | ||
|
|
a2cb480244 | ||
|
|
506beafe10 | ||
|
|
31d908fc74 | ||
|
|
0731097ee5 | ||
|
|
233b73b385 | ||
|
|
0145e2c101 | ||
|
|
09fc64e0c5 | ||
|
|
fc803ce9d4 | ||
|
|
697c2ba71f | ||
|
|
f54c057001 | ||
|
|
32848e9c8a | ||
|
|
86b75759d1 | ||
|
|
94c006236e | ||
|
|
5b6b911946 | ||
|
|
b9a5d437db | ||
|
|
21bd91a773 | ||
|
|
5db14d315b | ||
|
|
b63cea1f17 | ||
|
|
b7c5540075 | ||
|
|
b01f7c848b | ||
|
|
3476705bbb | ||
|
|
ba6b5a59f9 | ||
|
|
28d6362964 | ||
|
|
b4a03989b1 | ||
|
|
19b6892c8d | ||
|
|
b5c2b25a76 | ||
|
|
8faeb34367 | ||
|
|
61a40e293d | ||
|
|
239ffa49e1 | ||
|
|
a4978ee5ff | ||
|
|
a8ca7e9c04 | ||
|
|
ee6ce78fed | ||
|
|
3ff62ef289 | ||
|
|
f8f36d0c17 | ||
|
|
05763b2fe3 | ||
|
|
7ec61ceec9 | ||
|
|
119beb210a | ||
|
|
0d3fad7764 | ||
|
|
450a10facf | ||
|
|
c208532693 | ||
|
|
4a577fff4a | ||
|
|
03071a9152 | ||
|
|
092be31b2b | ||
|
|
62545b985f | ||
|
|
5e72c2a870 | ||
|
|
2a8242ac90 | ||
|
|
d211f88d23 | ||
|
|
fe0bcd14d2 | ||
|
|
e84463648a | ||
|
|
24809c4219 | ||
|
|
f8365c5375 | ||
|
|
2c8049270a | ||
|
|
6840a4e5bc | ||
|
|
5b320d6714 | ||
|
|
534bb0620d | ||
|
|
5bafb2b160 | ||
|
|
ee415de45f | ||
|
|
4acb4730a5 | ||
|
|
4567360fd9 | ||
|
|
4c396bcc91 | ||
|
|
8a24f9f280 | ||
|
|
f4b361f04d | ||
|
|
649072d140 | ||
|
|
6253b95f82 | ||
|
|
bffde7c6b4 | ||
|
|
7e87916642 | ||
|
|
29f0762b6c | ||
|
|
10af3c7e58 | ||
|
|
c0aa8f63fd | ||
|
|
0c27aaecb3 | ||
|
|
8e5d50b85b | ||
|
|
625bf09830 | ||
|
|
5a0a8ce30a | ||
|
|
d9a5dc2dfe | ||
|
|
d4926626d8 | ||
|
|
2a973109d4 | ||
|
|
2e62f16149 | ||
|
|
f2601ce52c | ||
|
|
a58c48f629 | ||
|
|
ddbcab2b5b | ||
|
|
7497deff7a | ||
|
|
e78b726ed8 | ||
|
|
998542b048 | ||
|
|
6363fdab88 | ||
|
|
e6f51966a1 | ||
|
|
9da9ef860b | ||
|
|
134463f043 | ||
|
|
a47fd1d723 | ||
|
|
ef0e1cb2ba | ||
|
|
c73af0a52f | ||
|
|
e42cf21703 | ||
|
|
2c114f7df6 | ||
|
|
49f3ec7f35 | ||
|
|
748840519c | ||
|
|
8b59776320 | ||
|
|
206be2b348 | ||
|
|
51b25b5c22 | ||
|
|
2f274b2a89 | ||
|
|
88fb623efa | ||
|
|
df98d94a24 | ||
|
|
c7da6283cc | ||
|
|
7ceb792a58 | ||
|
|
83af7b30eb | ||
|
|
3d0147aafc | ||
|
|
1b3f20bdf4 | ||
|
|
4c28d2c2e2 | ||
|
|
a204510cfc | ||
|
|
34be7830a3 | ||
|
|
d312a13f8a | ||
|
|
20a0956fb2 | ||
|
|
6f918ed99b | ||
|
|
7fb9569c15 | ||
|
|
fc8702a8f8 | ||
|
|
ab59982bf7 | ||
|
|
685933b5c8 | ||
|
|
172e0df2d8 | ||
|
|
7341ab3980 | ||
|
|
ca72efe701 | ||
|
|
cb112a4012 | ||
|
|
f3c2e71ca7 | ||
|
|
208f525a11 | ||
|
|
697c838455 | ||
|
|
1683e2f144 | ||
|
|
2f1d9284b7 | ||
|
|
68a46c3627 | ||
|
|
3a1053bf0c | ||
|
|
14d9a4189f | ||
|
|
9c01119b3c | ||
|
|
9dba8e5b0d | ||
|
|
03ac3fb91a | ||
|
|
0201d1e0b4 | ||
|
|
f48b6b583e | ||
|
|
d8fc23a5e9 | ||
|
|
6206150e27 | ||
|
|
e88cad29e5 | ||
|
|
9b7d849879 | ||
|
|
c4677c21a9 | ||
|
|
26318b5b6a | ||
|
|
4266f0da85 | ||
|
|
c50093d68c | ||
|
|
1cad1cbbfc | ||
|
|
fbc922ad46 | ||
|
|
f435304209 | ||
|
|
508ccde363 | ||
|
|
9f7987c532 | ||
|
|
cb52acbf3d | ||
|
|
f8b997b25c | ||
|
|
73a5856fb8 | ||
|
|
e3b6fa2c30 | ||
|
|
ceb5164114 | ||
|
|
24a108d876 | ||
|
|
5c0b161563 | ||
|
|
ad4645c59b | ||
|
|
37047a6fde | ||
|
|
fc78408ee4 | ||
|
|
37f49ce304 | ||
|
|
cc428330a9 | ||
|
|
1475ace6f1 | ||
|
|
dd4e8b9e66 | ||
|
|
b188e5d3aa | ||
|
|
e3d3daec92 | ||
|
|
ced8e4d88e | ||
|
|
fa1abd8201 | ||
|
|
ee4e43f1b6 | ||
|
|
d61e1e24a7 | ||
|
|
3c03d53e3e | ||
|
|
8ab664a52c | ||
|
|
2044426634 | ||
|
|
02fa6f6fc2 | ||
|
|
80a00cd241 | ||
|
|
06f725d51b | ||
|
|
baf6d82cd4 | ||
|
|
28ec7fbb81 | ||
|
|
0415e853d5 | ||
|
|
1c9b818342 | ||
|
|
0d7f4842f3 | ||
|
|
ab017129d8 | ||
|
|
71fb17c507 | ||
|
|
97e437c632 | ||
|
|
66667d1eef | ||
|
|
dce22a965e | ||
|
|
5f452dbca2 | ||
|
|
b2a92097ee | ||
|
|
eb35d25a7d | ||
|
|
8742d4ab90 | ||
|
|
b829f72c17 | ||
|
|
ffa8310d04 | ||
|
|
3fda539c46 | ||
|
|
b444b326cb | ||
|
|
f196288e2d | ||
|
|
e30cc131b4 | ||
|
|
09c8a84935 | ||
|
|
6e5996a815 | ||
|
|
c8f56e38b1 | ||
|
|
cfd3b0ff7b | ||
|
|
afe23cf85a | ||
|
|
f915c24279 | ||
|
|
bdd9e015ab | ||
|
|
6bbab4b55a | ||
|
|
7450b788f3 | ||
|
|
0c03519393 | ||
|
|
636eff2e9a | ||
|
|
6c8f4002d9 | ||
|
|
91bc5aefa4 | ||
|
|
2f3564b85f | ||
|
|
d61a544400 | ||
|
|
8061bacee3 | ||
|
|
77dadfedfe | ||
|
|
0023b37bfc | ||
|
|
4ece4a635f | ||
|
|
77c2aecf93 | ||
|
|
3ee56c196c | ||
|
|
3b1f6eaab8 | ||
|
|
44fbe27d31 | ||
|
|
a824119367 | ||
|
|
16366cf9f2 | ||
|
|
1e51a7ac44 | ||
|
|
d547a86e31 | ||
|
|
4bb04cef9d | ||
|
|
89700c3682 | ||
|
|
7609402200 | ||
|
|
a0ec9cf383 | ||
|
|
eb318c1626 | ||
|
|
5e5a124ae1 | ||
|
|
65e751ca33 | ||
|
|
17cf04558b | ||
|
|
36ae564b61 | ||
|
|
110195cdae | ||
|
|
b7d5e6480a | ||
|
|
0fa9f05313 | ||
|
|
051f49ce9a | ||
|
|
e5670ba081 | ||
|
|
e4262f97af | ||
|
|
944a0df436 | ||
|
|
a1be61949d | ||
|
|
a092e2dc03 | ||
|
|
b1c7fa1dac | ||
|
|
df66237428 | ||
|
|
ca513f52bf | ||
|
|
e9c9a8a269 | ||
|
|
315321bf8c | ||
|
|
c747a57b7e | ||
|
|
f73c8e5841 | ||
|
|
f7a0834f54 | ||
|
|
83d513aef4 | ||
|
|
b440e1a467 | ||
|
|
5c4f9e57d8 | ||
|
|
05f8001ee9 | ||
|
|
b93c67438c | ||
|
|
fdec966226 | ||
|
|
9041f734fd | ||
|
|
844c7ad22e | ||
|
|
926f377c6c | ||
|
|
26a8cac0d8 | ||
|
|
c7aae6bd62 | ||
|
|
851121ffd4 | ||
|
|
e48daa92c0 | ||
|
|
d9f12879e2 | ||
|
|
42dd511fc2 | ||
|
|
571c5e7407 | ||
|
|
c76295251b | ||
|
|
b057b4697f | ||
|
|
57424e4743 | ||
|
|
2b6dab9197 | ||
|
|
70b0c4d63d | ||
|
|
875d1ef263 | ||
|
|
e1a2e8a3aa | ||
|
|
a829281841 | ||
|
|
592568ff87 | ||
|
|
83afe56a61 | ||
|
|
e468f9d2da | ||
|
|
1ce2652a89 | ||
|
|
784d51c40f | ||
|
|
0079c99c2c | ||
|
|
230eb12f72 | ||
|
|
dd3956eaf1 | ||
|
|
122d6c9e4d | ||
|
|
19e89a8b2d | ||
|
|
919ffe7655 | ||
|
|
841a4e35ea | ||
|
|
175ce05fd1 | ||
|
|
e518941445 | ||
|
|
10b8174c1b | ||
|
|
21fd1c8b80 | ||
|
|
c80bd698f8 | ||
|
|
03419da6f1 | ||
|
|
f56960ab5b | ||
|
|
4d827924f0 | ||
|
|
25b4591539 | ||
|
|
afbf527aa2 | ||
|
|
eb9ea20313 | ||
|
|
3d2ab4e58c | ||
|
|
ff0060aa36 | ||
|
|
d791c6cdb1 | ||
|
|
c7725e31d9 | ||
|
|
e26620d1cf | ||
|
|
9dabf491f0 | ||
|
|
f2dcc98216 | ||
|
|
23bbfc4b94 | ||
|
|
98aefcca83 | ||
|
|
9be1e9aab1 | ||
|
|
33b60bc16d | ||
|
|
0355b9dfab | ||
|
|
6bec76cd5d | ||
|
|
d4f47aa653 | ||
|
|
5112fcebeb | ||
|
|
dcf7f714f7 | ||
|
|
16f668b8e3 | ||
|
|
0f4e52bde8 | ||
|
|
dfe37b0a07 | ||
|
|
2da37988b5 | ||
|
|
05955e4faa | ||
|
|
1d043b37fb | ||
|
|
18d39e3f81 | ||
|
|
cc3a28a8e8 | ||
|
|
0f17e82154 | ||
|
|
a316428686 | ||
|
|
355266988d | ||
|
|
72007c9a62 | ||
|
|
c2feffac9d | ||
|
|
4b7b5db58c | ||
|
|
58ba833792 | ||
|
|
f021b401f4 | ||
|
|
47f6d4e5a7 | ||
|
|
e60f029525 | ||
|
|
d7b5c61ec8 | ||
|
|
23d42e3eaf | ||
|
|
b2fc4064c0 | ||
|
|
bba3db9378 | ||
|
|
5078f0b5ef | ||
|
|
607bfd3b1c | ||
|
|
87cb498a41 | ||
|
|
6420df3975 | ||
|
|
83498ebf2b | ||
|
|
1fb1fecb0a | ||
|
|
bc99a86bb7 | ||
|
|
fcfe4e2c14 | ||
|
|
ef511976be | ||
|
|
c80aaca0c5 | ||
|
|
234d6ce5f5 | ||
|
|
96a0568fb7 | ||
|
|
b6828e5ce8 | ||
|
|
78d3ce4090 | ||
|
|
d01559f9bc | ||
|
|
645f662853 | ||
|
|
d42cb111f4 | ||
|
|
dce6e96c16 | ||
|
|
4280bff10a | ||
|
|
ea5b289459 | ||
|
|
09503333af | ||
|
|
775370fd7d | ||
|
|
1077f2771e | ||
|
|
f4eea0db2e | ||
|
|
ed361ff6a2 | ||
|
|
7f9a365d8f | ||
|
|
255d8f7cf8 | ||
|
|
22f76ac1a7 | ||
|
|
25cc05b45c | ||
|
|
a4766e296f | ||
|
|
2f26a860a9 | ||
|
|
f1fe505649 | ||
|
|
9826b7b5c1 | ||
|
|
6fc9036063 | ||
|
|
2b74163a48 | ||
|
|
71ea7aee3b | ||
|
|
48b376fdc9 | ||
|
|
f98c6fb2cf | ||
|
|
1ace5a27bc | ||
|
|
dd6594621f | ||
|
|
68afe4fdda | ||
|
|
6f297132b4 | ||
|
|
8fe134e361 | ||
|
|
7aabbb0426 | ||
|
|
85c6a3dd0c | ||
|
|
81dcc12c62 | ||
|
|
1fd8fbe6d1 | ||
|
|
7eb226b3fc | ||
|
|
9426caa061 | ||
|
|
7cad943fde | ||
|
|
29da105dd5 | ||
|
|
8fdf309a4a | ||
|
|
f01af006e1 | ||
|
|
01488c4f91 | ||
|
|
18e911002f | ||
|
|
54c6d482b6 | ||
|
|
32c7fcd78c | ||
|
|
fff349a644 | ||
|
|
90c2d17042 | ||
|
|
c6e69fae17 | ||
|
|
e5d497ee08 | ||
|
|
229f3dab22 | ||
|
|
67f9da0846 | ||
|
|
ab455e1c43 | ||
|
|
986d271ea7 | ||
|
|
98a18e04f7 | ||
|
|
3ea86da16f | ||
|
|
3173f87dc3 | ||
|
|
6592314984 | ||
|
|
93b6fdb8e5 | ||
|
|
e79d1b27b1 | ||
|
|
1a0eedb787 | ||
|
|
8db0333b04 | ||
|
|
a13c8b70dd | ||
|
|
ddc649bdb8 | ||
|
|
33c896c23d | ||
|
|
19b6c4444e | ||
|
|
8e39281699 | ||
|
|
8294981ab5 | ||
|
|
a3105c92a4 | ||
|
|
a6c3d49bb9 | ||
|
|
5a38bbbd22 | ||
|
|
196586e352 | ||
|
|
a1d8e50ec1 | ||
|
|
24bc9fd0a0 | ||
|
|
03f02804e5 | ||
|
|
41b0a5cf10 | ||
|
|
739236e968 | ||
|
|
f14e48d202 | ||
|
|
634b275931 | ||
|
|
8000151aa9 | ||
|
|
f0f0a52793 | ||
|
|
907b2f0521 | ||
|
|
0ad582eec4 | ||
|
|
58ed81b698 | ||
|
|
83319c8a6d | ||
|
|
4deb8cce8d | ||
|
|
8d79226445 | ||
|
|
5abca0f867 | ||
|
|
68945ac53e | ||
|
|
49887d6934 | ||
|
|
d867897746 | ||
|
|
1f58ce80f2 | ||
|
|
ed772e6baf | ||
|
|
559725d8f5 | ||
|
|
f0da3b74f8 | ||
|
|
cee9f4b013 | ||
|
|
ae31aa2759 | ||
|
|
82a7aca5a6 | ||
|
|
b34f19a46f | ||
|
|
09ace088ac | ||
|
|
49ba4ed49c | ||
|
|
06af0310f7 | ||
|
|
1fa19c69a6 | ||
|
|
5ba1d3edec | ||
|
|
e4525b80f8 | ||
|
|
18a2a50227 | ||
|
|
172a475515 | ||
|
|
471e02d48f | ||
|
|
39da72161f | ||
|
|
daa777440d | ||
|
|
79ba22673b | ||
|
|
074e78301a | ||
|
|
fbeee1f832 | ||
|
|
bff259731f | ||
|
|
6c5b9b43c1 | ||
|
|
f29c6e5661 | ||
|
|
000077facf | ||
|
|
2b249f9e68 | ||
|
|
e13ecc07bc | ||
|
|
bef25c7290 | ||
|
|
65b13968a2 | ||
|
|
9afc6f6f5c | ||
|
|
82d271cb5b | ||
|
|
77ad6d7fbb | ||
|
|
d6ab416168 | ||
|
|
8f07135201 | ||
|
|
1dfddf0a29 | ||
|
|
cf8f003916 | ||
|
|
00292450e0 | ||
|
|
49c01c60b7 | ||
|
|
863d7ccb6d | ||
|
|
d270f6b953 | ||
|
|
08f516ce9a | ||
|
|
9cff5cfe3a | ||
|
|
0abee5668a | ||
|
|
c58b6903b8 | ||
|
|
11b6ce46e2 | ||
|
|
8c8357387e | ||
|
|
25ced2e3c2 | ||
|
|
f248da5921 | ||
|
|
89ce49d5b7 | ||
|
|
30f3efe697 | ||
|
|
023a60806a | ||
|
|
2c602bb0e5 | ||
|
|
857134d6dc | ||
|
|
d8980c25d2 | ||
|
|
299a0bcbaa | ||
|
|
cfb06bac8a | ||
|
|
b88ba44b32 | ||
|
|
851ab13f94 | ||
|
|
5249345312 | ||
|
|
1b593f616f | ||
|
|
ea7756b362 | ||
|
|
1ecd00a113 | ||
|
|
29c31f020e | ||
|
|
c512d43e8c | ||
|
|
05a6c31ad8 | ||
|
|
9810745465 | ||
|
|
4b61d4ba6a | ||
|
|
b4fbb9bc08 | ||
|
|
83378b856f | ||
|
|
648d0054de | ||
|
|
a8312d623d | ||
|
|
9e5d115e72 | ||
|
|
822580cb12 | ||
|
|
8b764a5477 | ||
|
|
660b4cee76 | ||
|
|
e74ae89c84 | ||
|
|
b0a6146362 | ||
|
|
77945fc905 | ||
|
|
6827bf114a | ||
|
|
2b6280ad56 | ||
|
|
f21780cef3 | ||
|
|
c64dc82e21 | ||
|
|
9268308543 | ||
|
|
7fb52ddf32 | ||
|
|
dc01aef0cf | ||
|
|
e9a756b5fc | ||
|
|
203cb7a7a2 | ||
|
|
93b88a905a |
@@ -13,12 +13,6 @@ rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
linker = "clang"
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
|
||||
[target.aarch64-apple-darwin]
|
||||
rustflags = ["-C", "link-args=-all_load"]
|
||||
|
||||
[target.x86_64-apple-darwin]
|
||||
rustflags = ["-C", "link-args=-all_load"]
|
||||
|
||||
[target.'cfg(target_os = "windows")']
|
||||
rustflags = [
|
||||
"--cfg",
|
||||
|
||||
@@ -30,3 +30,7 @@ ffdda588b41f7d9d270ffe76cab116f828ad545e
|
||||
# 2024-07-05 Improved formatting of default keymaps (single line per bind)
|
||||
# https://github.com/zed-industries/zed/pull/13887
|
||||
813cc3f5e537372fc86720b5e71b6e1c815440ab
|
||||
|
||||
# 2024-07-24 docs: Format docs
|
||||
# https://github.com/zed-industries/zed/pull/15352
|
||||
3a44a59f8ec114ac1ba22f7da1652717ef7e4e5c
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/01_bug_agent.yml
vendored
@@ -29,8 +29,8 @@ body:
|
||||
id: environment
|
||||
attributes:
|
||||
label: Zed Version and System Specs
|
||||
description: 'Open Zed, and in the command palette select "zed: Copy System Specs Into Clipboard"'
|
||||
description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"'
|
||||
placeholder: |
|
||||
Output of "zed: Copy System Specs Into Clipboard"
|
||||
Output of "zed: copy system specs into clipboard"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -29,8 +29,8 @@ body:
|
||||
id: environment
|
||||
attributes:
|
||||
label: Zed Version and System Specs
|
||||
description: 'Open Zed, and in the command palette select "zed: Copy System Specs Into Clipboard"'
|
||||
description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"'
|
||||
placeholder: |
|
||||
Output of "zed: Copy System Specs Into Clipboard"
|
||||
Output of "zed: copy system specs into clipboard"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/03_bug_git.yml
vendored
@@ -28,8 +28,8 @@ body:
|
||||
id: environment
|
||||
attributes:
|
||||
label: Zed Version and System Specs
|
||||
description: 'Open Zed, and in the command palette select "zed: Copy System Specs Into Clipboard"'
|
||||
description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"'
|
||||
placeholder: |
|
||||
Output of "zed: Copy System Specs Into Clipboard"
|
||||
Output of "zed: copy system specs into clipboard"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
35
.github/ISSUE_TEMPLATE/04_bug_debugger.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: Bug Report (Debugger)
|
||||
description: Zed Debugger-Related Bugs
|
||||
type: "Bug"
|
||||
labels: ["debugger"]
|
||||
title: "Debugger: <a short description of the Debugger bug>"
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Summary
|
||||
description: Describe the bug with a one line summary, and provide detailed reproduction steps
|
||||
value: |
|
||||
<!-- Please insert a one line summary of the issue below -->
|
||||
SUMMARY_SENTENCE_HERE
|
||||
|
||||
### Description
|
||||
<!-- Describe with sufficient detail to reproduce from a clean Zed install. -->
|
||||
Steps to trigger the problem:
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
Actual Behavior:
|
||||
Expected Behavior:
|
||||
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: environment
|
||||
attributes:
|
||||
label: Zed Version and System Specs
|
||||
description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"'
|
||||
placeholder: |
|
||||
Output of "zed: copy system specs into clipboard"
|
||||
validations:
|
||||
required: true
|
||||
4
.github/ISSUE_TEMPLATE/10_bug_report.yml
vendored
@@ -49,8 +49,8 @@ body:
|
||||
attributes:
|
||||
label: Zed Version and System Specs
|
||||
description: |
|
||||
Open Zed, from the command palette select "zed: Copy System Specs Into Clipboard"
|
||||
Open Zed, from the command palette select "zed: copy system specs into clipboard"
|
||||
placeholder: |
|
||||
Output of "zed: Copy System Specs Into Clipboard"
|
||||
Output of "zed: copy system specs into clipboard"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/11_crash_report.yml
vendored
@@ -26,9 +26,9 @@ body:
|
||||
id: environment
|
||||
attributes:
|
||||
label: Zed Version and System Specs
|
||||
description: 'Open Zed, and in the command palette select "zed: Copy System Specs Into Clipboard"'
|
||||
description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"'
|
||||
placeholder: |
|
||||
Output of "zed: Copy System Specs Into Clipboard"
|
||||
Output of "zed: copy system specs into clipboard"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
||||
50
.github/workflows/ci.yml
vendored
@@ -524,7 +524,6 @@ jobs:
|
||||
APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }}
|
||||
APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }}
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
steps:
|
||||
@@ -611,7 +610,6 @@ jobs:
|
||||
needs: [linux_tests]
|
||||
env:
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
steps:
|
||||
@@ -669,7 +667,6 @@ jobs:
|
||||
needs: [linux_tests]
|
||||
env:
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
steps:
|
||||
@@ -717,49 +714,12 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
nix-build:
|
||||
timeout-minutes: 60
|
||||
name: Nix Build
|
||||
continue-on-error: true
|
||||
uses: ./.github/workflows/nix.yml
|
||||
if: github.repository_owner == 'zed-industries' && contains(github.event.pull_request.labels.*.name, 'run-nix')
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
system:
|
||||
- os: x86 Linux
|
||||
runner: buildjet-16vcpu-ubuntu-2204
|
||||
install_nix: true
|
||||
- os: arm Mac
|
||||
runner: [macOS, ARM64, test]
|
||||
install_nix: false
|
||||
runs-on: ${{ matrix.system.runner }}
|
||||
env:
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
||||
GIT_LFS_SKIP_SMUDGE: 1 # breaks the livekit rust sdk examples which we don't actually depend on
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
clean: false
|
||||
- name: Set path
|
||||
if: ${{ ! matrix.system.install_nix }}
|
||||
run: |
|
||||
echo "/nix/var/nix/profiles/default/bin" >> $GITHUB_PATH
|
||||
echo "/Users/administrator/.nix-profile/bin" >> $GITHUB_PATH
|
||||
|
||||
- uses: cachix/install-nix-action@d1ca217b388ee87b2507a9a93bf01368bde7cec2 # v31
|
||||
if: ${{ matrix.system.install_nix }}
|
||||
with:
|
||||
github_access_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
with:
|
||||
name: zed-industries
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
skipPush: true
|
||||
- run: nix build .#debug
|
||||
- name: Limit /nix/store to 50GB
|
||||
run: "[ $(du -sm /nix/store | cut -f1) -gt 50000 ] && nix-collect-garbage -d"
|
||||
with:
|
||||
flake-output: debug
|
||||
# excludes the final package to only cache dependencies
|
||||
cachix-filter: "-zed-editor-[0-9.]*-nightly"
|
||||
|
||||
auto-release-preview:
|
||||
name: Auto release preview
|
||||
|
||||
65
.github/workflows/nix.yml
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
name: "Nix build"
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
flake-output:
|
||||
type: string
|
||||
default: "default"
|
||||
cachix-filter:
|
||||
type: string
|
||||
default: ""
|
||||
|
||||
jobs:
|
||||
nix-build:
|
||||
timeout-minutes: 60
|
||||
name: (${{ matrix.system.os }}) Nix Build
|
||||
continue-on-error: true # TODO: remove when we want this to start blocking CI
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
system:
|
||||
- os: x86 Linux
|
||||
runner: buildjet-16vcpu-ubuntu-2204
|
||||
install_nix: true
|
||||
- os: arm Mac
|
||||
runner: [macOS, ARM64, test]
|
||||
install_nix: false
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: ${{ matrix.system.runner }}
|
||||
env:
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
||||
GIT_LFS_SKIP_SMUDGE: 1 # breaks the livekit rust sdk examples which we don't actually depend on
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
# on our macs we manually install nix. for some reason the cachix action is running
|
||||
# under a non-login /bin/bash shell which doesn't source the proper script to add the
|
||||
# nix profile to PATH, so we manually add them here
|
||||
- name: Set path
|
||||
if: ${{ ! matrix.system.install_nix }}
|
||||
run: |
|
||||
echo "/nix/var/nix/profiles/default/bin" >> $GITHUB_PATH
|
||||
echo "/Users/administrator/.nix-profile/bin" >> $GITHUB_PATH
|
||||
|
||||
- uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31
|
||||
if: ${{ matrix.system.install_nix }}
|
||||
with:
|
||||
github_access_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
with:
|
||||
name: zed
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
pushFilter: "${{ inputs.cachix-filter }}"
|
||||
|
||||
- run: nix build .#${{ inputs.flake-output }} -L --accept-flake-config
|
||||
|
||||
- name: Limit /nix/store to 50GB on macs
|
||||
if: ${{ ! matrix.system.install_nix }}
|
||||
run: |
|
||||
[ $(du -sm /nix/store | cut -f1) -gt 50000 ] && nix-collect-garbage -d || :
|
||||
7
.github/workflows/release_nightly.yml
vendored
@@ -68,7 +68,6 @@ jobs:
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
||||
steps:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
@@ -104,7 +103,6 @@ jobs:
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
@@ -144,7 +142,6 @@ jobs:
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
@@ -170,6 +167,10 @@ jobs:
|
||||
- name: Upload Zed Nightly
|
||||
run: script/upload-nightly linux-targz
|
||||
|
||||
bundle-nix:
|
||||
needs: tests
|
||||
uses: ./.github/workflows/nix.yml
|
||||
|
||||
update-nightly-tag:
|
||||
name: Update nightly tag
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
|
||||
1
.gitignore
vendored
@@ -2,6 +2,7 @@
|
||||
**/cargo-target
|
||||
**/target
|
||||
**/venv
|
||||
**/.direnv
|
||||
*.wasm
|
||||
*.xcodeproj
|
||||
.DS_Store
|
||||
|
||||
20
.mailmap
@@ -19,6 +19,8 @@ amtoaer <amtoaer@gmail.com>
|
||||
amtoaer <amtoaer@gmail.com> <amtoaer@outlook.com>
|
||||
Andrei Zvonimir Crnković <andrei@0x7f.dev>
|
||||
Andrei Zvonimir Crnković <andrei@0x7f.dev> <andreicek@0x7f.dev>
|
||||
Angelk90 <angelo.k90@hotmail.it>
|
||||
Angelk90 <angelo.k90@hotmail.it> <20476002+Angelk90@users.noreply.github.com>
|
||||
Antonio Scandurra <me@as-cii.com>
|
||||
Antonio Scandurra <me@as-cii.com> <antonio@zed.dev>
|
||||
Ben Kunkle <ben@zed.dev>
|
||||
@@ -38,6 +40,8 @@ Dairon Medina <dairon.medina@gmail.com>
|
||||
Danilo Leal <danilo@zed.dev>
|
||||
Danilo Leal <danilo@zed.dev> <67129314+danilo-leal@users.noreply.github.com>
|
||||
Edwin Aronsson <75266237+4teapo@users.noreply.github.com>
|
||||
Elvis Pranskevichus <elvis@geldata.com>
|
||||
Elvis Pranskevichus <elvis@geldata.com> <elvis@magic.io>
|
||||
Evren Sen <nervenes@icloud.com>
|
||||
Evren Sen <nervenes@icloud.com> <146845123+evrensen467@users.noreply.github.com>
|
||||
Evren Sen <nervenes@icloud.com> <146845123+evrsen@users.noreply.github.com>
|
||||
@@ -69,6 +73,8 @@ Lilith Iris <itslirissama@gmail.com> <83819417+Irilith@users.noreply.github.com>
|
||||
LoganDark <contact@logandark.mozmail.com>
|
||||
LoganDark <contact@logandark.mozmail.com> <git@logandark.mozmail.com>
|
||||
LoganDark <contact@logandark.mozmail.com> <github@logandark.mozmail.com>
|
||||
Marko Kungla <marko.kungla@gmail.com>
|
||||
Marko Kungla <marko.kungla@gmail.com> <marko@mkungla.dev>
|
||||
Marshall Bowers <git@maxdeviant.com>
|
||||
Marshall Bowers <git@maxdeviant.com> <elliott.codes@gmail.com>
|
||||
Marshall Bowers <git@maxdeviant.com> <marshall@zed.dev>
|
||||
@@ -84,6 +90,7 @@ Michael Sloan <michael@zed.dev> <mgsloan@google.com>
|
||||
Mikayla Maki <mikayla@zed.dev>
|
||||
Mikayla Maki <mikayla@zed.dev> <mikayla.c.maki@gmail.com>
|
||||
Mikayla Maki <mikayla@zed.dev> <mikayla.c.maki@icloud.com>
|
||||
Morgan Krey <morgan@zed.dev>
|
||||
Muhammad Talal Anwar <mail@talal.io>
|
||||
Muhammad Talal Anwar <mail@talal.io> <talalanwar@outlook.com>
|
||||
Nate Butler <iamnbutler@gmail.com>
|
||||
@@ -116,11 +123,18 @@ Shish <webmaster@shishnet.org>
|
||||
Shish <webmaster@shishnet.org> <shish@shishnet.org>
|
||||
Smit Barmase <0xtimsb@gmail.com>
|
||||
Smit Barmase <0xtimsb@gmail.com> <smit@zed.dev>
|
||||
Thomas <github.thomaub@gmail.com>
|
||||
Thomas <github.thomaub@gmail.com> <thomas.aubry94@gmail.com>
|
||||
Thomas <github.thomaub@gmail.com> <thomas.aubry@paylead.fr>
|
||||
Thomas Heartman <thomasheartman+github@gmail.com>
|
||||
Thomas Heartman <thomasheartman+github@gmail.com> <thomas@getunleash.io>
|
||||
Thomas Mickley-Doyle <tmickleydoyle@gmail.com>
|
||||
Thomas Mickley-Doyle <tmickleydoyle@gmail.com> <thomas@zed.dev>
|
||||
Thorben Kröger <dev@thorben.net>
|
||||
Thorben Kröger <dev@thorben.net> <thorben.kroeger@hexagon.com>
|
||||
Thorsten Ball <thorsten@zed.dev>
|
||||
Thorsten Ball <thorsten@zed.dev> <me@thorstenball.com>
|
||||
Thorsten Ball <thorsten@zed.dev> <mrnugget@gmail.com>
|
||||
Thorsten Ball <mrnugget@gmail.com>
|
||||
Thorsten Ball <mrnugget@gmail.com> <me@thorstenball.com>
|
||||
Thorsten Ball <mrnugget@gmail.com> <thorsten@zed.dev>
|
||||
Tristan Hume <tris.hume@gmail.com>
|
||||
Tristan Hume <tris.hume@gmail.com> <tristan@anthropic.com>
|
||||
Uladzislau Kaminski <i@uladkaminski.com>
|
||||
|
||||
2
.rules
@@ -115,7 +115,7 @@ Other entities can then register a callback to handle these events by doing `cx.
|
||||
GPUI has had some changes to its APIs. Always write code using the new APIs:
|
||||
|
||||
* `spawn` methods now take async closures (`AsyncFn`), and so should be called like `cx.spawn(async move |cx| ...)`.
|
||||
* Use `Entity<T>`. This replaces `Model<T>` and `View<T>` which longer exists and should NEVER be used.
|
||||
* Use `Entity<T>`. This replaces `Model<T>` and `View<T>` which no longer exist and should NEVER be used.
|
||||
* Use `App` references. This replaces `AppContext` which no longer exists and should NEVER be used.
|
||||
* Use `Context<T>` references. This replaces `ModelContext<T>` which no longer exists and should NEVER be used.
|
||||
* `Window` is now passed around explicitly. The new interface adds a `Window` reference parameter to some methods, and adds some new "*_in" methods for plumbing `Window`. The old types `WindowContext` and `ViewContext<T>` should NEVER be used.
|
||||
|
||||
@@ -3,15 +3,13 @@
|
||||
"label": "Debug Zed (CodeLLDB)",
|
||||
"adapter": "CodeLLDB",
|
||||
"program": "$ZED_WORKTREE_ROOT/target/debug/zed",
|
||||
"request": "launch",
|
||||
"cwd": "$ZED_WORKTREE_ROOT"
|
||||
"request": "launch"
|
||||
},
|
||||
{
|
||||
"label": "Debug Zed (GDB)",
|
||||
"adapter": "GDB",
|
||||
"program": "$ZED_WORKTREE_ROOT/target/debug/zed",
|
||||
"request": "launch",
|
||||
"cwd": "$ZED_WORKTREE_ROOT",
|
||||
"initialize_args": {
|
||||
"stopAtBeginningOfMainSubprogram": true
|
||||
}
|
||||
|
||||
2033
Cargo.lock
generated
49
Cargo.toml
@@ -3,11 +3,11 @@ resolver = "2"
|
||||
members = [
|
||||
"crates/activity_indicator",
|
||||
"crates/agent",
|
||||
"crates/agent_settings",
|
||||
"crates/anthropic",
|
||||
"crates/askpass",
|
||||
"crates/assets",
|
||||
"crates/assistant_context_editor",
|
||||
"crates/assistant_settings",
|
||||
"crates/assistant_slash_command",
|
||||
"crates/assistant_slash_commands",
|
||||
"crates/assistant_tool",
|
||||
@@ -31,13 +31,13 @@ members = [
|
||||
"crates/command_palette",
|
||||
"crates/command_palette_hooks",
|
||||
"crates/component",
|
||||
"crates/component_preview",
|
||||
"crates/context_server",
|
||||
"crates/copilot",
|
||||
"crates/credentials_provider",
|
||||
"crates/dap",
|
||||
"crates/dap_adapters",
|
||||
"crates/db",
|
||||
"crates/debug_adapter_extension",
|
||||
"crates/debugger_tools",
|
||||
"crates/debugger_ui",
|
||||
"crates/deepseek",
|
||||
@@ -73,12 +73,14 @@ members = [
|
||||
"crates/indexed_docs",
|
||||
"crates/inline_completion",
|
||||
"crates/inline_completion_button",
|
||||
"crates/inspector_ui",
|
||||
"crates/install_cli",
|
||||
"crates/jj",
|
||||
"crates/jj_ui",
|
||||
"crates/journal",
|
||||
"crates/language",
|
||||
"crates/language_extension",
|
||||
"crates/language_model",
|
||||
"crates/language_model_selector",
|
||||
"crates/language_models",
|
||||
"crates/language_selector",
|
||||
"crates/language_tools",
|
||||
@@ -209,12 +211,12 @@ edition = "2024"
|
||||
|
||||
activity_indicator = { path = "crates/activity_indicator" }
|
||||
agent = { path = "crates/agent" }
|
||||
agent_settings = { path = "crates/agent_settings" }
|
||||
ai = { path = "crates/ai" }
|
||||
anthropic = { path = "crates/anthropic" }
|
||||
askpass = { path = "crates/askpass" }
|
||||
assets = { path = "crates/assets" }
|
||||
assistant_context_editor = { path = "crates/assistant_context_editor" }
|
||||
assistant_settings = { path = "crates/assistant_settings" }
|
||||
assistant_slash_command = { path = "crates/assistant_slash_command" }
|
||||
assistant_slash_commands = { path = "crates/assistant_slash_commands" }
|
||||
assistant_tool = { path = "crates/assistant_tool" }
|
||||
@@ -238,13 +240,13 @@ collections = { path = "crates/collections" }
|
||||
command_palette = { path = "crates/command_palette" }
|
||||
command_palette_hooks = { path = "crates/command_palette_hooks" }
|
||||
component = { path = "crates/component" }
|
||||
component_preview = { path = "crates/component_preview" }
|
||||
context_server = { path = "crates/context_server" }
|
||||
copilot = { path = "crates/copilot" }
|
||||
credentials_provider = { path = "crates/credentials_provider" }
|
||||
dap = { path = "crates/dap" }
|
||||
dap_adapters = { path = "crates/dap_adapters" }
|
||||
db = { path = "crates/db" }
|
||||
debug_adapter_extension = { path = "crates/debug_adapter_extension" }
|
||||
debugger_tools = { path = "crates/debugger_tools" }
|
||||
debugger_ui = { path = "crates/debugger_ui" }
|
||||
deepseek = { path = "crates/deepseek" }
|
||||
@@ -278,12 +280,14 @@ image_viewer = { path = "crates/image_viewer" }
|
||||
indexed_docs = { path = "crates/indexed_docs" }
|
||||
inline_completion = { path = "crates/inline_completion" }
|
||||
inline_completion_button = { path = "crates/inline_completion_button" }
|
||||
inspector_ui = { path = "crates/inspector_ui" }
|
||||
install_cli = { path = "crates/install_cli" }
|
||||
jj = { path = "crates/jj" }
|
||||
jj_ui = { path = "crates/jj_ui" }
|
||||
journal = { path = "crates/journal" }
|
||||
language = { path = "crates/language" }
|
||||
language_extension = { path = "crates/language_extension" }
|
||||
language_model = { path = "crates/language_model" }
|
||||
language_model_selector = { path = "crates/language_model_selector" }
|
||||
language_models = { path = "crates/language_models" }
|
||||
language_selector = { path = "crates/language_selector" }
|
||||
language_tools = { path = "crates/language_tools" }
|
||||
@@ -410,9 +414,9 @@ aws-smithy-runtime-api = { version = "1.7.4", features = ["http-1x", "client"] }
|
||||
aws-smithy-types = { version = "1.3.0", features = ["http-body-1-x"] }
|
||||
base64 = "0.22"
|
||||
bitflags = "2.6.0"
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "b16f5c7bd873c7126f48c82c39e7ae64602ae74f" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "b16f5c7bd873c7126f48c82c39e7ae64602ae74f" }
|
||||
blade-util = { git = "https://github.com/kvark/blade", rev = "b16f5c7bd873c7126f48c82c39e7ae64602ae74f" }
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "416375211bb0b5826b3584dccdb6a43369e499ad" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "416375211bb0b5826b3584dccdb6a43369e499ad" }
|
||||
blade-util = { git = "https://github.com/kvark/blade", rev = "416375211bb0b5826b3584dccdb6a43369e499ad" }
|
||||
blake3 = "1.5.3"
|
||||
bytes = "1.0"
|
||||
cargo_metadata = "0.19"
|
||||
@@ -426,8 +430,9 @@ convert_case = "0.8.0"
|
||||
core-foundation = "0.10.0"
|
||||
core-foundation-sys = "0.8.6"
|
||||
core-video = { version = "0.4.3", features = ["metal"] }
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
ctor = "0.4.0"
|
||||
dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "be69a016ba710191b9fdded28c8b042af4b617f7" }
|
||||
dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "68516de327fa1be15214133a0a2e52a12982ce75" }
|
||||
dashmap = "6.0"
|
||||
derive_more = "0.99.17"
|
||||
dirs = "4.0"
|
||||
@@ -444,6 +449,7 @@ futures-batch = "0.6.1"
|
||||
futures-lite = "1.13"
|
||||
git2 = { version = "0.20.1", default-features = false }
|
||||
globset = "0.4"
|
||||
hashbrown = "0.15.3"
|
||||
handlebars = "4.3"
|
||||
heck = "0.5"
|
||||
heed = { version = "0.21.0", features = ["read-txn-no-tls"] }
|
||||
@@ -458,6 +464,8 @@ indexmap = { version = "2.7.0", features = ["serde"] }
|
||||
indoc = "2"
|
||||
inventory = "0.3.19"
|
||||
itertools = "0.14.0"
|
||||
jj-lib = { git = "https://github.com/jj-vcs/jj", rev = "e18eb8e05efaa153fad5ef46576af145bba1807f" }
|
||||
json_dotpath = "1.1"
|
||||
jsonschema = "0.30.0"
|
||||
jsonwebtoken = "9.3"
|
||||
jupyter-protocol = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
|
||||
@@ -465,13 +473,13 @@ jupyter-websocket-client = { git = "https://github.com/ConradIrwin/runtimed" ,r
|
||||
libc = "0.2"
|
||||
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
|
||||
linkify = "0.10.0"
|
||||
linkme = "0.3.31"
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
|
||||
lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "c9c189f1c5dd53c624a419ce35bc77ad6a908d18" }
|
||||
markup5ever_rcdom = "0.3.0"
|
||||
metal = "0.29"
|
||||
mlua = { version = "0.10", features = ["lua54", "vendored", "async", "send"] }
|
||||
naga = { version = "23.1.0", features = ["wgsl-in"] }
|
||||
moka = { version = "0.12.10", features = ["sync"] }
|
||||
naga = { version = "25.0", features = ["wgsl-in"] }
|
||||
nanoid = "0.4"
|
||||
nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
|
||||
nix = "0.29"
|
||||
@@ -545,13 +553,13 @@ streaming-iterator = "0.1"
|
||||
strsim = "0.11"
|
||||
strum = { version = "0.27.0", features = ["derive"] }
|
||||
subtle = "2.5.0"
|
||||
syn = { version = "1.0.72", features = ["full", "extra-traits"] }
|
||||
syn = { version = "2.0.101", features = ["full", "extra-traits"] }
|
||||
sys-locale = "0.3.1"
|
||||
sysinfo = "0.31.0"
|
||||
take-until = "0.2.0"
|
||||
tempfile = "3.9.0"
|
||||
tempfile = "3.20.0"
|
||||
thiserror = "2.0.12"
|
||||
tiktoken-rs = "0.6.0"
|
||||
tiktoken-rs = "0.7.0"
|
||||
time = { version = "0.3", features = [
|
||||
"macros",
|
||||
"parsing",
|
||||
@@ -594,7 +602,7 @@ unindent = "0.2.0"
|
||||
url = "2.2"
|
||||
urlencoding = "2.1.2"
|
||||
uuid = { version = "1.1.2", features = ["v4", "v5", "v7", "serde"] }
|
||||
walkdir = "2.3"
|
||||
walkdir = "2.5"
|
||||
wasm-encoder = "0.221"
|
||||
wasmparser = "0.221"
|
||||
wasmtime = { version = "29", default-features = false, features = [
|
||||
@@ -603,12 +611,13 @@ wasmtime = { version = "29", default-features = false, features = [
|
||||
"runtime",
|
||||
"cranelift",
|
||||
"component-model",
|
||||
"incremental-cache",
|
||||
"parallel-compilation",
|
||||
] }
|
||||
wasmtime-wasi = "29"
|
||||
which = "6.0.0"
|
||||
wit-component = "0.221"
|
||||
workspace-hack = "0.1.0"
|
||||
zed_llm_client = "0.8.0"
|
||||
zed_llm_client = "0.8.3"
|
||||
zstd = "0.11"
|
||||
|
||||
[workspace.dependencies.async-stripe]
|
||||
@@ -788,6 +797,9 @@ let_underscore_future = "allow"
|
||||
# running afoul of the borrow checker.
|
||||
too_many_arguments = "allow"
|
||||
|
||||
# We often have large enum variants yet we rarely actually bother with splitting them up.
|
||||
large_enum_variant = "allow"
|
||||
|
||||
[workspace.metadata.cargo-machete]
|
||||
ignored = [
|
||||
"bindgen",
|
||||
@@ -795,7 +807,6 @@ ignored = [
|
||||
"prost_build",
|
||||
"serde",
|
||||
"component",
|
||||
"linkme",
|
||||
"documented",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax = docker/dockerfile:1.2
|
||||
|
||||
FROM rust:1.86-bookworm as builder
|
||||
FROM rust:1.87-bookworm as builder
|
||||
WORKDIR app
|
||||
COPY . .
|
||||
|
||||
|
||||
@@ -8,10 +8,6 @@ Welcome to Zed, a high-performance, multiplayer code editor from the creators of
|
||||
|
||||
### Installation
|
||||
|
||||
<a href="https://repology.org/project/zed-editor/versions">
|
||||
<img src="https://repology.org/badge/vertical-allrepos/zed-editor.svg?minversion=0.143.5" alt="Packaging status" align="right">
|
||||
</a>
|
||||
|
||||
On macOS and Linux you can [download Zed directly](https://zed.dev/download) or [install Zed via your local package manager](https://zed.dev/docs/linux#installing-via-a-package-manager).
|
||||
|
||||
Other platforms are not yet available:
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.76019 3.50003H6.50231C6.71012 3.50003 6.89761 3.62971 6.95698 3.82346C7.04292 4.01876 6.98823 4.23906 6.83199 4.37656L2.83214 7.87643C2.65558 8.02954 2.39731 8.04204 2.20857 7.90455C2.01967 7.76705 1.95092 7.51706 2.04295 7.30301L3.24462 4.49999H1.48844C1.29423 4.49999 1.10767 4.37031 1.0344 4.17657C0.961132 3.98126 1.01643 3.76096 1.17323 3.62346L5.17261 0.123753C5.34917 -0.0299914 5.60697 -0.0417097 5.79603 0.0954726C5.98508 0.232749 6.05383 0.482177 5.96165 0.69695L4.76013 3.49981L4.76019 3.50003Z" fill="white"/>
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.3 1.75L3 7.35H5.8L4.7 12.25L11 6.65H8.2L9.3 1.75Z" stroke="black" stroke-width="1.25" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 633 B After Width: | Height: | Size: 227 B |
3
assets/icons/bolt_filled.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.76019 3.50003H6.50231C6.71012 3.50003 6.89761 3.62971 6.95698 3.82346C7.04292 4.01876 6.98823 4.23906 6.83199 4.37656L2.83214 7.87643C2.65558 8.02954 2.39731 8.04204 2.20857 7.90455C2.01967 7.76705 1.95092 7.51706 2.04295 7.30301L3.24462 4.49999H1.48844C1.29423 4.49999 1.10767 4.37031 1.0344 4.17657C0.961132 3.98126 1.01643 3.76096 1.17323 3.62346L5.17261 0.123753C5.34917 -0.0299914 5.60697 -0.0417097 5.79603 0.0954726C5.98508 0.232749 6.05383 0.482177 5.96165 0.69695L4.76013 3.49981L4.76019 3.50003Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 633 B |
1
assets/icons/load_circle.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-loader-circle-icon lucide-loader-circle"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>
|
||||
|
After Width: | Height: | Size: 289 B |
3
assets/icons/zed_burn_mode.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.99207 8.14741C5.37246 8.14741 5.73726 7.9963 6.00623 7.72733C6.27521 7.45836 6.42631 7.09355 6.42631 6.71317C6.42631 5.92147 6.13946 5.56578 5.85262 4.99208C5.23761 3.76265 5.72411 2.66631 7.00001 1.5499C7.28686 2.98414 8.1474 4.36101 9.2948 5.27893C10.4422 6.19684 11.0159 7.28687 11.0159 8.43426C11.0159 8.96163 10.912 9.48384 10.7102 9.97107C10.5084 10.4583 10.2126 10.901 9.83967 11.2739C9.46676 11.6468 9.02405 11.9426 8.53682 12.1444C8.04959 12.3463 7.52738 12.4501 7.00001 12.4501C6.47264 12.4501 5.95043 12.3463 5.4632 12.1444C4.97597 11.9426 4.53326 11.6468 4.16035 11.2739C3.78745 10.901 3.49164 10.4583 3.28982 9.97107C3.088 9.48384 2.98413 8.96163 2.98413 8.43426C2.98413 7.77279 3.23254 7.1182 3.55783 6.71317C3.55783 7.09355 3.70894 7.45836 3.97791 7.72733C4.24688 7.9963 4.61169 8.14741 4.99207 8.14741Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1018 B |
13
assets/icons/zed_burn_mode_on.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2595_5640)">
|
||||
<path d="M4.99207 8.14741C5.37246 8.14741 5.73726 7.9963 6.00623 7.72733C6.27521 7.45836 6.42631 7.09355 6.42631 6.71317C6.42631 5.92147 6.13946 5.56578 5.85262 4.99208C5.23761 3.76265 5.72411 2.66631 7.00001 1.5499C7.28686 2.98414 8.1474 4.36101 9.2948 5.27893C10.4422 6.19684 11.0159 7.28687 11.0159 8.43426C11.0159 8.96163 10.912 9.48384 10.7102 9.97107C10.5084 10.4583 10.2126 10.901 9.83967 11.2739C9.46676 11.6468 9.02405 11.9426 8.53682 12.1444C8.04959 12.3463 7.52738 12.4501 7.00001 12.4501C6.47264 12.4501 5.95043 12.3463 5.4632 12.1444C4.97597 11.9426 4.53326 11.6468 4.16035 11.2739C3.78745 10.901 3.49164 10.4583 3.28982 9.97107C3.088 9.48384 2.98413 8.96163 2.98413 8.43426C2.98413 7.77279 3.23254 7.1182 3.55783 6.71317C3.55783 7.09355 3.70894 7.45836 3.97791 7.72733C4.24688 7.9963 4.61169 8.14741 4.99207 8.14741Z" fill="black" fill-opacity="0.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2 4C2.55228 4 3 3.55228 3 3C3 2.44772 2.55228 2 2 2C1.44772 2 1 2.44772 1 3C1 3.55228 1.44772 4 2 4Z" fill="black"/>
|
||||
<path d="M10 2C10.5523 2 11 1.55228 11 1C11 0.44772 10.5523 0 10 0C9.44772 0 9 0.44772 9 1C9 1.55228 9.44772 2 10 2Z" fill="black"/>
|
||||
<path d="M13 5C13.5522 5 14 4.55228 14 4C14 3.44772 13.5522 3 13 3C12.4478 3 12 3.44772 12 4C12 4.55228 12.4478 5 13 5Z" fill="black"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2595_5640">
|
||||
<rect width="14" height="14" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -1,14 +0,0 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2489_484)">
|
||||
<path d="M11 8.9V11C8.51716 11 7.48284 11 5 11V10.4L11 5.6V5H5V7.1" stroke="black" stroke-width="1.5"/>
|
||||
<path d="M1.5 5.5V1.5H5" stroke="black" stroke-opacity="0.5" stroke-width="1.5"/>
|
||||
<path d="M14.5 5.5V1.5H11" stroke="black" stroke-opacity="0.5" stroke-width="1.5"/>
|
||||
<path d="M1.5 10.5V14.5H5" stroke="black" stroke-opacity="0.5" stroke-width="1.5"/>
|
||||
<path d="M14.5 10.5V14.5H11" stroke="black" stroke-opacity="0.5" stroke-width="1.5"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2489_484">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 687 B |
@@ -31,8 +31,10 @@
|
||||
"ctrl-,": "zed::OpenSettings",
|
||||
"ctrl-q": "zed::Quit",
|
||||
"f4": "debugger::Start",
|
||||
"alt-f4": "debugger::RerunLastSession",
|
||||
"f5": "debugger::Continue",
|
||||
"shift-f5": "debugger::Stop",
|
||||
"ctrl-shift-f5": "debugger::Restart",
|
||||
"f6": "debugger::Pause",
|
||||
"f7": "debugger::StepOver",
|
||||
"cmd-f11": "debugger::StepInto",
|
||||
@@ -217,7 +219,6 @@
|
||||
"context": "ContextEditor > Editor",
|
||||
"bindings": {
|
||||
"ctrl-enter": "assistant::Assist",
|
||||
"ctrl-shift-enter": "assistant::Edit",
|
||||
"ctrl-s": "workspace::Save",
|
||||
"save": "workspace::Save",
|
||||
"ctrl->": "assistant::QuoteSelection",
|
||||
@@ -245,9 +246,11 @@
|
||||
"ctrl-shift-a": "agent::ToggleContextPicker",
|
||||
"ctrl-shift-o": "agent::ToggleNavigationMenu",
|
||||
"ctrl-shift-i": "agent::ToggleOptionsMenu",
|
||||
"shift-escape": "agent::ExpandMessageEditor",
|
||||
"shift-alt-escape": "agent::ExpandMessageEditor",
|
||||
"ctrl-alt-e": "agent::RemoveAllContext",
|
||||
"ctrl-shift-e": "project_panel::ToggleFocus"
|
||||
"ctrl-shift-e": "project_panel::ToggleFocus",
|
||||
"ctrl-shift-enter": "agent::ContinueThread",
|
||||
"alt-enter": "agent::ContinueWithBurnMode"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -274,6 +277,7 @@
|
||||
"context": "MessageEditor > Editor",
|
||||
"bindings": {
|
||||
"enter": "agent::Chat",
|
||||
"ctrl-enter": "agent::ChatWithFollow",
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff"
|
||||
}
|
||||
@@ -513,6 +517,8 @@
|
||||
"alt-ctrl-o": "projects::OpenRecent",
|
||||
"alt-shift-open": "projects::OpenRemote",
|
||||
"alt-ctrl-shift-o": "projects::OpenRemote",
|
||||
// Change to open path modal for existing remote connection by setting the parameter
|
||||
// "alt-ctrl-shift-o": "["projects::OpenRemote", { "from_existing_connection": true }]",
|
||||
"alt-ctrl-shift-b": "branches::OpenRecent",
|
||||
"alt-shift-enter": "toast::RunAction",
|
||||
"ctrl-~": "workspace::NewTerminal",
|
||||
@@ -557,6 +563,7 @@
|
||||
"ctrl-shift-e": "project_panel::ToggleFocus",
|
||||
"ctrl-shift-b": "outline_panel::ToggleFocus",
|
||||
"ctrl-shift-g": "git_panel::ToggleFocus",
|
||||
"ctrl-shift-d": "debug_panel::ToggleFocus",
|
||||
"ctrl-?": "agent::ToggleFocus",
|
||||
"alt-save": "workspace::SaveAll",
|
||||
"ctrl-alt-s": "workspace::SaveAll",
|
||||
@@ -594,7 +601,6 @@
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"ctrl-shift-d": "editor::DuplicateLineDown",
|
||||
"ctrl-shift-j": "editor::JoinLines",
|
||||
"ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",
|
||||
"ctrl-alt-h": "editor::DeleteToPreviousSubwordStart",
|
||||
@@ -673,7 +679,8 @@
|
||||
{
|
||||
"bindings": {
|
||||
"ctrl-alt-shift-f": "workspace::FollowNextCollaborator",
|
||||
"ctrl-alt-i": "zed::DebugElements"
|
||||
// Only available in debug builds: opens an element inspector for development.
|
||||
"ctrl-alt-i": "dev::ToggleInspector"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -767,7 +774,7 @@
|
||||
"alt-ctrl-r": "project_panel::RevealInFileManager",
|
||||
"ctrl-shift-enter": "project_panel::OpenWithSystem",
|
||||
"shift-find": "project_panel::NewSearchInDirectory",
|
||||
"ctrl-shift-f": "project_panel::NewSearchInDirectory",
|
||||
"ctrl-alt-shift-f": "project_panel::NewSearchInDirectory",
|
||||
"shift-down": "menu::SelectNext",
|
||||
"shift-up": "menu::SelectPrevious",
|
||||
"escape": "menu::Cancel"
|
||||
@@ -861,6 +868,30 @@
|
||||
"alt-l": "git::GenerateCommitMessage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "DebugPanel",
|
||||
"bindings": {
|
||||
"ctrl-t": "debugger::ToggleThreadPicker",
|
||||
"ctrl-i": "debugger::ToggleSessionPicker"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "VariableList",
|
||||
"bindings": {
|
||||
"left": "variable_list::CollapseSelectedEntry",
|
||||
"right": "variable_list::ExpandSelectedEntry",
|
||||
"enter": "variable_list::EditVariable",
|
||||
"ctrl-c": "variable_list::CopyVariableValue",
|
||||
"ctrl-alt-c": "variable_list::CopyVariableName"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "BreakpointList",
|
||||
"bindings": {
|
||||
"space": "debugger::ToggleEnableBreakpoint",
|
||||
"backspace": "debugger::UnsetBreakpoint"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "CollabPanel && not_editing",
|
||||
"bindings": {
|
||||
@@ -929,12 +960,14 @@
|
||||
"alt-b": ["terminal::SendText", "\u001bb"],
|
||||
"alt-f": ["terminal::SendText", "\u001bf"],
|
||||
"alt-.": ["terminal::SendText", "\u001b."],
|
||||
"ctrl-delete": ["terminal::SendText", "\u001bd"],
|
||||
// Overrides for conflicting keybindings
|
||||
"ctrl-b": ["terminal::SendKeystroke", "ctrl-b"],
|
||||
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"],
|
||||
"ctrl-e": ["terminal::SendKeystroke", "ctrl-e"],
|
||||
"ctrl-o": ["terminal::SendKeystroke", "ctrl-o"],
|
||||
"ctrl-w": ["terminal::SendKeystroke", "ctrl-w"],
|
||||
"ctrl-backspace": ["terminal::SendKeystroke", "ctrl-w"],
|
||||
"ctrl-shift-a": "editor::SelectAll",
|
||||
"find": "buffer_search::Deploy",
|
||||
"ctrl-shift-f": "buffer_search::Deploy",
|
||||
@@ -952,7 +985,10 @@
|
||||
"shift-down": "terminal::ScrollLineDown",
|
||||
"shift-home": "terminal::ScrollToTop",
|
||||
"shift-end": "terminal::ScrollToBottom",
|
||||
"ctrl-shift-space": "terminal::ToggleViMode"
|
||||
"ctrl-shift-space": "terminal::ToggleViMode",
|
||||
"ctrl-shift-r": "terminal::RerunTask",
|
||||
"ctrl-alt-r": "terminal::RerunTask",
|
||||
"alt-t": "terminal::RerunTask"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -975,5 +1011,12 @@
|
||||
"bindings": {
|
||||
"ctrl-r": "diagnostics::ToggleDiagnosticsRefresh"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "DebugConsole > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "menu::Confirm"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,22 +1,13 @@
|
||||
[
|
||||
// Moved before Standard macOS bindings so that `cmd-w` is not the last binding for
|
||||
// `workspace::CloseWindow` and displayed/intercepted by macOS
|
||||
{
|
||||
"context": "PromptLibrary",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-n": "rules_library::NewRule",
|
||||
"cmd-shift-s": "rules_library::ToggleDefaultRule",
|
||||
"cmd-w": "workspace::CloseWindow"
|
||||
}
|
||||
},
|
||||
// Standard macOS bindings
|
||||
{
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"f4": "debugger::Start",
|
||||
"alt-f4": "debugger::RerunLastSession",
|
||||
"f5": "debugger::Continue",
|
||||
"shift-f5": "debugger::Stop",
|
||||
"shift-cmd-f5": "debugger::Restart",
|
||||
"f6": "debugger::Pause",
|
||||
"f7": "debugger::StepOver",
|
||||
"f11": "debugger::StepInto",
|
||||
@@ -263,7 +254,6 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-enter": "assistant::Assist",
|
||||
"cmd-shift-enter": "assistant::Edit",
|
||||
"cmd-s": "workspace::Save",
|
||||
"cmd->": "assistant::QuoteSelection",
|
||||
"cmd-<": "assistant::InsertIntoEditor",
|
||||
@@ -291,9 +281,11 @@
|
||||
"cmd-shift-a": "agent::ToggleContextPicker",
|
||||
"cmd-shift-o": "agent::ToggleNavigationMenu",
|
||||
"cmd-shift-i": "agent::ToggleOptionsMenu",
|
||||
"shift-escape": "agent::ExpandMessageEditor",
|
||||
"shift-alt-escape": "agent::ExpandMessageEditor",
|
||||
"cmd-alt-e": "agent::RemoveAllContext",
|
||||
"cmd-shift-e": "project_panel::ToggleFocus"
|
||||
"cmd-shift-e": "project_panel::ToggleFocus",
|
||||
"cmd-shift-enter": "agent::ContinueThread",
|
||||
"alt-enter": "agent::ContinueWithBurnMode"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -322,6 +314,7 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "agent::Chat",
|
||||
"cmd-enter": "agent::ChatWithFollow",
|
||||
"cmd-i": "agent::ToggleProfileSelector",
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff"
|
||||
}
|
||||
@@ -368,18 +361,21 @@
|
||||
"ctrl--": "pane::GoBack"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ThreadHistory",
|
||||
"bindings": {
|
||||
"ctrl--": "pane::GoBack"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ThreadHistory > Editor",
|
||||
"bindings": {
|
||||
"shift-backspace": "agent::RemoveSelectedThread"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "PromptLibrary",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-n": "rules_library::NewRule",
|
||||
"cmd-shift-s": "rules_library::ToggleDefaultRule",
|
||||
"cmd-w": "workspace::CloseWindow"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar",
|
||||
"use_key_equivalents": true,
|
||||
@@ -589,6 +585,7 @@
|
||||
// "alt-cmd-o": ["projects::OpenRecent", {"create_new_window": true }],
|
||||
"alt-cmd-o": "projects::OpenRecent",
|
||||
"ctrl-cmd-o": "projects::OpenRemote",
|
||||
"ctrl-cmd-shift-o": ["projects::OpenRemote", { "from_existing_connection": true }],
|
||||
"alt-cmd-b": "branches::OpenRecent",
|
||||
"ctrl-~": "workspace::NewTerminal",
|
||||
"cmd-s": "workspace::Save",
|
||||
@@ -624,6 +621,7 @@
|
||||
"cmd-shift-e": "project_panel::ToggleFocus",
|
||||
"cmd-shift-b": "outline_panel::ToggleFocus",
|
||||
"ctrl-shift-g": "git_panel::ToggleFocus",
|
||||
"cmd-shift-d": "debug_panel::ToggleFocus",
|
||||
"cmd-?": "agent::ToggleFocus",
|
||||
"cmd-alt-s": "workspace::SaveAll",
|
||||
"cmd-k m": "language_selector::Toggle",
|
||||
@@ -741,7 +739,8 @@
|
||||
"ctrl-alt-cmd-f": "workspace::FollowNextCollaborator",
|
||||
// TODO: Move this to a dock open action
|
||||
"cmd-shift-c": "collab_panel::ToggleFocus",
|
||||
"cmd-alt-i": "zed::DebugElements"
|
||||
// Only available in debug builds: opens an element inspector for development.
|
||||
"cmd-alt-i": "dev::ToggleInspector"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -826,7 +825,7 @@
|
||||
"alt-cmd-r": "project_panel::RevealInFileManager",
|
||||
"ctrl-shift-enter": "project_panel::OpenWithSystem",
|
||||
"cmd-alt-backspace": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"cmd-shift-f": "project_panel::NewSearchInDirectory",
|
||||
"cmd-alt-shift-f": "project_panel::NewSearchInDirectory",
|
||||
"shift-down": "menu::SelectNext",
|
||||
"shift-up": "menu::SelectPrevious",
|
||||
"escape": "menu::Cancel"
|
||||
@@ -844,7 +843,10 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"left": "variable_list::CollapseSelectedEntry",
|
||||
"right": "variable_list::ExpandSelectedEntry"
|
||||
"right": "variable_list::ExpandSelectedEntry",
|
||||
"enter": "variable_list::EditVariable",
|
||||
"cmd-c": "variable_list::CopyVariableValue",
|
||||
"cmd-alt-c": "variable_list::CopyVariableName"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -929,6 +931,20 @@
|
||||
"alt-tab": "git::GenerateCommitMessage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "DebugPanel",
|
||||
"bindings": {
|
||||
"cmd-t": "debugger::ToggleThreadPicker",
|
||||
"cmd-i": "debugger::ToggleSessionPicker"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "BreakpointList",
|
||||
"bindings": {
|
||||
"space": "debugger::ToggleEnableBreakpoint",
|
||||
"backspace": "debugger::UnsetBreakpoint"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "CollabPanel && not_editing",
|
||||
"use_key_equivalents": true,
|
||||
@@ -1012,7 +1028,7 @@
|
||||
"alt-right": ["terminal::SendText", "\u001bf"],
|
||||
"alt-b": ["terminal::SendText", "\u001bb"],
|
||||
"alt-f": ["terminal::SendText", "\u001bf"],
|
||||
"alt-.": ["terminal::SendText", "\u001b."],
|
||||
"ctrl-delete": ["terminal::SendText", "\u001bd"],
|
||||
// There are conflicting bindings for these keys in the global context.
|
||||
// these bindings override them, remove at your own risk:
|
||||
"up": ["terminal::SendKeystroke", "up"],
|
||||
@@ -1022,6 +1038,7 @@
|
||||
"escape": ["terminal::SendKeystroke", "escape"],
|
||||
"enter": ["terminal::SendKeystroke", "enter"],
|
||||
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"],
|
||||
"ctrl-backspace": ["terminal::SendKeystroke", "ctrl-w"],
|
||||
"shift-pageup": "terminal::ScrollPageUp",
|
||||
"cmd-up": "terminal::ScrollPageUp",
|
||||
"shift-pagedown": "terminal::ScrollPageDown",
|
||||
@@ -1038,7 +1055,8 @@
|
||||
"ctrl-alt-up": "pane::SplitUp",
|
||||
"ctrl-alt-down": "pane::SplitDown",
|
||||
"ctrl-alt-left": "pane::SplitLeft",
|
||||
"ctrl-alt-right": "pane::SplitRight"
|
||||
"ctrl-alt-right": "pane::SplitRight",
|
||||
"cmd-alt-r": "terminal::RerunTask"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1083,5 +1101,12 @@
|
||||
"bindings": {
|
||||
"ctrl-r": "diagnostics::ToggleDiagnosticsRefresh"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "DebugConsole > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "menu::Confirm"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -72,7 +72,9 @@
|
||||
"alt-left": "editor::SelectToPreviousWordStart",
|
||||
"alt-right": "editor::SelectToNextWordEnd",
|
||||
"pagedown": "editor::SelectPageDown",
|
||||
"ctrl-v": "editor::SelectPageDown",
|
||||
"pageup": "editor::SelectPageUp",
|
||||
"alt-v": "editor::SelectPageUp",
|
||||
"ctrl-f": "editor::SelectRight",
|
||||
"ctrl-b": "editor::SelectLeft",
|
||||
"ctrl-n": "editor::SelectDown",
|
||||
|
||||
@@ -51,9 +51,7 @@
|
||||
"ctrl-k ctrl-l": "editor::ConvertToLowerCase",
|
||||
"shift-alt-m": "markdown::OpenPreviewToTheSide",
|
||||
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-delete": "editor::DeleteToNextWordEnd",
|
||||
"f3": "editor::FindNextMatch",
|
||||
"shift-f3": "editor::FindPreviousMatch"
|
||||
"ctrl-delete": "editor::DeleteToNextWordEnd"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -72,7 +72,9 @@
|
||||
"alt-left": "editor::SelectToPreviousWordStart",
|
||||
"alt-right": "editor::SelectToNextWordEnd",
|
||||
"pagedown": "editor::SelectPageDown",
|
||||
"ctrl-v": "editor::SelectPageDown",
|
||||
"pageup": "editor::SelectPageUp",
|
||||
"alt-v": "editor::SelectPageUp",
|
||||
"ctrl-f": "editor::SelectRight",
|
||||
"ctrl-b": "editor::SelectLeft",
|
||||
"ctrl-n": "editor::SelectDown",
|
||||
|
||||
@@ -53,9 +53,7 @@
|
||||
"cmd-shift-j": "editor::JoinLines",
|
||||
"shift-alt-m": "markdown::OpenPreviewToTheSide",
|
||||
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-delete": "editor::DeleteToNextWordEnd",
|
||||
"cmd-g": "editor::FindNextMatch",
|
||||
"cmd-shift-g": "editor::FindPreviousMatch"
|
||||
"ctrl-delete": "editor::DeleteToNextWordEnd"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -152,6 +152,7 @@
|
||||
"g end": ["vim::EndOfLine", { "display_lines": true }],
|
||||
"g 0": ["vim::StartOfLine", { "display_lines": true }],
|
||||
"g home": ["vim::StartOfLine", { "display_lines": true }],
|
||||
"g shift-m": ["vim::MiddleOfLine", { "display_lines": true }],
|
||||
"g ^": ["vim::FirstNonWhitespace", { "display_lines": true }],
|
||||
"g v": "vim::RestoreVisualSelection",
|
||||
"g ]": "editor::GoToDiagnostic",
|
||||
@@ -845,13 +846,5 @@
|
||||
// and Windows.
|
||||
"alt-l": "editor::AcceptEditPrediction"
|
||||
}
|
||||
},
|
||||
{
|
||||
// Fixes https://github.com/zed-industries/zed/issues/29095 by ensuring that
|
||||
// the last binding for editor::ToggleComments is not ctrl-c.
|
||||
"context": "hack_to_fix_ctrl-c",
|
||||
"bindings": {
|
||||
"g c": "editor::ToggleComments"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -49,10 +49,9 @@ And here's the section to rewrite based on that prompt again for reference:
|
||||
</rewrite_this>
|
||||
|
||||
{{#if diagnostic_errors}}
|
||||
{{#each diagnostic_errors}}
|
||||
|
||||
Below are the diagnostic errors visible to the user. If the user requests problems to be fixed, use this information, but do not try to fix these errors if the user hasn't asked you to.
|
||||
|
||||
{{#each diagnostic_errors}}
|
||||
<diagnostic_error>
|
||||
<line_number>{{line_number}}</line_number>
|
||||
<error_message>{{error_message}}</error_message>
|
||||
|
||||
@@ -1,206 +0,0 @@
|
||||
<task_description>
|
||||
|
||||
The user of a code editor wants to make a change to their codebase.
|
||||
You must describe the change using the following XML structure:
|
||||
|
||||
- <patch> - A group of related code changes.
|
||||
Child tags:
|
||||
- <title> (required) - A high-level description of the changes. This should be as short
|
||||
as possible, possibly using common abbreviations.
|
||||
- <edit> (1 or more) - An edit to make at a particular range within a file.
|
||||
Includes the following child tags:
|
||||
- <path> (required) - The path to the file that will be changed.
|
||||
- <description> (optional) - An arbitrarily-long comment that describes the purpose
|
||||
of this edit.
|
||||
- <old_text> (optional) - An excerpt from the file's current contents that uniquely
|
||||
identifies a range within the file where the edit should occur. Required for all operations
|
||||
except `create`.
|
||||
- <new_text> (required) - The new text to insert into the file.
|
||||
- <operation> (required) - The type of change that should occur at the given range
|
||||
of the file. Must be one of the following values:
|
||||
- `update`: Replaces the entire range with the new text.
|
||||
- `insert_before`: Inserts the new text before the range.
|
||||
- `insert_after`: Inserts new text after the range.
|
||||
- `create`: Creates or overwrites a file with the given path and the new text.
|
||||
- `delete`: Deletes the specified range from the file.
|
||||
|
||||
<guidelines>
|
||||
- Never provide multiple edits whose ranges intersect each other. Instead, merge them into one edit.
|
||||
- Prefer multiple edits to smaller, disjoint ranges, rather than one edit to a larger range.
|
||||
- There's no need to escape angle brackets within XML tags.
|
||||
- Always ensure imports are added if you're referencing symbols that are not in scope.
|
||||
</guidelines>
|
||||
|
||||
Here are some concrete examples.
|
||||
|
||||
<example>
|
||||
<message role="user">
|
||||
|
||||
```rs src/shapes.rs
|
||||
pub mod rectangle;
|
||||
pub mod circle;
|
||||
```
|
||||
|
||||
```rs src/shapes/rectangle.rs
|
||||
pub struct Rectangle {
|
||||
width: f64,
|
||||
height: f64,
|
||||
}
|
||||
|
||||
impl Rectangle {
|
||||
pub fn new(width: f64, height: f64) -> Self {
|
||||
Rectangle { width, height }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```rs src/shapes/circle.rs
|
||||
pub struct Circle {
|
||||
radius: f64,
|
||||
}
|
||||
|
||||
impl Circle {
|
||||
pub fn new(radius: f64) -> Self {
|
||||
Circle { radius }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Update all shapes to store their origin as an (x, y) tuple and implement Display.
|
||||
</message>
|
||||
|
||||
<message role="assistant">
|
||||
We'll need to update both the rectangle and circle modules.
|
||||
|
||||
<patch>
|
||||
<title>Add origins and display impls to shapes</title>
|
||||
<edit>
|
||||
<path>src/shapes/rectangle.rs</path>
|
||||
<description>Add the origin field to Rectangle struct</description>
|
||||
<operation>insert_after</operation>
|
||||
<old_text>
|
||||
pub struct Rectangle {
|
||||
</old_text>
|
||||
<new_text>
|
||||
origin: (f64, f64),
|
||||
</new_text>
|
||||
</edit>
|
||||
|
||||
<edit>
|
||||
<path>src/shapes/rectangle.rs</path>
|
||||
<operation>update</operation>
|
||||
<old_text>
|
||||
fn new(width: f64, height: f64) -> Self {
|
||||
Rectangle { width, height }
|
||||
}
|
||||
</old_text>
|
||||
<new_text>
|
||||
fn new(origin: (f64, f64), width: f64, height: f64) -> Self {
|
||||
Rectangle { origin, width, height }
|
||||
}
|
||||
</new_text>
|
||||
</edit>
|
||||
|
||||
<edit>
|
||||
<path>src/shapes/circle.rs</path>
|
||||
<description>Add the origin field to Circle struct</description>
|
||||
<operation>insert_after</operation>
|
||||
<old_text>
|
||||
pub struct Circle {
|
||||
radius: f64,
|
||||
</old_text>
|
||||
<new_text>
|
||||
origin: (f64, f64),
|
||||
</new_text>
|
||||
</edit>
|
||||
|
||||
<edit>
|
||||
<path>src/shapes/circle.rs</path>
|
||||
<operation>update</operation>
|
||||
<old_text>
|
||||
fn new(radius: f64) -> Self {
|
||||
Circle { radius }
|
||||
}
|
||||
</old_text>
|
||||
<new_text>
|
||||
fn new(origin: (f64, f64), radius: f64) -> Self {
|
||||
Circle { origin, radius }
|
||||
}
|
||||
</new_text>
|
||||
</edit>
|
||||
</step>
|
||||
|
||||
<edit>
|
||||
<path>src/shapes/rectangle.rs</path>
|
||||
<operation>insert_before</operation>
|
||||
<old_text>
|
||||
struct Rectangle {
|
||||
</old_text>
|
||||
<new_text>
|
||||
use std::fmt;
|
||||
|
||||
</new_text>
|
||||
</edit>
|
||||
|
||||
<edit>
|
||||
<path>src/shapes/rectangle.rs</path>
|
||||
<description>
|
||||
Add a manual Display implementation for Rectangle.
|
||||
Currently, this is the same as a derived Display implementation.
|
||||
</description>
|
||||
<operation>insert_after</operation>
|
||||
<old_text>
|
||||
Rectangle { width, height }
|
||||
}
|
||||
}
|
||||
</old_text>
|
||||
<new_text>
|
||||
impl fmt::Display for Rectangle {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.format_struct(f, "Rectangle")
|
||||
.field("origin", &self.origin)
|
||||
.field("width", &self.width)
|
||||
.field("height", &self.height)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
</new_text>
|
||||
</edit>
|
||||
|
||||
<edit>
|
||||
<path>src/shapes/circle.rs</path>
|
||||
<operation>insert_before</operation>
|
||||
<old_text>
|
||||
struct Circle {
|
||||
</old_text>
|
||||
<new_text>
|
||||
use std::fmt;
|
||||
</new_text>
|
||||
</edit>
|
||||
|
||||
<edit>
|
||||
<path>src/shapes/circle.rs</path>
|
||||
<operation>insert_after</operation>
|
||||
<old_text>
|
||||
Circle { radius }
|
||||
}
|
||||
}
|
||||
</old_text>
|
||||
<new_text>
|
||||
impl fmt::Display for Rectangle {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.format_struct(f, "Rectangle")
|
||||
.field("origin", &self.origin)
|
||||
.field("width", &self.width)
|
||||
.field("height", &self.height)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
</new_text>
|
||||
</edit>
|
||||
</patch>
|
||||
|
||||
</message>
|
||||
</example>
|
||||
|
||||
</task_description>
|
||||
@@ -113,8 +113,8 @@
|
||||
// Whether to show the informational hover box when moving the mouse
|
||||
// over symbols in the editor.
|
||||
"hover_popover_enabled": true,
|
||||
// Time to wait before showing the informational hover box
|
||||
"hover_popover_delay": 350,
|
||||
// Time to wait in milliseconds before showing the informational hover box.
|
||||
"hover_popover_delay": 300,
|
||||
// Whether to confirm before quitting Zed.
|
||||
"confirm_quit": false,
|
||||
// Whether to restore last closed project when fresh Zed instance is opened.
|
||||
@@ -213,11 +213,30 @@
|
||||
// Whether to show the signature help after completion or a bracket pair inserted.
|
||||
// If `auto_signature_help` is enabled, this setting will be treated as enabled also.
|
||||
"show_signature_help_after_edits": false,
|
||||
// Whether to show code action button at start of buffer line.
|
||||
"inline_code_actions": true,
|
||||
// What to do when go to definition yields no results.
|
||||
//
|
||||
// 1. Do nothing: `none`
|
||||
// 2. Find references for the same symbol: `find_all_references` (default)
|
||||
"go_to_definition_fallback": "find_all_references",
|
||||
// Which level to use to filter out diagnostics displayed in the editor.
|
||||
//
|
||||
// Affects the editor rendering only, and does not interrupt
|
||||
// the functionality of diagnostics fetching and project diagnostics editor.
|
||||
// Which files containing diagnostic errors/warnings to mark in the tabs.
|
||||
// Diagnostics are only shown when file icons are also active.
|
||||
// This setting only works when can take the following three values:
|
||||
//
|
||||
// Which diagnostic indicators to show in the scrollbar, their level should be more or equal to the specified severity level.
|
||||
// Possible values:
|
||||
// - "off" — no diagnostics are allowed
|
||||
// - "error"
|
||||
// - "warning"
|
||||
// - "info"
|
||||
// - "hint"
|
||||
// - null — allow all diagnostics (default)
|
||||
"diagnostics_max_severity": null,
|
||||
// Whether to show wrap guides (vertical rulers) in the editor.
|
||||
// Setting this to true will show a guide at the 'preferred_line_length' value
|
||||
// if 'soft_wrap' is set to 'preferred_line_length', and will show any
|
||||
@@ -305,16 +324,24 @@
|
||||
// Whether to show the Selections menu in the editor toolbar.
|
||||
"selections_menu": true,
|
||||
// Whether to show agent review buttons in the editor toolbar.
|
||||
"agent_review": true
|
||||
"agent_review": true,
|
||||
// Whether to show code action buttons in the editor toolbar.
|
||||
"code_actions": false
|
||||
},
|
||||
// Titlebar related settings
|
||||
"title_bar": {
|
||||
// Whether to show the branch icon beside branch switcher in the titlebar.
|
||||
"show_branch_icon": false,
|
||||
// Whether to show the branch name button in the titlebar.
|
||||
"show_branch_name": true,
|
||||
// Whether to show the project host and name in the titlebar.
|
||||
"show_project_items": true,
|
||||
// Whether to show onboarding banners in the titlebar.
|
||||
"show_onboarding_banner": true,
|
||||
// Whether to show user picture in the titlebar.
|
||||
"show_user_picture": true
|
||||
"show_user_picture": true,
|
||||
// Whether to show the sign in button in the titlebar.
|
||||
"show_sign_in": true
|
||||
},
|
||||
// Scrollbar related settings
|
||||
"scrollbar": {
|
||||
@@ -393,11 +420,7 @@
|
||||
// 1. `null` to inherit the editor `current_line_highlight` setting (default)
|
||||
// 2. "line" or "all" to highlight the current line in the minimap.
|
||||
// 3. "gutter" or "none" to not highlight the current line in the minimap.
|
||||
"current_line_highlight": null,
|
||||
// The width of the minimap in pixels.
|
||||
"width": 100,
|
||||
// The font size of the minimap in pixels.
|
||||
"font_size": 2
|
||||
"current_line_highlight": null
|
||||
},
|
||||
// Enable middle-click paste on Linux.
|
||||
"middle_click_paste": true,
|
||||
@@ -452,11 +475,17 @@
|
||||
// Scroll sensitivity multiplier. This multiplier is applied
|
||||
// to both the horizontal and vertical delta values while scrolling.
|
||||
"scroll_sensitivity": 1.0,
|
||||
// Scroll sensitivity multiplier for fast scrolling. This multiplier is applied
|
||||
// to both the horizontal and vertical delta values while scrolling. Fast scrolling
|
||||
// happens when a user holds the alt or option key while scrolling.
|
||||
"fast_scroll_sensitivity": 4.0,
|
||||
"relative_line_numbers": false,
|
||||
// If 'search_wrap' is disabled, search result do not wrap around the end of the file.
|
||||
"search_wrap": true,
|
||||
// Search options to enable by default when opening new project and buffer searches.
|
||||
"search": {
|
||||
// Whether to show the project search button in the status bar.
|
||||
"button": true,
|
||||
"whole_word": false,
|
||||
"case_sensitive": false,
|
||||
"include_ignored": false,
|
||||
@@ -700,14 +729,14 @@
|
||||
// The provider to use.
|
||||
"provider": "zed.dev",
|
||||
// The model to use.
|
||||
"model": "claude-3-7-sonnet-latest"
|
||||
"model": "claude-sonnet-4"
|
||||
},
|
||||
// The model to use when applying edits from the agent.
|
||||
"editor_model": {
|
||||
// The provider to use.
|
||||
"provider": "zed.dev",
|
||||
// The model to use.
|
||||
"model": "claude-3-7-sonnet-latest"
|
||||
"model": "claude-sonnet-4"
|
||||
},
|
||||
// Additional parameters for language model requests. When making a request to a model, parameters will be taken
|
||||
// from the last entry in this list that matches the model's provider and name. In each entry, both provider
|
||||
@@ -727,7 +756,7 @@
|
||||
// To set parameters for a specific provider and model:
|
||||
// {
|
||||
// "provider": "zed.dev",
|
||||
// "model": "claude-3-7-sonnet-latest",
|
||||
// "model": "claude-sonnet-4",
|
||||
// "temperature": 1.0
|
||||
// }
|
||||
],
|
||||
@@ -737,6 +766,8 @@
|
||||
"stream_edits": false,
|
||||
// When enabled, agent edits will be displayed in single-file editors for review
|
||||
"single_file_review": true,
|
||||
// When enabled, show voting thumbs for feedback on agent edits.
|
||||
"enable_feedback": true,
|
||||
"default_profile": "write",
|
||||
"profiles": {
|
||||
"write": {
|
||||
@@ -791,7 +822,12 @@
|
||||
// "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"
|
||||
"notify_when_agent_waiting": "primary_screen",
|
||||
// Whether to play a sound when the agent has either completed
|
||||
// its response, or needs user input.
|
||||
|
||||
// Default: false
|
||||
"play_sound_when_agent_done": false
|
||||
},
|
||||
// The settings for slash commands.
|
||||
"slash_commands": {
|
||||
@@ -923,7 +959,17 @@
|
||||
// "skip_focus_for_active_in_search": false
|
||||
//
|
||||
// Default: true
|
||||
"skip_focus_for_active_in_search": true
|
||||
"skip_focus_for_active_in_search": true,
|
||||
// Whether to show the git status in the file finder.
|
||||
"git_status": true,
|
||||
// Whether to use gitignored files when searching.
|
||||
// Only the file Zed had indexed will be used, not necessary all the gitignored files.
|
||||
//
|
||||
// Can accept 3 values:
|
||||
// * `true`: Use all gitignored files
|
||||
// * `false`: Use only the files Zed had indexed
|
||||
// * `null`: Be smart and search for ignored when called from a gitignored worktree
|
||||
"include_ignored": null
|
||||
},
|
||||
// Whether or not to remove any trailing whitespace from lines of a buffer
|
||||
// before saving it.
|
||||
@@ -989,6 +1035,8 @@
|
||||
"auto_update": true,
|
||||
// Diagnostics configuration.
|
||||
"diagnostics": {
|
||||
// Whether to show the project diagnostics button in the status bar.
|
||||
"button": true,
|
||||
// Whether to show warnings or not by default.
|
||||
"include_warnings": true,
|
||||
// Settings for inline diagnostics
|
||||
@@ -1006,7 +1054,7 @@
|
||||
// longer than this value will still push diagnostics further to the right.
|
||||
"min_column": 0,
|
||||
// The minimum severity of the diagnostics to show inline.
|
||||
// Shows all diagnostics when not specified.
|
||||
// Inherits editor's diagnostics' max severity settings when `null`.
|
||||
"max_severity": null
|
||||
},
|
||||
"cargo": {
|
||||
@@ -1284,21 +1332,22 @@
|
||||
"JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json", "**/.vscode/**/*.json"],
|
||||
"Shell Script": [".env.*"]
|
||||
},
|
||||
// By default use a recent system version of node, or install our own.
|
||||
// You can override this to use a version of node that is not in $PATH with:
|
||||
// {
|
||||
// "node": {
|
||||
// "path": "/path/to/node"
|
||||
// "npm_path": "/path/to/npm" (defaults to node_path/../npm)
|
||||
// }
|
||||
// }
|
||||
// or to ensure Zed always downloads and installs an isolated version of node:
|
||||
// {
|
||||
// "node": {
|
||||
// "ignore_system_version": true,
|
||||
// }
|
||||
// NOTE: changing this setting currently requires restarting Zed.
|
||||
"node": {},
|
||||
// Settings for which version of Node.js and NPM to use when installing
|
||||
// language servers and Copilot.
|
||||
//
|
||||
// Note: changing this setting currently requires restarting Zed.
|
||||
"node": {
|
||||
// By default, Zed will look for `node` and `npm` on your `$PATH`, and use the
|
||||
// existing executables if their version is recent enough. Set this to `true`
|
||||
// to prevent this, and force Zed to always download and install its own
|
||||
// version of Node.
|
||||
"ignore_system_version": false,
|
||||
// You can also specify alternative paths to Node and NPM. If you specify
|
||||
// `path`, but not `npm_path`, Zed will assume that `npm` is located at
|
||||
// `${path}/../npm`.
|
||||
"path": null,
|
||||
"npm_path": null
|
||||
},
|
||||
// The extensions that Zed should automatically install on startup.
|
||||
//
|
||||
// If you don't want any of these extensions, add this field to your settings
|
||||
@@ -1403,7 +1452,9 @@
|
||||
"language_servers": ["erlang-ls", "!elp", "..."]
|
||||
},
|
||||
"Git Commit": {
|
||||
"allow_rewrap": "anywhere"
|
||||
"allow_rewrap": "anywhere",
|
||||
"preferred_line_length": 72,
|
||||
"soft_wrap": "bounded"
|
||||
},
|
||||
"Go": {
|
||||
"code_actions_on_format": {
|
||||
@@ -1689,6 +1740,8 @@
|
||||
// }
|
||||
// ]
|
||||
"ssh_connections": [],
|
||||
// Whether to read ~/.ssh/config for ssh connection sources.
|
||||
"read_ssh_config": true,
|
||||
// Configures context servers for use by the agent.
|
||||
"context_servers": {},
|
||||
"debugger": {
|
||||
|
||||
@@ -1,32 +1,30 @@
|
||||
[
|
||||
{
|
||||
"label": "Debug active PHP file",
|
||||
"adapter": "php",
|
||||
"adapter": "PHP",
|
||||
"program": "$ZED_FILE",
|
||||
"request": "launch",
|
||||
"cwd": "$ZED_WORKTREE_ROOT"
|
||||
},
|
||||
{
|
||||
"label": "Debug active Python file",
|
||||
"adapter": "python",
|
||||
"adapter": "Debugpy",
|
||||
"program": "$ZED_FILE",
|
||||
"request": "launch",
|
||||
"cwd": "$ZED_WORKTREE_ROOT"
|
||||
},
|
||||
{
|
||||
"label": "Debug active JavaScript file",
|
||||
"adapter": "javascript",
|
||||
"adapter": "JavaScript",
|
||||
"program": "$ZED_FILE",
|
||||
"request": "launch",
|
||||
"cwd": "$ZED_WORKTREE_ROOT"
|
||||
},
|
||||
{
|
||||
"label": "JavaScript debug terminal",
|
||||
"adapter": "javascript",
|
||||
"adapter": "JavaScript",
|
||||
"request": "launch",
|
||||
"cwd": "$ZED_WORKTREE_ROOT",
|
||||
"initialize_args": {
|
||||
"console": "integratedTerminal"
|
||||
}
|
||||
"console": "integratedTerminal"
|
||||
}
|
||||
]
|
||||
|
||||
BIN
assets/sounds/agent_done.wav
Executable file
@@ -24,8 +24,9 @@ project.workspace = true
|
||||
smallvec.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
release_channel.workspace = true
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage};
|
||||
use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage, VersionCheckType};
|
||||
use editor::Editor;
|
||||
use extension_host::ExtensionStore;
|
||||
use futures::StreamExt;
|
||||
@@ -60,6 +60,7 @@ struct Content {
|
||||
message: String,
|
||||
on_click:
|
||||
Option<Arc<dyn Fn(&mut ActivityIndicator, &mut Window, &mut Context<ActivityIndicator>)>>,
|
||||
tooltip_message: Option<String>,
|
||||
}
|
||||
|
||||
impl ActivityIndicator {
|
||||
@@ -262,6 +263,7 @@ impl ActivityIndicator {
|
||||
});
|
||||
window.dispatch_action(Box::new(workspace::OpenLog), cx);
|
||||
})),
|
||||
tooltip_message: None,
|
||||
});
|
||||
}
|
||||
// Show any language server has pending activity.
|
||||
@@ -305,6 +307,7 @@ impl ActivityIndicator {
|
||||
),
|
||||
message,
|
||||
on_click: Some(Arc::new(Self::toggle_language_server_work_context_menu)),
|
||||
tooltip_message: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -332,6 +335,7 @@ impl ActivityIndicator {
|
||||
),
|
||||
message: job_info.message.into(),
|
||||
on_click: None,
|
||||
tooltip_message: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -374,6 +378,7 @@ impl ActivityIndicator {
|
||||
.retain(|status| !downloading.contains(&status.name));
|
||||
this.dismiss_error_message(&DismissErrorMessage, window, cx)
|
||||
})),
|
||||
tooltip_message: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -402,6 +407,7 @@ impl ActivityIndicator {
|
||||
.retain(|status| !checking_for_update.contains(&status.name));
|
||||
this.dismiss_error_message(&DismissErrorMessage, window, cx)
|
||||
})),
|
||||
tooltip_message: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -428,6 +434,7 @@ impl ActivityIndicator {
|
||||
on_click: Some(Arc::new(|this, window, cx| {
|
||||
this.show_error_message(&Default::default(), window, cx)
|
||||
})),
|
||||
tooltip_message: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -446,6 +453,7 @@ impl ActivityIndicator {
|
||||
});
|
||||
window.dispatch_action(Box::new(workspace::OpenLog), cx);
|
||||
})),
|
||||
tooltip_message: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -462,6 +470,7 @@ impl ActivityIndicator {
|
||||
on_click: Some(Arc::new(|this, window, cx| {
|
||||
this.dismiss_error_message(&DismissErrorMessage, window, cx)
|
||||
})),
|
||||
tooltip_message: None,
|
||||
}),
|
||||
AutoUpdateStatus::Downloading => Some(Content {
|
||||
icon: Some(
|
||||
@@ -473,6 +482,7 @@ impl ActivityIndicator {
|
||||
on_click: Some(Arc::new(|this, window, cx| {
|
||||
this.dismiss_error_message(&DismissErrorMessage, window, cx)
|
||||
})),
|
||||
tooltip_message: None,
|
||||
}),
|
||||
AutoUpdateStatus::Installing => Some(Content {
|
||||
icon: Some(
|
||||
@@ -484,8 +494,12 @@ impl ActivityIndicator {
|
||||
on_click: Some(Arc::new(|this, window, cx| {
|
||||
this.dismiss_error_message(&DismissErrorMessage, window, cx)
|
||||
})),
|
||||
tooltip_message: None,
|
||||
}),
|
||||
AutoUpdateStatus::Updated { binary_path } => Some(Content {
|
||||
AutoUpdateStatus::Updated {
|
||||
binary_path,
|
||||
version,
|
||||
} => Some(Content {
|
||||
icon: None,
|
||||
message: "Click to restart and update Zed".to_string(),
|
||||
on_click: Some(Arc::new({
|
||||
@@ -494,6 +508,7 @@ impl ActivityIndicator {
|
||||
};
|
||||
move |_, _, cx| workspace::reload(&reload, cx)
|
||||
})),
|
||||
tooltip_message: Some(Self::install_version_tooltip_message(&version)),
|
||||
}),
|
||||
AutoUpdateStatus::Errored => Some(Content {
|
||||
icon: Some(
|
||||
@@ -505,6 +520,7 @@ impl ActivityIndicator {
|
||||
on_click: Some(Arc::new(|this, window, cx| {
|
||||
this.dismiss_error_message(&DismissErrorMessage, window, cx)
|
||||
})),
|
||||
tooltip_message: None,
|
||||
}),
|
||||
AutoUpdateStatus::Idle => None,
|
||||
};
|
||||
@@ -524,6 +540,7 @@ impl ActivityIndicator {
|
||||
on_click: Some(Arc::new(|this, window, cx| {
|
||||
this.dismiss_error_message(&DismissErrorMessage, window, cx)
|
||||
})),
|
||||
tooltip_message: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -531,6 +548,17 @@ impl ActivityIndicator {
|
||||
None
|
||||
}
|
||||
|
||||
fn install_version_tooltip_message(version: &VersionCheckType) -> String {
|
||||
format!("Install version: {}", {
|
||||
match version {
|
||||
auto_update::VersionCheckType::Sha(sha) => format!("{}…", sha.short()),
|
||||
auto_update::VersionCheckType::Semantic(semantic_version) => {
|
||||
semantic_version.to_string()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn toggle_language_server_work_context_menu(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
@@ -575,7 +603,14 @@ impl Render for ActivityIndicator {
|
||||
)
|
||||
.tooltip(Tooltip::text(content.message))
|
||||
} else {
|
||||
button.child(Label::new(content.message).size(LabelSize::Small))
|
||||
button
|
||||
.child(Label::new(content.message).size(LabelSize::Small))
|
||||
.when_some(
|
||||
content.tooltip_message,
|
||||
|this, tooltip_message| {
|
||||
this.tooltip(Tooltip::text(tooltip_message))
|
||||
},
|
||||
)
|
||||
}
|
||||
})
|
||||
.when_some(content.on_click, |this, handler| {
|
||||
@@ -655,3 +690,26 @@ impl StatusItemView for ActivityIndicator {
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use gpui::SemanticVersion;
|
||||
use release_channel::AppCommitSha;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_install_version_tooltip_message() {
|
||||
let message = ActivityIndicator::install_version_tooltip_message(
|
||||
&VersionCheckType::Semantic(SemanticVersion::new(1, 0, 0)),
|
||||
);
|
||||
|
||||
assert_eq!(message, "Install version: 1.0.0");
|
||||
|
||||
let message = ActivityIndicator::install_version_tooltip_message(&VersionCheckType::Sha(
|
||||
AppCommitSha::new("14d9a4189f058d8736339b06ff2340101eaea5af".to_string()),
|
||||
));
|
||||
|
||||
assert_eq!(message, "Install version: 14d9a41…");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,13 +19,14 @@ test-support = [
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
agent_settings.workspace = true
|
||||
anyhow.workspace = true
|
||||
assistant_context_editor.workspace = true
|
||||
assistant_settings.workspace = true
|
||||
assistant_slash_command.workspace = true
|
||||
assistant_slash_commands.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
async-watch.workspace = true
|
||||
audio.workspace = true
|
||||
buffer_diff.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
@@ -47,12 +48,11 @@ heed.workspace = true
|
||||
html_to_markdown.workspace = true
|
||||
http_client.workspace = true
|
||||
indexed_docs.workspace = true
|
||||
inventory.workspace = true
|
||||
itertools.workspace = true
|
||||
jsonschema.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
language_model_selector.workspace = true
|
||||
linkme.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
markdown.workspace = true
|
||||
@@ -77,7 +77,6 @@ serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_json_lenient.workspace = true
|
||||
settings.workspace = true
|
||||
smallvec.workspace = true
|
||||
smol.workspace = true
|
||||
streaming_diff.workspace = true
|
||||
telemetry.workspace = true
|
||||
@@ -108,3 +107,4 @@ language = { workspace = true, "features" = ["test-support"] }
|
||||
language_model = { workspace = true, "features" = ["test-support"] }
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
rand.workspace = true
|
||||
tempfile.workspace = true
|
||||
|
||||
@@ -28,7 +28,7 @@ mod ui;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_settings::{AgentProfileId, AssistantSettings, LanguageModelSelection};
|
||||
use agent_settings::{AgentProfileId, AgentSettings, LanguageModelSelection};
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use client::Client;
|
||||
use feature_flags::FeatureFlagAppExt as _;
|
||||
@@ -49,7 +49,7 @@ pub use crate::context::{ContextLoadResult, LoadedContext};
|
||||
pub use crate::inline_assistant::InlineAssistant;
|
||||
use crate::slash_command_settings::SlashCommandSettings;
|
||||
pub use crate::thread::{Message, MessageSegment, Thread, ThreadEvent};
|
||||
pub use crate::thread_store::{TextThreadStore, ThreadStore};
|
||||
pub use crate::thread_store::{SerializedThread, TextThreadStore, ThreadStore};
|
||||
pub use agent_diff::{AgentDiffPane, AgentDiffToolbar};
|
||||
pub use context_store::ContextStore;
|
||||
pub use ui::preview::{all_agent_previews, get_agent_preview};
|
||||
@@ -69,6 +69,7 @@ actions!(
|
||||
AddContextServer,
|
||||
RemoveSelectedThread,
|
||||
Chat,
|
||||
ChatWithFollow,
|
||||
CycleNextInlineAssist,
|
||||
CyclePreviousInlineAssist,
|
||||
FocusUp,
|
||||
@@ -85,6 +86,9 @@ actions!(
|
||||
KeepAll,
|
||||
Follow,
|
||||
ResetTrialUpsell,
|
||||
ResetTrialEndUpsell,
|
||||
ContinueThread,
|
||||
ContinueWithBurnMode,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -116,14 +120,19 @@ pub fn init(
|
||||
client: Arc<Client>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
is_eval: bool,
|
||||
cx: &mut App,
|
||||
) {
|
||||
AssistantSettings::register(cx);
|
||||
AgentSettings::register(cx);
|
||||
SlashCommandSettings::register(cx);
|
||||
|
||||
assistant_context_editor::init(client.clone(), cx);
|
||||
rules_library::init(cx);
|
||||
init_language_model_settings(cx);
|
||||
if !is_eval {
|
||||
// Initializing the language model from the user settings messes with the eval, so we only initialize them when
|
||||
// we're not running inside of the eval.
|
||||
init_language_model_settings(cx);
|
||||
}
|
||||
assistant_slash_command::init(cx);
|
||||
thread_store::init(cx);
|
||||
agent_panel::init(cx);
|
||||
@@ -167,7 +176,7 @@ fn init_language_model_settings(cx: &mut App) {
|
||||
}
|
||||
|
||||
fn update_active_language_model_from_settings(cx: &mut App) {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
let settings = AgentSettings::get_global(cx);
|
||||
|
||||
fn to_selected_model(selection: &LanguageModelSelection) -> language_model::SelectedModel {
|
||||
language_model::SelectedModel {
|
||||
@@ -216,7 +225,6 @@ fn register_slash_commands(cx: &mut App) {
|
||||
slash_command_registry.register_command(assistant_slash_commands::PromptSlashCommand, true);
|
||||
slash_command_registry.register_command(assistant_slash_commands::SelectionCommand, true);
|
||||
slash_command_registry.register_command(assistant_slash_commands::DefaultSlashCommand, false);
|
||||
slash_command_registry.register_command(assistant_slash_commands::TerminalSlashCommand, true);
|
||||
slash_command_registry.register_command(assistant_slash_commands::NowSlashCommand, false);
|
||||
slash_command_registry
|
||||
.register_command(assistant_slash_commands::DiagnosticsSlashCommand, true);
|
||||
|
||||
@@ -5,7 +5,7 @@ mod tool_picker;
|
||||
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use assistant_settings::AssistantSettings;
|
||||
use agent_settings::AgentSettings;
|
||||
use assistant_tool::{ToolSource, ToolWorkingSet};
|
||||
use collections::HashMap;
|
||||
use context_server::ContextServerId;
|
||||
@@ -18,8 +18,8 @@ use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageMod
|
||||
use project::context_server_store::{ContextServerStatus, ContextServerStore};
|
||||
use settings::{Settings, update_settings_file};
|
||||
use ui::{
|
||||
Disclosure, Divider, DividerColor, ElevationIndex, Indicator, Scrollbar, ScrollbarState,
|
||||
Switch, SwitchColor, Tooltip, prelude::*,
|
||||
Disclosure, ElevationIndex, Indicator, Scrollbar, ScrollbarState, Switch, SwitchColor, Tooltip,
|
||||
prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use zed_actions::ExtensionCategoryFilter;
|
||||
@@ -36,6 +36,7 @@ pub struct AgentConfiguration {
|
||||
configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>,
|
||||
context_server_store: Entity<ContextServerStore>,
|
||||
expanded_context_server_tools: HashMap<ContextServerId, bool>,
|
||||
expanded_provider_configurations: HashMap<LanguageModelProviderId, bool>,
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
_registry_subscription: Subscription,
|
||||
scroll_handle: ScrollHandle,
|
||||
@@ -78,6 +79,7 @@ impl AgentConfiguration {
|
||||
configuration_views_by_provider: HashMap::default(),
|
||||
context_server_store,
|
||||
expanded_context_server_tools: HashMap::default(),
|
||||
expanded_provider_configurations: HashMap::default(),
|
||||
tools,
|
||||
_registry_subscription: registry_subscription,
|
||||
scroll_handle,
|
||||
@@ -96,6 +98,7 @@ impl AgentConfiguration {
|
||||
|
||||
fn remove_provider_configuration_view(&mut self, provider_id: &LanguageModelProviderId) {
|
||||
self.configuration_views_by_provider.remove(provider_id);
|
||||
self.expanded_provider_configurations.remove(provider_id);
|
||||
}
|
||||
|
||||
fn add_provider_configuration_view(
|
||||
@@ -135,9 +138,14 @@ impl AgentConfiguration {
|
||||
.get(&provider.id())
|
||||
.cloned();
|
||||
|
||||
let is_expanded = self
|
||||
.expanded_provider_configurations
|
||||
.get(&provider.id())
|
||||
.copied()
|
||||
.unwrap_or(false);
|
||||
|
||||
v_flex()
|
||||
.pt_3()
|
||||
.pb_1()
|
||||
.gap_1p5()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border.opacity(0.6))
|
||||
@@ -152,36 +160,63 @@ impl AgentConfiguration {
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Label::new(provider_name.clone()).size(LabelSize::Large)),
|
||||
.child(Label::new(provider_name.clone()).size(LabelSize::Large))
|
||||
.when(provider.is_authenticated(cx) && !is_expanded, |parent| {
|
||||
parent.child(Icon::new(IconName::Check).color(Color::Success))
|
||||
}),
|
||||
)
|
||||
.when(provider.is_authenticated(cx), |parent| {
|
||||
parent.child(
|
||||
Button::new(
|
||||
SharedString::from(format!("new-thread-{provider_id}")),
|
||||
"Start New Thread",
|
||||
)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon(IconName::Plus)
|
||||
.icon_size(IconSize::Small)
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click(cx.listener({
|
||||
let provider = provider.clone();
|
||||
move |_this, _event, _window, cx| {
|
||||
cx.emit(AssistantConfigurationEvent::NewThread(
|
||||
provider.clone(),
|
||||
))
|
||||
}
|
||||
})),
|
||||
)
|
||||
}),
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.when(provider.is_authenticated(cx), |parent| {
|
||||
parent.child(
|
||||
Button::new(
|
||||
SharedString::from(format!("new-thread-{provider_id}")),
|
||||
"Start New Thread",
|
||||
)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon(IconName::Plus)
|
||||
.icon_size(IconSize::Small)
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click(cx.listener({
|
||||
let provider = provider.clone();
|
||||
move |_this, _event, _window, cx| {
|
||||
cx.emit(AssistantConfigurationEvent::NewThread(
|
||||
provider.clone(),
|
||||
))
|
||||
}
|
||||
})),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
Disclosure::new(
|
||||
SharedString::from(format!(
|
||||
"provider-disclosure-{provider_id}"
|
||||
)),
|
||||
is_expanded,
|
||||
)
|
||||
.opened_icon(IconName::ChevronUp)
|
||||
.closed_icon(IconName::ChevronDown)
|
||||
.on_click(cx.listener({
|
||||
let provider_id = provider.id().clone();
|
||||
move |this, _event, _window, _cx| {
|
||||
let is_expanded = this
|
||||
.expanded_provider_configurations
|
||||
.entry(provider_id.clone())
|
||||
.or_insert(false);
|
||||
|
||||
*is_expanded = !*is_expanded;
|
||||
}
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
.map(|parent| match configuration_view {
|
||||
.when(is_expanded, |parent| match configuration_view {
|
||||
Some(configuration_view) => parent.child(configuration_view),
|
||||
None => parent.child(div().child(Label::new(format!(
|
||||
None => parent.child(Label::new(format!(
|
||||
"No configuration view for {provider_name}",
|
||||
)))),
|
||||
))),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -195,7 +230,8 @@ impl AgentConfiguration {
|
||||
.p(DynamicSpacing::Base16.rems(cx))
|
||||
.pr(DynamicSpacing::Base20.rems(cx))
|
||||
.gap_4()
|
||||
.flex_1()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
@@ -213,7 +249,7 @@ impl AgentConfiguration {
|
||||
}
|
||||
|
||||
fn render_command_permission(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let always_allow_tool_actions = AssistantSettings::get_global(cx).always_allow_tool_actions;
|
||||
let always_allow_tool_actions = AgentSettings::get_global(cx).always_allow_tool_actions;
|
||||
|
||||
h_flex()
|
||||
.gap_4()
|
||||
@@ -241,7 +277,7 @@ impl AgentConfiguration {
|
||||
let fs = self.fs.clone();
|
||||
move |state, _window, cx| {
|
||||
let allow = state == &ToggleState::Selected;
|
||||
update_settings_file::<AssistantSettings>(
|
||||
update_settings_file::<AgentSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _| {
|
||||
@@ -254,7 +290,7 @@ impl AgentConfiguration {
|
||||
}
|
||||
|
||||
fn render_single_file_review(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let single_file_review = AssistantSettings::get_global(cx).single_file_review;
|
||||
let single_file_review = AgentSettings::get_global(cx).single_file_review;
|
||||
|
||||
h_flex()
|
||||
.gap_4()
|
||||
@@ -279,7 +315,7 @@ impl AgentConfiguration {
|
||||
let fs = self.fs.clone();
|
||||
move |state, _window, cx| {
|
||||
let allow = state == &ToggleState::Selected;
|
||||
update_settings_file::<AssistantSettings>(
|
||||
update_settings_file::<AgentSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _| {
|
||||
@@ -291,15 +327,55 @@ impl AgentConfiguration {
|
||||
)
|
||||
}
|
||||
|
||||
fn render_sound_notification(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let play_sound_when_agent_done = AgentSettings::get_global(cx).play_sound_when_agent_done;
|
||||
|
||||
h_flex()
|
||||
.gap_4()
|
||||
.justify_between()
|
||||
.flex_wrap()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.max_w_5_6()
|
||||
.child(Label::new("Play sound when finished generating"))
|
||||
.child(
|
||||
Label::new(
|
||||
"Hear a notification sound when the agent is done generating changes or needs your input.",
|
||||
)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Switch::new("play-sound-notification-switch", play_sound_when_agent_done.into())
|
||||
.color(SwitchColor::Accent)
|
||||
.on_click({
|
||||
let fs = self.fs.clone();
|
||||
move |state, _window, cx| {
|
||||
let allow = state == &ToggleState::Selected;
|
||||
update_settings_file::<AgentSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _| {
|
||||
settings.set_play_sound_when_agent_done(allow);
|
||||
},
|
||||
);
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_general_settings_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.p(DynamicSpacing::Base16.rems(cx))
|
||||
.pr(DynamicSpacing::Base20.rems(cx))
|
||||
.gap_2p5()
|
||||
.flex_1()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(Headline::new("General Settings"))
|
||||
.child(self.render_command_permission(cx))
|
||||
.child(self.render_single_file_review(cx))
|
||||
.child(self.render_sound_notification(cx))
|
||||
}
|
||||
|
||||
fn render_context_servers_section(
|
||||
@@ -309,18 +385,17 @@ impl AgentConfiguration {
|
||||
) -> impl IntoElement {
|
||||
let context_server_ids = self.context_server_store.read(cx).all_server_ids().clone();
|
||||
|
||||
const SUBHEADING: &str = "Connect to context servers via the Model Context Protocol either via Zed extensions or directly.";
|
||||
|
||||
v_flex()
|
||||
.p(DynamicSpacing::Base16.rems(cx))
|
||||
.pr(DynamicSpacing::Base20.rems(cx))
|
||||
.gap_2()
|
||||
.flex_1()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(Headline::new("Model Context Protocol (MCP) Servers"))
|
||||
.child(Label::new(SUBHEADING).color(Color::Muted)),
|
||||
.child(Label::new("Connect to context servers via the Model Context Protocol either via Zed extensions or directly.").color(Color::Muted)),
|
||||
)
|
||||
.children(
|
||||
context_server_ids.into_iter().map(|context_server_id| {
|
||||
@@ -387,6 +462,7 @@ impl AgentConfiguration {
|
||||
.unwrap_or(ContextServerStatus::Stopped);
|
||||
|
||||
let is_running = matches!(server_status, ContextServerStatus::Running);
|
||||
let item_id = SharedString::from(context_server_id.0.clone());
|
||||
|
||||
let error = if let ContextServerStatus::Error(error) = server_status.clone() {
|
||||
Some(error)
|
||||
@@ -408,9 +484,38 @@ impl AgentConfiguration {
|
||||
let tool_count = tools.len();
|
||||
|
||||
let border_color = cx.theme().colors().border.opacity(0.6);
|
||||
let success_color = Color::Success.color(cx);
|
||||
|
||||
let (status_indicator, tooltip_text) = match server_status {
|
||||
ContextServerStatus::Starting => (
|
||||
Indicator::dot()
|
||||
.color(Color::Success)
|
||||
.with_animation(
|
||||
SharedString::from(format!("{}-starting", context_server_id.0.clone(),)),
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.4, 1.)),
|
||||
move |this, delta| this.color(success_color.alpha(delta).into()),
|
||||
)
|
||||
.into_any_element(),
|
||||
"Server is starting.",
|
||||
),
|
||||
ContextServerStatus::Running => (
|
||||
Indicator::dot().color(Color::Success).into_any_element(),
|
||||
"Server is running.",
|
||||
),
|
||||
ContextServerStatus::Error(_) => (
|
||||
Indicator::dot().color(Color::Error).into_any_element(),
|
||||
"Server has an error.",
|
||||
),
|
||||
ContextServerStatus::Stopped => (
|
||||
Indicator::dot().color(Color::Muted).into_any_element(),
|
||||
"Server is stopped.",
|
||||
),
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.id(SharedString::from(context_server_id.0.clone()))
|
||||
.id(item_id.clone())
|
||||
.border_1()
|
||||
.rounded_md()
|
||||
.border_color(border_color)
|
||||
@@ -445,35 +550,12 @@ impl AgentConfiguration {
|
||||
}
|
||||
})),
|
||||
)
|
||||
.child(match server_status {
|
||||
ContextServerStatus::Starting => {
|
||||
let color = Color::Success.color(cx);
|
||||
Indicator::dot()
|
||||
.color(Color::Success)
|
||||
.with_animation(
|
||||
SharedString::from(format!(
|
||||
"{}-starting",
|
||||
context_server_id.0.clone(),
|
||||
)),
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.4, 1.)),
|
||||
move |this, delta| {
|
||||
this.color(color.alpha(delta).into())
|
||||
},
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
ContextServerStatus::Running => {
|
||||
Indicator::dot().color(Color::Success).into_any_element()
|
||||
}
|
||||
ContextServerStatus::Error(_) => {
|
||||
Indicator::dot().color(Color::Error).into_any_element()
|
||||
}
|
||||
ContextServerStatus::Stopped => {
|
||||
Indicator::dot().color(Color::Muted).into_any_element()
|
||||
}
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.id(item_id.clone())
|
||||
.tooltip(Tooltip::text(tooltip_text))
|
||||
.child(status_indicator),
|
||||
)
|
||||
.child(Label::new(context_server_id.0.clone()).ml_0p5())
|
||||
.when(is_running, |this| {
|
||||
this.child(
|
||||
@@ -555,7 +637,7 @@ impl AgentConfiguration {
|
||||
.hover(|style| style.bg(cx.theme().colors().element_hover))
|
||||
.rounded_sm()
|
||||
.child(
|
||||
Label::new(tool.name())
|
||||
Label::new(tool.ui_name())
|
||||
.buffer_font(cx)
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
@@ -588,9 +670,7 @@ impl Render for AgentConfiguration {
|
||||
.size_full()
|
||||
.overflow_y_scroll()
|
||||
.child(self.render_general_settings_section(cx))
|
||||
.child(Divider::horizontal().color(DividerColor::Border))
|
||||
.child(self.render_context_servers_section(window, cx))
|
||||
.child(Divider::horizontal().color(DividerColor::Border))
|
||||
.child(self.render_provider_configuration_section(cx)),
|
||||
)
|
||||
.child(
|
||||
|
||||
@@ -30,7 +30,6 @@ pub(crate) struct ConfigureContextServerModal {
|
||||
context_server_store: Entity<ContextServerStore>,
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
enum Configuration {
|
||||
NotAvailable,
|
||||
Required(ConfigurationRequiredState),
|
||||
|
||||
@@ -2,7 +2,7 @@ mod profile_modal_header;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_settings::{AgentProfile, AgentProfileId, AssistantSettings, builtin_profiles};
|
||||
use agent_settings::{AgentProfile, AgentProfileId, AgentSettings, builtin_profiles};
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use convert_case::{Case, Casing as _};
|
||||
use editor::Editor;
|
||||
@@ -42,7 +42,7 @@ enum Mode {
|
||||
|
||||
impl Mode {
|
||||
pub fn choose_profile(_window: &mut Window, cx: &mut Context<ManageProfilesModal>) -> Self {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
let settings = AgentSettings::get_global(cx);
|
||||
|
||||
let mut builtin_profiles = Vec::new();
|
||||
let mut custom_profiles = Vec::new();
|
||||
@@ -196,7 +196,7 @@ impl ManageProfilesModal {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
let settings = AgentSettings::get_global(cx);
|
||||
let Some(profile) = settings.profiles.get(&profile_id).cloned() else {
|
||||
return;
|
||||
};
|
||||
@@ -234,7 +234,7 @@ impl ManageProfilesModal {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
let settings = AgentSettings::get_global(cx);
|
||||
let Some(profile) = settings.profiles.get(&profile_id).cloned() else {
|
||||
return;
|
||||
};
|
||||
@@ -270,7 +270,7 @@ impl ManageProfilesModal {
|
||||
match &self.mode {
|
||||
Mode::ChooseProfile { .. } => {}
|
||||
Mode::NewProfile(mode) => {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
let settings = AgentSettings::get_global(cx);
|
||||
|
||||
let base_profile = mode
|
||||
.base_profile_id
|
||||
@@ -332,7 +332,7 @@ impl ManageProfilesModal {
|
||||
profile: AgentProfile,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
update_settings_file::<AssistantSettings>(self.fs.clone(), cx, {
|
||||
update_settings_file::<AgentSettings>(self.fs.clone(), cx, {
|
||||
move |settings, _cx| {
|
||||
settings.create_profile(profile_id, profile).log_err();
|
||||
}
|
||||
@@ -485,7 +485,7 @@ impl ManageProfilesModal {
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
let settings = AgentSettings::get_global(cx);
|
||||
|
||||
let base_profile_name = mode.base_profile_id.as_ref().map(|base_profile_id| {
|
||||
settings
|
||||
@@ -518,7 +518,7 @@ impl ManageProfilesModal {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
let settings = AgentSettings::get_global(cx);
|
||||
|
||||
let profile_id = &settings.default_profile;
|
||||
let profile_name = settings
|
||||
@@ -712,7 +712,7 @@ impl ManageProfilesModal {
|
||||
|
||||
impl Render for ManageProfilesModal {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
let settings = AgentSettings::get_global(cx);
|
||||
|
||||
let go_back_item = div()
|
||||
.id("cancel-item")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::{collections::BTreeMap, sync::Arc};
|
||||
|
||||
use assistant_settings::{
|
||||
AgentProfile, AgentProfileContent, AgentProfileId, AssistantSettings, AssistantSettingsContent,
|
||||
use agent_settings::{
|
||||
AgentProfile, AgentProfileContent, AgentProfileId, AgentSettings, AgentSettingsContent,
|
||||
ContextServerPresetContent,
|
||||
};
|
||||
use assistant_tool::{ToolSource, ToolWorkingSet};
|
||||
@@ -117,7 +117,7 @@ impl ToolPickerDelegate {
|
||||
ToolSource::Native => {
|
||||
if mode == ToolPickerMode::BuiltinTools {
|
||||
items.extend(tools.into_iter().map(|tool| PickerItem::Tool {
|
||||
name: tool.name().into(),
|
||||
name: tool.ui_name().into(),
|
||||
server_id: None,
|
||||
}));
|
||||
}
|
||||
@@ -129,7 +129,7 @@ impl ToolPickerDelegate {
|
||||
server_id: server_id.clone(),
|
||||
});
|
||||
items.extend(tools.into_iter().map(|tool| PickerItem::Tool {
|
||||
name: tool.name().into(),
|
||||
name: tool.ui_name().into(),
|
||||
server_id: Some(server_id.clone()),
|
||||
}));
|
||||
}
|
||||
@@ -259,7 +259,7 @@ impl PickerDelegate for ToolPickerDelegate {
|
||||
is_enabled
|
||||
};
|
||||
|
||||
let active_profile_id = &AssistantSettings::get_global(cx).default_profile;
|
||||
let active_profile_id = &AgentSettings::get_global(cx).default_profile;
|
||||
if active_profile_id == &self.profile_id {
|
||||
self.thread_store
|
||||
.update(cx, |this, cx| {
|
||||
@@ -268,12 +268,12 @@ impl PickerDelegate for ToolPickerDelegate {
|
||||
.log_err();
|
||||
}
|
||||
|
||||
update_settings_file::<AssistantSettings>(self.fs.clone(), cx, {
|
||||
update_settings_file::<AgentSettings>(self.fs.clone(), cx, {
|
||||
let profile_id = self.profile_id.clone();
|
||||
let default_profile = self.profile.clone();
|
||||
let server_id = server_id.clone();
|
||||
let tool_name = tool_name.clone();
|
||||
move |settings: &mut AssistantSettingsContent, _cx| {
|
||||
move |settings: &mut AgentSettingsContent, _cx| {
|
||||
settings
|
||||
.v2_setting(|v2_settings| {
|
||||
let profiles = v2_settings.profiles.get_or_insert_default();
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use crate::{
|
||||
Keep, KeepAll, OpenAgentDiff, Reject, RejectAll, Thread, ThreadEvent, ui::AnimatedLabel,
|
||||
};
|
||||
use crate::{Keep, KeepAll, OpenAgentDiff, Reject, RejectAll, Thread, ThreadEvent};
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::Result;
|
||||
use assistant_settings::AssistantSettings;
|
||||
use buffer_diff::DiffHunkStatus;
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::{
|
||||
@@ -11,8 +9,9 @@ use editor::{
|
||||
scroll::Autoscroll,
|
||||
};
|
||||
use gpui::{
|
||||
Action, AnyElement, AnyView, App, AppContext, Empty, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, Global, SharedString, Subscription, Task, WeakEntity, Window, prelude::*,
|
||||
Action, Animation, AnimationExt, AnyElement, AnyView, App, AppContext, Empty, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, Global, SharedString, Subscription, Task, Transformation,
|
||||
WeakEntity, Window, percentage, prelude::*,
|
||||
};
|
||||
|
||||
use language::{Buffer, Capability, DiskState, OffsetRangeExt, Point};
|
||||
@@ -25,6 +24,7 @@ use std::{
|
||||
collections::hash_map::Entry,
|
||||
ops::Range,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use ui::{IconButtonShape, KeyBinding, Tooltip, prelude::*, vertical_divider};
|
||||
use util::ResultExt;
|
||||
@@ -215,11 +215,7 @@ impl AgentDiffPane {
|
||||
}
|
||||
|
||||
fn update_title(&mut self, cx: &mut Context<Self>) {
|
||||
let new_title = self
|
||||
.thread
|
||||
.read(cx)
|
||||
.summary()
|
||||
.unwrap_or("Agent Changes".into());
|
||||
let new_title = self.thread.read(cx).summary().unwrap_or("Agent Changes");
|
||||
if new_title != self.title {
|
||||
self.title = new_title;
|
||||
cx.emit(EditorEvent::TitleChanged);
|
||||
@@ -469,11 +465,7 @@ impl Item for AgentDiffPane {
|
||||
}
|
||||
|
||||
fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
|
||||
let summary = self
|
||||
.thread
|
||||
.read(cx)
|
||||
.summary()
|
||||
.unwrap_or("Agent Changes".into());
|
||||
let summary = self.thread.read(cx).summary().unwrap_or("Agent Changes");
|
||||
Label::new(format!("Review: {}", summary))
|
||||
.color(if params.selected {
|
||||
Color::Default
|
||||
@@ -707,7 +699,7 @@ fn render_diff_hunk_controls(
|
||||
.rounded_b_md()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.gap_1()
|
||||
.occlude()
|
||||
.stop_mouse_events_except_scroll()
|
||||
.shadow_md()
|
||||
.children(vec![
|
||||
Button::new(("reject", row as u64), "Reject")
|
||||
@@ -978,9 +970,20 @@ impl ToolbarItemView for AgentDiffToolbar {
|
||||
|
||||
impl Render for AgentDiffToolbar {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let generating_label = div()
|
||||
.w(rems_from_px(110.)) // Arbitrary size so the label doesn't dance around
|
||||
.child(AnimatedLabel::new("Generating"))
|
||||
let spinner_icon = div()
|
||||
.px_0p5()
|
||||
.id("generating")
|
||||
.tooltip(Tooltip::text("Generating Changes…"))
|
||||
.child(
|
||||
Icon::new(IconName::LoadCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Accent)
|
||||
.with_animation(
|
||||
"load_circle",
|
||||
Animation::new(Duration::from_secs(3)).repeat(),
|
||||
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||
),
|
||||
)
|
||||
.into_any();
|
||||
|
||||
let Some(active_item) = self.active_item.as_ref() else {
|
||||
@@ -997,7 +1000,7 @@ impl Render for AgentDiffToolbar {
|
||||
|
||||
let content = match state {
|
||||
EditorState::Idle => return Empty.into_any(),
|
||||
EditorState::Generating => vec![generating_label],
|
||||
EditorState::Generating => vec![spinner_icon],
|
||||
EditorState::Reviewing => vec![
|
||||
h_flex()
|
||||
.child(
|
||||
@@ -1115,7 +1118,7 @@ impl Render for AgentDiffToolbar {
|
||||
|
||||
let is_generating = agent_diff.read(cx).thread.read(cx).is_generating();
|
||||
if is_generating {
|
||||
return div().px_2().child(generating_label).into_any();
|
||||
return div().px_2().child(spinner_icon).into_any();
|
||||
}
|
||||
|
||||
let is_empty = agent_diff.read(cx).multibuffer.read(cx).is_empty();
|
||||
@@ -1250,9 +1253,9 @@ impl AgentDiff {
|
||||
|
||||
let settings_subscription = cx.observe_global_in::<SettingsStore>(window, {
|
||||
let workspace = workspace.clone();
|
||||
let mut was_active = AssistantSettings::get_global(cx).single_file_review;
|
||||
let mut was_active = AgentSettings::get_global(cx).single_file_review;
|
||||
move |this, window, cx| {
|
||||
let is_active = AssistantSettings::get_global(cx).single_file_review;
|
||||
let is_active = AgentSettings::get_global(cx).single_file_review;
|
||||
if was_active != is_active {
|
||||
was_active = is_active;
|
||||
this.update_reviewing_editors(&workspace, window, cx);
|
||||
@@ -1345,6 +1348,7 @@ impl AgentDiff {
|
||||
ThreadEvent::NewRequest
|
||||
| ThreadEvent::Stopped(Ok(StopReason::EndTurn))
|
||||
| ThreadEvent::Stopped(Ok(StopReason::MaxTokens))
|
||||
| ThreadEvent::Stopped(Ok(StopReason::Refusal))
|
||||
| ThreadEvent::Stopped(Err(_))
|
||||
| ThreadEvent::ShowError(_)
|
||||
| ThreadEvent::CompletionCanceled => {
|
||||
@@ -1457,7 +1461,7 @@ impl AgentDiff {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if !AssistantSettings::get_global(cx).single_file_review {
|
||||
if !AgentSettings::get_global(cx).single_file_review {
|
||||
for (editor, _) in self.reviewing_editors.drain() {
|
||||
editor
|
||||
.update(cx, |editor, cx| editor.end_temporary_diff_override(cx))
|
||||
@@ -1732,7 +1736,7 @@ impl editor::Addon for EditorAgentDiffAddon {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{Keep, ThreadStore, thread_store};
|
||||
use assistant_settings::AssistantSettings;
|
||||
use agent_settings::AgentSettings;
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use editor::EditorSettings;
|
||||
use gpui::{TestAppContext, UpdateGlobal, VisualTestContext};
|
||||
@@ -1751,7 +1755,7 @@ mod tests {
|
||||
cx.set_global(settings_store);
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
AssistantSettings::register(cx);
|
||||
AgentSettings::register(cx);
|
||||
prompt_store::init(cx);
|
||||
thread_store::init(cx);
|
||||
workspace::init_settings(cx);
|
||||
@@ -1907,7 +1911,7 @@ mod tests {
|
||||
cx.set_global(settings_store);
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
AssistantSettings::register(cx);
|
||||
AgentSettings::register(cx);
|
||||
prompt_store::init(cx);
|
||||
thread_store::init(cx);
|
||||
workspace::init_settings(cx);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use assistant_settings::AssistantSettings;
|
||||
use agent_settings::AgentSettings;
|
||||
use fs::Fs;
|
||||
use gpui::{Entity, FocusHandle, SharedString};
|
||||
|
||||
use crate::Thread;
|
||||
use language_model::{ConfiguredModel, LanguageModelRegistry};
|
||||
use language_model_selector::{
|
||||
use assistant_context_editor::language_model_selector::{
|
||||
LanguageModelSelector, LanguageModelSelectorPopoverMenu, ToggleModelSelector,
|
||||
};
|
||||
use language_model::{ConfiguredModel, LanguageModelRegistry};
|
||||
use settings::update_settings_file;
|
||||
use std::sync::Arc;
|
||||
use ui::{PopoverMenuHandle, Tooltip, prelude::*};
|
||||
@@ -63,7 +63,7 @@ impl AgentModelSelector {
|
||||
);
|
||||
}
|
||||
});
|
||||
update_settings_file::<AssistantSettings>(
|
||||
update_settings_file::<AgentSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _cx| {
|
||||
@@ -72,7 +72,7 @@ impl AgentModelSelector {
|
||||
);
|
||||
}
|
||||
ModelType::InlineAssistant => {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
update_settings_file::<AgentSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _cx| {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::context::ContextLoadResult;
|
||||
use crate::inline_prompt_editor::CodegenStatus;
|
||||
use crate::{context::load_context, context_store::ContextStore};
|
||||
use anyhow::Result;
|
||||
use assistant_settings::AssistantSettings;
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::{Context as _, Result};
|
||||
use client::telemetry::Telemetry;
|
||||
use collections::HashSet;
|
||||
use editor::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint};
|
||||
@@ -419,16 +419,16 @@ impl CodegenAlternative {
|
||||
if start_buffer.remote_id() == end_buffer.remote_id() {
|
||||
(start_buffer.clone(), start_buffer_offset..end_buffer_offset)
|
||||
} else {
|
||||
return Err(anyhow::anyhow!("invalid transformation range"));
|
||||
anyhow::bail!("invalid transformation range");
|
||||
}
|
||||
} else {
|
||||
return Err(anyhow::anyhow!("invalid transformation range"));
|
||||
anyhow::bail!("invalid transformation range");
|
||||
};
|
||||
|
||||
let prompt = self
|
||||
.builder
|
||||
.generate_inline_transformation_prompt(user_prompt, language_name, buffer, range)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to generate content prompt: {}", e))?;
|
||||
.context("generating content prompt")?;
|
||||
|
||||
let context_task = self.context_store.as_ref().map(|context_store| {
|
||||
if let Some(project) = self.project.upgrade() {
|
||||
@@ -443,7 +443,7 @@ impl CodegenAlternative {
|
||||
}
|
||||
});
|
||||
|
||||
let temperature = AssistantSettings::temperature_for_model(&model, cx);
|
||||
let temperature = AgentSettings::temperature_for_model(&model, cx);
|
||||
|
||||
Ok(cx.spawn(async move |_cx| {
|
||||
let mut request_message = LanguageModelRequestMessage {
|
||||
|
||||
@@ -586,10 +586,7 @@ impl ThreadContextHandle {
|
||||
}
|
||||
|
||||
pub fn title(&self, cx: &App) -> SharedString {
|
||||
self.thread
|
||||
.read(cx)
|
||||
.summary()
|
||||
.unwrap_or_else(|| "New thread".into())
|
||||
self.thread.read(cx).summary().or_default()
|
||||
}
|
||||
|
||||
fn load(self, cx: &App) -> Task<Option<(AgentContext, Vec<Entity<Buffer>>)>> {
|
||||
@@ -597,9 +594,7 @@ impl ThreadContextHandle {
|
||||
let text = Thread::wait_for_detailed_summary_or_text(&self.thread, cx).await?;
|
||||
let title = self
|
||||
.thread
|
||||
.read_with(cx, |thread, _cx| {
|
||||
thread.summary().unwrap_or_else(|| "New thread".into())
|
||||
})
|
||||
.read_with(cx, |thread, _cx| thread.summary().or_default())
|
||||
.ok()?;
|
||||
let context = AgentContext::Thread(ThreadContext {
|
||||
title,
|
||||
@@ -642,7 +637,7 @@ impl TextThreadContextHandle {
|
||||
}
|
||||
|
||||
pub fn title(&self, cx: &App) -> SharedString {
|
||||
self.context.read(cx).summary_or_default()
|
||||
self.context.read(cx).summary().or_default()
|
||||
}
|
||||
|
||||
fn load(self, cx: &App) -> Task<Option<(AgentContext, Vec<Entity<Buffer>>)>> {
|
||||
@@ -754,11 +749,11 @@ pub enum ImageStatus {
|
||||
|
||||
impl ImageContext {
|
||||
pub fn eq_for_key(&self, other: &Self) -> bool {
|
||||
self.original_image.id == other.original_image.id
|
||||
self.original_image.id() == other.original_image.id()
|
||||
}
|
||||
|
||||
pub fn hash_for_key<H: Hasher>(&self, state: &mut H) {
|
||||
self.original_image.id.hash(state);
|
||||
self.original_image.id().hash(state);
|
||||
}
|
||||
|
||||
pub fn image(&self) -> Option<LanguageModelImage> {
|
||||
@@ -830,23 +825,20 @@ pub fn load_context(
|
||||
prompt_store: &Option<Entity<PromptStore>>,
|
||||
cx: &mut App,
|
||||
) -> Task<ContextLoadResult> {
|
||||
let mut load_tasks = Vec::new();
|
||||
|
||||
for context in contexts.iter().cloned() {
|
||||
match context {
|
||||
AgentContextHandle::File(context) => load_tasks.push(context.load(cx)),
|
||||
AgentContextHandle::Directory(context) => {
|
||||
load_tasks.push(context.load(project.clone(), cx))
|
||||
}
|
||||
AgentContextHandle::Symbol(context) => load_tasks.push(context.load(cx)),
|
||||
AgentContextHandle::Selection(context) => load_tasks.push(context.load(cx)),
|
||||
AgentContextHandle::FetchedUrl(context) => load_tasks.push(context.load()),
|
||||
AgentContextHandle::Thread(context) => load_tasks.push(context.load(cx)),
|
||||
AgentContextHandle::TextThread(context) => load_tasks.push(context.load(cx)),
|
||||
AgentContextHandle::Rules(context) => load_tasks.push(context.load(prompt_store, cx)),
|
||||
AgentContextHandle::Image(context) => load_tasks.push(context.load(cx)),
|
||||
}
|
||||
}
|
||||
let load_tasks: Vec<_> = contexts
|
||||
.into_iter()
|
||||
.map(|context| match context {
|
||||
AgentContextHandle::File(context) => context.load(cx),
|
||||
AgentContextHandle::Directory(context) => context.load(project.clone(), cx),
|
||||
AgentContextHandle::Symbol(context) => context.load(cx),
|
||||
AgentContextHandle::Selection(context) => context.load(cx),
|
||||
AgentContextHandle::FetchedUrl(context) => context.load(),
|
||||
AgentContextHandle::Thread(context) => context.load(cx),
|
||||
AgentContextHandle::TextThread(context) => context.load(cx),
|
||||
AgentContextHandle::Rules(context) => context.load(prompt_store, cx),
|
||||
AgentContextHandle::Image(context) => context.load(cx),
|
||||
})
|
||||
.collect();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let load_results = future::join_all(load_tasks).await;
|
||||
|
||||
@@ -381,6 +381,16 @@ impl ContextPicker {
|
||||
cx.focus_self(window);
|
||||
}
|
||||
|
||||
pub fn select_first(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
match &self.mode {
|
||||
ContextPickerState::Default(entity) => entity.update(cx, |entity, cx| {
|
||||
entity.select_first(&Default::default(), window, cx)
|
||||
}),
|
||||
// Other variants already select their first entry on open automatically
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn recent_menu_item(
|
||||
&self,
|
||||
context_picker: Entity<ContextPicker>,
|
||||
@@ -756,6 +766,7 @@ pub(crate) fn insert_crease_for_mention(
|
||||
|
||||
let ids = editor.insert_creases(vec![crease.clone()], cx);
|
||||
editor.fold_creases(vec![crease], false, window, cx);
|
||||
|
||||
Some(ids[0])
|
||||
})
|
||||
}
|
||||
@@ -932,8 +943,8 @@ impl MentionLink {
|
||||
format!("[@{}]({}:{})", title, Self::THREAD, id)
|
||||
}
|
||||
ThreadContextEntry::Context { path, title } => {
|
||||
let filename = path.file_name().unwrap_or_default();
|
||||
let escaped_filename = urlencoding::encode(&filename.to_string_lossy()).to_string();
|
||||
let filename = path.file_name().unwrap_or_default().to_string_lossy();
|
||||
let escaped_filename = urlencoding::encode(&filename);
|
||||
format!(
|
||||
"[@{}]({}:{}{})",
|
||||
title,
|
||||
|
||||
@@ -322,7 +322,10 @@ impl ContextPickerCompletionProvider {
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let new_text = selection_infos.iter().map(|(_, link, _)| link).join(" ");
|
||||
let new_text = format!(
|
||||
"{} ",
|
||||
selection_infos.iter().map(|(_, link, _)| link).join(" ")
|
||||
);
|
||||
|
||||
let callback = Arc::new({
|
||||
let context_store = context_store.clone();
|
||||
@@ -420,7 +423,7 @@ impl ContextPickerCompletionProvider {
|
||||
} else {
|
||||
IconName::MessageBubbles
|
||||
};
|
||||
let new_text = MentionLink::for_thread(&thread_entry);
|
||||
let new_text = format!("{} ", MentionLink::for_thread(&thread_entry));
|
||||
let new_text_len = new_text.len();
|
||||
Completion {
|
||||
replace_range: source_range.clone(),
|
||||
@@ -435,7 +438,7 @@ impl ContextPickerCompletionProvider {
|
||||
thread_entry.title().clone(),
|
||||
excerpt_id,
|
||||
source_range.start,
|
||||
new_text_len,
|
||||
new_text_len - 1,
|
||||
editor.clone(),
|
||||
context_store.clone(),
|
||||
move |window, cx| match &thread_entry {
|
||||
@@ -489,7 +492,7 @@ impl ContextPickerCompletionProvider {
|
||||
editor: Entity<Editor>,
|
||||
context_store: Entity<ContextStore>,
|
||||
) -> Completion {
|
||||
let new_text = MentionLink::for_rule(&rules);
|
||||
let new_text = format!("{} ", MentionLink::for_rule(&rules));
|
||||
let new_text_len = new_text.len();
|
||||
Completion {
|
||||
replace_range: source_range.clone(),
|
||||
@@ -504,7 +507,7 @@ impl ContextPickerCompletionProvider {
|
||||
rules.title.clone(),
|
||||
excerpt_id,
|
||||
source_range.start,
|
||||
new_text_len,
|
||||
new_text_len - 1,
|
||||
editor.clone(),
|
||||
context_store.clone(),
|
||||
move |_, cx| {
|
||||
@@ -526,7 +529,7 @@ impl ContextPickerCompletionProvider {
|
||||
context_store: Entity<ContextStore>,
|
||||
http_client: Arc<HttpClientWithUrl>,
|
||||
) -> Completion {
|
||||
let new_text = MentionLink::for_fetch(&url_to_fetch);
|
||||
let new_text = format!("{} ", MentionLink::for_fetch(&url_to_fetch));
|
||||
let new_text_len = new_text.len();
|
||||
Completion {
|
||||
replace_range: source_range.clone(),
|
||||
@@ -541,7 +544,7 @@ impl ContextPickerCompletionProvider {
|
||||
url_to_fetch.clone(),
|
||||
excerpt_id,
|
||||
source_range.start,
|
||||
new_text_len,
|
||||
new_text_len - 1,
|
||||
editor.clone(),
|
||||
context_store.clone(),
|
||||
move |_, cx| {
|
||||
@@ -550,7 +553,7 @@ impl ContextPickerCompletionProvider {
|
||||
let url_to_fetch = url_to_fetch.clone();
|
||||
cx.spawn(async move |cx| {
|
||||
if let Some(context) = context_store
|
||||
.update(cx, |context_store, _| {
|
||||
.read_with(cx, |context_store, _| {
|
||||
context_store.get_url_context(url_to_fetch.clone())
|
||||
})
|
||||
.ok()?
|
||||
@@ -611,7 +614,7 @@ impl ContextPickerCompletionProvider {
|
||||
crease_icon_path.clone()
|
||||
};
|
||||
|
||||
let new_text = MentionLink::for_file(&file_name, &full_path);
|
||||
let new_text = format!("{} ", MentionLink::for_file(&file_name, &full_path));
|
||||
let new_text_len = new_text.len();
|
||||
Completion {
|
||||
replace_range: source_range.clone(),
|
||||
@@ -626,7 +629,7 @@ impl ContextPickerCompletionProvider {
|
||||
file_name,
|
||||
excerpt_id,
|
||||
source_range.start,
|
||||
new_text_len,
|
||||
new_text_len - 1,
|
||||
editor,
|
||||
context_store.clone(),
|
||||
move |_, cx| {
|
||||
@@ -682,7 +685,7 @@ impl ContextPickerCompletionProvider {
|
||||
label.push_str(" ", None);
|
||||
label.push_str(&file_name, comment_id);
|
||||
|
||||
let new_text = MentionLink::for_symbol(&symbol.name, &full_path);
|
||||
let new_text = format!("{} ", MentionLink::for_symbol(&symbol.name, &full_path));
|
||||
let new_text_len = new_text.len();
|
||||
Some(Completion {
|
||||
replace_range: source_range.clone(),
|
||||
@@ -697,7 +700,7 @@ impl ContextPickerCompletionProvider {
|
||||
symbol.name.clone().into(),
|
||||
excerpt_id,
|
||||
source_range.start,
|
||||
new_text_len,
|
||||
new_text_len - 1,
|
||||
editor.clone(),
|
||||
context_store.clone(),
|
||||
move |_, cx| {
|
||||
@@ -1213,7 +1216,7 @@ mod tests {
|
||||
assert_eq!(worktrees.len(), 1);
|
||||
worktrees.pop().unwrap()
|
||||
});
|
||||
let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
|
||||
let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
|
||||
|
||||
let mut cx = VisualTestContext::from_window(*window.deref(), cx);
|
||||
|
||||
@@ -1286,7 +1289,7 @@ mod tests {
|
||||
.map(Entity::downgrade)
|
||||
});
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
editor.set_completion_provider(Some(Box::new(ContextPickerCompletionProvider::new(
|
||||
editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
|
||||
workspace.downgrade(),
|
||||
context_store.downgrade(),
|
||||
None,
|
||||
@@ -1353,7 +1356,7 @@ mod tests {
|
||||
});
|
||||
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(editor.text(cx), "Lorem [@one.txt](@file:dir/a/one.txt)",);
|
||||
assert_eq!(editor.text(cx), "Lorem [@one.txt](@file:dir/a/one.txt) ");
|
||||
assert!(!editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
fold_ranges(editor, cx),
|
||||
@@ -1364,7 +1367,7 @@ mod tests {
|
||||
cx.simulate_input(" ");
|
||||
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(editor.text(cx), "Lorem [@one.txt](@file:dir/a/one.txt) ",);
|
||||
assert_eq!(editor.text(cx), "Lorem [@one.txt](@file:dir/a/one.txt) ");
|
||||
assert!(!editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
fold_ranges(editor, cx),
|
||||
@@ -1377,7 +1380,7 @@ mod tests {
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum ",
|
||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum ",
|
||||
);
|
||||
assert!(!editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
@@ -1391,7 +1394,7 @@ mod tests {
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum @file ",
|
||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum @file ",
|
||||
);
|
||||
assert!(editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
@@ -1409,14 +1412,14 @@ mod tests {
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@seven.txt](@file:dir/b/seven.txt)"
|
||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@seven.txt](@file:dir/b/seven.txt) "
|
||||
);
|
||||
assert!(!editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
fold_ranges(editor, cx),
|
||||
vec![
|
||||
Point::new(0, 6)..Point::new(0, 37),
|
||||
Point::new(0, 44)..Point::new(0, 79)
|
||||
Point::new(0, 45)..Point::new(0, 80)
|
||||
]
|
||||
);
|
||||
});
|
||||
@@ -1426,14 +1429,14 @@ mod tests {
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@seven.txt](@file:dir/b/seven.txt)\n@"
|
||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@seven.txt](@file:dir/b/seven.txt) \n@"
|
||||
);
|
||||
assert!(editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
fold_ranges(editor, cx),
|
||||
vec![
|
||||
Point::new(0, 6)..Point::new(0, 37),
|
||||
Point::new(0, 44)..Point::new(0, 79)
|
||||
Point::new(0, 45)..Point::new(0, 80)
|
||||
]
|
||||
);
|
||||
});
|
||||
@@ -1447,14 +1450,14 @@ mod tests {
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@seven.txt](@file:dir/b/seven.txt)\n[@six.txt](@file:dir/b/six.txt)"
|
||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@seven.txt](@file:dir/b/seven.txt) \n[@six.txt](@file:dir/b/six.txt) "
|
||||
);
|
||||
assert!(!editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
fold_ranges(editor, cx),
|
||||
vec![
|
||||
Point::new(0, 6)..Point::new(0, 37),
|
||||
Point::new(0, 44)..Point::new(0, 79),
|
||||
Point::new(0, 45)..Point::new(0, 80),
|
||||
Point::new(1, 0)..Point::new(1, 31)
|
||||
]
|
||||
);
|
||||
|
||||
@@ -30,6 +30,10 @@ impl ContextServerTool {
|
||||
|
||||
impl Tool for ContextServerTool {
|
||||
fn name(&self) -> String {
|
||||
format!("{}-{}", self.server_id, self.tool.name)
|
||||
}
|
||||
|
||||
fn ui_name(&self) -> String {
|
||||
self.tool.name.clone()
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::ops::Range;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_context_editor::AssistantContext;
|
||||
use collections::{HashSet, IndexSet};
|
||||
use futures::{self, FutureExt};
|
||||
@@ -58,9 +58,10 @@ impl ContextStore {
|
||||
self.context_set.iter().map(|entry| entry.as_ref())
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
pub fn clear(&mut self, cx: &mut Context<Self>) {
|
||||
self.context_set.clear();
|
||||
self.context_thread_ids.clear();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn new_context_for_thread(
|
||||
@@ -142,17 +143,12 @@ impl ContextStore {
|
||||
remove_if_exists: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Result<Option<AgentContextHandle>> {
|
||||
let Some(project) = self.project.upgrade() else {
|
||||
return Err(anyhow!("failed to read project"));
|
||||
};
|
||||
|
||||
let Some(entry_id) = project
|
||||
let project = self.project.upgrade().context("failed to read project")?;
|
||||
let entry_id = project
|
||||
.read(cx)
|
||||
.entry_for_path(project_path, cx)
|
||||
.map(|entry| entry.id)
|
||||
else {
|
||||
return Err(anyhow!("no entry found for directory context"));
|
||||
};
|
||||
.context("no entry found for directory context")?;
|
||||
|
||||
let context_id = self.next_context_id.post_inc();
|
||||
let context = AgentContextHandle::Directory(DirectoryContextHandle {
|
||||
|
||||
@@ -84,6 +84,12 @@ impl ContextStrip {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether or not the context strip has items to display
|
||||
pub fn has_context_items(&self, cx: &App) -> bool {
|
||||
self.context_store.read(cx).context().next().is_some()
|
||||
|| self.suggested_context(cx).is_some()
|
||||
}
|
||||
|
||||
fn added_contexts(&self, cx: &App) -> Vec<AddedContext> {
|
||||
if let Some(workspace) = self.workspace.upgrade() {
|
||||
let project = workspace.read(cx).project().read(cx);
|
||||
@@ -104,14 +110,14 @@ impl ContextStrip {
|
||||
}
|
||||
}
|
||||
|
||||
fn suggested_context(&self, cx: &Context<Self>) -> Option<SuggestedContext> {
|
||||
fn suggested_context(&self, cx: &App) -> Option<SuggestedContext> {
|
||||
match self.suggest_context_kind {
|
||||
SuggestContextKind::File => self.suggested_file(cx),
|
||||
SuggestContextKind::Thread => self.suggested_thread(cx),
|
||||
}
|
||||
}
|
||||
|
||||
fn suggested_file(&self, cx: &Context<Self>) -> Option<SuggestedContext> {
|
||||
fn suggested_file(&self, cx: &App) -> Option<SuggestedContext> {
|
||||
let workspace = self.workspace.upgrade()?;
|
||||
let active_item = workspace.read(cx).active_item(cx)?;
|
||||
|
||||
@@ -138,7 +144,7 @@ impl ContextStrip {
|
||||
})
|
||||
}
|
||||
|
||||
fn suggested_thread(&self, cx: &Context<Self>) -> Option<SuggestedContext> {
|
||||
fn suggested_thread(&self, cx: &App) -> Option<SuggestedContext> {
|
||||
if !self.context_picker.read(cx).allow_threads() {
|
||||
return None;
|
||||
}
|
||||
@@ -160,7 +166,7 @@ impl ContextStrip {
|
||||
}
|
||||
|
||||
Some(SuggestedContext::Thread {
|
||||
name: active_thread.summary_or_default(),
|
||||
name: active_thread.summary().or_default(),
|
||||
thread: weak_active_thread,
|
||||
})
|
||||
} else if let Some(active_context_editor) = panel.active_context_editor() {
|
||||
@@ -174,7 +180,7 @@ impl ContextStrip {
|
||||
}
|
||||
|
||||
Some(SuggestedContext::TextThread {
|
||||
name: context.summary_or_default(),
|
||||
name: context.summary().or_default(),
|
||||
context: weak_context,
|
||||
})
|
||||
} else {
|
||||
@@ -420,12 +426,25 @@ impl Render for ContextStrip {
|
||||
})
|
||||
.child(
|
||||
PopoverMenu::new("context-picker")
|
||||
.menu(move |window, cx| {
|
||||
context_picker.update(cx, |this, cx| {
|
||||
this.init(window, cx);
|
||||
});
|
||||
.menu({
|
||||
let context_picker = context_picker.clone();
|
||||
move |window, cx| {
|
||||
context_picker.update(cx, |this, cx| {
|
||||
this.init(window, cx);
|
||||
});
|
||||
|
||||
Some(context_picker.clone())
|
||||
Some(context_picker.clone())
|
||||
}
|
||||
})
|
||||
.on_open({
|
||||
let context_picker = context_picker.downgrade();
|
||||
Rc::new(move |window, cx| {
|
||||
context_picker
|
||||
.update(cx, |context_picker, cx| {
|
||||
context_picker.select_first(window, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
})
|
||||
.trigger_with_tooltip(
|
||||
IconButton::new("add-context", IconName::Plus)
|
||||
|
||||
@@ -75,7 +75,7 @@ impl Default for DebugAccountState {
|
||||
Self {
|
||||
enabled: false,
|
||||
trial_expired: false,
|
||||
plan: Plan::Free,
|
||||
plan: Plan::ZedFree,
|
||||
custom_prompt_usage: RequestUsage {
|
||||
limit: UsageLimit::Unlimited,
|
||||
amount: 0,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::{collections::VecDeque, path::Path};
|
||||
use std::{collections::VecDeque, path::Path, sync::Arc};
|
||||
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use anyhow::Context as _;
|
||||
use assistant_context_editor::{AssistantContext, SavedContextMetadata};
|
||||
use chrono::{DateTime, Utc};
|
||||
use futures::future::{TryFutureExt as _, join_all};
|
||||
@@ -34,6 +34,20 @@ impl HistoryEntry {
|
||||
HistoryEntry::Context(context) => context.mtime.to_utc(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> HistoryEntryId {
|
||||
match self {
|
||||
HistoryEntry::Thread(thread) => HistoryEntryId::Thread(thread.id.clone()),
|
||||
HistoryEntry::Context(context) => HistoryEntryId::Context(context.path.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generic identifier for a history entry.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub enum HistoryEntryId {
|
||||
Thread(ThreadId),
|
||||
Context(Arc<Path>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -57,8 +71,8 @@ impl Eq for RecentEntry {}
|
||||
impl RecentEntry {
|
||||
pub(crate) fn summary(&self, cx: &App) -> SharedString {
|
||||
match self {
|
||||
RecentEntry::Thread(_, thread) => thread.read(cx).summary_or_default(),
|
||||
RecentEntry::Context(context) => context.read(cx).summary_or_default(),
|
||||
RecentEntry::Thread(_, thread) => thread.read(cx).summary().or_default(),
|
||||
RecentEntry::Context(context) => context.read(cx).summary().or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,7 +130,10 @@ impl HistoryStore {
|
||||
.boxed()
|
||||
})
|
||||
.unwrap_or_else(|_| {
|
||||
async { Err(anyhow!("no thread store")) }.boxed()
|
||||
async {
|
||||
anyhow::bail!("no thread store");
|
||||
}
|
||||
.boxed()
|
||||
}),
|
||||
SerializedRecentEntry::Context(id) => context_store
|
||||
.update(cx, |context_store, cx| {
|
||||
@@ -126,7 +143,10 @@ impl HistoryStore {
|
||||
.boxed()
|
||||
})
|
||||
.unwrap_or_else(|_| {
|
||||
async { Err(anyhow!("no context store")) }.boxed()
|
||||
async {
|
||||
anyhow::bail!("no context store");
|
||||
}
|
||||
.boxed()
|
||||
}),
|
||||
});
|
||||
let entries = join_all(entries)
|
||||
|
||||
@@ -4,8 +4,8 @@ use std::ops::Range;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::{Context as _, Result};
|
||||
use assistant_settings::AssistantSettings;
|
||||
use client::telemetry::Telemetry;
|
||||
use collections::{HashMap, HashSet, VecDeque, hash_map};
|
||||
use editor::display_map::EditorMargins;
|
||||
@@ -134,7 +134,7 @@ impl InlineAssistant {
|
||||
let Some(terminal_panel) = workspace.read(cx).panel::<TerminalPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
let enabled = AssistantSettings::get_global(cx).enabled;
|
||||
let enabled = AgentSettings::get_global(cx).enabled;
|
||||
terminal_panel.update(cx, |terminal_panel, cx| {
|
||||
terminal_panel.set_assistant_enabled(enabled, cx)
|
||||
});
|
||||
@@ -219,7 +219,7 @@ impl InlineAssistant {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
let settings = AgentSettings::get_global(cx);
|
||||
if !settings.enabled {
|
||||
return;
|
||||
}
|
||||
@@ -338,13 +338,27 @@ impl InlineAssistant {
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let (snapshot, initial_selections) = editor.update(cx, |editor, cx| {
|
||||
(
|
||||
editor.snapshot(window, cx),
|
||||
editor.selections.all::<Point>(cx),
|
||||
)
|
||||
let (snapshot, initial_selections, newest_selection) = editor.update(cx, |editor, cx| {
|
||||
let selections = editor.selections.all::<Point>(cx);
|
||||
let newest_selection = editor.selections.newest::<Point>(cx);
|
||||
(editor.snapshot(window, cx), selections, newest_selection)
|
||||
});
|
||||
|
||||
// Check if there is already an inline assistant that contains the
|
||||
// newest selection, if there is, focus it
|
||||
if let Some(editor_assists) = self.assists_by_editor.get(&editor.downgrade()) {
|
||||
for assist_id in &editor_assists.assist_ids {
|
||||
let assist = &self.assists[assist_id];
|
||||
let range = assist.range.to_point(&snapshot.buffer_snapshot);
|
||||
if range.start.row <= newest_selection.start.row
|
||||
&& newest_selection.end.row <= range.end.row
|
||||
{
|
||||
self.focus_assist(*assist_id, window, cx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut selections = Vec::<Selection<Point>>::new();
|
||||
let mut newest_selection = None;
|
||||
for mut selection in initial_selections {
|
||||
@@ -1407,7 +1421,7 @@ impl InlineAssistant {
|
||||
|
||||
enum DeletedLines {}
|
||||
let mut editor = Editor::for_multibuffer(multi_buffer, None, window, cx);
|
||||
editor.disable_scrollbars_and_minimap(cx);
|
||||
editor.disable_scrollbars_and_minimap(window, cx);
|
||||
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx);
|
||||
editor.set_show_wrap_guides(false, cx);
|
||||
editor.set_show_gutter(false, cx);
|
||||
@@ -1757,7 +1771,7 @@ impl CodeActionProvider for AssistantCodeActionProvider {
|
||||
_: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Vec<CodeAction>>> {
|
||||
if !AssistantSettings::get_global(cx).enabled {
|
||||
if !AgentSettings::get_global(cx).enabled {
|
||||
return Task::ready(Ok(Vec::new()));
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,10 @@ use crate::terminal_codegen::TerminalCodegen;
|
||||
use crate::thread_store::{TextThreadStore, ThreadStore};
|
||||
use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist};
|
||||
use crate::{RemoveAllContext, ToggleContextPicker};
|
||||
use assistant_context_editor::language_model_selector::ToggleModelSelector;
|
||||
use client::ErrorExt;
|
||||
use collections::VecDeque;
|
||||
use db::kvp::Dismissable;
|
||||
use editor::display_map::EditorMargins;
|
||||
use editor::{
|
||||
ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
|
||||
@@ -23,17 +25,16 @@ use gpui::{
|
||||
Focusable, FontWeight, Subscription, TextStyle, WeakEntity, Window, anchored, deferred, point,
|
||||
};
|
||||
use language_model::{LanguageModel, LanguageModelRegistry};
|
||||
use language_model_selector::ToggleModelSelector;
|
||||
use parking_lot::Mutex;
|
||||
use settings::Settings;
|
||||
use std::cmp;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use theme::ThemeSettings;
|
||||
use ui::utils::WithRemSize;
|
||||
use ui::{
|
||||
CheckboxWithLabel, IconButtonShape, KeyBinding, Popover, PopoverMenuHandle, Tooltip, prelude::*,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub struct PromptEditor<T> {
|
||||
@@ -326,9 +327,7 @@ impl<T: 'static> PromptEditor<T> {
|
||||
EditorEvent::Edited { .. } => {
|
||||
if let Some(workspace) = window.root::<Workspace>().flatten() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
let is_via_ssh = workspace
|
||||
.project()
|
||||
.update(cx, |project, _| project.is_via_ssh());
|
||||
let is_via_ssh = workspace.project().read(cx).is_via_ssh();
|
||||
|
||||
workspace
|
||||
.client()
|
||||
@@ -373,7 +372,7 @@ impl<T: 'static> PromptEditor<T> {
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.context_store.update(cx, |store, _cx| store.clear());
|
||||
self.context_store.update(cx, |store, cx| store.clear(cx));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -451,7 +450,7 @@ impl<T: 'static> PromptEditor<T> {
|
||||
editor.move_to_end(&Default::default(), window, cx)
|
||||
});
|
||||
}
|
||||
} else {
|
||||
} else if self.context_strip.read(cx).has_context_items(cx) {
|
||||
self.context_strip.focus_handle(cx).focus(window);
|
||||
}
|
||||
}
|
||||
@@ -722,7 +721,7 @@ impl<T: 'static> PromptEditor<T> {
|
||||
.child(CheckboxWithLabel::new(
|
||||
"dont-show-again",
|
||||
Label::new("Don't show again"),
|
||||
if dismissed_rate_limit_notice() {
|
||||
if RateLimitNotice::dismissed() {
|
||||
ui::ToggleState::Selected
|
||||
} else {
|
||||
ui::ToggleState::Unselected
|
||||
@@ -734,7 +733,7 @@ impl<T: 'static> PromptEditor<T> {
|
||||
ui::ToggleState::Selected => true,
|
||||
};
|
||||
|
||||
set_rate_limit_notice_dismissed(is_dismissed, cx)
|
||||
RateLimitNotice::set_dismissed(is_dismissed, cx);
|
||||
},
|
||||
))
|
||||
.child(
|
||||
@@ -892,7 +891,7 @@ impl PromptEditor<BufferCodegen> {
|
||||
|
||||
let prompt_editor_entity = prompt_editor.downgrade();
|
||||
prompt_editor.update(cx, |editor, _| {
|
||||
editor.set_completion_provider(Some(Box::new(ContextPickerCompletionProvider::new(
|
||||
editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
|
||||
workspace.clone(),
|
||||
context_store.downgrade(),
|
||||
thread_store.clone(),
|
||||
@@ -974,7 +973,7 @@ impl PromptEditor<BufferCodegen> {
|
||||
CodegenStatus::Error(error) => {
|
||||
if cx.has_flag::<ZedProFeatureFlag>()
|
||||
&& error.error_code() == proto::ErrorCode::RateLimitExceeded
|
||||
&& !dismissed_rate_limit_notice()
|
||||
&& !RateLimitNotice::dismissed()
|
||||
{
|
||||
self.show_rate_limit_notice = true;
|
||||
cx.notify();
|
||||
@@ -1063,7 +1062,7 @@ impl PromptEditor<TerminalCodegen> {
|
||||
|
||||
let prompt_editor_entity = prompt_editor.downgrade();
|
||||
prompt_editor.update(cx, |editor, _| {
|
||||
editor.set_completion_provider(Some(Box::new(ContextPickerCompletionProvider::new(
|
||||
editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
|
||||
workspace.clone(),
|
||||
context_store.downgrade(),
|
||||
thread_store.clone(),
|
||||
@@ -1180,27 +1179,10 @@ impl PromptEditor<TerminalCodegen> {
|
||||
}
|
||||
}
|
||||
|
||||
const DISMISSED_RATE_LIMIT_NOTICE_KEY: &str = "dismissed-rate-limit-notice";
|
||||
struct RateLimitNotice;
|
||||
|
||||
fn dismissed_rate_limit_notice() -> bool {
|
||||
db::kvp::KEY_VALUE_STORE
|
||||
.read_kvp(DISMISSED_RATE_LIMIT_NOTICE_KEY)
|
||||
.log_err()
|
||||
.map_or(false, |s| s.is_some())
|
||||
}
|
||||
|
||||
fn set_rate_limit_notice_dismissed(is_dismissed: bool, cx: &mut App) {
|
||||
db::write_and_log(cx, move || async move {
|
||||
if is_dismissed {
|
||||
db::kvp::KEY_VALUE_STORE
|
||||
.write_kvp(DISMISSED_RATE_LIMIT_NOTICE_KEY.into(), "1".into())
|
||||
.await
|
||||
} else {
|
||||
db::kvp::KEY_VALUE_STORE
|
||||
.delete_kvp(DISMISSED_RATE_LIMIT_NOTICE_KEY.into())
|
||||
.await
|
||||
}
|
||||
})
|
||||
impl Dismissable for RateLimitNotice {
|
||||
const KEY: &'static str = "dismissed-rate-limit-notice";
|
||||
}
|
||||
|
||||
pub enum CodegenStatus {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::agent_model_selector::{AgentModelSelector, ModelType};
|
||||
@@ -8,7 +9,8 @@ use crate::ui::{
|
||||
AnimatedLabel, MaxModeTooltip,
|
||||
preview::{AgentPreview, UsageCallout},
|
||||
};
|
||||
use assistant_settings::{AssistantSettings, CompletionMode};
|
||||
use agent_settings::{AgentSettings, CompletionMode};
|
||||
use assistant_context_editor::language_model_selector::ToggleModelSelector;
|
||||
use buffer_diff::BufferDiff;
|
||||
use client::UserStore;
|
||||
use collections::{HashMap, HashSet};
|
||||
@@ -30,7 +32,6 @@ use language_model::{
|
||||
ConfiguredModel, LanguageModelRequestMessage, MessageContent, RequestUsage,
|
||||
ZED_CLOUD_PROVIDER_ID,
|
||||
};
|
||||
use language_model_selector::ToggleModelSelector;
|
||||
use multi_buffer;
|
||||
use project::Project;
|
||||
use prompt_store::PromptStore;
|
||||
@@ -49,8 +50,9 @@ use crate::profile_selector::ProfileSelector;
|
||||
use crate::thread::{MessageCrease, Thread, TokenUsageRatio};
|
||||
use crate::thread_store::{TextThreadStore, ThreadStore};
|
||||
use crate::{
|
||||
ActiveThread, AgentDiffPane, Chat, ExpandMessageEditor, Follow, NewThread, OpenAgentDiff,
|
||||
RemoveAllContext, ToggleContextPicker, ToggleProfileSelector, register_agent_preview,
|
||||
ActiveThread, AgentDiffPane, Chat, ChatWithFollow, ExpandMessageEditor, Follow, NewThread,
|
||||
OpenAgentDiff, RemoveAllContext, ToggleContextPicker, ToggleProfileSelector,
|
||||
register_agent_preview,
|
||||
};
|
||||
|
||||
#[derive(RegisterComponent)]
|
||||
@@ -120,7 +122,7 @@ pub(crate) fn create_editor(
|
||||
|
||||
let editor_entity = editor.downgrade();
|
||||
editor.update(cx, |editor, _| {
|
||||
editor.set_completion_provider(Some(Box::new(ContextPickerCompletionProvider::new(
|
||||
editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
|
||||
workspace,
|
||||
context_store,
|
||||
Some(thread_store),
|
||||
@@ -200,7 +202,13 @@ impl MessageEditor {
|
||||
});
|
||||
|
||||
let profile_selector = cx.new(|cx| {
|
||||
ProfileSelector::new(thread.clone(), thread_store, editor.focus_handle(cx), cx)
|
||||
ProfileSelector::new(
|
||||
fs,
|
||||
thread.clone(),
|
||||
thread_store,
|
||||
editor.focus_handle(cx),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
Self {
|
||||
@@ -272,7 +280,7 @@ impl MessageEditor {
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.context_store.update(cx, |store, _cx| store.clear());
|
||||
self.context_store.update(cx, |store, cx| store.clear(cx));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -296,6 +304,21 @@ impl MessageEditor {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn chat_with_follow(
|
||||
&mut self,
|
||||
_: &ChatWithFollow,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.workspace
|
||||
.update(cx, |this, cx| {
|
||||
this.follow(CollaboratorId::Agent, window, cx)
|
||||
})
|
||||
.log_err();
|
||||
|
||||
self.chat(&Chat, window, cx);
|
||||
}
|
||||
|
||||
fn is_editor_empty(&self, cx: &App) -> bool {
|
||||
self.editor.read(cx).text(cx).trim().is_empty()
|
||||
}
|
||||
@@ -395,7 +418,7 @@ impl MessageEditor {
|
||||
fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.context_picker_menu_handle.is_deployed() {
|
||||
cx.propagate();
|
||||
} else {
|
||||
} else if self.context_strip.read(cx).has_context_items(cx) {
|
||||
self.context_strip.focus_handle(cx).focus(window);
|
||||
}
|
||||
}
|
||||
@@ -457,16 +480,18 @@ impl MessageEditor {
|
||||
|
||||
let active_completion_mode = thread.completion_mode();
|
||||
let max_mode_enabled = active_completion_mode == CompletionMode::Max;
|
||||
let icon = if max_mode_enabled {
|
||||
IconName::ZedBurnModeOn
|
||||
} else {
|
||||
IconName::ZedBurnMode
|
||||
};
|
||||
|
||||
Some(
|
||||
Button::new("max-mode", "Max Mode")
|
||||
.label_size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.icon(IconName::ZedMaxMode)
|
||||
IconButton::new("burn-mode", icon)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.toggle_state(max_mode_enabled)
|
||||
.selected_icon_color(Color::Error)
|
||||
.on_click(cx.listener(move |this, _event, _window, cx| {
|
||||
this.thread.update(cx, |thread, _cx| {
|
||||
thread.set_completion_mode(match active_completion_mode {
|
||||
@@ -556,6 +581,7 @@ impl MessageEditor {
|
||||
v_flex()
|
||||
.key_context("MessageEditor")
|
||||
.on_action(cx.listener(Self::chat))
|
||||
.on_action(cx.listener(Self::chat_with_follow))
|
||||
.on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| {
|
||||
this.profile_selector
|
||||
.read(cx)
|
||||
@@ -662,7 +688,6 @@ impl MessageEditor {
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(self.render_follow_toggle(cx))
|
||||
.children(self.render_max_mode_toggle(cx)),
|
||||
)
|
||||
@@ -836,7 +861,7 @@ impl MessageEditor {
|
||||
.border_b_0()
|
||||
.border_color(border_color)
|
||||
.rounded_t_md()
|
||||
.shadow(smallvec::smallvec![gpui::BoxShadow {
|
||||
.shadow(vec![gpui::BoxShadow {
|
||||
color: gpui::black().opacity(0.15),
|
||||
offset: point(px(1.), px(-1.)),
|
||||
blur_radius: px(3.),
|
||||
@@ -1079,11 +1104,11 @@ impl MessageEditor {
|
||||
let plan = user_store
|
||||
.current_plan()
|
||||
.map(|plan| match plan {
|
||||
Plan::Free => zed_llm_client::Plan::Free,
|
||||
Plan::Free => zed_llm_client::Plan::ZedFree,
|
||||
Plan::ZedPro => zed_llm_client::Plan::ZedPro,
|
||||
Plan::ZedProTrial => zed_llm_client::Plan::ZedProTrial,
|
||||
})
|
||||
.unwrap_or(zed_llm_client::Plan::Free);
|
||||
.unwrap_or(zed_llm_client::Plan::ZedFree);
|
||||
let usage = self.thread.read(cx).last_usage().or_else(|| {
|
||||
maybe!({
|
||||
let amount = user_store.model_request_usage_amount()?;
|
||||
@@ -1161,9 +1186,10 @@ impl MessageEditor {
|
||||
fn reload_context(&mut self, cx: &mut Context<Self>) -> Task<Option<ContextLoadResult>> {
|
||||
let load_task = cx.spawn(async move |this, cx| {
|
||||
let Ok(load_task) = this.update(cx, |this, cx| {
|
||||
let new_context = this.context_store.read_with(cx, |context_store, cx| {
|
||||
context_store.new_context_for_thread(this.thread.read(cx), None)
|
||||
});
|
||||
let new_context = this
|
||||
.context_store
|
||||
.read(cx)
|
||||
.new_context_for_thread(this.thread.read(cx), None);
|
||||
load_context(new_context, &this.project, &this.prompt_store, cx)
|
||||
}) else {
|
||||
return;
|
||||
@@ -1247,7 +1273,7 @@ impl MessageEditor {
|
||||
tools: vec![],
|
||||
tool_choice: None,
|
||||
stop: vec![],
|
||||
temperature: AssistantSettings::temperature_for_model(&model.model, cx),
|
||||
temperature: AgentSettings::temperature_for_model(&model.model, cx),
|
||||
};
|
||||
|
||||
Some(model.model.count_tokens(request, cx))
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
use assistant_settings::{
|
||||
AgentProfile, AgentProfileId, AssistantDockPosition, AssistantSettings, GroupedAgentProfiles,
|
||||
use std::sync::Arc;
|
||||
|
||||
use agent_settings::{
|
||||
AgentDockPosition, AgentProfile, AgentProfileId, AgentSettings, GroupedAgentProfiles,
|
||||
builtin_profiles,
|
||||
};
|
||||
use gpui::{Action, Entity, FocusHandle, Subscription, WeakEntity, prelude::*};
|
||||
use fs::Fs;
|
||||
use gpui::{Action, Empty, Entity, FocusHandle, Subscription, WeakEntity, prelude::*};
|
||||
use language_model::LanguageModelRegistry;
|
||||
use settings::{Settings as _, SettingsStore};
|
||||
use settings::{Settings as _, SettingsStore, update_settings_file};
|
||||
use ui::{
|
||||
ContextMenu, ContextMenuEntry, DocumentationSide, PopoverMenu, PopoverMenuHandle, Tooltip,
|
||||
prelude::*,
|
||||
@@ -15,6 +18,7 @@ use crate::{ManageProfiles, Thread, ThreadStore, ToggleProfileSelector};
|
||||
|
||||
pub struct ProfileSelector {
|
||||
profiles: GroupedAgentProfiles,
|
||||
fs: Arc<dyn Fs>,
|
||||
thread: Entity<Thread>,
|
||||
thread_store: WeakEntity<ThreadStore>,
|
||||
menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
@@ -24,6 +28,7 @@ pub struct ProfileSelector {
|
||||
|
||||
impl ProfileSelector {
|
||||
pub fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
thread: Entity<Thread>,
|
||||
thread_store: WeakEntity<ThreadStore>,
|
||||
focus_handle: FocusHandle,
|
||||
@@ -34,7 +39,8 @@ impl ProfileSelector {
|
||||
});
|
||||
|
||||
Self {
|
||||
profiles: GroupedAgentProfiles::from_settings(AssistantSettings::get_global(cx)),
|
||||
profiles: GroupedAgentProfiles::from_settings(AgentSettings::get_global(cx)),
|
||||
fs,
|
||||
thread,
|
||||
thread_store,
|
||||
menu_handle: PopoverMenuHandle::default(),
|
||||
@@ -48,7 +54,7 @@ impl ProfileSelector {
|
||||
}
|
||||
|
||||
fn refresh_profiles(&mut self, cx: &mut Context<Self>) {
|
||||
self.profiles = GroupedAgentProfiles::from_settings(AssistantSettings::get_global(cx));
|
||||
self.profiles = GroupedAgentProfiles::from_settings(AgentSettings::get_global(cx));
|
||||
}
|
||||
|
||||
fn build_context_menu(
|
||||
@@ -57,10 +63,14 @@ impl ProfileSelector {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Entity<ContextMenu> {
|
||||
ContextMenu::build(window, cx, |mut menu, _window, cx| {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
let settings = AgentSettings::get_global(cx);
|
||||
for (profile_id, profile) in self.profiles.builtin.iter() {
|
||||
menu =
|
||||
menu.item(self.menu_entry_for_profile(profile_id.clone(), profile, settings));
|
||||
menu = menu.item(self.menu_entry_for_profile(
|
||||
profile_id.clone(),
|
||||
profile,
|
||||
settings,
|
||||
cx,
|
||||
));
|
||||
}
|
||||
|
||||
if !self.profiles.custom.is_empty() {
|
||||
@@ -70,6 +80,7 @@ impl ProfileSelector {
|
||||
profile_id.clone(),
|
||||
profile,
|
||||
settings,
|
||||
cx,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -89,7 +100,8 @@ impl ProfileSelector {
|
||||
&self,
|
||||
profile_id: AgentProfileId,
|
||||
profile: &AgentProfile,
|
||||
settings: &AssistantSettings,
|
||||
settings: &AgentSettings,
|
||||
_cx: &App,
|
||||
) -> ContextMenuEntry {
|
||||
let documentation = match profile.name.to_lowercase().as_str() {
|
||||
builtin_profiles::WRITE => Some("Get help to write anything."),
|
||||
@@ -110,15 +122,15 @@ impl ProfileSelector {
|
||||
};
|
||||
|
||||
entry.handler({
|
||||
let fs = self.fs.clone();
|
||||
let thread_store = self.thread_store.clone();
|
||||
let profile_id = profile_id.clone();
|
||||
let profile = profile.clone();
|
||||
|
||||
let thread = self.thread.clone();
|
||||
|
||||
move |_window, cx| {
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.set_configured_profile(Some(profile.clone()), cx);
|
||||
update_settings_file::<AgentSettings>(fs.clone(), cx, {
|
||||
let profile_id = profile_id.clone();
|
||||
move |settings, _cx| {
|
||||
settings.set_profile(profile_id.clone());
|
||||
}
|
||||
});
|
||||
|
||||
thread_store
|
||||
@@ -133,31 +145,23 @@ impl ProfileSelector {
|
||||
|
||||
impl Render for ProfileSelector {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
let profile = self
|
||||
.thread
|
||||
.read_with(cx, |thread, _cx| thread.configured_profile())
|
||||
.or_else(|| {
|
||||
let profile_id = &settings.default_profile;
|
||||
let profile = settings.profiles.get(profile_id);
|
||||
profile.cloned()
|
||||
});
|
||||
let settings = AgentSettings::get_global(cx);
|
||||
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 configured_model = self
|
||||
.thread
|
||||
.read_with(cx, |thread, _cx| thread.configured_model())
|
||||
.or_else(|| {
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
model_registry.default_model()
|
||||
});
|
||||
let supports_tools =
|
||||
configured_model.map_or(false, |default| default.model.supports_tools());
|
||||
let configured_model = self.thread.read(cx).configured_model().or_else(|| {
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
model_registry.default_model()
|
||||
});
|
||||
let Some(configured_model) = configured_model else {
|
||||
return Empty.into_any_element();
|
||||
};
|
||||
|
||||
if supports_tools {
|
||||
if configured_model.model.supports_tools() {
|
||||
let this = cx.entity().clone();
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
let trigger_button = Button::new("profile-selector-model", selected_profile)
|
||||
@@ -204,10 +208,10 @@ impl Render for ProfileSelector {
|
||||
}
|
||||
}
|
||||
|
||||
fn documentation_side(position: AssistantDockPosition) -> DocumentationSide {
|
||||
fn documentation_side(position: AgentDockPosition) -> DocumentationSide {
|
||||
match position {
|
||||
AssistantDockPosition::Left => DocumentationSide::Right,
|
||||
AssistantDockPosition::Bottom => DocumentationSide::Left,
|
||||
AssistantDockPosition::Right => DocumentationSide::Left,
|
||||
AgentDockPosition::Left => DocumentationSide::Right,
|
||||
AgentDockPosition::Bottom => DocumentationSide::Left,
|
||||
AgentDockPosition::Right => DocumentationSide::Left,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,7 +193,8 @@ impl TerminalTransaction {
|
||||
});
|
||||
}
|
||||
|
||||
fn sanitize_input(input: String) -> String {
|
||||
input.replace(['\r', '\n'], "")
|
||||
fn sanitize_input(mut input: String) -> String {
|
||||
input.retain(|c| c != '\r' && c != '\n');
|
||||
input
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ use crate::inline_prompt_editor::{
|
||||
};
|
||||
use crate::terminal_codegen::{CLEAR_INPUT, CodegenEvent, TerminalCodegen};
|
||||
use crate::thread_store::{TextThreadStore, ThreadStore};
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::{Context as _, Result};
|
||||
use assistant_settings::AssistantSettings;
|
||||
use client::telemetry::Telemetry;
|
||||
use collections::{HashMap, VecDeque};
|
||||
use editor::{MultiBuffer, actions::SelectAll};
|
||||
@@ -191,7 +191,7 @@ impl TerminalInlineAssistant {
|
||||
};
|
||||
|
||||
self.prompt_history.retain(|prompt| *prompt != user_prompt);
|
||||
self.prompt_history.push_back(user_prompt.clone());
|
||||
self.prompt_history.push_back(user_prompt);
|
||||
if self.prompt_history.len() > PROMPT_HISTORY_MAX_LEN {
|
||||
self.prompt_history.pop_front();
|
||||
}
|
||||
@@ -271,7 +271,7 @@ impl TerminalInlineAssistant {
|
||||
.inline_assistant_model()
|
||||
.context("No inline assistant model")?;
|
||||
|
||||
let temperature = AssistantSettings::temperature_for_model(&model, cx);
|
||||
let temperature = AgentSettings::temperature_for_model(&model, cx);
|
||||
|
||||
Ok(cx.background_spawn(async move {
|
||||
let mut request_message = LanguageModelRequestMessage {
|
||||
|
||||
@@ -4,8 +4,8 @@ use std::ops::Range;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
use agent_settings::{AgentSettings, CompletionMode};
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_settings::{AgentProfile, AgentProfileId, AssistantSettings, CompletionMode};
|
||||
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet};
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::HashMap;
|
||||
@@ -22,9 +22,9 @@ use language_model::{
|
||||
ConfiguredModel, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
|
||||
LanguageModelId, LanguageModelKnownError, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
|
||||
LanguageModelToolUseId, MaxMonthlySpendReachedError, MessageContent,
|
||||
LanguageModelToolResultContent, LanguageModelToolUseId, MessageContent,
|
||||
ModelRequestLimitReachedError, PaymentRequiredError, RequestUsage, Role, SelectedModel,
|
||||
StopReason, TokenUsage,
|
||||
StopReason, TokenUsage, WrappedTextContent,
|
||||
};
|
||||
use postage::stream::Stream as _;
|
||||
use project::Project;
|
||||
@@ -36,7 +36,7 @@ use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
use thiserror::Error;
|
||||
use ui::Window;
|
||||
use util::{ResultExt as _, TryFutureExt as _, post_inc};
|
||||
use util::{ResultExt as _, post_inc};
|
||||
use uuid::Uuid;
|
||||
use zed_llm_client::CompletionRequestStatus;
|
||||
|
||||
@@ -115,6 +115,7 @@ pub struct Message {
|
||||
pub segments: Vec<MessageSegment>,
|
||||
pub loaded_context: LoadedContext,
|
||||
pub creases: Vec<MessageCrease>,
|
||||
pub is_hidden: bool,
|
||||
}
|
||||
|
||||
impl Message {
|
||||
@@ -214,7 +215,7 @@ pub struct GitState {
|
||||
pub diff: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ThreadCheckpoint {
|
||||
message_id: MessageId,
|
||||
git_checkpoint: GitStoreCheckpoint,
|
||||
@@ -324,12 +325,12 @@ pub enum QueueState {
|
||||
pub struct Thread {
|
||||
id: ThreadId,
|
||||
updated_at: DateTime<Utc>,
|
||||
summary: Option<SharedString>,
|
||||
summary: ThreadSummary,
|
||||
pending_summary: Task<Option<()>>,
|
||||
detailed_summary_task: Task<Option<()>>,
|
||||
detailed_summary_tx: postage::watch::Sender<DetailedSummaryState>,
|
||||
detailed_summary_rx: postage::watch::Receiver<DetailedSummaryState>,
|
||||
completion_mode: assistant_settings::CompletionMode,
|
||||
completion_mode: agent_settings::CompletionMode,
|
||||
messages: Vec<Message>,
|
||||
next_message_id: MessageId,
|
||||
last_prompt_id: PromptId,
|
||||
@@ -359,7 +360,33 @@ pub struct Thread {
|
||||
>,
|
||||
remaining_turns: u32,
|
||||
configured_model: Option<ConfiguredModel>,
|
||||
configured_profile: Option<AgentProfile>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum ThreadSummary {
|
||||
Pending,
|
||||
Generating,
|
||||
Ready(SharedString),
|
||||
Error,
|
||||
}
|
||||
|
||||
impl ThreadSummary {
|
||||
pub const DEFAULT: SharedString = SharedString::new_static("New Thread");
|
||||
|
||||
pub fn or_default(&self) -> SharedString {
|
||||
self.unwrap_or(Self::DEFAULT)
|
||||
}
|
||||
|
||||
pub fn unwrap_or(&self, message: impl Into<SharedString>) -> SharedString {
|
||||
self.ready().unwrap_or_else(|| message.into())
|
||||
}
|
||||
|
||||
pub fn ready(&self) -> Option<SharedString> {
|
||||
match self {
|
||||
ThreadSummary::Ready(summary) => Some(summary.clone()),
|
||||
ThreadSummary::Pending | ThreadSummary::Generating | ThreadSummary::Error => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -380,19 +407,16 @@ impl Thread {
|
||||
) -> Self {
|
||||
let (detailed_summary_tx, detailed_summary_rx) = postage::watch::channel();
|
||||
let configured_model = LanguageModelRegistry::read_global(cx).default_model();
|
||||
let assistant_settings = AssistantSettings::get_global(cx);
|
||||
let profile_id = &assistant_settings.default_profile;
|
||||
let configured_profile = assistant_settings.profiles.get(profile_id).cloned();
|
||||
|
||||
Self {
|
||||
id: ThreadId::new(),
|
||||
updated_at: Utc::now(),
|
||||
summary: None,
|
||||
summary: ThreadSummary::Pending,
|
||||
pending_summary: Task::ready(None),
|
||||
detailed_summary_task: Task::ready(None),
|
||||
detailed_summary_tx,
|
||||
detailed_summary_rx,
|
||||
completion_mode: AssistantSettings::get_global(cx).preferred_completion_mode,
|
||||
completion_mode: AgentSettings::get_global(cx).preferred_completion_mode,
|
||||
messages: Vec::new(),
|
||||
next_message_id: MessageId(0),
|
||||
last_prompt_id: PromptId::new(),
|
||||
@@ -425,7 +449,6 @@ impl Thread {
|
||||
request_callback: None,
|
||||
remaining_turns: u32::MAX,
|
||||
configured_model,
|
||||
configured_profile,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -436,7 +459,7 @@ impl Thread {
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
project_context: SharedProjectContext,
|
||||
window: &mut Window,
|
||||
window: Option<&mut Window>, // None in headless mode
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let next_message_id = MessageId(
|
||||
@@ -471,19 +494,12 @@ impl Thread {
|
||||
|
||||
let completion_mode = serialized
|
||||
.completion_mode
|
||||
.unwrap_or_else(|| AssistantSettings::get_global(cx).preferred_completion_mode);
|
||||
|
||||
let configured_profile = serialized.profile.and_then(|profile| {
|
||||
AssistantSettings::get_global(cx)
|
||||
.profiles
|
||||
.get(&profile)
|
||||
.cloned()
|
||||
});
|
||||
.unwrap_or_else(|| AgentSettings::get_global(cx).preferred_completion_mode);
|
||||
|
||||
Self {
|
||||
id,
|
||||
updated_at: serialized.updated_at,
|
||||
summary: Some(serialized.summary),
|
||||
summary: ThreadSummary::Ready(serialized.summary),
|
||||
pending_summary: Task::ready(None),
|
||||
detailed_summary_task: Task::ready(None),
|
||||
detailed_summary_tx,
|
||||
@@ -525,6 +541,7 @@ impl Thread {
|
||||
context: None,
|
||||
})
|
||||
.collect(),
|
||||
is_hidden: message.is_hidden,
|
||||
})
|
||||
.collect(),
|
||||
next_message_id,
|
||||
@@ -545,7 +562,7 @@ impl Thread {
|
||||
cumulative_token_usage: serialized.cumulative_token_usage,
|
||||
exceeded_window_error: None,
|
||||
last_usage: None,
|
||||
tool_use_limit_reached: false,
|
||||
tool_use_limit_reached: serialized.tool_use_limit_reached,
|
||||
feedback: None,
|
||||
message_feedback: HashMap::default(),
|
||||
last_auto_capture_at: None,
|
||||
@@ -553,7 +570,6 @@ impl Thread {
|
||||
request_callback: None,
|
||||
remaining_turns: u32::MAX,
|
||||
configured_model,
|
||||
configured_profile,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -585,10 +601,6 @@ impl Thread {
|
||||
self.last_prompt_id = PromptId::new();
|
||||
}
|
||||
|
||||
pub fn summary(&self) -> Option<SharedString> {
|
||||
self.summary.clone()
|
||||
}
|
||||
|
||||
pub fn project_context(&self) -> SharedProjectContext {
|
||||
self.project_context.clone()
|
||||
}
|
||||
@@ -609,39 +621,25 @@ impl Thread {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn configured_profile(&self) -> Option<AgentProfile> {
|
||||
self.configured_profile.clone()
|
||||
}
|
||||
|
||||
pub fn set_configured_profile(
|
||||
&mut self,
|
||||
profile: Option<AgentProfile>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.configured_profile = profile;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub const DEFAULT_SUMMARY: SharedString = SharedString::new_static("New Thread");
|
||||
|
||||
pub fn summary_or_default(&self) -> SharedString {
|
||||
self.summary.clone().unwrap_or(Self::DEFAULT_SUMMARY)
|
||||
pub fn summary(&self) -> &ThreadSummary {
|
||||
&self.summary
|
||||
}
|
||||
|
||||
pub fn set_summary(&mut self, new_summary: impl Into<SharedString>, cx: &mut Context<Self>) {
|
||||
let Some(current_summary) = &self.summary else {
|
||||
// Don't allow setting summary until generated
|
||||
return;
|
||||
let current_summary = match &self.summary {
|
||||
ThreadSummary::Pending | ThreadSummary::Generating => return,
|
||||
ThreadSummary::Ready(summary) => summary,
|
||||
ThreadSummary::Error => &ThreadSummary::DEFAULT,
|
||||
};
|
||||
|
||||
let mut new_summary = new_summary.into();
|
||||
|
||||
if new_summary.is_empty() {
|
||||
new_summary = Self::DEFAULT_SUMMARY;
|
||||
new_summary = ThreadSummary::DEFAULT;
|
||||
}
|
||||
|
||||
if current_summary != &new_summary {
|
||||
self.summary = Some(new_summary);
|
||||
self.summary = ThreadSummary::Ready(new_summary);
|
||||
cx.emit(ThreadEvent::SummaryChanged);
|
||||
}
|
||||
}
|
||||
@@ -761,6 +759,14 @@ impl Thread {
|
||||
return;
|
||||
};
|
||||
|
||||
self.finalize_checkpoint(pending_checkpoint, cx);
|
||||
}
|
||||
|
||||
fn finalize_checkpoint(
|
||||
&mut self,
|
||||
pending_checkpoint: ThreadCheckpoint,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let git_store = self.project.read(cx).git_store().clone();
|
||||
let final_checkpoint = git_store.update(cx, |git_store, cx| git_store.checkpoint(cx));
|
||||
cx.spawn(async move |this, cx| match final_checkpoint.await {
|
||||
@@ -845,7 +851,7 @@ impl Thread {
|
||||
.get(ix + 1)
|
||||
.and_then(|message| {
|
||||
self.message(message.id)
|
||||
.map(|next_message| next_message.role == Role::User)
|
||||
.map(|next_message| next_message.role == Role::User && !next_message.is_hidden)
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
@@ -884,7 +890,16 @@ impl Thread {
|
||||
}
|
||||
|
||||
pub fn output_for_tool(&self, id: &LanguageModelToolUseId) -> Option<&Arc<str>> {
|
||||
Some(&self.tool_use.tool_result(id)?.content)
|
||||
match &self.tool_use.tool_result(id)?.content {
|
||||
LanguageModelToolResultContent::Text(text)
|
||||
| LanguageModelToolResultContent::WrappedText(WrappedTextContent { text, .. }) => {
|
||||
Some(text)
|
||||
}
|
||||
LanguageModelToolResultContent::Image(_) => {
|
||||
// TODO: We should display image
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn card_for_tool(&self, id: &LanguageModelToolUseId) -> Option<AnyToolCard> {
|
||||
@@ -938,6 +953,7 @@ impl Thread {
|
||||
vec![MessageSegment::Text(text.into())],
|
||||
loaded_context.loaded_context,
|
||||
creases,
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -953,6 +969,20 @@ impl Thread {
|
||||
message_id
|
||||
}
|
||||
|
||||
pub fn insert_invisible_continue_message(&mut self, cx: &mut Context<Self>) -> MessageId {
|
||||
let id = self.insert_message(
|
||||
Role::User,
|
||||
vec![MessageSegment::Text("Continue where you left off".into())],
|
||||
LoadedContext::default(),
|
||||
vec![],
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
self.pending_checkpoint = None;
|
||||
|
||||
id
|
||||
}
|
||||
|
||||
pub fn insert_assistant_message(
|
||||
&mut self,
|
||||
segments: Vec<MessageSegment>,
|
||||
@@ -963,6 +993,7 @@ impl Thread {
|
||||
segments,
|
||||
LoadedContext::default(),
|
||||
Vec::new(),
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
@@ -973,6 +1004,7 @@ impl Thread {
|
||||
segments: Vec<MessageSegment>,
|
||||
loaded_context: LoadedContext,
|
||||
creases: Vec<MessageCrease>,
|
||||
is_hidden: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> MessageId {
|
||||
let id = self.next_message_id.post_inc();
|
||||
@@ -982,6 +1014,7 @@ impl Thread {
|
||||
segments,
|
||||
loaded_context,
|
||||
creases,
|
||||
is_hidden,
|
||||
});
|
||||
self.touch_updated_at();
|
||||
cx.emit(ThreadEvent::MessageAdded(id));
|
||||
@@ -994,6 +1027,7 @@ impl Thread {
|
||||
new_role: Role,
|
||||
new_segments: Vec<MessageSegment>,
|
||||
loaded_context: Option<LoadedContext>,
|
||||
checkpoint: Option<GitStoreCheckpoint>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
let Some(message) = self.messages.iter_mut().find(|message| message.id == id) else {
|
||||
@@ -1004,6 +1038,15 @@ impl Thread {
|
||||
if let Some(context) = loaded_context {
|
||||
message.loaded_context = context;
|
||||
}
|
||||
if let Some(git_checkpoint) = checkpoint {
|
||||
self.checkpoints_by_message.insert(
|
||||
id,
|
||||
ThreadCheckpoint {
|
||||
message_id: id,
|
||||
git_checkpoint,
|
||||
},
|
||||
);
|
||||
}
|
||||
self.touch_updated_at();
|
||||
cx.emit(ThreadEvent::MessageEdited(id));
|
||||
true
|
||||
@@ -1055,7 +1098,7 @@ impl Thread {
|
||||
let initial_project_snapshot = initial_project_snapshot.await;
|
||||
this.read_with(cx, |this, cx| SerializedThread {
|
||||
version: SerializedThread::VERSION.to_string(),
|
||||
summary: this.summary_or_default(),
|
||||
summary: this.summary().or_default(),
|
||||
updated_at: this.updated_at(),
|
||||
messages: this
|
||||
.messages()
|
||||
@@ -1112,6 +1155,7 @@ impl Thread {
|
||||
label: crease.metadata.label.clone(),
|
||||
})
|
||||
.collect(),
|
||||
is_hidden: message.is_hidden,
|
||||
})
|
||||
.collect(),
|
||||
initial_project_snapshot,
|
||||
@@ -1126,11 +1170,8 @@ impl Thread {
|
||||
provider: model.provider.id().0.to_string(),
|
||||
model: model.model.id().0.to_string(),
|
||||
}),
|
||||
profile: this
|
||||
.configured_profile
|
||||
.as_ref()
|
||||
.map(|profile| AgentProfileId(profile.name.clone().into())),
|
||||
completion_mode: Some(this.completion_mode),
|
||||
tool_use_limit_reached: this.tool_use_limit_reached,
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1185,7 +1226,7 @@ impl Thread {
|
||||
tools: Vec::new(),
|
||||
tool_choice: None,
|
||||
stop: Vec::new(),
|
||||
temperature: AssistantSettings::temperature_for_model(&model, cx),
|
||||
temperature: AgentSettings::temperature_for_model(&model, cx),
|
||||
};
|
||||
|
||||
let available_tools = self.available_tools(cx, model.clone());
|
||||
@@ -1344,7 +1385,7 @@ impl Thread {
|
||||
tools: Vec::new(),
|
||||
tool_choice: None,
|
||||
stop: Vec::new(),
|
||||
temperature: AssistantSettings::temperature_for_model(model, cx),
|
||||
temperature: AgentSettings::temperature_for_model(model, cx),
|
||||
};
|
||||
|
||||
for message in &self.messages {
|
||||
@@ -1619,7 +1660,7 @@ impl Thread {
|
||||
CompletionRequestStatus::Failed {
|
||||
code, message, request_id
|
||||
} => {
|
||||
return Err(anyhow!("completion request failed. request_id: {request_id}, code: {code}, message: {message}"));
|
||||
anyhow::bail!("completion request failed. request_id: {request_id}, code: {code}, message: {message}");
|
||||
}
|
||||
CompletionRequestStatus::UsageUpdated {
|
||||
amount, limit
|
||||
@@ -1655,7 +1696,7 @@ impl Thread {
|
||||
|
||||
// If there is a response without tool use, summarize the message. Otherwise,
|
||||
// allow two tool uses before summarizing.
|
||||
if thread.summary.is_none()
|
||||
if matches!(thread.summary, ThreadSummary::Pending)
|
||||
&& thread.messages.len() >= 2
|
||||
&& (!thread.has_pending_tool_uses() || thread.messages.len() >= 6)
|
||||
{
|
||||
@@ -1682,6 +1723,43 @@ impl Thread {
|
||||
project.set_agent_location(None, cx);
|
||||
});
|
||||
}
|
||||
StopReason::Refusal => {
|
||||
thread.project.update(cx, |project, cx| {
|
||||
project.set_agent_location(None, cx);
|
||||
});
|
||||
|
||||
// Remove the turn that was refused.
|
||||
//
|
||||
// https://docs.anthropic.com/en/docs/test-and-evaluate/strengthen-guardrails/handle-streaming-refusals#reset-context-after-refusal
|
||||
{
|
||||
let mut messages_to_remove = Vec::new();
|
||||
|
||||
for (ix, message) in thread.messages.iter().enumerate().rev() {
|
||||
messages_to_remove.push(message.id);
|
||||
|
||||
if message.role == Role::User {
|
||||
if ix == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
if let Some(prev_message) = thread.messages.get(ix - 1) {
|
||||
if prev_message.role == Role::Assistant {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for message_id in messages_to_remove {
|
||||
thread.delete_message(message_id, cx);
|
||||
}
|
||||
}
|
||||
|
||||
cx.emit(ThreadEvent::ShowError(ThreadError::Message {
|
||||
header: "Language model refusal".into(),
|
||||
message: "Model refused to generate content for safety reasons.".into(),
|
||||
}));
|
||||
}
|
||||
},
|
||||
Err(error) => {
|
||||
thread.project.update(cx, |project, cx| {
|
||||
@@ -1690,10 +1768,6 @@ impl Thread {
|
||||
|
||||
if error.is::<PaymentRequiredError>() {
|
||||
cx.emit(ThreadEvent::ShowError(ThreadError::PaymentRequired));
|
||||
} else if error.is::<MaxMonthlySpendReachedError>() {
|
||||
cx.emit(ThreadEvent::ShowError(
|
||||
ThreadError::MaxMonthlySpendReached,
|
||||
));
|
||||
} else if let Some(error) =
|
||||
error.downcast_ref::<ModelRequestLimitReachedError>()
|
||||
{
|
||||
@@ -1729,6 +1803,7 @@ impl Thread {
|
||||
thread.cancel_last_completion(window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
cx.emit(ThreadEvent::Stopped(result.map_err(Arc::new)));
|
||||
|
||||
if let Some((request_callback, (request, response_events))) = thread
|
||||
@@ -1769,6 +1844,7 @@ impl Thread {
|
||||
|
||||
pub fn summarize(&mut self, cx: &mut Context<Self>) {
|
||||
let Some(model) = LanguageModelRegistry::read_global(cx).thread_summary_model() else {
|
||||
println!("No thread summary model");
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -1783,13 +1859,17 @@ impl Thread {
|
||||
|
||||
let request = self.to_summarize_request(&model.model, added_user_message.into(), cx);
|
||||
|
||||
self.summary = ThreadSummary::Generating;
|
||||
|
||||
self.pending_summary = cx.spawn(async move |this, cx| {
|
||||
async move {
|
||||
let result = async {
|
||||
let mut messages = model.model.stream_completion(request, &cx).await?;
|
||||
|
||||
let mut new_summary = String::new();
|
||||
while let Some(event) = messages.next().await {
|
||||
let event = event?;
|
||||
let Ok(event) = event else {
|
||||
continue;
|
||||
};
|
||||
let text = match event {
|
||||
LanguageModelCompletionEvent::Text(text) => text,
|
||||
LanguageModelCompletionEvent::StatusUpdate(
|
||||
@@ -1815,18 +1895,29 @@ impl Thread {
|
||||
}
|
||||
}
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
if !new_summary.is_empty() {
|
||||
this.summary = Some(new_summary.into());
|
||||
}
|
||||
|
||||
cx.emit(ThreadEvent::SummaryGenerated);
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
anyhow::Ok(new_summary)
|
||||
}
|
||||
.log_err()
|
||||
.await
|
||||
.await;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
match result {
|
||||
Ok(new_summary) => {
|
||||
if new_summary.is_empty() {
|
||||
this.summary = ThreadSummary::Error;
|
||||
} else {
|
||||
this.summary = ThreadSummary::Ready(new_summary.into());
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
this.summary = ThreadSummary::Error;
|
||||
log::error!("Failed to generate thread summary: {}", err);
|
||||
}
|
||||
}
|
||||
cx.emit(ThreadEvent::SummaryGenerated);
|
||||
})
|
||||
.log_err()?;
|
||||
|
||||
Some(())
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1971,7 +2062,7 @@ impl Thread {
|
||||
for tool_use in pending_tool_uses.iter() {
|
||||
if let Some(tool) = self.tools.read(cx).tool(&tool_use.name, cx) {
|
||||
if tool.needs_confirmation(&tool_use.input, cx)
|
||||
&& !AssistantSettings::get_global(cx).always_allow_tool_actions
|
||||
&& !AgentSettings::get_global(cx).always_allow_tool_actions
|
||||
{
|
||||
self.tool_use.confirm_tool_use(
|
||||
tool_use.id.clone(),
|
||||
@@ -2188,10 +2279,17 @@ impl Thread {
|
||||
);
|
||||
}
|
||||
|
||||
self.finalize_pending_checkpoint(cx);
|
||||
|
||||
if canceled {
|
||||
cx.emit(ThreadEvent::CompletionCanceled);
|
||||
|
||||
// When canceled, we always want to insert the checkpoint.
|
||||
// (We skip over finalize_pending_checkpoint, because it
|
||||
// would conclude we didn't have anything to insert here.)
|
||||
if let Some(checkpoint) = self.pending_checkpoint.take() {
|
||||
self.insert_checkpoint(checkpoint, cx);
|
||||
}
|
||||
} else {
|
||||
self.finalize_pending_checkpoint(cx);
|
||||
}
|
||||
|
||||
canceled
|
||||
@@ -2233,7 +2331,7 @@ impl Thread {
|
||||
.read(cx)
|
||||
.enabled_tools(cx)
|
||||
.iter()
|
||||
.map(|tool| tool.name().to_string())
|
||||
.map(|tool| tool.name())
|
||||
.collect();
|
||||
|
||||
self.message_feedback.insert(message_id, feedback);
|
||||
@@ -2436,9 +2534,8 @@ impl Thread {
|
||||
pub fn to_markdown(&self, cx: &App) -> Result<String> {
|
||||
let mut markdown = Vec::new();
|
||||
|
||||
if let Some(summary) = self.summary() {
|
||||
writeln!(markdown, "# {summary}\n")?;
|
||||
};
|
||||
let summary = self.summary().or_default();
|
||||
writeln!(markdown, "# {summary}\n")?;
|
||||
|
||||
for message in self.messages() {
|
||||
writeln!(
|
||||
@@ -2495,7 +2592,26 @@ impl Thread {
|
||||
}
|
||||
|
||||
writeln!(markdown, "**\n")?;
|
||||
writeln!(markdown, "{}", tool_result.content)?;
|
||||
match &tool_result.content {
|
||||
LanguageModelToolResultContent::Text(text)
|
||||
| LanguageModelToolResultContent::WrappedText(WrappedTextContent {
|
||||
text,
|
||||
..
|
||||
}) => {
|
||||
writeln!(markdown, "{text}")?;
|
||||
}
|
||||
LanguageModelToolResultContent::Image(image) => {
|
||||
writeln!(markdown, "", image.source)?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(output) = tool_result.output.as_ref() {
|
||||
writeln!(
|
||||
markdown,
|
||||
"\n\nDebug Output:\n\n```json\n{}\n```\n",
|
||||
serde_json::to_string_pretty(output)?
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2559,7 +2675,7 @@ impl Thread {
|
||||
.read(cx)
|
||||
.current_user()
|
||||
.map(|user| user.github_login.clone());
|
||||
let client = self.project.read(cx).client().clone();
|
||||
let client = self.project.read(cx).client();
|
||||
let serialize_task = self.serialize(cx);
|
||||
|
||||
cx.background_executor()
|
||||
@@ -2678,8 +2794,6 @@ impl Thread {
|
||||
pub enum ThreadError {
|
||||
#[error("Payment required")]
|
||||
PaymentRequired,
|
||||
#[error("Max monthly spend reached")]
|
||||
MaxMonthlySpendReached,
|
||||
#[error("Model request limit reached")]
|
||||
ModelRequestLimitReached { plan: Plan },
|
||||
#[error("Message {header}: {message}")]
|
||||
@@ -2744,11 +2858,11 @@ struct PendingCompletion {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{ThreadStore, context::load_context, context_store::ContextStore, thread_store};
|
||||
use assistant_settings::{AssistantSettings, LanguageModelParameters};
|
||||
use agent_settings::{AgentSettings, LanguageModelParameters};
|
||||
use assistant_tool::ToolRegistry;
|
||||
use editor::EditorSettings;
|
||||
use gpui::TestAppContext;
|
||||
use language_model::fake_provider::FakeLanguageModel;
|
||||
use language_model::fake_provider::{FakeLanguageModel, FakeLanguageModelProvider};
|
||||
use project::{FakeFs, Project};
|
||||
use prompt_store::PromptBuilder;
|
||||
use serde_json::json;
|
||||
@@ -2775,7 +2889,8 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let context = context_store.update(cx, |store, _| store.context().next().cloned().unwrap());
|
||||
let context =
|
||||
context_store.read_with(cx, |store, _| store.context().next().cloned().unwrap());
|
||||
let loaded_context = cx
|
||||
.update(|cx| load_context(vec![context], &project, &None, cx))
|
||||
.await;
|
||||
@@ -3086,7 +3201,8 @@ fn main() {{
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let context = context_store.update(cx, |store, _| store.context().next().cloned().unwrap());
|
||||
let context =
|
||||
context_store.read_with(cx, |store, _| store.context().next().cloned().unwrap());
|
||||
let loaded_context = cx
|
||||
.update(|cx| load_context(vec![context], &project, &None, cx))
|
||||
.await;
|
||||
@@ -3170,14 +3286,14 @@ fn main() {{
|
||||
|
||||
// Both model and provider
|
||||
cx.update(|cx| {
|
||||
AssistantSettings::override_global(
|
||||
AssistantSettings {
|
||||
AgentSettings::override_global(
|
||||
AgentSettings {
|
||||
model_parameters: vec![LanguageModelParameters {
|
||||
provider: Some(model.provider_id().0.to_string().into()),
|
||||
model: Some(model.id().0.clone()),
|
||||
temperature: Some(0.66),
|
||||
}],
|
||||
..AssistantSettings::get_global(cx).clone()
|
||||
..AgentSettings::get_global(cx).clone()
|
||||
},
|
||||
cx,
|
||||
);
|
||||
@@ -3190,14 +3306,14 @@ fn main() {{
|
||||
|
||||
// Only model
|
||||
cx.update(|cx| {
|
||||
AssistantSettings::override_global(
|
||||
AssistantSettings {
|
||||
AgentSettings::override_global(
|
||||
AgentSettings {
|
||||
model_parameters: vec![LanguageModelParameters {
|
||||
provider: None,
|
||||
model: Some(model.id().0.clone()),
|
||||
temperature: Some(0.66),
|
||||
}],
|
||||
..AssistantSettings::get_global(cx).clone()
|
||||
..AgentSettings::get_global(cx).clone()
|
||||
},
|
||||
cx,
|
||||
);
|
||||
@@ -3210,14 +3326,14 @@ fn main() {{
|
||||
|
||||
// Only provider
|
||||
cx.update(|cx| {
|
||||
AssistantSettings::override_global(
|
||||
AssistantSettings {
|
||||
AgentSettings::override_global(
|
||||
AgentSettings {
|
||||
model_parameters: vec![LanguageModelParameters {
|
||||
provider: Some(model.provider_id().0.to_string().into()),
|
||||
model: None,
|
||||
temperature: Some(0.66),
|
||||
}],
|
||||
..AssistantSettings::get_global(cx).clone()
|
||||
..AgentSettings::get_global(cx).clone()
|
||||
},
|
||||
cx,
|
||||
);
|
||||
@@ -3230,14 +3346,14 @@ fn main() {{
|
||||
|
||||
// Same model name, different provider
|
||||
cx.update(|cx| {
|
||||
AssistantSettings::override_global(
|
||||
AssistantSettings {
|
||||
AgentSettings::override_global(
|
||||
AgentSettings {
|
||||
model_parameters: vec![LanguageModelParameters {
|
||||
provider: Some("anthropic".into()),
|
||||
model: Some(model.id().0.clone()),
|
||||
temperature: Some(0.66),
|
||||
}],
|
||||
..AssistantSettings::get_global(cx).clone()
|
||||
..AgentSettings::get_global(cx).clone()
|
||||
},
|
||||
cx,
|
||||
);
|
||||
@@ -3249,13 +3365,203 @@ fn main() {{
|
||||
assert_eq!(request.temperature, None);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_thread_summary(cx: &mut TestAppContext) {
|
||||
init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
|
||||
let (_, _thread_store, thread, _context_store, model) =
|
||||
setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Initial state should be pending
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert!(matches!(thread.summary(), ThreadSummary::Pending));
|
||||
assert_eq!(thread.summary().or_default(), ThreadSummary::DEFAULT);
|
||||
});
|
||||
|
||||
// Manually setting the summary should not be allowed in this state
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.set_summary("This should not work", cx);
|
||||
});
|
||||
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert!(matches!(thread.summary(), ThreadSummary::Pending));
|
||||
});
|
||||
|
||||
// Send a message
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.insert_user_message("Hi!", ContextLoadResult::default(), None, vec![], cx);
|
||||
thread.send_to_model(model.clone(), None, cx);
|
||||
});
|
||||
|
||||
let fake_model = model.as_fake();
|
||||
simulate_successful_response(&fake_model, cx);
|
||||
|
||||
// Should start generating summary when there are >= 2 messages
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert_eq!(*thread.summary(), ThreadSummary::Generating);
|
||||
});
|
||||
|
||||
// Should not be able to set the summary while generating
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.set_summary("This should not work either", cx);
|
||||
});
|
||||
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert!(matches!(thread.summary(), ThreadSummary::Generating));
|
||||
assert_eq!(thread.summary().or_default(), ThreadSummary::DEFAULT);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
fake_model.stream_last_completion_response("Brief".into());
|
||||
fake_model.stream_last_completion_response(" Introduction".into());
|
||||
fake_model.end_last_completion_stream();
|
||||
cx.run_until_parked();
|
||||
|
||||
// Summary should be set
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert!(matches!(thread.summary(), ThreadSummary::Ready(_)));
|
||||
assert_eq!(thread.summary().or_default(), "Brief Introduction");
|
||||
});
|
||||
|
||||
// Now we should be able to set a summary
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.set_summary("Brief Intro", cx);
|
||||
});
|
||||
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert_eq!(thread.summary().or_default(), "Brief Intro");
|
||||
});
|
||||
|
||||
// Test setting an empty summary (should default to DEFAULT)
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.set_summary("", cx);
|
||||
});
|
||||
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert!(matches!(thread.summary(), ThreadSummary::Ready(_)));
|
||||
assert_eq!(thread.summary().or_default(), ThreadSummary::DEFAULT);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_thread_summary_error_set_manually(cx: &mut TestAppContext) {
|
||||
init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
|
||||
let (_, _thread_store, thread, _context_store, model) =
|
||||
setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
test_summarize_error(&model, &thread, cx);
|
||||
|
||||
// Now we should be able to set a summary
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.set_summary("Brief Intro", cx);
|
||||
});
|
||||
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert!(matches!(thread.summary(), ThreadSummary::Ready(_)));
|
||||
assert_eq!(thread.summary().or_default(), "Brief Intro");
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_thread_summary_error_retry(cx: &mut TestAppContext) {
|
||||
init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
|
||||
let (_, _thread_store, thread, _context_store, model) =
|
||||
setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
test_summarize_error(&model, &thread, cx);
|
||||
|
||||
// Sending another message should not trigger another summarize request
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.insert_user_message(
|
||||
"How are you?",
|
||||
ContextLoadResult::default(),
|
||||
None,
|
||||
vec![],
|
||||
cx,
|
||||
);
|
||||
thread.send_to_model(model.clone(), None, cx);
|
||||
});
|
||||
|
||||
let fake_model = model.as_fake();
|
||||
simulate_successful_response(&fake_model, cx);
|
||||
|
||||
thread.read_with(cx, |thread, _| {
|
||||
// State is still Error, not Generating
|
||||
assert!(matches!(thread.summary(), ThreadSummary::Error));
|
||||
});
|
||||
|
||||
// But the summarize request can be invoked manually
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.summarize(cx);
|
||||
});
|
||||
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert!(matches!(thread.summary(), ThreadSummary::Generating));
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
fake_model.stream_last_completion_response("A successful summary".into());
|
||||
fake_model.end_last_completion_stream();
|
||||
cx.run_until_parked();
|
||||
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert!(matches!(thread.summary(), ThreadSummary::Ready(_)));
|
||||
assert_eq!(thread.summary().or_default(), "A successful summary");
|
||||
});
|
||||
}
|
||||
|
||||
fn test_summarize_error(
|
||||
model: &Arc<dyn LanguageModel>,
|
||||
thread: &Entity<Thread>,
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.insert_user_message("Hi!", ContextLoadResult::default(), None, vec![], cx);
|
||||
thread.send_to_model(model.clone(), None, cx);
|
||||
});
|
||||
|
||||
let fake_model = model.as_fake();
|
||||
simulate_successful_response(&fake_model, cx);
|
||||
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert!(matches!(thread.summary(), ThreadSummary::Generating));
|
||||
assert_eq!(thread.summary().or_default(), ThreadSummary::DEFAULT);
|
||||
});
|
||||
|
||||
// Simulate summary request ending
|
||||
cx.run_until_parked();
|
||||
fake_model.end_last_completion_stream();
|
||||
cx.run_until_parked();
|
||||
|
||||
// State is set to Error and default message
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert!(matches!(thread.summary(), ThreadSummary::Error));
|
||||
assert_eq!(thread.summary().or_default(), ThreadSummary::DEFAULT);
|
||||
});
|
||||
}
|
||||
|
||||
fn simulate_successful_response(fake_model: &FakeLanguageModel, cx: &mut TestAppContext) {
|
||||
cx.run_until_parked();
|
||||
fake_model.stream_last_completion_response("Assistant response".into());
|
||||
fake_model.end_last_completion_stream();
|
||||
cx.run_until_parked();
|
||||
}
|
||||
|
||||
fn init_test_settings(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
AssistantSettings::register(cx);
|
||||
AgentSettings::register(cx);
|
||||
prompt_store::init(cx);
|
||||
thread_store::init(cx);
|
||||
workspace::init_settings(cx);
|
||||
@@ -3305,9 +3611,29 @@ fn main() {{
|
||||
let thread = thread_store.update(cx, |store, cx| store.create_thread(cx));
|
||||
let context_store = cx.new(|_cx| ContextStore::new(project.downgrade(), None));
|
||||
|
||||
let model = FakeLanguageModel::default();
|
||||
let provider = Arc::new(FakeLanguageModelProvider);
|
||||
let model = provider.test_model();
|
||||
let model: Arc<dyn LanguageModel> = Arc::new(model);
|
||||
|
||||
cx.update(|_, cx| {
|
||||
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
|
||||
registry.set_default_model(
|
||||
Some(ConfiguredModel {
|
||||
provider: provider.clone(),
|
||||
model: model.clone(),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
registry.set_thread_summary_model(
|
||||
Some(ConfiguredModel {
|
||||
provider,
|
||||
model: model.clone(),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
});
|
||||
|
||||
(workspace, thread_store, thread, context_store, model)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,11 @@ use std::fmt::Display;
|
||||
use std::ops::Range;
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_context_editor::SavedContextMetadata;
|
||||
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
|
||||
use editor::{Editor, EditorEvent};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
App, Empty, Entity, FocusHandle, Focusable, ScrollStrategy, Stateful, Task,
|
||||
App, ClickEvent, Empty, Entity, FocusHandle, Focusable, ScrollStrategy, Stateful, Task,
|
||||
UniformListScrollHandle, WeakEntity, Window, uniform_list,
|
||||
};
|
||||
use time::{OffsetDateTime, UtcOffset};
|
||||
@@ -18,7 +17,6 @@ use ui::{
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::history_store::{HistoryEntry, HistoryStore};
|
||||
use crate::thread_store::SerializedThreadMetadata;
|
||||
use crate::{AgentPanel, RemoveSelectedThread};
|
||||
|
||||
pub struct ThreadHistory {
|
||||
@@ -26,11 +24,14 @@ pub struct ThreadHistory {
|
||||
history_store: Entity<HistoryStore>,
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
selected_index: usize,
|
||||
hovered_index: Option<usize>,
|
||||
search_editor: Entity<Editor>,
|
||||
all_entries: Arc<Vec<HistoryEntry>>,
|
||||
// When the search is empty, we display date separators between history entries
|
||||
// This vector contains an enum of either a separator or an actual entry
|
||||
separated_items: Vec<HistoryListItem>,
|
||||
separated_items: Vec<ListItemType>,
|
||||
// Maps entry indexes to list item indexes
|
||||
separated_item_indexes: Vec<u32>,
|
||||
_separated_items_task: Option<Task<()>>,
|
||||
search_state: SearchState,
|
||||
scrollbar_visibility: bool,
|
||||
@@ -50,7 +51,7 @@ enum SearchState {
|
||||
},
|
||||
}
|
||||
|
||||
enum HistoryListItem {
|
||||
enum ListItemType {
|
||||
BucketSeparator(TimeBucket),
|
||||
Entry {
|
||||
index: usize,
|
||||
@@ -58,11 +59,11 @@ enum HistoryListItem {
|
||||
},
|
||||
}
|
||||
|
||||
impl HistoryListItem {
|
||||
impl ListItemType {
|
||||
fn entry_index(&self) -> Option<usize> {
|
||||
match self {
|
||||
HistoryListItem::BucketSeparator(_) => None,
|
||||
HistoryListItem::Entry { index, .. } => Some(*index),
|
||||
ListItemType::BucketSeparator(_) => None,
|
||||
ListItemType::Entry { index, .. } => Some(*index),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,9 +101,11 @@ impl ThreadHistory {
|
||||
history_store,
|
||||
scroll_handle,
|
||||
selected_index: 0,
|
||||
hovered_index: None,
|
||||
search_state: SearchState::Empty,
|
||||
all_entries: Default::default(),
|
||||
separated_items: Default::default(),
|
||||
separated_item_indexes: Default::default(),
|
||||
search_editor,
|
||||
scrollbar_visibility: true,
|
||||
scrollbar_state,
|
||||
@@ -114,35 +117,21 @@ impl ThreadHistory {
|
||||
}
|
||||
|
||||
fn update_all_entries(&mut self, cx: &mut Context<Self>) {
|
||||
self.all_entries = self
|
||||
let new_entries: Arc<Vec<HistoryEntry>> = self
|
||||
.history_store
|
||||
.update(cx, |store, cx| store.entries(cx))
|
||||
.into();
|
||||
|
||||
self.set_selected_index(0, cx);
|
||||
self.update_separated_items(cx);
|
||||
|
||||
match &self.search_state {
|
||||
SearchState::Empty => {}
|
||||
SearchState::Searching { query, .. } | SearchState::Searched { query, .. } => {
|
||||
self.search(query.clone(), cx);
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn update_separated_items(&mut self, cx: &mut Context<Self>) {
|
||||
self._separated_items_task.take();
|
||||
|
||||
let mut separated_items = std::mem::take(&mut self.separated_items);
|
||||
separated_items.clear();
|
||||
let all_entries = self.all_entries.clone();
|
||||
let mut items = Vec::with_capacity(new_entries.len() + 1);
|
||||
let mut indexes = Vec::with_capacity(new_entries.len() + 1);
|
||||
|
||||
let bg_task = cx.background_spawn(async move {
|
||||
let mut bucket = None;
|
||||
let today = Local::now().naive_local().date();
|
||||
|
||||
for (index, entry) in all_entries.iter().enumerate() {
|
||||
for (index, entry) in new_entries.iter().enumerate() {
|
||||
let entry_date = entry
|
||||
.updated_at()
|
||||
.with_timezone(&Local)
|
||||
@@ -152,20 +141,50 @@ impl ThreadHistory {
|
||||
|
||||
if Some(entry_bucket) != bucket {
|
||||
bucket = Some(entry_bucket);
|
||||
separated_items.push(HistoryListItem::BucketSeparator(entry_bucket));
|
||||
items.push(ListItemType::BucketSeparator(entry_bucket));
|
||||
}
|
||||
separated_items.push(HistoryListItem::Entry {
|
||||
|
||||
indexes.push(items.len() as u32);
|
||||
items.push(ListItemType::Entry {
|
||||
index,
|
||||
format: entry_bucket.into(),
|
||||
});
|
||||
}
|
||||
separated_items
|
||||
(new_entries, items, indexes)
|
||||
});
|
||||
|
||||
let task = cx.spawn(async move |this, cx| {
|
||||
let separated_items = bg_task.await;
|
||||
let (new_entries, items, indexes) = bg_task.await;
|
||||
this.update(cx, |this, cx| {
|
||||
this.separated_items = separated_items;
|
||||
let previously_selected_entry =
|
||||
this.all_entries.get(this.selected_index).map(|e| e.id());
|
||||
|
||||
this.all_entries = new_entries;
|
||||
this.separated_items = items;
|
||||
this.separated_item_indexes = indexes;
|
||||
|
||||
match &this.search_state {
|
||||
SearchState::Empty => {
|
||||
if this.selected_index >= this.all_entries.len() {
|
||||
this.set_selected_entry_index(
|
||||
this.all_entries.len().saturating_sub(1),
|
||||
cx,
|
||||
);
|
||||
} else if let Some(prev_id) = previously_selected_entry {
|
||||
if let Some(new_ix) = this
|
||||
.all_entries
|
||||
.iter()
|
||||
.position(|probe| probe.id() == prev_id)
|
||||
{
|
||||
this.set_selected_entry_index(new_ix, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
SearchState::Searching { query, .. } | SearchState::Searched { query, .. } => {
|
||||
this.search(query.clone(), cx);
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
})
|
||||
.log_err();
|
||||
@@ -233,7 +252,7 @@ impl ThreadHistory {
|
||||
matches,
|
||||
};
|
||||
|
||||
this.set_selected_index(0, cx);
|
||||
this.set_selected_entry_index(0, cx);
|
||||
cx.notify();
|
||||
};
|
||||
})
|
||||
@@ -241,10 +260,7 @@ impl ThreadHistory {
|
||||
}
|
||||
});
|
||||
|
||||
self.search_state = SearchState::Searching {
|
||||
query: query.clone(),
|
||||
_task: task,
|
||||
};
|
||||
self.search_state = SearchState::Searching { query, _task: task };
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -291,9 +307,9 @@ impl ThreadHistory {
|
||||
let count = self.matched_count();
|
||||
if count > 0 {
|
||||
if self.selected_index == 0 {
|
||||
self.set_selected_index(count - 1, cx);
|
||||
self.set_selected_entry_index(count - 1, cx);
|
||||
} else {
|
||||
self.set_selected_index(self.selected_index - 1, cx);
|
||||
self.set_selected_entry_index(self.selected_index - 1, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -307,9 +323,9 @@ impl ThreadHistory {
|
||||
let count = self.matched_count();
|
||||
if count > 0 {
|
||||
if self.selected_index == count - 1 {
|
||||
self.set_selected_index(0, cx);
|
||||
self.set_selected_entry_index(0, cx);
|
||||
} else {
|
||||
self.set_selected_index(self.selected_index + 1, cx);
|
||||
self.set_selected_entry_index(self.selected_index + 1, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -322,21 +338,32 @@ impl ThreadHistory {
|
||||
) {
|
||||
let count = self.matched_count();
|
||||
if count > 0 {
|
||||
self.set_selected_index(0, cx);
|
||||
self.set_selected_entry_index(0, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
let count = self.matched_count();
|
||||
if count > 0 {
|
||||
self.set_selected_index(count - 1, cx);
|
||||
self.set_selected_entry_index(count - 1, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, index: usize, cx: &mut Context<Self>) {
|
||||
self.selected_index = index;
|
||||
fn set_selected_entry_index(&mut self, entry_index: usize, cx: &mut Context<Self>) {
|
||||
self.selected_index = entry_index;
|
||||
|
||||
let scroll_ix = match self.search_state {
|
||||
SearchState::Empty | SearchState::Searching { .. } => self
|
||||
.separated_item_indexes
|
||||
.get(entry_index)
|
||||
.map(|ix| *ix as usize)
|
||||
.unwrap_or(entry_index + 1),
|
||||
SearchState::Searched { .. } => entry_index,
|
||||
};
|
||||
|
||||
self.scroll_handle
|
||||
.scroll_to_item(index, ScrollStrategy::Top);
|
||||
.scroll_to_item(scroll_ix, ScrollStrategy::Top);
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -445,7 +472,7 @@ impl ThreadHistory {
|
||||
.map(|(ix, m)| {
|
||||
self.render_list_item(
|
||||
Some(range_start + ix),
|
||||
&HistoryListItem::Entry {
|
||||
&ListItemType::Entry {
|
||||
index: m.candidate_id,
|
||||
format: EntryTimeFormat::DateAndTime,
|
||||
},
|
||||
@@ -463,25 +490,36 @@ impl ThreadHistory {
|
||||
fn render_list_item(
|
||||
&self,
|
||||
list_entry_ix: Option<usize>,
|
||||
item: &HistoryListItem,
|
||||
item: &ListItemType,
|
||||
highlight_positions: Vec<usize>,
|
||||
cx: &App,
|
||||
cx: &Context<Self>,
|
||||
) -> AnyElement {
|
||||
match item {
|
||||
HistoryListItem::Entry { index, format } => match self.all_entries.get(*index) {
|
||||
ListItemType::Entry { index, format } => match self.all_entries.get(*index) {
|
||||
Some(entry) => h_flex()
|
||||
.w_full()
|
||||
.pb_1()
|
||||
.child(self.render_history_entry(
|
||||
entry,
|
||||
list_entry_ix == Some(self.selected_index),
|
||||
highlight_positions,
|
||||
*format,
|
||||
))
|
||||
.child(
|
||||
HistoryEntryElement::new(entry.clone(), self.agent_panel.clone())
|
||||
.highlight_positions(highlight_positions)
|
||||
.timestamp_format(*format)
|
||||
.selected(list_entry_ix == Some(self.selected_index))
|
||||
.hovered(list_entry_ix == self.hovered_index)
|
||||
.on_hover(cx.listener(move |this, is_hovered, _window, cx| {
|
||||
if *is_hovered {
|
||||
this.hovered_index = list_entry_ix;
|
||||
} else if this.hovered_index == list_entry_ix {
|
||||
this.hovered_index = None;
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}))
|
||||
.into_any_element(),
|
||||
)
|
||||
.into_any(),
|
||||
None => Empty.into_any_element(),
|
||||
},
|
||||
HistoryListItem::BucketSeparator(bucket) => div()
|
||||
ListItemType::BucketSeparator(bucket) => div()
|
||||
.px(DynamicSpacing::Base06.rems(cx))
|
||||
.pt_2()
|
||||
.pb_1()
|
||||
@@ -493,33 +531,6 @@ impl ThreadHistory {
|
||||
.into_any_element(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_history_entry(
|
||||
&self,
|
||||
entry: &HistoryEntry,
|
||||
is_active: bool,
|
||||
highlight_positions: Vec<usize>,
|
||||
format: EntryTimeFormat,
|
||||
) -> AnyElement {
|
||||
match entry {
|
||||
HistoryEntry::Thread(thread) => PastThread::new(
|
||||
thread.clone(),
|
||||
self.agent_panel.clone(),
|
||||
is_active,
|
||||
highlight_positions,
|
||||
format,
|
||||
)
|
||||
.into_any_element(),
|
||||
HistoryEntry::Context(context) => PastContext::new(
|
||||
context.clone(),
|
||||
self.agent_panel.clone(),
|
||||
is_active,
|
||||
highlight_positions,
|
||||
format,
|
||||
)
|
||||
.into_any_element(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for ThreadHistory {
|
||||
@@ -601,155 +612,97 @@ impl Render for ThreadHistory {
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct PastThread {
|
||||
thread: SerializedThreadMetadata,
|
||||
pub struct HistoryEntryElement {
|
||||
entry: HistoryEntry,
|
||||
agent_panel: WeakEntity<AgentPanel>,
|
||||
selected: bool,
|
||||
hovered: bool,
|
||||
highlight_positions: Vec<usize>,
|
||||
timestamp_format: EntryTimeFormat,
|
||||
on_hover: Box<dyn Fn(&bool, &mut Window, &mut App) + 'static>,
|
||||
}
|
||||
|
||||
impl PastThread {
|
||||
pub fn new(
|
||||
thread: SerializedThreadMetadata,
|
||||
agent_panel: WeakEntity<AgentPanel>,
|
||||
selected: bool,
|
||||
highlight_positions: Vec<usize>,
|
||||
timestamp_format: EntryTimeFormat,
|
||||
) -> Self {
|
||||
impl HistoryEntryElement {
|
||||
pub fn new(entry: HistoryEntry, agent_panel: WeakEntity<AgentPanel>) -> Self {
|
||||
Self {
|
||||
thread,
|
||||
entry,
|
||||
agent_panel,
|
||||
selected,
|
||||
highlight_positions,
|
||||
timestamp_format,
|
||||
selected: false,
|
||||
hovered: false,
|
||||
highlight_positions: vec![],
|
||||
timestamp_format: EntryTimeFormat::DateAndTime,
|
||||
on_hover: Box::new(|_, _, _| {}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn hovered(mut self, hovered: bool) -> Self {
|
||||
self.hovered = hovered;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn highlight_positions(mut self, positions: Vec<usize>) -> Self {
|
||||
self.highlight_positions = positions;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_hover(mut self, on_hover: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self {
|
||||
self.on_hover = Box::new(on_hover);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn timestamp_format(mut self, format: EntryTimeFormat) -> Self {
|
||||
self.timestamp_format = format;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for PastThread {
|
||||
impl RenderOnce for HistoryEntryElement {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let summary = self.thread.summary;
|
||||
let (id, summary, timestamp) = match &self.entry {
|
||||
HistoryEntry::Thread(thread) => (
|
||||
thread.id.to_string(),
|
||||
thread.summary.clone(),
|
||||
thread.updated_at.timestamp(),
|
||||
),
|
||||
HistoryEntry::Context(context) => (
|
||||
context.path.to_string_lossy().to_string(),
|
||||
context.title.clone().into(),
|
||||
context.mtime.timestamp(),
|
||||
),
|
||||
};
|
||||
|
||||
let thread_timestamp = self.timestamp_format.format_timestamp(
|
||||
&self.agent_panel,
|
||||
self.thread.updated_at.timestamp(),
|
||||
cx,
|
||||
);
|
||||
let thread_timestamp =
|
||||
self.timestamp_format
|
||||
.format_timestamp(&self.agent_panel, timestamp, cx);
|
||||
|
||||
ListItem::new(SharedString::from(self.thread.id.to_string()))
|
||||
ListItem::new(SharedString::from(id))
|
||||
.rounded()
|
||||
.toggle_state(self.selected)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.start_slot(
|
||||
div().max_w_4_5().child(
|
||||
HighlightedLabel::new(summary, self.highlight_positions)
|
||||
.size(LabelSize::Small)
|
||||
.truncate(),
|
||||
),
|
||||
)
|
||||
.end_slot(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(
|
||||
HighlightedLabel::new(summary, self.highlight_positions)
|
||||
.size(LabelSize::Small)
|
||||
.truncate(),
|
||||
)
|
||||
.child(
|
||||
Label::new(thread_timestamp)
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::XSmall),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("delete", IconName::TrashAlt)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx)
|
||||
})
|
||||
.on_click({
|
||||
let agent_panel = self.agent_panel.clone();
|
||||
let id = self.thread.id.clone();
|
||||
move |_event, _window, cx| {
|
||||
agent_panel
|
||||
.update(cx, |this, cx| {
|
||||
this.delete_thread(&id, cx).detach_and_log_err(cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
.on_click({
|
||||
let agent_panel = self.agent_panel.clone();
|
||||
let id = self.thread.id.clone();
|
||||
move |_event, window, cx| {
|
||||
agent_panel
|
||||
.update(cx, |this, cx| {
|
||||
this.open_thread_by_id(&id, window, cx)
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct PastContext {
|
||||
context: SavedContextMetadata,
|
||||
agent_panel: WeakEntity<AgentPanel>,
|
||||
selected: bool,
|
||||
highlight_positions: Vec<usize>,
|
||||
timestamp_format: EntryTimeFormat,
|
||||
}
|
||||
|
||||
impl PastContext {
|
||||
pub fn new(
|
||||
context: SavedContextMetadata,
|
||||
agent_panel: WeakEntity<AgentPanel>,
|
||||
selected: bool,
|
||||
highlight_positions: Vec<usize>,
|
||||
timestamp_format: EntryTimeFormat,
|
||||
) -> Self {
|
||||
Self {
|
||||
context,
|
||||
agent_panel,
|
||||
selected,
|
||||
highlight_positions,
|
||||
timestamp_format,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for PastContext {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let summary = self.context.title;
|
||||
let context_timestamp = self.timestamp_format.format_timestamp(
|
||||
&self.agent_panel,
|
||||
self.context.mtime.timestamp(),
|
||||
cx,
|
||||
);
|
||||
|
||||
ListItem::new(SharedString::from(
|
||||
self.context.path.to_string_lossy().to_string(),
|
||||
))
|
||||
.rounded()
|
||||
.toggle_state(self.selected)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.start_slot(
|
||||
div().max_w_4_5().child(
|
||||
HighlightedLabel::new(summary, self.highlight_positions)
|
||||
.size(LabelSize::Small)
|
||||
.truncate(),
|
||||
),
|
||||
)
|
||||
.end_slot(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
Label::new(context_timestamp)
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::XSmall),
|
||||
)
|
||||
.child(
|
||||
.on_hover(self.on_hover)
|
||||
.end_slot::<IconButton>(if self.hovered || self.selected {
|
||||
Some(
|
||||
IconButton::new("delete", IconName::TrashAlt)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
@@ -759,30 +712,70 @@ impl RenderOnce for PastContext {
|
||||
})
|
||||
.on_click({
|
||||
let agent_panel = self.agent_panel.clone();
|
||||
let path = self.context.path.clone();
|
||||
move |_event, _window, cx| {
|
||||
agent_panel
|
||||
.update(cx, |this, cx| {
|
||||
this.delete_context(path.clone(), cx)
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
let f: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static> =
|
||||
match &self.entry {
|
||||
HistoryEntry::Thread(thread) => {
|
||||
let id = thread.id.clone();
|
||||
|
||||
Box::new(move |_event, _window, cx| {
|
||||
agent_panel
|
||||
.update(cx, |this, cx| {
|
||||
this.delete_thread(&id, cx)
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
HistoryEntry::Context(context) => {
|
||||
let path = context.path.clone();
|
||||
|
||||
Box::new(move |_event, _window, cx| {
|
||||
agent_panel
|
||||
.update(cx, |this, cx| {
|
||||
this.delete_context(path.clone(), cx)
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
};
|
||||
f
|
||||
}),
|
||||
),
|
||||
)
|
||||
.on_click({
|
||||
let agent_panel = self.agent_panel.clone();
|
||||
let path = self.context.path.clone();
|
||||
move |_event, window, cx| {
|
||||
agent_panel
|
||||
.update(cx, |this, cx| {
|
||||
this.open_saved_prompt_editor(path.clone(), window, cx)
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.on_click({
|
||||
let agent_panel = self.agent_panel.clone();
|
||||
|
||||
let f: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static> = match &self.entry
|
||||
{
|
||||
HistoryEntry::Thread(thread) => {
|
||||
let id = thread.id.clone();
|
||||
Box::new(move |_event, window, cx| {
|
||||
agent_panel
|
||||
.update(cx, |this, cx| {
|
||||
this.open_thread_by_id(&id, window, cx)
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
HistoryEntry::Context(context) => {
|
||||
let path = context.path.clone();
|
||||
Box::new(move |_event, window, cx| {
|
||||
agent_panel
|
||||
.update(cx, |this, cx| {
|
||||
this.open_saved_prompt_editor(path.clone(), window, cx)
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
};
|
||||
f
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
use std::borrow::Cow;
|
||||
use std::cell::{Ref, RefCell};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use agent_settings::{AgentProfile, AgentProfileId, AgentSettings, CompletionMode};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_settings::{AgentProfile, AgentProfileId, AssistantSettings, CompletionMode};
|
||||
use assistant_tool::{ToolId, ToolSource, ToolWorkingSet};
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::HashMap;
|
||||
use context_server::ContextServerId;
|
||||
use db::sqlez::bindable::Column;
|
||||
use db::sqlez::statement::Statement;
|
||||
use db::sqlez_macros::sql;
|
||||
use db::{define_connection, query};
|
||||
use futures::channel::{mpsc, oneshot};
|
||||
use futures::future::{self, BoxFuture, Shared};
|
||||
use futures::{FutureExt as _, StreamExt as _};
|
||||
@@ -17,9 +21,8 @@ use gpui::{
|
||||
App, BackgroundExecutor, Context, Entity, EventEmitter, Global, ReadGlobal, SharedString,
|
||||
Subscription, Task, prelude::*,
|
||||
};
|
||||
use heed::Database;
|
||||
use heed::types::SerdeBincode;
|
||||
use language_model::{LanguageModelToolUseId, Role, TokenUsage};
|
||||
use heed;
|
||||
use language_model::{LanguageModelToolResultContent, LanguageModelToolUseId, Role, TokenUsage};
|
||||
use project::context_server_store::{ContextServerStatus, ContextServerStore};
|
||||
use project::{Project, ProjectItem, ProjectPath, Worktree};
|
||||
use prompt_store::{
|
||||
@@ -36,6 +39,31 @@ use crate::thread::{
|
||||
DetailedSummaryState, ExceededWindowError, MessageId, ProjectSnapshot, Thread, ThreadId,
|
||||
};
|
||||
|
||||
// Implement Bind trait for ThreadId to use in SQL queries
|
||||
// impl db::sqlez::bindable::Bind for ThreadId {
|
||||
// fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result<i32> {
|
||||
// self.to_string().bind(statement, start_index)
|
||||
// }
|
||||
// }
|
||||
|
||||
// Implement Column trait for SerializedThreadMetadata
|
||||
impl Column for SerializedThreadMetadata {
|
||||
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
|
||||
let (id_str, next_index): (String, i32) = Column::column(statement, start_index)?;
|
||||
let (summary, next_index): (String, i32) = Column::column(statement, next_index)?;
|
||||
let (updated_at_timestamp, next_index): (i64, i32) = Column::column(statement, next_index)?;
|
||||
|
||||
Ok((
|
||||
Self {
|
||||
id: ThreadId::from(id_str.as_str()),
|
||||
summary: summary.into(),
|
||||
updated_at: DateTime::from_timestamp(updated_at_timestamp, 0).unwrap_or_default(),
|
||||
},
|
||||
next_index,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
const RULES_FILE_NAMES: [&'static str; 6] = [
|
||||
".rules",
|
||||
".cursorrules",
|
||||
@@ -386,6 +414,25 @@ impl ThreadStore {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn create_thread_from_serialized(
|
||||
&mut self,
|
||||
serialized: SerializedThread,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Entity<Thread> {
|
||||
cx.new(|cx| {
|
||||
Thread::deserialize(
|
||||
ThreadId::new(),
|
||||
serialized,
|
||||
self.project.clone(),
|
||||
self.tools.clone(),
|
||||
self.prompt_builder.clone(),
|
||||
self.project_context.clone(),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn open_thread(
|
||||
&self,
|
||||
id: &ThreadId,
|
||||
@@ -400,7 +447,7 @@ impl ThreadStore {
|
||||
let thread = database
|
||||
.try_find_thread(id.clone())
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("no thread found with ID: {id:?}"))?;
|
||||
.with_context(|| format!("no thread found with ID: {id:?}"))?;
|
||||
|
||||
let thread = this.update_in(cx, |this, window, cx| {
|
||||
cx.new(|cx| {
|
||||
@@ -411,7 +458,7 @@ impl ThreadStore {
|
||||
this.tools.clone(),
|
||||
this.prompt_builder.clone(),
|
||||
this.project_context.clone(),
|
||||
window,
|
||||
Some(window),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
@@ -466,13 +513,13 @@ impl ThreadStore {
|
||||
}
|
||||
|
||||
fn load_default_profile(&self, cx: &mut Context<Self>) {
|
||||
let assistant_settings = AssistantSettings::get_global(cx);
|
||||
let assistant_settings = AgentSettings::get_global(cx);
|
||||
|
||||
self.load_profile_by_id(assistant_settings.default_profile.clone(), cx);
|
||||
}
|
||||
|
||||
pub fn load_profile_by_id(&self, profile_id: AgentProfileId, cx: &mut Context<Self>) {
|
||||
let assistant_settings = AssistantSettings::get_global(cx);
|
||||
let assistant_settings = AgentSettings::get_global(cx);
|
||||
|
||||
if let Some(profile) = assistant_settings.profiles.get(&profile_id) {
|
||||
self.load_profile(profile.clone(), cx);
|
||||
@@ -486,8 +533,8 @@ impl ThreadStore {
|
||||
ToolSource::Native,
|
||||
&profile
|
||||
.tools
|
||||
.iter()
|
||||
.filter_map(|(tool, enabled)| enabled.then(|| tool.clone()))
|
||||
.into_iter()
|
||||
.filter_map(|(tool, enabled)| enabled.then(|| tool))
|
||||
.collect::<Vec<_>>(),
|
||||
cx,
|
||||
);
|
||||
@@ -511,32 +558,32 @@ impl ThreadStore {
|
||||
});
|
||||
}
|
||||
// Enable all the tools from all context servers, but disable the ones that are explicitly disabled
|
||||
for (context_server_id, preset) in &profile.context_servers {
|
||||
for (context_server_id, preset) in profile.context_servers {
|
||||
self.tools.update(cx, |tools, cx| {
|
||||
tools.disable(
|
||||
ToolSource::ContextServer {
|
||||
id: context_server_id.clone().into(),
|
||||
id: context_server_id.into(),
|
||||
},
|
||||
&preset
|
||||
.tools
|
||||
.iter()
|
||||
.filter_map(|(tool, enabled)| (!enabled).then(|| tool.clone()))
|
||||
.into_iter()
|
||||
.filter_map(|(tool, enabled)| (!enabled).then(|| tool))
|
||||
.collect::<Vec<_>>(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
for (context_server_id, preset) in &profile.context_servers {
|
||||
for (context_server_id, preset) in profile.context_servers {
|
||||
self.tools.update(cx, |tools, cx| {
|
||||
tools.enable(
|
||||
ToolSource::ContextServer {
|
||||
id: context_server_id.clone().into(),
|
||||
id: context_server_id.into(),
|
||||
},
|
||||
&preset
|
||||
.tools
|
||||
.iter()
|
||||
.filter_map(|(tool, enabled)| enabled.then(|| tool.clone()))
|
||||
.into_iter()
|
||||
.filter_map(|(tool, enabled)| enabled.then(|| tool))
|
||||
.collect::<Vec<_>>(),
|
||||
cx,
|
||||
)
|
||||
@@ -638,6 +685,7 @@ pub struct SerializedThreadMetadata {
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[cfg_attr(test, derive(Clone))]
|
||||
pub struct SerializedThread {
|
||||
pub version: String,
|
||||
pub summary: SharedString,
|
||||
@@ -658,10 +706,11 @@ pub struct SerializedThread {
|
||||
#[serde(default)]
|
||||
pub completion_mode: Option<CompletionMode>,
|
||||
#[serde(default)]
|
||||
pub profile: Option<AgentProfileId>,
|
||||
pub tool_use_limit_reached: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[cfg_attr(test, derive(Clone))]
|
||||
pub struct SerializedLanguageModel {
|
||||
pub provider: String,
|
||||
pub model: String,
|
||||
@@ -682,20 +731,14 @@ impl SerializedThread {
|
||||
SerializedThread::VERSION => Ok(serde_json::from_value::<SerializedThread>(
|
||||
saved_thread_json,
|
||||
)?),
|
||||
_ => Err(anyhow!(
|
||||
"unrecognized serialized thread version: {}",
|
||||
version
|
||||
)),
|
||||
_ => anyhow::bail!("unrecognized serialized thread version: {version:?}"),
|
||||
},
|
||||
None => {
|
||||
let saved_thread =
|
||||
serde_json::from_value::<LegacySerializedThread>(saved_thread_json)?;
|
||||
Ok(saved_thread.upgrade())
|
||||
}
|
||||
version => Err(anyhow!(
|
||||
"unrecognized serialized thread version: {:?}",
|
||||
version
|
||||
)),
|
||||
version => anyhow::bail!("unrecognized serialized thread version: {version:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -732,7 +775,7 @@ impl SerializedThreadV0_1_0 {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct SerializedMessage {
|
||||
pub id: MessageId,
|
||||
pub role: Role,
|
||||
@@ -746,9 +789,11 @@ pub struct SerializedMessage {
|
||||
pub context: String,
|
||||
#[serde(default)]
|
||||
pub creases: Vec<SerializedCrease>,
|
||||
#[serde(default)]
|
||||
pub is_hidden: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum SerializedMessageSegment {
|
||||
#[serde(rename = "text")]
|
||||
@@ -766,18 +811,18 @@ pub enum SerializedMessageSegment {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct SerializedToolUse {
|
||||
pub id: LanguageModelToolUseId,
|
||||
pub name: SharedString,
|
||||
pub input: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct SerializedToolResult {
|
||||
pub tool_use_id: LanguageModelToolUseId,
|
||||
pub is_error: bool,
|
||||
pub content: Arc<str>,
|
||||
pub content: LanguageModelToolResultContent,
|
||||
pub output: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
@@ -804,7 +849,7 @@ impl LegacySerializedThread {
|
||||
exceeded_window_error: None,
|
||||
model: None,
|
||||
completion_mode: None,
|
||||
profile: None,
|
||||
tool_use_limit_reached: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -830,11 +875,12 @@ impl LegacySerializedMessage {
|
||||
tool_results: self.tool_results,
|
||||
context: String::new(),
|
||||
creases: Vec::new(),
|
||||
is_hidden: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct SerializedCrease {
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
@@ -850,26 +896,6 @@ impl Global for GlobalThreadsDatabase {}
|
||||
|
||||
pub(crate) struct ThreadsDatabase {
|
||||
executor: BackgroundExecutor,
|
||||
env: heed::Env,
|
||||
threads: Database<SerdeBincode<ThreadId>, SerializedThread>,
|
||||
}
|
||||
|
||||
impl heed::BytesEncode<'_> for SerializedThread {
|
||||
type EItem = SerializedThread;
|
||||
|
||||
fn bytes_encode(item: &Self::EItem) -> Result<Cow<[u8]>, heed::BoxedError> {
|
||||
serde_json::to_vec(item).map(Cow::Owned).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> heed::BytesDecode<'a> for SerializedThread {
|
||||
type DItem = SerializedThread;
|
||||
|
||||
fn bytes_decode(bytes: &'a [u8]) -> Result<Self::DItem, heed::BoxedError> {
|
||||
// We implement this type manually because we want to call `SerializedThread::from_json`,
|
||||
// instead of the Deserialize trait implementation for `SerializedThread`.
|
||||
SerializedThread::from_json(bytes).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl ThreadsDatabase {
|
||||
@@ -884,8 +910,7 @@ impl ThreadsDatabase {
|
||||
let database_future = executor
|
||||
.spawn({
|
||||
let executor = executor.clone();
|
||||
let database_path = paths::data_dir().join("threads/threads-db.1.mdb");
|
||||
async move { ThreadsDatabase::new(database_path, executor) }
|
||||
async move { ThreadsDatabase::new(executor).await }
|
||||
})
|
||||
.then(|result| future::ready(result.map(Arc::new).map_err(Arc::new)))
|
||||
.boxed()
|
||||
@@ -894,80 +919,511 @@ impl ThreadsDatabase {
|
||||
cx.set_global(GlobalThreadsDatabase(database_future));
|
||||
}
|
||||
|
||||
pub fn new(path: PathBuf, executor: BackgroundExecutor) -> Result<Self> {
|
||||
std::fs::create_dir_all(&path)?;
|
||||
|
||||
const ONE_GB_IN_BYTES: usize = 1024 * 1024 * 1024;
|
||||
let env = unsafe {
|
||||
heed::EnvOpenOptions::new()
|
||||
.map_size(ONE_GB_IN_BYTES)
|
||||
.max_dbs(1)
|
||||
.open(path)?
|
||||
};
|
||||
|
||||
let mut txn = env.write_txn()?;
|
||||
let threads = env.create_database(&mut txn, Some("threads"))?;
|
||||
txn.commit()?;
|
||||
|
||||
Ok(Self {
|
||||
executor,
|
||||
env,
|
||||
threads,
|
||||
})
|
||||
pub async fn new(executor: BackgroundExecutor) -> Result<Self> {
|
||||
Ok(Self { executor })
|
||||
}
|
||||
|
||||
pub fn list_threads(&self) -> Task<Result<Vec<SerializedThreadMetadata>>> {
|
||||
let env = self.env.clone();
|
||||
let threads = self.threads;
|
||||
|
||||
self.executor.spawn(async move {
|
||||
let txn = env.read_txn()?;
|
||||
let mut iter = threads.iter(&txn)?;
|
||||
let mut threads = Vec::new();
|
||||
while let Some((key, value)) = iter.next().transpose()? {
|
||||
threads.push(SerializedThreadMetadata {
|
||||
id: key,
|
||||
summary: value.summary,
|
||||
updated_at: value.updated_at,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(threads)
|
||||
})
|
||||
self.executor
|
||||
.spawn(async move { AGENT_THREADS.all_threads().await })
|
||||
}
|
||||
|
||||
pub fn try_find_thread(&self, id: ThreadId) -> Task<Result<Option<SerializedThread>>> {
|
||||
let env = self.env.clone();
|
||||
let threads = self.threads;
|
||||
|
||||
self.executor.spawn(async move {
|
||||
let txn = env.read_txn()?;
|
||||
let thread = threads.get(&txn, &id)?;
|
||||
Ok(thread)
|
||||
})
|
||||
self.executor
|
||||
.spawn(async move { AGENT_THREADS.get_thread(id).await })
|
||||
}
|
||||
|
||||
pub fn save_thread(&self, id: ThreadId, thread: SerializedThread) -> Task<Result<()>> {
|
||||
let env = self.env.clone();
|
||||
let threads = self.threads;
|
||||
|
||||
self.executor.spawn(async move {
|
||||
let mut txn = env.write_txn()?;
|
||||
threads.put(&mut txn, &id, &thread)?;
|
||||
txn.commit()?;
|
||||
Ok(())
|
||||
})
|
||||
self.executor
|
||||
.spawn(async move { AGENT_THREADS.save_thread(id, thread).await })
|
||||
}
|
||||
|
||||
pub fn delete_thread(&self, id: ThreadId) -> Task<Result<()>> {
|
||||
let env = self.env.clone();
|
||||
let threads = self.threads;
|
||||
self.executor
|
||||
.spawn(async move { AGENT_THREADS.delete_thread_by_id(id).await })
|
||||
}
|
||||
|
||||
self.executor.spawn(async move {
|
||||
let mut txn = env.write_txn()?;
|
||||
threads.delete(&mut txn, &id)?;
|
||||
txn.commit()?;
|
||||
Ok(())
|
||||
})
|
||||
/// Migrate a legacy `heed` LMDB database to SQLite
|
||||
pub async fn migrate_from_heed(heed_path: &Path) -> Result<()> {
|
||||
Self::migrate_from_heed_to_db(heed_path, &AGENT_THREADS).await
|
||||
}
|
||||
|
||||
/// Migrate a legacy `heed` LMDB database to a specific SQLite database
|
||||
pub async fn migrate_from_heed_to_db(heed_path: &Path, db: &ThreadStoreDB) -> Result<()> {
|
||||
if !heed_path.exists() {
|
||||
return Ok(()); // No migration needed
|
||||
}
|
||||
|
||||
// Open the old heed database
|
||||
let env = unsafe {
|
||||
heed::EnvOpenOptions::new()
|
||||
.map_size(1024 * 1024 * 1024) // 1GB
|
||||
.max_dbs(1)
|
||||
.open(&heed_path)?
|
||||
};
|
||||
|
||||
let txn = env.read_txn()?;
|
||||
let old_threads: heed::Database<heed::types::SerdeBincode<ThreadId>, SerializedThread> =
|
||||
env.open_database(&txn, Some("threads"))?
|
||||
.ok_or_else(|| anyhow!("threads database not found"))?;
|
||||
|
||||
// Migrate all threads
|
||||
for result in old_threads.iter(&txn)? {
|
||||
if let Some((id, thread)) = result.log_err() {
|
||||
db.save_thread(id, thread).await.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
drop(txn);
|
||||
drop(env);
|
||||
|
||||
// Rename the old heed database with .bak suffix
|
||||
let mut backup_path = heed_path.to_path_buf();
|
||||
let file_name = heed_path
|
||||
.file_name()
|
||||
.ok_or_else(|| anyhow!("invalid heed path"))?;
|
||||
let new_name = format!("{}.bak", file_name.to_string_lossy());
|
||||
backup_path.set_file_name(new_name);
|
||||
std::fs::rename(&heed_path, &backup_path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// Heed serialization helpers for migration
|
||||
impl heed::BytesEncode<'_> for SerializedThread {
|
||||
type EItem = SerializedThread;
|
||||
|
||||
fn bytes_encode(item: &Self::EItem) -> Result<Cow<[u8]>, heed::BoxedError> {
|
||||
serde_json::to_vec(item).map(Cow::Owned).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> heed::BytesDecode<'a> for SerializedThread {
|
||||
type DItem = SerializedThread;
|
||||
|
||||
fn bytes_decode(bytes: &'a [u8]) -> Result<Self::DItem, heed::BoxedError> {
|
||||
SerializedThread::from_json(bytes).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
define_connection!(pub static ref AGENT_THREADS: ThreadStoreDB<()> =
|
||||
&[sql!(
|
||||
CREATE TABLE IF NOT EXISTS agent_threads(
|
||||
id TEXT PRIMARY KEY,
|
||||
summary TEXT NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
data TEXT NOT NULL
|
||||
) STRICT;
|
||||
)];
|
||||
);
|
||||
|
||||
impl ThreadStoreDB {
|
||||
query! {
|
||||
pub async fn all_threads() -> Result<Vec<SerializedThreadMetadata>> {
|
||||
SELECT id, summary, updated_at
|
||||
FROM agent_threads
|
||||
ORDER BY updated_at DESC
|
||||
}
|
||||
}
|
||||
|
||||
query! {
|
||||
async fn get_thread_data(id: String) -> Result<Option<String>> {
|
||||
SELECT data FROM agent_threads WHERE id = (?)
|
||||
}
|
||||
}
|
||||
|
||||
query! {
|
||||
async fn save_thread_data(id: String, summary: String, updated_at: i64, data: String) -> Result<()> {
|
||||
INSERT OR REPLACE INTO agent_threads (id, summary, updated_at, data)
|
||||
VALUES ((?), (?), (?), (?))
|
||||
}
|
||||
}
|
||||
|
||||
query! {
|
||||
async fn delete_thread_data(id: String) -> Result<()> {
|
||||
DELETE FROM agent_threads WHERE id = (?)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_thread(&self, id: ThreadId) -> Result<Option<SerializedThread>> {
|
||||
let id_str = id.to_string();
|
||||
let result = self.get_thread_data(id_str).await?;
|
||||
|
||||
match result {
|
||||
Some(json_str) => {
|
||||
let thread = SerializedThread::from_json(json_str.as_bytes())?;
|
||||
Ok(Some(thread))
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn save_thread(&self, id: ThreadId, thread: SerializedThread) -> Result<()> {
|
||||
let thread_json = serde_json::to_string(&thread)?;
|
||||
let updated_at = thread.updated_at.timestamp();
|
||||
let id_str = id.to_string();
|
||||
let summary = thread.summary.clone();
|
||||
|
||||
self.save_thread_data(id_str, summary.to_string(), updated_at, thread_json)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn delete_thread_by_id(&self, id: ThreadId) -> Result<()> {
|
||||
let id_str = id.to_string();
|
||||
self.delete_thread_data(id_str).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use chrono::Utc;
|
||||
use gpui::TestAppContext;
|
||||
use std::sync::Arc;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_save_load_delete_threads(_cx: &mut TestAppContext) {
|
||||
let db = ThreadStoreDB::open_test_db("test_save_load_delete_threads").await;
|
||||
|
||||
// Test that no threads exist initially
|
||||
let threads = db.all_threads().await.unwrap();
|
||||
assert_eq!(threads.len(), 0);
|
||||
|
||||
// Create test thread data
|
||||
let thread_id = ThreadId::from("test-thread-1");
|
||||
let thread = SerializedThread {
|
||||
version: SerializedThread::VERSION.to_string(),
|
||||
summary: SharedString::from("Test thread summary"),
|
||||
updated_at: Utc::now(),
|
||||
messages: vec![],
|
||||
initial_project_snapshot: None,
|
||||
cumulative_token_usage: TokenUsage::default(),
|
||||
request_token_usage: vec![],
|
||||
detailed_summary_state: DetailedSummaryState::NotGenerated,
|
||||
exceeded_window_error: None,
|
||||
model: None,
|
||||
completion_mode: Some(CompletionMode::Normal),
|
||||
tool_use_limit_reached: false,
|
||||
};
|
||||
|
||||
let thread_summary = thread.summary.clone();
|
||||
let thread_version = thread.version.clone();
|
||||
|
||||
// Save thread
|
||||
db.save_thread(thread_id.clone(), thread.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Load all threads
|
||||
let threads = db.all_threads().await.unwrap();
|
||||
assert_eq!(threads.len(), 1);
|
||||
assert_eq!(threads[0].id, thread_id);
|
||||
assert_eq!(threads[0].summary, thread_summary);
|
||||
|
||||
// Load specific thread
|
||||
let loaded_thread = db.get_thread(thread_id.clone()).await.unwrap();
|
||||
assert!(loaded_thread.is_some());
|
||||
let loaded_thread = loaded_thread.unwrap();
|
||||
assert_eq!(loaded_thread.summary, thread_summary);
|
||||
assert_eq!(loaded_thread.version, thread_version);
|
||||
|
||||
// Update thread
|
||||
let updated_thread = SerializedThread {
|
||||
summary: SharedString::from("Updated summary"),
|
||||
updated_at: Utc::now(),
|
||||
..thread
|
||||
};
|
||||
db.save_thread(thread_id.clone(), updated_thread.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Verify update
|
||||
let loaded_thread = db.get_thread(thread_id.clone()).await.unwrap().unwrap();
|
||||
assert_eq!(loaded_thread.summary, SharedString::from("Updated summary"));
|
||||
|
||||
// Delete thread
|
||||
db.delete_thread_by_id(thread_id.clone()).await.unwrap();
|
||||
|
||||
// Verify deletion
|
||||
let loaded_thread = db.get_thread(thread_id.clone()).await.unwrap();
|
||||
assert!(loaded_thread.is_none());
|
||||
|
||||
let threads = db.all_threads().await.unwrap();
|
||||
assert_eq!(threads.len(), 0);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_multiple_threads(_cx: &mut TestAppContext) {
|
||||
let db = ThreadStoreDB::open_test_db("test_multiple_threads").await;
|
||||
|
||||
// Create multiple threads
|
||||
let thread_ids = [
|
||||
ThreadId::from("thread-1"),
|
||||
ThreadId::from("thread-2"),
|
||||
ThreadId::from("thread-3"),
|
||||
];
|
||||
|
||||
for (i, thread_id) in thread_ids.iter().enumerate() {
|
||||
let thread = SerializedThread {
|
||||
version: SerializedThread::VERSION.to_string(),
|
||||
summary: SharedString::from(format!("Thread {}", i + 1)),
|
||||
updated_at: Utc::now() - chrono::Duration::hours(i as i64),
|
||||
messages: vec![],
|
||||
initial_project_snapshot: None,
|
||||
cumulative_token_usage: TokenUsage::default(),
|
||||
request_token_usage: Vec::new(),
|
||||
detailed_summary_state: DetailedSummaryState::NotGenerated,
|
||||
exceeded_window_error: None,
|
||||
model: None,
|
||||
completion_mode: Some(CompletionMode::Normal),
|
||||
tool_use_limit_reached: false,
|
||||
};
|
||||
db.save_thread(thread_id.clone(), thread).await.unwrap();
|
||||
}
|
||||
|
||||
// Load all threads - should be ordered by updated_at DESC
|
||||
let threads = db.all_threads().await.unwrap();
|
||||
assert_eq!(threads.len(), 3);
|
||||
assert_eq!(threads[0].summary.as_ref(), "Thread 1");
|
||||
assert_eq!(threads[1].summary.as_ref(), "Thread 2");
|
||||
assert_eq!(threads[2].summary.as_ref(), "Thread 3");
|
||||
|
||||
// Delete middle thread
|
||||
db.delete_thread_by_id(thread_ids[1].clone()).await.unwrap();
|
||||
|
||||
let threads = db.all_threads().await.unwrap();
|
||||
assert_eq!(threads.len(), 2);
|
||||
assert_eq!(threads[0].summary.as_ref(), "Thread 1");
|
||||
assert_eq!(threads[1].summary.as_ref(), "Thread 3");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_heed_to_sqlite_migration(_cx: &mut TestAppContext) {
|
||||
use heed::types::SerdeBincode;
|
||||
|
||||
// Create a temporary directory for the heed database
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let heed_path = temp_dir.path().join("test-heed-db");
|
||||
|
||||
// Create and populate heed database
|
||||
{
|
||||
std::fs::create_dir_all(&heed_path).unwrap();
|
||||
let env = unsafe {
|
||||
heed::EnvOpenOptions::new()
|
||||
.map_size(1024 * 1024 * 1024)
|
||||
.max_dbs(1)
|
||||
.open(&heed_path)
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
let mut txn = env.write_txn().unwrap();
|
||||
let threads: heed::Database<SerdeBincode<ThreadId>, SerializedThread> =
|
||||
env.create_database(&mut txn, Some("threads")).unwrap();
|
||||
|
||||
// Insert test data
|
||||
let thread_ids = [
|
||||
ThreadId::from("legacy-thread-1"),
|
||||
ThreadId::from("legacy-thread-2"),
|
||||
ThreadId::from("legacy-thread-3"),
|
||||
];
|
||||
|
||||
for (i, thread_id) in thread_ids.iter().enumerate() {
|
||||
let thread = SerializedThread {
|
||||
version: SerializedThread::VERSION.to_string(),
|
||||
summary: SharedString::from(format!("Legacy Thread {}", i + 1)),
|
||||
updated_at: DateTime::from_timestamp(1700000000 - (i as i64) * 86400, 0)
|
||||
.unwrap(),
|
||||
messages: vec![SerializedMessage {
|
||||
id: MessageId(i),
|
||||
role: Role::User,
|
||||
segments: vec![SerializedMessageSegment::Text {
|
||||
text: format!("Test message {}", i),
|
||||
}],
|
||||
tool_uses: vec![],
|
||||
tool_results: vec![],
|
||||
context: String::new(),
|
||||
creases: vec![],
|
||||
is_hidden: false,
|
||||
}],
|
||||
initial_project_snapshot: None,
|
||||
cumulative_token_usage: TokenUsage {
|
||||
input_tokens: ((i + 1) * 100) as u32,
|
||||
output_tokens: ((i + 1) * 50) as u32,
|
||||
cache_creation_input_tokens: 0,
|
||||
cache_read_input_tokens: 0,
|
||||
},
|
||||
request_token_usage: vec![],
|
||||
detailed_summary_state: DetailedSummaryState::NotGenerated,
|
||||
exceeded_window_error: None,
|
||||
model: None,
|
||||
completion_mode: Some(CompletionMode::Normal),
|
||||
tool_use_limit_reached: false,
|
||||
};
|
||||
threads.put(&mut txn, thread_id, &thread).unwrap();
|
||||
}
|
||||
|
||||
txn.commit().unwrap();
|
||||
}
|
||||
|
||||
// Clear any existing SQLite data
|
||||
let db = ThreadStoreDB::open_test_db("test_heed_to_sqlite_migration").await;
|
||||
|
||||
// Verify SQLite is empty
|
||||
let threads_before = db.all_threads().await.unwrap();
|
||||
assert_eq!(threads_before.len(), 0);
|
||||
|
||||
// Run migration
|
||||
ThreadsDatabase::migrate_from_heed_to_db(&heed_path, &db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Verify all threads were migrated
|
||||
let threads_after = db.all_threads().await.unwrap();
|
||||
assert_eq!(threads_after.len(), 3);
|
||||
|
||||
// Verify thread metadata
|
||||
let thread_summaries: Vec<_> = threads_after.iter().map(|t| t.summary.as_ref()).collect();
|
||||
assert!(thread_summaries.contains(&"Legacy Thread 1"));
|
||||
assert!(thread_summaries.contains(&"Legacy Thread 2"));
|
||||
assert!(thread_summaries.contains(&"Legacy Thread 3"));
|
||||
|
||||
// Verify full thread data
|
||||
for i in 1..=3 {
|
||||
let thread_id = ThreadId::from(&format!("legacy-thread-{}", i) as &str);
|
||||
let thread = db.get_thread(thread_id).await.unwrap().unwrap();
|
||||
assert_eq!(thread.summary.as_ref(), format!("Legacy Thread {}", i));
|
||||
assert_eq!(thread.messages.len(), 1);
|
||||
assert_eq!(
|
||||
thread.messages[0].segments[0],
|
||||
SerializedMessageSegment::Text {
|
||||
text: format!("Test message {}", i - 1)
|
||||
}
|
||||
);
|
||||
assert_eq!(thread.cumulative_token_usage.input_tokens, (i * 100) as u32);
|
||||
assert_eq!(thread.cumulative_token_usage.output_tokens, (i * 50) as u32);
|
||||
}
|
||||
|
||||
// Verify heed database was renamed with .bak suffix
|
||||
assert!(!heed_path.exists());
|
||||
let mut backup_path = heed_path.to_path_buf();
|
||||
backup_path.set_file_name(format!("{}.bak", heed_path.file_name().unwrap().to_string_lossy()));
|
||||
assert!(backup_path.exists());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_thread_serialization_deserialization(_cx: &mut TestAppContext) {
|
||||
let db = ThreadStoreDB::open_test_db("test_thread_serialization_deserialization").await;
|
||||
|
||||
let thread_id = ThreadId::from("serialization-test");
|
||||
let original_thread = SerializedThread {
|
||||
version: SerializedThread::VERSION.to_string(),
|
||||
summary: SharedString::from("Serialization test thread"),
|
||||
updated_at: Utc::now(),
|
||||
messages: vec![
|
||||
SerializedMessage {
|
||||
id: MessageId(1),
|
||||
role: Role::User,
|
||||
segments: vec![
|
||||
SerializedMessageSegment::Text {
|
||||
text: "Hello".to_string(),
|
||||
},
|
||||
SerializedMessageSegment::Thinking {
|
||||
text: "Thinking about the response".to_string(),
|
||||
signature: Some("sig123".to_string()),
|
||||
},
|
||||
],
|
||||
tool_uses: vec![SerializedToolUse {
|
||||
id: LanguageModelToolUseId::from("tool-1"),
|
||||
name: SharedString::from("test_tool"),
|
||||
input: serde_json::json!({"key": "value"}),
|
||||
}],
|
||||
tool_results: vec![SerializedToolResult {
|
||||
tool_use_id: LanguageModelToolUseId::from("tool-1"),
|
||||
is_error: false,
|
||||
content: LanguageModelToolResultContent::Text("Result".into()),
|
||||
output: None,
|
||||
}],
|
||||
context: String::new(),
|
||||
creases: vec![SerializedCrease {
|
||||
start: 0,
|
||||
end: 5,
|
||||
icon_path: SharedString::from("icon.png"),
|
||||
label: SharedString::from("test-crease"),
|
||||
}],
|
||||
is_hidden: false,
|
||||
},
|
||||
SerializedMessage {
|
||||
id: MessageId(2),
|
||||
role: Role::Assistant,
|
||||
segments: vec![SerializedMessageSegment::RedactedThinking {
|
||||
data: vec![1, 2, 3, 4, 5],
|
||||
}],
|
||||
tool_uses: vec![],
|
||||
tool_results: vec![],
|
||||
context: String::new(),
|
||||
creases: vec![],
|
||||
is_hidden: true,
|
||||
},
|
||||
],
|
||||
initial_project_snapshot: Some(Arc::new(ProjectSnapshot {
|
||||
worktree_snapshots: vec![],
|
||||
unsaved_buffer_paths: vec![],
|
||||
timestamp: Utc::now(),
|
||||
})),
|
||||
cumulative_token_usage: TokenUsage {
|
||||
input_tokens: 1000,
|
||||
output_tokens: 500,
|
||||
cache_creation_input_tokens: 0,
|
||||
cache_read_input_tokens: 0,
|
||||
},
|
||||
request_token_usage: vec![TokenUsage {
|
||||
input_tokens: 100,
|
||||
output_tokens: 50,
|
||||
cache_creation_input_tokens: 0,
|
||||
cache_read_input_tokens: 0,
|
||||
}],
|
||||
detailed_summary_state: DetailedSummaryState::Generated {
|
||||
text: SharedString::from("Detailed summary"),
|
||||
message_id: MessageId(1),
|
||||
},
|
||||
exceeded_window_error: None,
|
||||
model: Some(SerializedLanguageModel {
|
||||
provider: "test-provider".to_string(),
|
||||
model: "test-model".to_string(),
|
||||
}),
|
||||
completion_mode: Some(CompletionMode::Normal),
|
||||
tool_use_limit_reached: true,
|
||||
};
|
||||
|
||||
// Save thread
|
||||
db.save_thread(thread_id.clone(), original_thread.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Load thread
|
||||
let loaded_thread = db.get_thread(thread_id).await.unwrap().unwrap();
|
||||
|
||||
// Verify all fields
|
||||
assert_eq!(loaded_thread.version, original_thread.version);
|
||||
assert_eq!(loaded_thread.summary, original_thread.summary);
|
||||
assert_eq!(loaded_thread.messages.len(), original_thread.messages.len());
|
||||
assert_eq!(loaded_thread.messages[0].segments.len(), 2);
|
||||
assert_eq!(loaded_thread.messages[0].tool_uses.len(), 1);
|
||||
assert_eq!(loaded_thread.messages[0].tool_results.len(), 1);
|
||||
assert_eq!(loaded_thread.messages[0].creases.len(), 1);
|
||||
assert_eq!(loaded_thread.messages[1].is_hidden, true);
|
||||
assert!(loaded_thread.initial_project_snapshot.is_some());
|
||||
assert_eq!(
|
||||
loaded_thread.cumulative_token_usage.input_tokens,
|
||||
original_thread.cumulative_token_usage.input_tokens
|
||||
);
|
||||
assert_eq!(
|
||||
loaded_thread.exceeded_window_error.is_none(),
|
||||
original_thread.exceeded_window_error.is_none()
|
||||
);
|
||||
assert!(loaded_thread.model.is_some());
|
||||
assert_eq!(loaded_thread.tool_use_limit_reached, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use assistant_tool::{AnyToolCard, Tool, ToolResultOutput, ToolUseStatus, ToolWorkingSet};
|
||||
use assistant_tool::{
|
||||
AnyToolCard, Tool, ToolResultContent, ToolResultOutput, ToolUseStatus, ToolWorkingSet,
|
||||
};
|
||||
use collections::HashMap;
|
||||
use futures::FutureExt as _;
|
||||
use futures::future::Shared;
|
||||
use gpui::{App, Entity, SharedString, Task};
|
||||
use language_model::{
|
||||
ConfiguredModel, LanguageModel, LanguageModelRequest, LanguageModelToolResult,
|
||||
LanguageModelToolUse, LanguageModelToolUseId, Role,
|
||||
LanguageModelToolResultContent, LanguageModelToolUse, LanguageModelToolUseId, Role,
|
||||
};
|
||||
use project::Project;
|
||||
use ui::{IconName, Window};
|
||||
@@ -52,15 +54,19 @@ impl ToolUseState {
|
||||
/// Constructs a [`ToolUseState`] from the given list of [`SerializedMessage`]s.
|
||||
///
|
||||
/// Accepts a function to filter the tools that should be used to populate the state.
|
||||
///
|
||||
/// If `window` is `None` (e.g., when in headless mode or when running evals),
|
||||
/// tool cards won't be deserialized
|
||||
pub fn from_serialized_messages(
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
messages: &[SerializedMessage],
|
||||
project: Entity<Project>,
|
||||
window: &mut Window,
|
||||
window: Option<&mut Window>, // None in headless mode
|
||||
cx: &mut App,
|
||||
) -> Self {
|
||||
let mut this = Self::new(tools);
|
||||
let mut tool_names_by_id = HashMap::default();
|
||||
let mut window = window;
|
||||
|
||||
for message in messages {
|
||||
match message.role {
|
||||
@@ -105,12 +111,17 @@ impl ToolUseState {
|
||||
},
|
||||
);
|
||||
|
||||
if let Some(tool) = this.tools.read(cx).tool(tool_use, cx) {
|
||||
if let Some(output) = tool_result.output.clone() {
|
||||
if let Some(card) =
|
||||
tool.deserialize_card(output, project.clone(), window, cx)
|
||||
{
|
||||
this.tool_result_cards.insert(tool_use_id, card);
|
||||
if let Some(window) = &mut window {
|
||||
if let Some(tool) = this.tools.read(cx).tool(tool_use, cx) {
|
||||
if let Some(output) = tool_result.output.clone() {
|
||||
if let Some(card) = tool.deserialize_card(
|
||||
output,
|
||||
project.clone(),
|
||||
window,
|
||||
cx,
|
||||
) {
|
||||
this.tool_result_cards.insert(tool_use_id, card);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -165,10 +176,16 @@ impl ToolUseState {
|
||||
|
||||
let status = (|| {
|
||||
if let Some(tool_result) = tool_result {
|
||||
let content = tool_result
|
||||
.content
|
||||
.to_str()
|
||||
.map(|str| str.to_owned().into())
|
||||
.unwrap_or_default();
|
||||
|
||||
return if tool_result.is_error {
|
||||
ToolUseStatus::Error(tool_result.content.clone().into())
|
||||
ToolUseStatus::Error(content)
|
||||
} else {
|
||||
ToolUseStatus::Finished(tool_result.content.clone().into())
|
||||
ToolUseStatus::Finished(content)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -399,21 +416,45 @@ impl ToolUseState {
|
||||
let tool_result = output.content;
|
||||
const BYTES_PER_TOKEN_ESTIMATE: usize = 3;
|
||||
|
||||
// Protect from clearly large output
|
||||
let old_use = self.pending_tool_uses_by_id.remove(&tool_use_id);
|
||||
|
||||
// Protect from overly large output
|
||||
let tool_output_limit = configured_model
|
||||
.map(|model| model.model.max_token_count() * BYTES_PER_TOKEN_ESTIMATE)
|
||||
.unwrap_or(usize::MAX);
|
||||
|
||||
let tool_result = if tool_result.len() <= tool_output_limit {
|
||||
tool_result
|
||||
} else {
|
||||
let truncated = truncate_lines_to_byte_limit(&tool_result, tool_output_limit);
|
||||
let content = match tool_result {
|
||||
ToolResultContent::Text(text) => {
|
||||
let text = if text.len() < tool_output_limit {
|
||||
text
|
||||
} else {
|
||||
let truncated = truncate_lines_to_byte_limit(&text, tool_output_limit);
|
||||
format!(
|
||||
"Tool result too long. The first {} bytes:\n\n{}",
|
||||
truncated.len(),
|
||||
truncated
|
||||
)
|
||||
};
|
||||
LanguageModelToolResultContent::Text(text.into())
|
||||
}
|
||||
ToolResultContent::Image(language_model_image) => {
|
||||
if language_model_image.estimate_tokens() < tool_output_limit {
|
||||
LanguageModelToolResultContent::Image(language_model_image)
|
||||
} else {
|
||||
self.tool_results.insert(
|
||||
tool_use_id.clone(),
|
||||
LanguageModelToolResult {
|
||||
tool_use_id: tool_use_id.clone(),
|
||||
tool_name,
|
||||
content: "Tool responded with an image that would exceeded the remaining tokens".into(),
|
||||
is_error: true,
|
||||
output: None,
|
||||
},
|
||||
);
|
||||
|
||||
format!(
|
||||
"Tool result too long. The first {} bytes:\n\n{}",
|
||||
truncated.len(),
|
||||
truncated
|
||||
)
|
||||
return old_use;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
self.tool_results.insert(
|
||||
@@ -421,12 +462,13 @@ impl ToolUseState {
|
||||
LanguageModelToolResult {
|
||||
tool_use_id: tool_use_id.clone(),
|
||||
tool_name,
|
||||
content: tool_result.into(),
|
||||
content,
|
||||
is_error: false,
|
||||
output: output.output,
|
||||
},
|
||||
);
|
||||
self.pending_tool_uses_by_id.remove(&tool_use_id)
|
||||
|
||||
old_use
|
||||
}
|
||||
Err(err) => {
|
||||
self.tool_results.insert(
|
||||
@@ -434,7 +476,7 @@ impl ToolUseState {
|
||||
LanguageModelToolResult {
|
||||
tool_use_id: tool_use_id.clone(),
|
||||
tool_name,
|
||||
content: err.to_string().into(),
|
||||
content: LanguageModelToolResultContent::Text(err.to_string().into()),
|
||||
is_error: true,
|
||||
output: None,
|
||||
},
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# Build better with Zed Pro
|
||||
|
||||
Try [Zed Pro](https://zed.dev/pricing) for free for 14 days - no credit card required. Only $20/month afterward. Cancel anytime.
|
||||
@@ -18,18 +18,24 @@ impl MaxModeTooltip {
|
||||
|
||||
impl Render for MaxModeTooltip {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let icon = if self.selected {
|
||||
IconName::ZedBurnModeOn
|
||||
} else {
|
||||
IconName::ZedBurnMode
|
||||
};
|
||||
|
||||
let title = h_flex()
|
||||
.gap_1()
|
||||
.child(Icon::new(icon).size(IconSize::Small))
|
||||
.child(Label::new("Burn Mode"));
|
||||
|
||||
tooltip_container(window, cx, |this, _, _| {
|
||||
this.gap_1()
|
||||
this.gap_0p5()
|
||||
.map(|header| if self.selected {
|
||||
header.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(Icon::new(IconName::ZedMaxMode).size(IconSize::Small).color(Color::Accent))
|
||||
.child(Label::new("Zed's Max Mode"))
|
||||
)
|
||||
.child(title)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
@@ -38,18 +44,13 @@ impl Render for MaxModeTooltip {
|
||||
)
|
||||
)
|
||||
} else {
|
||||
header.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(Icon::new(IconName::ZedMaxMode).size(IconSize::Small))
|
||||
.child(Label::new("Zed's Max Mode"))
|
||||
)
|
||||
header.child(title)
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.max_w_72()
|
||||
.child(
|
||||
Label::new("This mode enables models to use large context windows, unlimited tool calls, and other capabilities for expanded reasoning, offering an unfettered agentic experience.")
|
||||
Label::new("Enables models to use large context windows, unlimited tool calls, and other capabilities for expanded reasoning, offering an unfettered agentic experience.")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use collections::HashMap;
|
||||
use component::ComponentId;
|
||||
use gpui::{App, Entity, WeakEntity};
|
||||
use linkme::distributed_slice;
|
||||
use std::sync::OnceLock;
|
||||
use ui::{AnyElement, Component, ComponentScope, Window};
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -12,9 +12,15 @@ use crate::ActiveThread;
|
||||
pub type PreviewFn =
|
||||
fn(WeakEntity<Workspace>, Entity<ActiveThread>, &mut Window, &mut App) -> Option<AnyElement>;
|
||||
|
||||
/// Distributed slice for preview registration functions
|
||||
#[distributed_slice]
|
||||
pub static __ALL_AGENT_PREVIEWS: [fn() -> (ComponentId, PreviewFn)] = [..];
|
||||
pub struct AgentPreviewFn(fn() -> (ComponentId, PreviewFn));
|
||||
|
||||
impl AgentPreviewFn {
|
||||
pub const fn new(f: fn() -> (ComponentId, PreviewFn)) -> Self {
|
||||
Self(f)
|
||||
}
|
||||
}
|
||||
|
||||
inventory::collect!(AgentPreviewFn);
|
||||
|
||||
/// Trait that must be implemented by components that provide agent previews.
|
||||
pub trait AgentPreview: Component + Sized {
|
||||
@@ -36,16 +42,14 @@ pub trait AgentPreview: Component + Sized {
|
||||
#[macro_export]
|
||||
macro_rules! register_agent_preview {
|
||||
($type:ty) => {
|
||||
#[linkme::distributed_slice($crate::ui::preview::__ALL_AGENT_PREVIEWS)]
|
||||
static __REGISTER_AGENT_PREVIEW: fn() -> (
|
||||
component::ComponentId,
|
||||
$crate::ui::preview::PreviewFn,
|
||||
) = || {
|
||||
(
|
||||
<$type as component::Component>::id(),
|
||||
<$type as $crate::ui::preview::AgentPreview>::agent_preview,
|
||||
)
|
||||
};
|
||||
inventory::submit! {
|
||||
$crate::ui::preview::AgentPreviewFn::new(|| {
|
||||
(
|
||||
<$type as component::Component>::id(),
|
||||
<$type as $crate::ui::preview::AgentPreview>::agent_preview,
|
||||
)
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -56,8 +60,8 @@ static AGENT_PREVIEW_REGISTRY: OnceLock<HashMap<ComponentId, PreviewFn>> = OnceL
|
||||
fn get_or_init_registry() -> &'static HashMap<ComponentId, PreviewFn> {
|
||||
AGENT_PREVIEW_REGISTRY.get_or_init(|| {
|
||||
let mut map = HashMap::default();
|
||||
for register_fn in __ALL_AGENT_PREVIEWS.iter() {
|
||||
let (id, preview_fn) = register_fn();
|
||||
for register_fn in inventory::iter::<AgentPreviewFn>() {
|
||||
let (id, preview_fn) = (register_fn.0)();
|
||||
map.insert(id, preview_fn);
|
||||
}
|
||||
map
|
||||
|
||||
@@ -39,7 +39,7 @@ impl RenderOnce for UsageCallout {
|
||||
|
||||
let (title, message, button_text, url) = if is_limit_reached {
|
||||
match self.plan {
|
||||
Plan::Free => (
|
||||
Plan::ZedFree => (
|
||||
"Out of free prompts",
|
||||
"Upgrade to continue, wait for the next reset, or switch to API key."
|
||||
.to_string(),
|
||||
@@ -61,7 +61,7 @@ impl RenderOnce for UsageCallout {
|
||||
}
|
||||
} else {
|
||||
match self.plan {
|
||||
Plan::Free => (
|
||||
Plan::ZedFree => (
|
||||
"Reaching free plan limit soon",
|
||||
format!(
|
||||
"{remaining} remaining - Upgrade to increase limit, or switch providers",
|
||||
@@ -120,7 +120,7 @@ impl Component for UsageCallout {
|
||||
single_example(
|
||||
"Approaching limit (90%)",
|
||||
UsageCallout::new(
|
||||
Plan::Free,
|
||||
Plan::ZedFree,
|
||||
RequestUsage {
|
||||
limit: UsageLimit::Limited(50),
|
||||
amount: 45, // 90% of limit
|
||||
@@ -131,7 +131,7 @@ impl Component for UsageCallout {
|
||||
single_example(
|
||||
"Limit reached (100%)",
|
||||
UsageCallout::new(
|
||||
Plan::Free,
|
||||
Plan::ZedFree,
|
||||
RequestUsage {
|
||||
limit: UsageLimit::Limited(50),
|
||||
amount: 50, // 100% of limit
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "assistant_settings"
|
||||
name = "agent_settings"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
@@ -9,7 +9,7 @@ license = "GPL-3.0-or-later"
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/assistant_settings.rs"
|
||||
path = "src/agent_settings.rs"
|
||||
|
||||
[dependencies]
|
||||
anthropic = { workspace = true, features = ["schemars"] }
|
||||
@@ -23,6 +23,7 @@ log.workspace = true
|
||||
ollama = { workspace = true, features = ["schemars"] }
|
||||
open_ai = { workspace = true, features = ["schemars"] }
|
||||
deepseek = { workspace = true, features = ["schemars"] }
|
||||
mistral = { workspace = true, features = ["schemars"] }
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
settings.workspace = true
|
||||
@@ -24,7 +24,7 @@ pub struct GroupedAgentProfiles {
|
||||
}
|
||||
|
||||
impl GroupedAgentProfiles {
|
||||
pub fn from_settings(settings: &crate::AssistantSettings) -> Self {
|
||||
pub fn from_settings(settings: &crate::AgentSettings) -> Self {
|
||||
let mut builtin = IndexMap::default();
|
||||
let mut custom = IndexMap::default();
|
||||
|
||||
@@ -8,8 +8,9 @@ use anyhow::{Result, bail};
|
||||
use collections::IndexMap;
|
||||
use deepseek::Model as DeepseekModel;
|
||||
use gpui::{App, Pixels, SharedString};
|
||||
use language_model::{CloudModel, LanguageModel};
|
||||
use language_model::LanguageModel;
|
||||
use lmstudio::Model as LmStudioModel;
|
||||
use mistral::Model as MistralModel;
|
||||
use ollama::Model as OllamaModel;
|
||||
use schemars::{JsonSchema, schema::Schema};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -18,18 +19,26 @@ use settings::{Settings, SettingsSources};
|
||||
pub use crate::agent_profile::*;
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
AssistantSettings::register(cx);
|
||||
AgentSettings::register(cx);
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AssistantDockPosition {
|
||||
pub enum AgentDockPosition {
|
||||
Left,
|
||||
#[default]
|
||||
Right,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DefaultView {
|
||||
#[default]
|
||||
Thread,
|
||||
TextThread,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum NotifyWhenAgentWaiting {
|
||||
@@ -41,9 +50,10 @@ pub enum NotifyWhenAgentWaiting {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
#[serde(tag = "name", rename_all = "snake_case")]
|
||||
pub enum AssistantProviderContentV1 {
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub enum AgentProviderContentV1 {
|
||||
#[serde(rename = "zed.dev")]
|
||||
ZedDotDev { default_model: Option<CloudModel> },
|
||||
ZedDotDev { default_model: Option<String> },
|
||||
#[serde(rename = "openai")]
|
||||
OpenAi {
|
||||
default_model: Option<OpenAiModel>,
|
||||
@@ -70,13 +80,18 @@ pub enum AssistantProviderContentV1 {
|
||||
default_model: Option<DeepseekModel>,
|
||||
api_url: Option<String>,
|
||||
},
|
||||
#[serde(rename = "mistral")]
|
||||
Mistral {
|
||||
default_model: Option<MistralModel>,
|
||||
api_url: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct AssistantSettings {
|
||||
pub struct AgentSettings {
|
||||
pub enabled: bool,
|
||||
pub button: bool,
|
||||
pub dock: AssistantDockPosition,
|
||||
pub dock: AgentDockPosition,
|
||||
pub default_width: Pixels,
|
||||
pub default_height: Pixels,
|
||||
pub default_model: LanguageModelSelection,
|
||||
@@ -85,18 +100,20 @@ pub struct AssistantSettings {
|
||||
pub thread_summary_model: Option<LanguageModelSelection>,
|
||||
pub inline_alternatives: Vec<LanguageModelSelection>,
|
||||
pub using_outdated_settings_version: bool,
|
||||
pub enable_experimental_live_diffs: bool,
|
||||
pub default_profile: AgentProfileId,
|
||||
pub default_view: DefaultView,
|
||||
pub profiles: IndexMap<AgentProfileId, AgentProfile>,
|
||||
pub always_allow_tool_actions: bool,
|
||||
pub notify_when_agent_waiting: NotifyWhenAgentWaiting,
|
||||
pub play_sound_when_agent_done: bool,
|
||||
pub stream_edits: bool,
|
||||
pub single_file_review: bool,
|
||||
pub model_parameters: Vec<LanguageModelParameters>,
|
||||
pub preferred_completion_mode: CompletionMode,
|
||||
pub enable_feedback: bool,
|
||||
}
|
||||
|
||||
impl AssistantSettings {
|
||||
impl AgentSettings {
|
||||
pub fn temperature_for_model(model: &Arc<dyn LanguageModel>, cx: &App) -> Option<f32> {
|
||||
let settings = Self::get_global(cx);
|
||||
settings
|
||||
@@ -106,10 +123,6 @@ impl AssistantSettings {
|
||||
.and_then(|m| m.temperature)
|
||||
}
|
||||
|
||||
pub fn are_live_diffs_enabled(&self, _cx: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn set_inline_assistant_model(&mut self, provider: String, model: String) {
|
||||
self.inline_assistant_model = Some(LanguageModelSelection {
|
||||
provider: provider.into(),
|
||||
@@ -155,58 +168,56 @@ impl LanguageModelParameters {
|
||||
}
|
||||
}
|
||||
|
||||
/// Assistant panel settings
|
||||
/// Agent panel settings
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, Default)]
|
||||
pub struct AssistantSettingsContent {
|
||||
pub struct AgentSettingsContent {
|
||||
#[serde(flatten)]
|
||||
pub inner: Option<AssistantSettingsContentInner>,
|
||||
pub inner: Option<AgentSettingsContentInner>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
#[serde(untagged)]
|
||||
pub enum AssistantSettingsContentInner {
|
||||
Versioned(Box<VersionedAssistantSettingsContent>),
|
||||
Legacy(LegacyAssistantSettingsContent),
|
||||
pub enum AgentSettingsContentInner {
|
||||
Versioned(Box<VersionedAgentSettingsContent>),
|
||||
Legacy(LegacyAgentSettingsContent),
|
||||
}
|
||||
|
||||
impl AssistantSettingsContentInner {
|
||||
fn for_v2(content: AssistantSettingsContentV2) -> Self {
|
||||
AssistantSettingsContentInner::Versioned(Box::new(VersionedAssistantSettingsContent::V2(
|
||||
content,
|
||||
)))
|
||||
impl AgentSettingsContentInner {
|
||||
fn for_v2(content: AgentSettingsContentV2) -> Self {
|
||||
AgentSettingsContentInner::Versioned(Box::new(VersionedAgentSettingsContent::V2(content)))
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonSchema for AssistantSettingsContent {
|
||||
impl JsonSchema for AgentSettingsContent {
|
||||
fn schema_name() -> String {
|
||||
VersionedAssistantSettingsContent::schema_name()
|
||||
VersionedAgentSettingsContent::schema_name()
|
||||
}
|
||||
|
||||
fn json_schema(r#gen: &mut schemars::r#gen::SchemaGenerator) -> Schema {
|
||||
VersionedAssistantSettingsContent::json_schema(r#gen)
|
||||
VersionedAgentSettingsContent::json_schema(r#gen)
|
||||
}
|
||||
|
||||
fn is_referenceable() -> bool {
|
||||
VersionedAssistantSettingsContent::is_referenceable()
|
||||
VersionedAgentSettingsContent::is_referenceable()
|
||||
}
|
||||
}
|
||||
|
||||
impl AssistantSettingsContent {
|
||||
impl AgentSettingsContent {
|
||||
pub fn is_version_outdated(&self) -> bool {
|
||||
match &self.inner {
|
||||
Some(AssistantSettingsContentInner::Versioned(settings)) => match **settings {
|
||||
VersionedAssistantSettingsContent::V1(_) => true,
|
||||
VersionedAssistantSettingsContent::V2(_) => false,
|
||||
Some(AgentSettingsContentInner::Versioned(settings)) => match **settings {
|
||||
VersionedAgentSettingsContent::V1(_) => true,
|
||||
VersionedAgentSettingsContent::V2(_) => false,
|
||||
},
|
||||
Some(AssistantSettingsContentInner::Legacy(_)) => true,
|
||||
Some(AgentSettingsContentInner::Legacy(_)) => true,
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn upgrade(&self) -> AssistantSettingsContentV2 {
|
||||
fn upgrade(&self) -> AgentSettingsContentV2 {
|
||||
match &self.inner {
|
||||
Some(AssistantSettingsContentInner::Versioned(settings)) => match **settings {
|
||||
VersionedAssistantSettingsContent::V1(ref settings) => AssistantSettingsContentV2 {
|
||||
Some(AgentSettingsContentInner::Versioned(settings)) => match **settings {
|
||||
VersionedAgentSettingsContent::V1(ref settings) => AgentSettingsContentV2 {
|
||||
enabled: settings.enabled,
|
||||
button: settings.button,
|
||||
dock: settings.dock,
|
||||
@@ -216,49 +227,49 @@ impl AssistantSettingsContent {
|
||||
.provider
|
||||
.clone()
|
||||
.and_then(|provider| match provider {
|
||||
AssistantProviderContentV1::ZedDotDev { default_model } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
AgentProviderContentV1::ZedDotDev { default_model } => default_model
|
||||
.map(|model| LanguageModelSelection {
|
||||
provider: "zed.dev".into(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
AssistantProviderContentV1::OpenAi { default_model, .. } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
model,
|
||||
}),
|
||||
AgentProviderContentV1::OpenAi { default_model, .. } => default_model
|
||||
.map(|model| LanguageModelSelection {
|
||||
provider: "openai".into(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
AssistantProviderContentV1::Anthropic { default_model, .. } => {
|
||||
}),
|
||||
AgentProviderContentV1::Anthropic { default_model, .. } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
provider: "anthropic".into(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
AssistantProviderContentV1::Ollama { default_model, .. } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
AgentProviderContentV1::Ollama { default_model, .. } => default_model
|
||||
.map(|model| LanguageModelSelection {
|
||||
provider: "ollama".into(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
AssistantProviderContentV1::LmStudio { default_model, .. } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
}),
|
||||
AgentProviderContentV1::LmStudio { default_model, .. } => default_model
|
||||
.map(|model| LanguageModelSelection {
|
||||
provider: "lmstudio".into(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
AssistantProviderContentV1::DeepSeek { default_model, .. } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
}),
|
||||
AgentProviderContentV1::DeepSeek { default_model, .. } => default_model
|
||||
.map(|model| LanguageModelSelection {
|
||||
provider: "deepseek".into(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
}),
|
||||
AgentProviderContentV1::Mistral { default_model, .. } => default_model
|
||||
.map(|model| LanguageModelSelection {
|
||||
provider: "mistral".into(),
|
||||
model: model.id().to_string(),
|
||||
}),
|
||||
}),
|
||||
inline_assistant_model: None,
|
||||
commit_message_model: None,
|
||||
thread_summary_model: None,
|
||||
inline_alternatives: None,
|
||||
enable_experimental_live_diffs: None,
|
||||
default_profile: None,
|
||||
default_view: None,
|
||||
profiles: None,
|
||||
always_allow_tool_actions: None,
|
||||
notify_when_agent_waiting: None,
|
||||
@@ -266,10 +277,12 @@ impl AssistantSettingsContent {
|
||||
single_file_review: None,
|
||||
model_parameters: Vec::new(),
|
||||
preferred_completion_mode: None,
|
||||
enable_feedback: None,
|
||||
play_sound_when_agent_done: None,
|
||||
},
|
||||
VersionedAssistantSettingsContent::V2(ref settings) => settings.clone(),
|
||||
VersionedAgentSettingsContent::V2(ref settings) => settings.clone(),
|
||||
},
|
||||
Some(AssistantSettingsContentInner::Legacy(settings)) => AssistantSettingsContentV2 {
|
||||
Some(AgentSettingsContentInner::Legacy(settings)) => AgentSettingsContentV2 {
|
||||
enabled: None,
|
||||
button: settings.button,
|
||||
dock: settings.dock,
|
||||
@@ -288,8 +301,8 @@ impl AssistantSettingsContent {
|
||||
commit_message_model: None,
|
||||
thread_summary_model: None,
|
||||
inline_alternatives: None,
|
||||
enable_experimental_live_diffs: None,
|
||||
default_profile: None,
|
||||
default_view: None,
|
||||
profiles: None,
|
||||
always_allow_tool_actions: None,
|
||||
notify_when_agent_waiting: None,
|
||||
@@ -297,31 +310,31 @@ impl AssistantSettingsContent {
|
||||
single_file_review: None,
|
||||
model_parameters: Vec::new(),
|
||||
preferred_completion_mode: None,
|
||||
enable_feedback: None,
|
||||
play_sound_when_agent_done: None,
|
||||
},
|
||||
None => AssistantSettingsContentV2::default(),
|
||||
None => AgentSettingsContentV2::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_dock(&mut self, dock: AssistantDockPosition) {
|
||||
pub fn set_dock(&mut self, dock: AgentDockPosition) {
|
||||
match &mut self.inner {
|
||||
Some(AssistantSettingsContentInner::Versioned(settings)) => match **settings {
|
||||
VersionedAssistantSettingsContent::V1(ref mut settings) => {
|
||||
Some(AgentSettingsContentInner::Versioned(settings)) => match **settings {
|
||||
VersionedAgentSettingsContent::V1(ref mut settings) => {
|
||||
settings.dock = Some(dock);
|
||||
}
|
||||
VersionedAssistantSettingsContent::V2(ref mut settings) => {
|
||||
VersionedAgentSettingsContent::V2(ref mut settings) => {
|
||||
settings.dock = Some(dock);
|
||||
}
|
||||
},
|
||||
Some(AssistantSettingsContentInner::Legacy(settings)) => {
|
||||
Some(AgentSettingsContentInner::Legacy(settings)) => {
|
||||
settings.dock = Some(dock);
|
||||
}
|
||||
None => {
|
||||
self.inner = Some(AssistantSettingsContentInner::for_v2(
|
||||
AssistantSettingsContentV2 {
|
||||
dock: Some(dock),
|
||||
..Default::default()
|
||||
},
|
||||
))
|
||||
self.inner = Some(AgentSettingsContentInner::for_v2(AgentSettingsContentV2 {
|
||||
dock: Some(dock),
|
||||
..Default::default()
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -331,105 +344,99 @@ impl AssistantSettingsContent {
|
||||
let provider = language_model.provider_id().0.to_string();
|
||||
|
||||
match &mut self.inner {
|
||||
Some(AssistantSettingsContentInner::Versioned(settings)) => match **settings {
|
||||
VersionedAssistantSettingsContent::V1(ref mut settings) => {
|
||||
match provider.as_ref() {
|
||||
"zed.dev" => {
|
||||
log::warn!("attempted to set zed.dev model on outdated settings");
|
||||
}
|
||||
"anthropic" => {
|
||||
let api_url = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::Anthropic { api_url, .. }) => {
|
||||
api_url.clone()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::Anthropic {
|
||||
default_model: AnthropicModel::from_id(&model).ok(),
|
||||
api_url,
|
||||
});
|
||||
}
|
||||
"ollama" => {
|
||||
let api_url = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::Ollama { api_url, .. }) => {
|
||||
api_url.clone()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::Ollama {
|
||||
default_model: Some(ollama::Model::new(
|
||||
&model,
|
||||
None,
|
||||
None,
|
||||
Some(language_model.supports_tools()),
|
||||
)),
|
||||
api_url,
|
||||
});
|
||||
}
|
||||
"lmstudio" => {
|
||||
let api_url = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::LmStudio { api_url, .. }) => {
|
||||
api_url.clone()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::LmStudio {
|
||||
default_model: Some(lmstudio::Model::new(&model, None, None)),
|
||||
api_url,
|
||||
});
|
||||
}
|
||||
"openai" => {
|
||||
let (api_url, available_models) = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::OpenAi {
|
||||
api_url,
|
||||
available_models,
|
||||
..
|
||||
}) => (api_url.clone(), available_models.clone()),
|
||||
_ => (None, None),
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::OpenAi {
|
||||
default_model: OpenAiModel::from_id(&model).ok(),
|
||||
Some(AgentSettingsContentInner::Versioned(settings)) => match **settings {
|
||||
VersionedAgentSettingsContent::V1(ref mut settings) => match provider.as_ref() {
|
||||
"zed.dev" => {
|
||||
log::warn!("attempted to set zed.dev model on outdated settings");
|
||||
}
|
||||
"anthropic" => {
|
||||
let api_url = match &settings.provider {
|
||||
Some(AgentProviderContentV1::Anthropic { api_url, .. }) => {
|
||||
api_url.clone()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
settings.provider = Some(AgentProviderContentV1::Anthropic {
|
||||
default_model: AnthropicModel::from_id(&model).ok(),
|
||||
api_url,
|
||||
});
|
||||
}
|
||||
"ollama" => {
|
||||
let api_url = match &settings.provider {
|
||||
Some(AgentProviderContentV1::Ollama { api_url, .. }) => api_url.clone(),
|
||||
_ => None,
|
||||
};
|
||||
settings.provider = Some(AgentProviderContentV1::Ollama {
|
||||
default_model: Some(ollama::Model::new(
|
||||
&model,
|
||||
None,
|
||||
None,
|
||||
Some(language_model.supports_tools()),
|
||||
)),
|
||||
api_url,
|
||||
});
|
||||
}
|
||||
"lmstudio" => {
|
||||
let api_url = match &settings.provider {
|
||||
Some(AgentProviderContentV1::LmStudio { api_url, .. }) => {
|
||||
api_url.clone()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
settings.provider = Some(AgentProviderContentV1::LmStudio {
|
||||
default_model: Some(lmstudio::Model::new(&model, None, None, false)),
|
||||
api_url,
|
||||
});
|
||||
}
|
||||
"openai" => {
|
||||
let (api_url, available_models) = match &settings.provider {
|
||||
Some(AgentProviderContentV1::OpenAi {
|
||||
api_url,
|
||||
available_models,
|
||||
});
|
||||
}
|
||||
"deepseek" => {
|
||||
let api_url = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::DeepSeek { api_url, .. }) => {
|
||||
api_url.clone()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::DeepSeek {
|
||||
default_model: DeepseekModel::from_id(&model).ok(),
|
||||
api_url,
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
..
|
||||
}) => (api_url.clone(), available_models.clone()),
|
||||
_ => (None, None),
|
||||
};
|
||||
settings.provider = Some(AgentProviderContentV1::OpenAi {
|
||||
default_model: OpenAiModel::from_id(&model).ok(),
|
||||
api_url,
|
||||
available_models,
|
||||
});
|
||||
}
|
||||
}
|
||||
VersionedAssistantSettingsContent::V2(ref mut settings) => {
|
||||
"deepseek" => {
|
||||
let api_url = match &settings.provider {
|
||||
Some(AgentProviderContentV1::DeepSeek { api_url, .. }) => {
|
||||
api_url.clone()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
settings.provider = Some(AgentProviderContentV1::DeepSeek {
|
||||
default_model: DeepseekModel::from_id(&model).ok(),
|
||||
api_url,
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
VersionedAgentSettingsContent::V2(ref mut settings) => {
|
||||
settings.default_model = Some(LanguageModelSelection {
|
||||
provider: provider.into(),
|
||||
model,
|
||||
});
|
||||
}
|
||||
},
|
||||
Some(AssistantSettingsContentInner::Legacy(settings)) => {
|
||||
Some(AgentSettingsContentInner::Legacy(settings)) => {
|
||||
if let Ok(model) = OpenAiModel::from_id(&language_model.id().0) {
|
||||
settings.default_open_ai_model = Some(model);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
self.inner = Some(AssistantSettingsContentInner::for_v2(
|
||||
AssistantSettingsContentV2 {
|
||||
default_model: Some(LanguageModelSelection {
|
||||
provider: provider.into(),
|
||||
model,
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
self.inner = Some(AgentSettingsContentInner::for_v2(AgentSettingsContentV2 {
|
||||
default_model: Some(LanguageModelSelection {
|
||||
provider: provider.into(),
|
||||
model,
|
||||
}),
|
||||
..Default::default()
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -458,15 +465,15 @@ impl AssistantSettingsContent {
|
||||
|
||||
pub fn v2_setting(
|
||||
&mut self,
|
||||
f: impl FnOnce(&mut AssistantSettingsContentV2) -> anyhow::Result<()>,
|
||||
f: impl FnOnce(&mut AgentSettingsContentV2) -> anyhow::Result<()>,
|
||||
) -> anyhow::Result<()> {
|
||||
match self.inner.get_or_insert_with(|| {
|
||||
AssistantSettingsContentInner::for_v2(AssistantSettingsContentV2 {
|
||||
AgentSettingsContentInner::for_v2(AgentSettingsContentV2 {
|
||||
..Default::default()
|
||||
})
|
||||
}) {
|
||||
AssistantSettingsContentInner::Versioned(boxed) => {
|
||||
if let VersionedAssistantSettingsContent::V2(ref mut settings) = **boxed {
|
||||
AgentSettingsContentInner::Versioned(boxed) => {
|
||||
if let VersionedAgentSettingsContent::V2(ref mut settings) = **boxed {
|
||||
f(settings)
|
||||
} else {
|
||||
Ok(())
|
||||
@@ -495,6 +502,14 @@ impl AssistantSettingsContent {
|
||||
.ok();
|
||||
}
|
||||
|
||||
pub fn set_play_sound_when_agent_done(&mut self, allow: bool) {
|
||||
self.v2_setting(|setting| {
|
||||
setting.play_sound_when_agent_done = Some(allow);
|
||||
Ok(())
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
pub fn set_single_file_review(&mut self, allow: bool) {
|
||||
self.v2_setting(|setting| {
|
||||
setting.single_file_review = Some(allow);
|
||||
@@ -550,16 +565,17 @@ impl AssistantSettingsContent {
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
#[serde(tag = "version")]
|
||||
pub enum VersionedAssistantSettingsContent {
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub enum VersionedAgentSettingsContent {
|
||||
#[serde(rename = "1")]
|
||||
V1(AssistantSettingsContentV1),
|
||||
V1(AgentSettingsContentV1),
|
||||
#[serde(rename = "2")]
|
||||
V2(AssistantSettingsContentV2),
|
||||
V2(AgentSettingsContentV2),
|
||||
}
|
||||
|
||||
impl Default for VersionedAssistantSettingsContent {
|
||||
impl Default for VersionedAgentSettingsContent {
|
||||
fn default() -> Self {
|
||||
Self::V2(AssistantSettingsContentV2 {
|
||||
Self::V2(AgentSettingsContentV2 {
|
||||
enabled: None,
|
||||
button: None,
|
||||
dock: None,
|
||||
@@ -570,8 +586,8 @@ impl Default for VersionedAssistantSettingsContent {
|
||||
commit_message_model: None,
|
||||
thread_summary_model: None,
|
||||
inline_alternatives: None,
|
||||
enable_experimental_live_diffs: None,
|
||||
default_profile: None,
|
||||
default_view: None,
|
||||
profiles: None,
|
||||
always_allow_tool_actions: None,
|
||||
notify_when_agent_waiting: None,
|
||||
@@ -579,29 +595,32 @@ impl Default for VersionedAssistantSettingsContent {
|
||||
single_file_review: None,
|
||||
model_parameters: Vec::new(),
|
||||
preferred_completion_mode: None,
|
||||
enable_feedback: None,
|
||||
play_sound_when_agent_done: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default)]
|
||||
pub struct AssistantSettingsContentV2 {
|
||||
/// Whether the Assistant is enabled.
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct AgentSettingsContentV2 {
|
||||
/// Whether the Agent is enabled.
|
||||
///
|
||||
/// Default: true
|
||||
enabled: Option<bool>,
|
||||
/// Whether to show the assistant panel button in the status bar.
|
||||
/// Whether to show the agent panel button in the status bar.
|
||||
///
|
||||
/// Default: true
|
||||
button: Option<bool>,
|
||||
/// Where to dock the assistant.
|
||||
/// Where to dock the agent panel.
|
||||
///
|
||||
/// Default: right
|
||||
dock: Option<AssistantDockPosition>,
|
||||
/// Default width in pixels when the assistant is docked to the left or right.
|
||||
dock: Option<AgentDockPosition>,
|
||||
/// Default width in pixels when the agent panel is docked to the left or right.
|
||||
///
|
||||
/// Default: 640
|
||||
default_width: Option<f32>,
|
||||
/// Default height in pixels when the assistant is docked to the bottom.
|
||||
/// Default height in pixels when the agent panel is docked to the bottom.
|
||||
///
|
||||
/// Default: 320
|
||||
default_height: Option<f32>,
|
||||
@@ -615,14 +634,14 @@ pub struct AssistantSettingsContentV2 {
|
||||
thread_summary_model: Option<LanguageModelSelection>,
|
||||
/// Additional models with which to generate alternatives when performing inline assists.
|
||||
inline_alternatives: Option<Vec<LanguageModelSelection>>,
|
||||
/// Enable experimental live diffs in the assistant panel.
|
||||
///
|
||||
/// Default: false
|
||||
enable_experimental_live_diffs: Option<bool>,
|
||||
/// The default profile to use in the Agent.
|
||||
///
|
||||
/// Default: write
|
||||
default_profile: Option<AgentProfileId>,
|
||||
/// Which view type to show by default in the agent panel.
|
||||
///
|
||||
/// Default: "thread"
|
||||
default_view: Option<DefaultView>,
|
||||
/// The available agent profiles.
|
||||
pub profiles: Option<IndexMap<AgentProfileId, AgentProfileContent>>,
|
||||
/// Whenever a tool action would normally wait for your confirmation
|
||||
@@ -634,6 +653,10 @@ pub struct AssistantSettingsContentV2 {
|
||||
///
|
||||
/// Default: "primary_screen"
|
||||
notify_when_agent_waiting: Option<NotifyWhenAgentWaiting>,
|
||||
/// Whether to play a sound when the agent has either completed its response, or needs user input.
|
||||
///
|
||||
/// Default: false
|
||||
play_sound_when_agent_done: Option<bool>,
|
||||
/// Whether to stream edits from the agent as they are received.
|
||||
///
|
||||
/// Default: false
|
||||
@@ -651,11 +674,14 @@ pub struct AssistantSettingsContentV2 {
|
||||
/// Default: []
|
||||
#[serde(default)]
|
||||
model_parameters: Vec<LanguageModelParameters>,
|
||||
|
||||
/// What completion mode to enable for new threads
|
||||
///
|
||||
/// Default: normal
|
||||
preferred_completion_mode: Option<CompletionMode>,
|
||||
/// Whether to show thumb buttons for feedback in the agent panel.
|
||||
///
|
||||
/// Default: true
|
||||
enable_feedback: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
|
||||
@@ -693,7 +719,7 @@ impl JsonSchema for LanguageModelProviderSetting {
|
||||
schemars::schema::SchemaObject {
|
||||
enum_values: Some(vec![
|
||||
"anthropic".into(),
|
||||
"bedrock".into(),
|
||||
"amazon-bedrock".into(),
|
||||
"google".into(),
|
||||
"lmstudio".into(),
|
||||
"ollama".into(),
|
||||
@@ -701,6 +727,7 @@ impl JsonSchema for LanguageModelProviderSetting {
|
||||
"zed.dev".into(),
|
||||
"copilot_chat".into(),
|
||||
"deepseek".into(),
|
||||
"mistral".into(),
|
||||
]),
|
||||
..Default::default()
|
||||
}
|
||||
@@ -746,49 +773,51 @@ pub struct ContextServerPresetContent {
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct AssistantSettingsContentV1 {
|
||||
/// Whether the Assistant is enabled.
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct AgentSettingsContentV1 {
|
||||
/// Whether the Agent is enabled.
|
||||
///
|
||||
/// Default: true
|
||||
enabled: Option<bool>,
|
||||
/// Whether to show the assistant panel button in the status bar.
|
||||
/// Whether to show the Agent panel button in the status bar.
|
||||
///
|
||||
/// Default: true
|
||||
button: Option<bool>,
|
||||
/// Where to dock the assistant.
|
||||
/// Where to dock the Agent.
|
||||
///
|
||||
/// Default: right
|
||||
dock: Option<AssistantDockPosition>,
|
||||
/// Default width in pixels when the assistant is docked to the left or right.
|
||||
dock: Option<AgentDockPosition>,
|
||||
/// Default width in pixels when the Agent is docked to the left or right.
|
||||
///
|
||||
/// Default: 640
|
||||
default_width: Option<f32>,
|
||||
/// Default height in pixels when the assistant is docked to the bottom.
|
||||
/// Default height in pixels when the Agent is docked to the bottom.
|
||||
///
|
||||
/// Default: 320
|
||||
default_height: Option<f32>,
|
||||
/// The provider of the assistant service.
|
||||
/// The provider of the Agent service.
|
||||
///
|
||||
/// This can be "openai", "anthropic", "ollama", "lmstudio", "deepseek", "zed.dev"
|
||||
/// each with their respective default models and configurations.
|
||||
provider: Option<AssistantProviderContentV1>,
|
||||
provider: Option<AgentProviderContentV1>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct LegacyAssistantSettingsContent {
|
||||
/// Whether to show the assistant panel button in the status bar.
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct LegacyAgentSettingsContent {
|
||||
/// Whether to show the Agent panel button in the status bar.
|
||||
///
|
||||
/// Default: true
|
||||
pub button: Option<bool>,
|
||||
/// Where to dock the assistant.
|
||||
/// Where to dock the Agent.
|
||||
///
|
||||
/// Default: right
|
||||
pub dock: Option<AssistantDockPosition>,
|
||||
/// Default width in pixels when the assistant is docked to the left or right.
|
||||
pub dock: Option<AgentDockPosition>,
|
||||
/// Default width in pixels when the Agent is docked to the left or right.
|
||||
///
|
||||
/// Default: 640
|
||||
pub default_width: Option<f32>,
|
||||
/// Default height in pixels when the assistant is docked to the bottom.
|
||||
/// Default height in pixels when the Agent is docked to the bottom.
|
||||
///
|
||||
/// Default: 320
|
||||
pub default_height: Option<f32>,
|
||||
@@ -802,20 +831,20 @@ pub struct LegacyAssistantSettingsContent {
|
||||
pub openai_api_url: Option<String>,
|
||||
}
|
||||
|
||||
impl Settings for AssistantSettings {
|
||||
impl Settings for AgentSettings {
|
||||
const KEY: Option<&'static str> = Some("agent");
|
||||
|
||||
const FALLBACK_KEY: Option<&'static str> = Some("assistant");
|
||||
|
||||
const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]);
|
||||
|
||||
type FileContent = AssistantSettingsContent;
|
||||
type FileContent = AgentSettingsContent;
|
||||
|
||||
fn load(
|
||||
sources: SettingsSources<Self::FileContent>,
|
||||
_: &mut gpui::App,
|
||||
) -> anyhow::Result<Self> {
|
||||
let mut settings = AssistantSettings::default();
|
||||
let mut settings = AgentSettings::default();
|
||||
|
||||
for value in sources.defaults_and_customizations() {
|
||||
if value.is_version_outdated() {
|
||||
@@ -845,10 +874,6 @@ impl Settings for AssistantSettings {
|
||||
.thread_summary_model
|
||||
.or(settings.thread_summary_model.take());
|
||||
merge(&mut settings.inline_alternatives, value.inline_alternatives);
|
||||
merge(
|
||||
&mut settings.enable_experimental_live_diffs,
|
||||
value.enable_experimental_live_diffs,
|
||||
);
|
||||
merge(
|
||||
&mut settings.always_allow_tool_actions,
|
||||
value.always_allow_tool_actions,
|
||||
@@ -857,13 +882,19 @@ impl Settings for AssistantSettings {
|
||||
&mut settings.notify_when_agent_waiting,
|
||||
value.notify_when_agent_waiting,
|
||||
);
|
||||
merge(
|
||||
&mut settings.play_sound_when_agent_done,
|
||||
value.play_sound_when_agent_done,
|
||||
);
|
||||
merge(&mut settings.stream_edits, value.stream_edits);
|
||||
merge(&mut settings.single_file_review, value.single_file_review);
|
||||
merge(&mut settings.default_profile, value.default_profile);
|
||||
merge(&mut settings.default_view, value.default_view);
|
||||
merge(
|
||||
&mut settings.preferred_completion_mode,
|
||||
value.preferred_completion_mode,
|
||||
);
|
||||
merge(&mut settings.enable_feedback, value.enable_feedback);
|
||||
|
||||
settings
|
||||
.model_parameters
|
||||
@@ -908,28 +939,25 @@ impl Settings for AssistantSettings {
|
||||
.and_then(|b| b.as_bool())
|
||||
{
|
||||
match &mut current.inner {
|
||||
Some(AssistantSettingsContentInner::Versioned(versioned)) => {
|
||||
match versioned.as_mut() {
|
||||
VersionedAssistantSettingsContent::V1(setting) => {
|
||||
setting.enabled = Some(b);
|
||||
setting.button = Some(b);
|
||||
}
|
||||
|
||||
VersionedAssistantSettingsContent::V2(setting) => {
|
||||
setting.enabled = Some(b);
|
||||
setting.button = Some(b);
|
||||
}
|
||||
Some(AgentSettingsContentInner::Versioned(versioned)) => match versioned.as_mut() {
|
||||
VersionedAgentSettingsContent::V1(setting) => {
|
||||
setting.enabled = Some(b);
|
||||
setting.button = Some(b);
|
||||
}
|
||||
}
|
||||
Some(AssistantSettingsContentInner::Legacy(setting)) => setting.button = Some(b),
|
||||
|
||||
VersionedAgentSettingsContent::V2(setting) => {
|
||||
setting.enabled = Some(b);
|
||||
setting.button = Some(b);
|
||||
}
|
||||
},
|
||||
Some(AgentSettingsContentInner::Legacy(setting)) => setting.button = Some(b),
|
||||
None => {
|
||||
current.inner = Some(AssistantSettingsContentInner::for_v2(
|
||||
AssistantSettingsContentV2 {
|
||||
current.inner =
|
||||
Some(AgentSettingsContentInner::for_v2(AgentSettingsContentV2 {
|
||||
enabled: Some(b),
|
||||
button: Some(b),
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -951,7 +979,7 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_deserialize_assistant_settings_with_version(cx: &mut TestAppContext) {
|
||||
async fn test_deserialize_agent_settings_with_version(cx: &mut TestAppContext) {
|
||||
let fs = fs::FakeFs::new(cx.executor().clone());
|
||||
fs.create_dir(paths::settings_file().parent().unwrap())
|
||||
.await
|
||||
@@ -960,51 +988,51 @@ mod tests {
|
||||
cx.update(|cx| {
|
||||
let test_settings = settings::SettingsStore::test(cx);
|
||||
cx.set_global(test_settings);
|
||||
AssistantSettings::register(cx);
|
||||
AgentSettings::register(cx);
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
assert!(!AssistantSettings::get_global(cx).using_outdated_settings_version);
|
||||
assert!(!AgentSettings::get_global(cx).using_outdated_settings_version);
|
||||
assert_eq!(
|
||||
AssistantSettings::get_global(cx).default_model,
|
||||
AgentSettings::get_global(cx).default_model,
|
||||
LanguageModelSelection {
|
||||
provider: "zed.dev".into(),
|
||||
model: "claude-3-7-sonnet-latest".into(),
|
||||
model: "claude-sonnet-4".into(),
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
settings::SettingsStore::global(cx).update_settings_file::<AssistantSettings>(
|
||||
settings::SettingsStore::global(cx).update_settings_file::<AgentSettings>(
|
||||
fs.clone(),
|
||||
|settings, _| {
|
||||
*settings = AssistantSettingsContent {
|
||||
inner: Some(AssistantSettingsContentInner::for_v2(
|
||||
AssistantSettingsContentV2 {
|
||||
default_model: Some(LanguageModelSelection {
|
||||
provider: "test-provider".into(),
|
||||
model: "gpt-99".into(),
|
||||
}),
|
||||
inline_assistant_model: None,
|
||||
commit_message_model: None,
|
||||
thread_summary_model: None,
|
||||
inline_alternatives: None,
|
||||
enabled: None,
|
||||
button: None,
|
||||
dock: None,
|
||||
default_width: None,
|
||||
default_height: None,
|
||||
enable_experimental_live_diffs: None,
|
||||
default_profile: None,
|
||||
profiles: None,
|
||||
always_allow_tool_actions: None,
|
||||
notify_when_agent_waiting: None,
|
||||
stream_edits: None,
|
||||
single_file_review: None,
|
||||
model_parameters: Vec::new(),
|
||||
preferred_completion_mode: None,
|
||||
},
|
||||
)),
|
||||
*settings = AgentSettingsContent {
|
||||
inner: Some(AgentSettingsContentInner::for_v2(AgentSettingsContentV2 {
|
||||
default_model: Some(LanguageModelSelection {
|
||||
provider: "test-provider".into(),
|
||||
model: "gpt-99".into(),
|
||||
}),
|
||||
inline_assistant_model: None,
|
||||
commit_message_model: None,
|
||||
thread_summary_model: None,
|
||||
inline_alternatives: None,
|
||||
enabled: None,
|
||||
button: None,
|
||||
dock: None,
|
||||
default_width: None,
|
||||
default_height: None,
|
||||
default_profile: None,
|
||||
default_view: None,
|
||||
profiles: None,
|
||||
always_allow_tool_actions: None,
|
||||
play_sound_when_agent_done: None,
|
||||
notify_when_agent_waiting: None,
|
||||
stream_edits: None,
|
||||
single_file_review: None,
|
||||
enable_feedback: None,
|
||||
model_parameters: Vec::new(),
|
||||
preferred_completion_mode: None,
|
||||
})),
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -1016,14 +1044,14 @@ mod tests {
|
||||
assert!(raw_settings_value.contains(r#""version": "2""#));
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AssistantSettingsTest {
|
||||
agent: AssistantSettingsContent,
|
||||
struct AgentSettingsTest {
|
||||
agent: AgentSettingsContent,
|
||||
}
|
||||
|
||||
let assistant_settings: AssistantSettingsTest =
|
||||
let agent_settings: AgentSettingsTest =
|
||||
serde_json_lenient::from_str(&raw_settings_value).unwrap();
|
||||
|
||||
assert!(!assistant_settings.agent.is_version_outdated());
|
||||
assert!(!agent_settings.agent.is_version_outdated());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -1048,29 +1076,27 @@ mod tests {
|
||||
.set_user_settings(user_settings_content, cx)
|
||||
.unwrap();
|
||||
cx.set_global(test_settings);
|
||||
AssistantSettings::register(cx);
|
||||
AgentSettings::register(cx);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
let assistant_settings = cx.update(|cx| AssistantSettings::get_global(cx).clone());
|
||||
assert!(assistant_settings.enabled);
|
||||
assert!(!assistant_settings.using_outdated_settings_version);
|
||||
assert_eq!(assistant_settings.default_model.model, "gpt-99");
|
||||
let agent_settings = cx.update(|cx| AgentSettings::get_global(cx).clone());
|
||||
assert!(agent_settings.enabled);
|
||||
assert!(!agent_settings.using_outdated_settings_version);
|
||||
assert_eq!(agent_settings.default_model.model, "gpt-99");
|
||||
|
||||
cx.update_global::<SettingsStore, _>(|settings_store, cx| {
|
||||
settings_store.update_user_settings::<AssistantSettings>(cx, |settings| {
|
||||
*settings = AssistantSettingsContent {
|
||||
inner: Some(AssistantSettingsContentInner::for_v2(
|
||||
AssistantSettingsContentV2 {
|
||||
enabled: Some(false),
|
||||
default_model: Some(LanguageModelSelection {
|
||||
provider: "xai".to_owned().into(),
|
||||
model: "grok".to_owned(),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
)),
|
||||
settings_store.update_user_settings::<AgentSettings>(cx, |settings| {
|
||||
*settings = AgentSettingsContent {
|
||||
inner: Some(AgentSettingsContentInner::for_v2(AgentSettingsContentV2 {
|
||||
enabled: Some(false),
|
||||
default_model: Some(LanguageModelSelection {
|
||||
provider: "xai".to_owned().into(),
|
||||
model: "grok".to_owned(),
|
||||
}),
|
||||
..Default::default()
|
||||
})),
|
||||
};
|
||||
});
|
||||
});
|
||||
@@ -1080,12 +1106,12 @@ mod tests {
|
||||
let settings = cx.update(|cx| SettingsStore::global(cx).raw_user_settings().clone());
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AssistantSettingsTest {
|
||||
assistant: AssistantSettingsContent,
|
||||
struct AgentSettingsTest {
|
||||
assistant: AgentSettingsContent,
|
||||
agent: Option<serde_json_lenient::Value>,
|
||||
}
|
||||
|
||||
let assistant_settings: AssistantSettingsTest = serde_json::from_value(settings).unwrap();
|
||||
assert!(assistant_settings.agent.is_none());
|
||||
let agent_settings: AgentSettingsTest = serde_json::from_value(settings).unwrap();
|
||||
assert!(agent_settings.agent.is_none());
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,6 @@ pub enum AnthropicModelMode {
|
||||
pub enum Model {
|
||||
#[serde(rename = "claude-3-5-sonnet", alias = "claude-3-5-sonnet-latest")]
|
||||
Claude3_5Sonnet,
|
||||
#[default]
|
||||
#[serde(rename = "claude-3-7-sonnet", alias = "claude-3-7-sonnet-latest")]
|
||||
Claude3_7Sonnet,
|
||||
#[serde(
|
||||
@@ -42,6 +41,21 @@ pub enum Model {
|
||||
alias = "claude-3-7-sonnet-thinking-latest"
|
||||
)]
|
||||
Claude3_7SonnetThinking,
|
||||
#[serde(rename = "claude-opus-4", alias = "claude-opus-4-latest")]
|
||||
ClaudeOpus4,
|
||||
#[serde(
|
||||
rename = "claude-opus-4-thinking",
|
||||
alias = "claude-opus-4-thinking-latest"
|
||||
)]
|
||||
ClaudeOpus4Thinking,
|
||||
#[default]
|
||||
#[serde(rename = "claude-sonnet-4", alias = "claude-sonnet-4-latest")]
|
||||
ClaudeSonnet4,
|
||||
#[serde(
|
||||
rename = "claude-sonnet-4-thinking",
|
||||
alias = "claude-sonnet-4-thinking-latest"
|
||||
)]
|
||||
ClaudeSonnet4Thinking,
|
||||
#[serde(rename = "claude-3-5-haiku", alias = "claude-3-5-haiku-latest")]
|
||||
Claude3_5Haiku,
|
||||
#[serde(rename = "claude-3-opus", alias = "claude-3-opus-latest")]
|
||||
@@ -89,13 +103,25 @@ impl Model {
|
||||
Ok(Self::Claude3Sonnet)
|
||||
} else if id.starts_with("claude-3-haiku") {
|
||||
Ok(Self::Claude3Haiku)
|
||||
} else if id.starts_with("claude-opus-4-thinking") {
|
||||
Ok(Self::ClaudeOpus4Thinking)
|
||||
} else if id.starts_with("claude-opus-4") {
|
||||
Ok(Self::ClaudeOpus4)
|
||||
} else if id.starts_with("claude-sonnet-4-thinking") {
|
||||
Ok(Self::ClaudeSonnet4Thinking)
|
||||
} else if id.starts_with("claude-sonnet-4") {
|
||||
Ok(Self::ClaudeSonnet4)
|
||||
} else {
|
||||
Err(anyhow!("invalid model id"))
|
||||
anyhow::bail!("invalid model id {id}");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> &str {
|
||||
match self {
|
||||
Model::ClaudeOpus4 => "claude-opus-4-latest",
|
||||
Model::ClaudeOpus4Thinking => "claude-opus-4-thinking-latest",
|
||||
Model::ClaudeSonnet4 => "claude-sonnet-4-latest",
|
||||
Model::ClaudeSonnet4Thinking => "claude-sonnet-4-thinking-latest",
|
||||
Model::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
|
||||
Model::Claude3_7Sonnet => "claude-3-7-sonnet-latest",
|
||||
Model::Claude3_7SonnetThinking => "claude-3-7-sonnet-thinking-latest",
|
||||
@@ -110,6 +136,8 @@ impl Model {
|
||||
/// The id of the model that should be used for making API requests
|
||||
pub fn request_id(&self) -> &str {
|
||||
match self {
|
||||
Model::ClaudeOpus4 | Model::ClaudeOpus4Thinking => "claude-opus-4-20250514",
|
||||
Model::ClaudeSonnet4 | Model::ClaudeSonnet4Thinking => "claude-sonnet-4-20250514",
|
||||
Model::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
|
||||
Model::Claude3_7Sonnet | Model::Claude3_7SonnetThinking => "claude-3-7-sonnet-latest",
|
||||
Model::Claude3_5Haiku => "claude-3-5-haiku-latest",
|
||||
@@ -122,6 +150,10 @@ impl Model {
|
||||
|
||||
pub fn display_name(&self) -> &str {
|
||||
match self {
|
||||
Model::ClaudeOpus4 => "Claude Opus 4",
|
||||
Model::ClaudeOpus4Thinking => "Claude Opus 4 Thinking",
|
||||
Model::ClaudeSonnet4 => "Claude Sonnet 4",
|
||||
Model::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking",
|
||||
Self::Claude3_7Sonnet => "Claude 3.7 Sonnet",
|
||||
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
|
||||
Self::Claude3_7SonnetThinking => "Claude 3.7 Sonnet Thinking",
|
||||
@@ -137,7 +169,11 @@ impl Model {
|
||||
|
||||
pub fn cache_configuration(&self) -> Option<AnthropicModelCacheConfiguration> {
|
||||
match self {
|
||||
Self::Claude3_5Sonnet
|
||||
Self::ClaudeOpus4
|
||||
| Self::ClaudeOpus4Thinking
|
||||
| Self::ClaudeSonnet4
|
||||
| Self::ClaudeSonnet4Thinking
|
||||
| Self::Claude3_5Sonnet
|
||||
| Self::Claude3_5Haiku
|
||||
| Self::Claude3_7Sonnet
|
||||
| Self::Claude3_7SonnetThinking
|
||||
@@ -156,7 +192,11 @@ impl Model {
|
||||
|
||||
pub fn max_token_count(&self) -> usize {
|
||||
match self {
|
||||
Self::Claude3_5Sonnet
|
||||
Self::ClaudeOpus4
|
||||
| Self::ClaudeOpus4Thinking
|
||||
| Self::ClaudeSonnet4
|
||||
| Self::ClaudeSonnet4Thinking
|
||||
| Self::Claude3_5Sonnet
|
||||
| Self::Claude3_5Haiku
|
||||
| Self::Claude3_7Sonnet
|
||||
| Self::Claude3_7SonnetThinking
|
||||
@@ -173,7 +213,11 @@ impl Model {
|
||||
Self::Claude3_5Sonnet
|
||||
| Self::Claude3_7Sonnet
|
||||
| Self::Claude3_7SonnetThinking
|
||||
| Self::Claude3_5Haiku => 8_192,
|
||||
| Self::Claude3_5Haiku
|
||||
| Self::ClaudeOpus4
|
||||
| Self::ClaudeOpus4Thinking
|
||||
| Self::ClaudeSonnet4
|
||||
| Self::ClaudeSonnet4Thinking => 8_192,
|
||||
Self::Custom {
|
||||
max_output_tokens, ..
|
||||
} => max_output_tokens.unwrap_or(4_096),
|
||||
@@ -182,7 +226,11 @@ impl Model {
|
||||
|
||||
pub fn default_temperature(&self) -> f32 {
|
||||
match self {
|
||||
Self::Claude3_5Sonnet
|
||||
Self::ClaudeOpus4
|
||||
| Self::ClaudeOpus4Thinking
|
||||
| Self::ClaudeSonnet4
|
||||
| Self::ClaudeSonnet4Thinking
|
||||
| Self::Claude3_5Sonnet
|
||||
| Self::Claude3_7Sonnet
|
||||
| Self::Claude3_7SonnetThinking
|
||||
| Self::Claude3_5Haiku
|
||||
@@ -201,10 +249,14 @@ impl Model {
|
||||
Self::Claude3_5Sonnet
|
||||
| Self::Claude3_7Sonnet
|
||||
| Self::Claude3_5Haiku
|
||||
| Self::ClaudeOpus4
|
||||
| Self::ClaudeSonnet4
|
||||
| Self::Claude3Opus
|
||||
| Self::Claude3Sonnet
|
||||
| Self::Claude3Haiku => AnthropicModelMode::Default,
|
||||
Self::Claude3_7SonnetThinking => AnthropicModelMode::Thinking {
|
||||
Self::Claude3_7SonnetThinking
|
||||
| Self::ClaudeOpus4Thinking
|
||||
| Self::ClaudeSonnet4Thinking => AnthropicModelMode::Thinking {
|
||||
budget_tokens: Some(4_096),
|
||||
},
|
||||
Self::Custom { mode, .. } => mode.clone(),
|
||||
@@ -385,10 +437,10 @@ impl RateLimitInfo {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_header<'a>(key: &str, headers: &'a HeaderMap) -> Result<&'a str, anyhow::Error> {
|
||||
fn get_header<'a>(key: &str, headers: &'a HeaderMap) -> anyhow::Result<&'a str> {
|
||||
Ok(headers
|
||||
.get(key)
|
||||
.ok_or_else(|| anyhow!("missing header `{key}`"))?
|
||||
.with_context(|| format!("missing header `{key}`"))?
|
||||
.to_str()?)
|
||||
}
|
||||
|
||||
@@ -534,12 +586,26 @@ pub enum RequestContent {
|
||||
ToolResult {
|
||||
tool_use_id: String,
|
||||
is_error: bool,
|
||||
content: String,
|
||||
content: ToolResultContent,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
cache_control: Option<CacheControl>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum ToolResultContent {
|
||||
Plain(String),
|
||||
Multipart(Vec<ToolResultPart>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "lowercase")]
|
||||
pub enum ToolResultPart {
|
||||
Text { text: String },
|
||||
Image { source: ImageSource },
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ResponseContent {
|
||||
|
||||
@@ -163,20 +163,26 @@ impl AskPassSession {
|
||||
#[cfg(unix)]
|
||||
fn get_shell_safe_zed_path() -> anyhow::Result<String> {
|
||||
let zed_path = std::env::current_exe()
|
||||
.context("Failed to figure out current executable path for use in askpass")?
|
||||
.context("Failed to determine current executable path for use in askpass")?
|
||||
.to_string_lossy()
|
||||
// see https://github.com/rust-lang/rust/issues/69343
|
||||
.trim_end_matches(" (deleted)")
|
||||
.to_string();
|
||||
|
||||
// sanity check on unix systems that the path exists and is executable
|
||||
// todo(windows): implement this check for windows (or just use `is-executable` crate)
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
let metadata = std::fs::metadata(&zed_path)
|
||||
.context("Failed to check metadata of Zed executable path for use in askpass")?;
|
||||
let is_executable = metadata.is_file() && metadata.mode() & 0o111 != 0;
|
||||
anyhow::ensure!(
|
||||
is_executable,
|
||||
"Failed to verify Zed executable path for use in askpass"
|
||||
);
|
||||
// NOTE: this was previously enabled, however, it caused errors when it shouldn't have
|
||||
// (see https://github.com/zed-industries/zed/issues/29819)
|
||||
// The zed path failing to execute within the askpass script results in very vague ssh
|
||||
// authentication failed errors, so this was done to try and surface a better error
|
||||
//
|
||||
// use std::os::unix::fs::MetadataExt;
|
||||
// let metadata = std::fs::metadata(&zed_path)
|
||||
// .context("Failed to check metadata of Zed executable path for use in askpass")?;
|
||||
// let is_executable = metadata.is_file() && metadata.mode() & 0o111 != 0;
|
||||
// anyhow::ensure!(
|
||||
// is_executable,
|
||||
// "Failed to verify Zed executable path for use in askpass"
|
||||
// );
|
||||
|
||||
// As of writing, this can only be fail if the path contains a null byte, which shouldn't be possible
|
||||
// but shlex has annotated the error as #[non_exhaustive] so we can't make it a compile error if other
|
||||
// errors are introduced in the future :(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// This crate was essentially pulled out verbatim from main `zed` crate to avoid having to run RustEmbed macro whenever zed has to be rebuilt. It saves a second or two on an incremental build.
|
||||
use anyhow::anyhow;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use gpui::{App, AssetSource, Result, SharedString};
|
||||
use rust_embed::RustEmbed;
|
||||
|
||||
@@ -21,7 +21,7 @@ impl AssetSource for Assets {
|
||||
fn load(&self, path: &str) -> Result<Option<std::borrow::Cow<'static, [u8]>>> {
|
||||
Self::get(path)
|
||||
.map(|f| Some(f.data))
|
||||
.ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path))
|
||||
.with_context(|| format!("loading asset at path {path:?}"))
|
||||
}
|
||||
|
||||
fn list(&self, path: &str) -> Result<Vec<SharedString>> {
|
||||
@@ -39,7 +39,7 @@ impl AssetSource for Assets {
|
||||
|
||||
impl Assets {
|
||||
/// Populate the [`TextSystem`] of the given [`AppContext`] with all `.ttf` fonts in the `fonts` directory.
|
||||
pub fn load_fonts(&self, cx: &App) -> gpui::Result<()> {
|
||||
pub fn load_fonts(&self, cx: &App) -> anyhow::Result<()> {
|
||||
let font_paths = self.list("fonts")?;
|
||||
let mut embedded_fonts = Vec::new();
|
||||
for font_path in font_paths {
|
||||
|
||||
@@ -12,8 +12,8 @@ workspace = true
|
||||
path = "src/assistant_context_editor.rs"
|
||||
|
||||
[dependencies]
|
||||
agent_settings.workspace = true
|
||||
anyhow.workspace = true
|
||||
assistant_settings.workspace = true
|
||||
assistant_slash_command.workspace = true
|
||||
assistant_slash_commands.workspace = true
|
||||
chrono.workspace = true
|
||||
@@ -22,6 +22,7 @@ clock.workspace = true
|
||||
collections.workspace = true
|
||||
context_server.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
@@ -29,15 +30,16 @@ gpui.workspace = true
|
||||
indexed_docs.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
language_model_selector.workspace = true
|
||||
log.workspace = true
|
||||
multi_buffer.workspace = true
|
||||
open_ai.workspace = true
|
||||
ordered-float.workspace = true
|
||||
parking_lot.workspace = true
|
||||
paths.workspace = true
|
||||
picker.workspace = true
|
||||
project.workspace = true
|
||||
prompt_store.workspace = true
|
||||
proto.workspace = true
|
||||
regex.workspace = true
|
||||
rope.workspace = true
|
||||
rpc.workspace = true
|
||||
@@ -46,7 +48,6 @@ serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
smallvec.workspace = true
|
||||
smol.workspace = true
|
||||
strum.workspace = true
|
||||
telemetry_events.workspace = true
|
||||
text.workspace = true
|
||||
theme.workspace = true
|
||||
|
||||
@@ -2,22 +2,34 @@ mod context;
|
||||
mod context_editor;
|
||||
mod context_history;
|
||||
mod context_store;
|
||||
mod patch;
|
||||
pub mod language_model_selector;
|
||||
mod slash_command;
|
||||
mod slash_command_picker;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use client::Client;
|
||||
use gpui::App;
|
||||
use gpui::{App, Context};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub use crate::context::*;
|
||||
pub use crate::context_editor::*;
|
||||
pub use crate::context_history::*;
|
||||
pub use crate::context_store::*;
|
||||
pub use crate::patch::*;
|
||||
pub use crate::slash_command::*;
|
||||
|
||||
pub fn init(client: Arc<Client>, _cx: &mut App) {
|
||||
pub fn init(client: Arc<Client>, cx: &mut App) {
|
||||
context_store::init(&client.into());
|
||||
workspace::FollowableViewRegistry::register::<ContextEditor>(cx);
|
||||
|
||||
cx.observe_new(
|
||||
|workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
|
||||
workspace
|
||||
.register_action(ContextEditor::quote_selection)
|
||||
.register_action(ContextEditor::insert_selection)
|
||||
.register_action(ContextEditor::copy_code)
|
||||
.register_action(ContextEditor::handle_insert_dragged_files);
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
#[cfg(test)]
|
||||
mod context_tests;
|
||||
|
||||
use crate::patch::{AssistantEdit, AssistantPatch, AssistantPatchStatus};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_settings::AssistantSettings;
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
use assistant_slash_command::{
|
||||
SlashCommandContent, SlashCommandEvent, SlashCommandLine, SlashCommandOutputSection,
|
||||
SlashCommandResult, SlashCommandWorkingSet,
|
||||
@@ -22,8 +21,8 @@ use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, P
|
||||
use language_model::{
|
||||
LanguageModel, LanguageModelCacheConfiguration, LanguageModelCompletionEvent,
|
||||
LanguageModelImage, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||
LanguageModelToolUseId, MaxMonthlySpendReachedError, MessageContent, PaymentRequiredError,
|
||||
Role, StopReason, report_assistant_event,
|
||||
LanguageModelToolUseId, MessageContent, PaymentRequiredError, Role, StopReason,
|
||||
report_assistant_event,
|
||||
};
|
||||
use open_ai::Model as OpenAiModel;
|
||||
use paths::contexts_dir;
|
||||
@@ -37,7 +36,6 @@ use std::{
|
||||
iter, mem,
|
||||
ops::Range,
|
||||
path::Path,
|
||||
str::FromStr as _,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
@@ -122,14 +120,6 @@ impl MessageStatus {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum RequestType {
|
||||
/// Request a normal chat response from the model.
|
||||
Chat,
|
||||
/// Add a preamble to the message, which tells the model to return a structured response that suggests edits.
|
||||
SuggestEdits,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ContextOperation {
|
||||
InsertMessage {
|
||||
@@ -143,7 +133,7 @@ pub enum ContextOperation {
|
||||
version: clock::Global,
|
||||
},
|
||||
UpdateSummary {
|
||||
summary: ContextSummary,
|
||||
summary: ContextSummaryContent,
|
||||
version: clock::Global,
|
||||
},
|
||||
SlashCommandStarted {
|
||||
@@ -213,7 +203,7 @@ impl ContextOperation {
|
||||
version: language::proto::deserialize_version(&update.version),
|
||||
}),
|
||||
proto::context_operation::Variant::UpdateSummary(update) => Ok(Self::UpdateSummary {
|
||||
summary: ContextSummary {
|
||||
summary: ContextSummaryContent {
|
||||
text: update.summary,
|
||||
done: update.done,
|
||||
timestamp: language::proto::deserialize_timestamp(
|
||||
@@ -457,17 +447,12 @@ impl ContextOperation {
|
||||
pub enum ContextEvent {
|
||||
ShowAssistError(SharedString),
|
||||
ShowPaymentRequiredError,
|
||||
ShowMaxMonthlySpendReachedError,
|
||||
MessagesEdited,
|
||||
SummaryChanged,
|
||||
SummaryGenerated,
|
||||
StreamedCompletion,
|
||||
StartedThoughtProcess(Range<language::Anchor>),
|
||||
EndedThoughtProcess(language::Anchor),
|
||||
PatchesUpdated {
|
||||
removed: Vec<Range<language::Anchor>>,
|
||||
updated: Vec<Range<language::Anchor>>,
|
||||
},
|
||||
InvokedSlashCommandChanged {
|
||||
command_id: InvokedSlashCommandId,
|
||||
},
|
||||
@@ -481,11 +466,73 @@ pub enum ContextEvent {
|
||||
Operation(ContextOperation),
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct ContextSummary {
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum ContextSummary {
|
||||
Pending,
|
||||
Content(ContextSummaryContent),
|
||||
Error,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ContextSummaryContent {
|
||||
pub text: String,
|
||||
pub done: bool,
|
||||
timestamp: clock::Lamport,
|
||||
pub timestamp: clock::Lamport,
|
||||
}
|
||||
|
||||
impl ContextSummary {
|
||||
pub const DEFAULT: &str = "New Text Thread";
|
||||
|
||||
pub fn or_default(&self) -> SharedString {
|
||||
self.unwrap_or(Self::DEFAULT)
|
||||
}
|
||||
|
||||
pub fn unwrap_or(&self, message: impl Into<SharedString>) -> SharedString {
|
||||
self.content()
|
||||
.map_or_else(|| message.into(), |content| content.text.clone().into())
|
||||
}
|
||||
|
||||
pub fn content(&self) -> Option<&ContextSummaryContent> {
|
||||
match self {
|
||||
ContextSummary::Content(content) => Some(content),
|
||||
ContextSummary::Pending | ContextSummary::Error => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn content_as_mut(&mut self) -> Option<&mut ContextSummaryContent> {
|
||||
match self {
|
||||
ContextSummary::Content(content) => Some(content),
|
||||
ContextSummary::Pending | ContextSummary::Error => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn content_or_set_empty(&mut self) -> &mut ContextSummaryContent {
|
||||
match self {
|
||||
ContextSummary::Content(content) => content,
|
||||
ContextSummary::Pending | ContextSummary::Error => {
|
||||
let content = ContextSummaryContent::default();
|
||||
*self = ContextSummary::Content(content);
|
||||
self.content_as_mut().unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_pending(&self) -> bool {
|
||||
matches!(self, ContextSummary::Pending)
|
||||
}
|
||||
|
||||
fn timestamp(&self) -> Option<clock::Lamport> {
|
||||
match self {
|
||||
ContextSummary::Content(content) => Some(content.timestamp),
|
||||
ContextSummary::Pending | ContextSummary::Error => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for ContextSummary {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
self.timestamp().partial_cmp(&other.timestamp())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
@@ -605,26 +652,6 @@ struct PendingCompletion {
|
||||
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
|
||||
pub struct InvokedSlashCommandId(clock::Lamport);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct XmlTag {
|
||||
pub kind: XmlTagKind,
|
||||
pub range: Range<text::Anchor>,
|
||||
pub is_open_tag: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, strum::EnumString, PartialEq, Eq, strum::AsRefStr)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum XmlTagKind {
|
||||
Patch,
|
||||
Title,
|
||||
Edit,
|
||||
Path,
|
||||
Description,
|
||||
OldText,
|
||||
NewText,
|
||||
Operation,
|
||||
}
|
||||
|
||||
pub struct AssistantContext {
|
||||
id: ContextId,
|
||||
timestamp: clock::Lamport,
|
||||
@@ -641,7 +668,7 @@ pub struct AssistantContext {
|
||||
message_anchors: Vec<MessageAnchor>,
|
||||
contents: Vec<Content>,
|
||||
messages_metadata: HashMap<MessageId, MessageMetadata>,
|
||||
summary: Option<ContextSummary>,
|
||||
summary: ContextSummary,
|
||||
summary_task: Task<Option<()>>,
|
||||
completion_count: usize,
|
||||
pending_completions: Vec<PendingCompletion>,
|
||||
@@ -653,8 +680,6 @@ pub struct AssistantContext {
|
||||
_subscriptions: Vec<Subscription>,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
patches: Vec<AssistantPatch>,
|
||||
xml_tags: Vec<XmlTag>,
|
||||
project: Option<Entity<Project>>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
}
|
||||
@@ -669,18 +694,6 @@ impl ContextAnnotation for ParsedSlashCommand {
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextAnnotation for AssistantPatch {
|
||||
fn range(&self) -> &Range<language::Anchor> {
|
||||
&self.range
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextAnnotation for XmlTag {
|
||||
fn range(&self) -> &Range<language::Anchor> {
|
||||
&self.range
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<ContextEvent> for AssistantContext {}
|
||||
|
||||
impl AssistantContext {
|
||||
@@ -742,7 +755,7 @@ impl AssistantContext {
|
||||
slash_command_output_sections: Vec::new(),
|
||||
thought_process_output_sections: Vec::new(),
|
||||
edits_since_last_parse: edits_since_last_slash_command_parse,
|
||||
summary: None,
|
||||
summary: ContextSummary::Pending,
|
||||
summary_task: Task::ready(None),
|
||||
completion_count: Default::default(),
|
||||
pending_completions: Default::default(),
|
||||
@@ -757,8 +770,6 @@ impl AssistantContext {
|
||||
project,
|
||||
language_registry,
|
||||
slash_commands,
|
||||
patches: Vec::new(),
|
||||
xml_tags: Vec::new(),
|
||||
prompt_builder,
|
||||
};
|
||||
|
||||
@@ -803,7 +814,7 @@ impl AssistantContext {
|
||||
.collect(),
|
||||
summary: self
|
||||
.summary
|
||||
.as_ref()
|
||||
.content()
|
||||
.map(|summary| summary.text.clone())
|
||||
.unwrap_or_default(),
|
||||
slash_command_output_sections: self
|
||||
@@ -989,12 +1000,10 @@ impl AssistantContext {
|
||||
summary: new_summary,
|
||||
..
|
||||
} => {
|
||||
if self
|
||||
.summary
|
||||
.as_ref()
|
||||
.map_or(true, |summary| new_summary.timestamp > summary.timestamp)
|
||||
{
|
||||
self.summary = Some(new_summary);
|
||||
if self.summary.timestamp().map_or(true, |current_timestamp| {
|
||||
new_summary.timestamp > current_timestamp
|
||||
}) {
|
||||
self.summary = ContextSummary::Content(new_summary);
|
||||
summary_generated = true;
|
||||
}
|
||||
}
|
||||
@@ -1152,50 +1161,8 @@ impl AssistantContext {
|
||||
self.path.as_ref()
|
||||
}
|
||||
|
||||
pub fn summary(&self) -> Option<&ContextSummary> {
|
||||
self.summary.as_ref()
|
||||
}
|
||||
|
||||
pub fn patch_containing(&self, position: Point, cx: &App) -> Option<&AssistantPatch> {
|
||||
let buffer = self.buffer.read(cx);
|
||||
let index = self.patches.binary_search_by(|patch| {
|
||||
let patch_range = patch.range.to_point(&buffer);
|
||||
if position < patch_range.start {
|
||||
Ordering::Greater
|
||||
} else if position > patch_range.end {
|
||||
Ordering::Less
|
||||
} else {
|
||||
Ordering::Equal
|
||||
}
|
||||
});
|
||||
if let Ok(ix) = index {
|
||||
Some(&self.patches[ix])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn patch_ranges(&self) -> impl Iterator<Item = Range<language::Anchor>> + '_ {
|
||||
self.patches.iter().map(|patch| patch.range.clone())
|
||||
}
|
||||
|
||||
pub fn patch_for_range(
|
||||
&self,
|
||||
range: &Range<language::Anchor>,
|
||||
cx: &App,
|
||||
) -> Option<&AssistantPatch> {
|
||||
let buffer = self.buffer.read(cx);
|
||||
let index = self.patch_index_for_range(range, buffer).ok()?;
|
||||
Some(&self.patches[index])
|
||||
}
|
||||
|
||||
fn patch_index_for_range(
|
||||
&self,
|
||||
tagged_range: &Range<text::Anchor>,
|
||||
buffer: &text::BufferSnapshot,
|
||||
) -> Result<usize, usize> {
|
||||
self.patches
|
||||
.binary_search_by(|probe| probe.range.cmp(&tagged_range, buffer))
|
||||
pub fn summary(&self) -> &ContextSummary {
|
||||
&self.summary
|
||||
}
|
||||
|
||||
pub fn parsed_slash_commands(&self) -> &[ParsedSlashCommand] {
|
||||
@@ -1277,7 +1244,7 @@ impl AssistantContext {
|
||||
let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
|
||||
return;
|
||||
};
|
||||
let request = self.to_completion_request(Some(&model.model), RequestType::Chat, cx);
|
||||
let request = self.to_completion_request(Some(&model.model), cx);
|
||||
let debounce = self.token_count.is_some();
|
||||
self.pending_token_count = cx.spawn(async move |this, cx| {
|
||||
async move {
|
||||
@@ -1423,7 +1390,7 @@ impl AssistantContext {
|
||||
}
|
||||
|
||||
let request = {
|
||||
let mut req = self.to_completion_request(Some(&model), RequestType::Chat, cx);
|
||||
let mut req = self.to_completion_request(Some(&model), cx);
|
||||
// Skip the last message because it's likely to change and
|
||||
// therefore would be a waste to cache.
|
||||
req.messages.pop();
|
||||
@@ -1498,8 +1465,6 @@ impl AssistantContext {
|
||||
|
||||
let mut removed_parsed_slash_command_ranges = Vec::new();
|
||||
let mut updated_parsed_slash_commands = Vec::new();
|
||||
let mut removed_patches = Vec::new();
|
||||
let mut updated_patches = Vec::new();
|
||||
while let Some(mut row_range) = row_ranges.next() {
|
||||
while let Some(next_row_range) = row_ranges.peek() {
|
||||
if row_range.end >= next_row_range.start {
|
||||
@@ -1524,13 +1489,6 @@ impl AssistantContext {
|
||||
cx,
|
||||
);
|
||||
self.invalidate_pending_slash_commands(&buffer, cx);
|
||||
self.reparse_patches_in_range(
|
||||
start..end,
|
||||
&buffer,
|
||||
&mut updated_patches,
|
||||
&mut removed_patches,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
if !updated_parsed_slash_commands.is_empty()
|
||||
@@ -1541,13 +1499,6 @@ impl AssistantContext {
|
||||
updated: updated_parsed_slash_commands,
|
||||
});
|
||||
}
|
||||
|
||||
if !updated_patches.is_empty() || !removed_patches.is_empty() {
|
||||
cx.emit(ContextEvent::PatchesUpdated {
|
||||
removed: removed_patches,
|
||||
updated: updated_patches,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn reparse_slash_commands_in_range(
|
||||
@@ -1638,267 +1589,6 @@ impl AssistantContext {
|
||||
}
|
||||
}
|
||||
|
||||
fn reparse_patches_in_range(
|
||||
&mut self,
|
||||
range: Range<text::Anchor>,
|
||||
buffer: &BufferSnapshot,
|
||||
updated: &mut Vec<Range<text::Anchor>>,
|
||||
removed: &mut Vec<Range<text::Anchor>>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
// Rebuild the XML tags in the edited range.
|
||||
let intersecting_tags_range =
|
||||
self.indices_intersecting_buffer_range(&self.xml_tags, range.clone(), cx);
|
||||
let new_tags = self.parse_xml_tags_in_range(buffer, range.clone(), cx);
|
||||
self.xml_tags
|
||||
.splice(intersecting_tags_range.clone(), new_tags);
|
||||
|
||||
// Find which patches intersect the changed range.
|
||||
let intersecting_patches_range =
|
||||
self.indices_intersecting_buffer_range(&self.patches, range.clone(), cx);
|
||||
|
||||
// Reparse all tags after the last unchanged patch before the change.
|
||||
let mut tags_start_ix = 0;
|
||||
if let Some(preceding_unchanged_patch) =
|
||||
self.patches[..intersecting_patches_range.start].last()
|
||||
{
|
||||
tags_start_ix = match self.xml_tags.binary_search_by(|tag| {
|
||||
tag.range
|
||||
.start
|
||||
.cmp(&preceding_unchanged_patch.range.end, buffer)
|
||||
.then(Ordering::Less)
|
||||
}) {
|
||||
Ok(ix) | Err(ix) => ix,
|
||||
};
|
||||
}
|
||||
|
||||
// Rebuild the patches in the range.
|
||||
let new_patches = self.parse_patches(tags_start_ix, range.end, buffer, cx);
|
||||
updated.extend(new_patches.iter().map(|patch| patch.range.clone()));
|
||||
let removed_patches = self.patches.splice(intersecting_patches_range, new_patches);
|
||||
removed.extend(
|
||||
removed_patches
|
||||
.map(|patch| patch.range)
|
||||
.filter(|range| !updated.contains(&range)),
|
||||
);
|
||||
}
|
||||
|
||||
fn parse_xml_tags_in_range(
|
||||
&self,
|
||||
buffer: &BufferSnapshot,
|
||||
range: Range<text::Anchor>,
|
||||
cx: &App,
|
||||
) -> Vec<XmlTag> {
|
||||
let mut messages = self.messages(cx).peekable();
|
||||
|
||||
let mut tags = Vec::new();
|
||||
let mut lines = buffer.text_for_range(range).lines();
|
||||
let mut offset = lines.offset();
|
||||
|
||||
while let Some(line) = lines.next() {
|
||||
while let Some(message) = messages.peek() {
|
||||
if offset < message.offset_range.end {
|
||||
break;
|
||||
} else {
|
||||
messages.next();
|
||||
}
|
||||
}
|
||||
|
||||
let is_assistant_message = messages
|
||||
.peek()
|
||||
.map_or(false, |message| message.role == Role::Assistant);
|
||||
if is_assistant_message {
|
||||
for (start_ix, _) in line.match_indices('<') {
|
||||
let mut name_start_ix = start_ix + 1;
|
||||
let closing_bracket_ix = line[start_ix..].find('>').map(|i| start_ix + i);
|
||||
if let Some(closing_bracket_ix) = closing_bracket_ix {
|
||||
let end_ix = closing_bracket_ix + 1;
|
||||
let mut is_open_tag = true;
|
||||
if line[name_start_ix..closing_bracket_ix].starts_with('/') {
|
||||
name_start_ix += 1;
|
||||
is_open_tag = false;
|
||||
}
|
||||
let tag_inner = &line[name_start_ix..closing_bracket_ix];
|
||||
let tag_name_len = tag_inner
|
||||
.find(|c: char| c.is_whitespace())
|
||||
.unwrap_or(tag_inner.len());
|
||||
if let Ok(kind) = XmlTagKind::from_str(&tag_inner[..tag_name_len]) {
|
||||
tags.push(XmlTag {
|
||||
range: buffer.anchor_after(offset + start_ix)
|
||||
..buffer.anchor_before(offset + end_ix),
|
||||
is_open_tag,
|
||||
kind,
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
offset = lines.offset();
|
||||
}
|
||||
tags
|
||||
}
|
||||
|
||||
fn parse_patches(
|
||||
&mut self,
|
||||
tags_start_ix: usize,
|
||||
buffer_end: text::Anchor,
|
||||
buffer: &BufferSnapshot,
|
||||
cx: &App,
|
||||
) -> Vec<AssistantPatch> {
|
||||
let mut new_patches = Vec::new();
|
||||
let mut pending_patch = None;
|
||||
let mut patch_tag_depth = 0;
|
||||
let mut tags = self.xml_tags[tags_start_ix..].iter().peekable();
|
||||
'tags: while let Some(tag) = tags.next() {
|
||||
if tag.range.start.cmp(&buffer_end, buffer).is_gt() && patch_tag_depth == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
if tag.kind == XmlTagKind::Patch && tag.is_open_tag {
|
||||
patch_tag_depth += 1;
|
||||
let patch_start = tag.range.start;
|
||||
let mut edits = Vec::<Result<AssistantEdit>>::new();
|
||||
let mut patch = AssistantPatch {
|
||||
range: patch_start..patch_start,
|
||||
title: String::new().into(),
|
||||
edits: Default::default(),
|
||||
status: crate::AssistantPatchStatus::Pending,
|
||||
};
|
||||
|
||||
while let Some(tag) = tags.next() {
|
||||
if tag.kind == XmlTagKind::Patch && !tag.is_open_tag {
|
||||
patch_tag_depth -= 1;
|
||||
if patch_tag_depth == 0 {
|
||||
patch.range.end = tag.range.end;
|
||||
|
||||
// Include the line immediately after this <patch> tag if it's empty.
|
||||
let patch_end_offset = patch.range.end.to_offset(buffer);
|
||||
let mut patch_end_chars = buffer.chars_at(patch_end_offset);
|
||||
if patch_end_chars.next() == Some('\n')
|
||||
&& patch_end_chars.next().map_or(true, |ch| ch == '\n')
|
||||
{
|
||||
let messages = self.messages_for_offsets(
|
||||
[patch_end_offset, patch_end_offset + 1],
|
||||
cx,
|
||||
);
|
||||
if messages.len() == 1 {
|
||||
patch.range.end = buffer.anchor_before(patch_end_offset + 1);
|
||||
}
|
||||
}
|
||||
|
||||
edits.sort_unstable_by(|a, b| {
|
||||
if let (Ok(a), Ok(b)) = (a, b) {
|
||||
a.path.cmp(&b.path)
|
||||
} else {
|
||||
Ordering::Equal
|
||||
}
|
||||
});
|
||||
patch.edits = edits.into();
|
||||
patch.status = AssistantPatchStatus::Ready;
|
||||
new_patches.push(patch);
|
||||
continue 'tags;
|
||||
}
|
||||
}
|
||||
|
||||
if tag.kind == XmlTagKind::Title && tag.is_open_tag {
|
||||
let content_start = tag.range.end;
|
||||
while let Some(tag) = tags.next() {
|
||||
if tag.kind == XmlTagKind::Title && !tag.is_open_tag {
|
||||
let content_end = tag.range.start;
|
||||
patch.title =
|
||||
trimmed_text_in_range(buffer, content_start..content_end)
|
||||
.into();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if tag.kind == XmlTagKind::Edit && tag.is_open_tag {
|
||||
let mut path = None;
|
||||
let mut old_text = None;
|
||||
let mut new_text = None;
|
||||
let mut operation = None;
|
||||
let mut description = None;
|
||||
|
||||
while let Some(tag) = tags.next() {
|
||||
if tag.kind == XmlTagKind::Edit && !tag.is_open_tag {
|
||||
edits.push(AssistantEdit::new(
|
||||
path,
|
||||
operation,
|
||||
old_text,
|
||||
new_text,
|
||||
description,
|
||||
));
|
||||
break;
|
||||
}
|
||||
|
||||
if tag.is_open_tag
|
||||
&& [
|
||||
XmlTagKind::Path,
|
||||
XmlTagKind::OldText,
|
||||
XmlTagKind::NewText,
|
||||
XmlTagKind::Operation,
|
||||
XmlTagKind::Description,
|
||||
]
|
||||
.contains(&tag.kind)
|
||||
{
|
||||
let kind = tag.kind;
|
||||
let content_start = tag.range.end;
|
||||
if let Some(tag) = tags.peek() {
|
||||
if tag.kind == kind && !tag.is_open_tag {
|
||||
let tag = tags.next().unwrap();
|
||||
let content_end = tag.range.start;
|
||||
let content = trimmed_text_in_range(
|
||||
buffer,
|
||||
content_start..content_end,
|
||||
);
|
||||
match kind {
|
||||
XmlTagKind::Path => path = Some(content),
|
||||
XmlTagKind::Operation => operation = Some(content),
|
||||
XmlTagKind::OldText => {
|
||||
old_text = Some(content).filter(|s| !s.is_empty())
|
||||
}
|
||||
XmlTagKind::NewText => {
|
||||
new_text = Some(content).filter(|s| !s.is_empty())
|
||||
}
|
||||
XmlTagKind::Description => {
|
||||
description =
|
||||
Some(content).filter(|s| !s.is_empty())
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
patch.edits = edits.into();
|
||||
pending_patch = Some(patch);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(mut pending_patch) = pending_patch {
|
||||
let patch_start = pending_patch.range.start.to_offset(buffer);
|
||||
if let Some(message) = self.message_for_offset(patch_start, cx) {
|
||||
if message.anchor_range.end == text::Anchor::MAX {
|
||||
pending_patch.range.end = text::Anchor::MAX;
|
||||
} else {
|
||||
let message_end = buffer.anchor_after(message.offset_range.end - 1);
|
||||
pending_patch.range.end = message_end;
|
||||
}
|
||||
} else {
|
||||
pending_patch.range.end = text::Anchor::MAX;
|
||||
}
|
||||
|
||||
new_patches.push(pending_patch);
|
||||
}
|
||||
|
||||
new_patches
|
||||
}
|
||||
|
||||
pub fn pending_command_for_position(
|
||||
&mut self,
|
||||
position: language::Anchor,
|
||||
@@ -2040,9 +1730,8 @@ impl AssistantContext {
|
||||
merge_same_roles,
|
||||
} => {
|
||||
if !merge_same_roles && Some(role) != last_role {
|
||||
let offset = this.buffer.read_with(cx, |buffer, _cx| {
|
||||
insert_position.to_offset(buffer)
|
||||
});
|
||||
let buffer = this.buffer.read(cx);
|
||||
let offset = insert_position.to_offset(buffer);
|
||||
this.insert_message_at_offset(
|
||||
offset,
|
||||
role,
|
||||
@@ -2303,11 +1992,7 @@ impl AssistantContext {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn assist(
|
||||
&mut self,
|
||||
request_type: RequestType,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<MessageAnchor> {
|
||||
pub fn assist(&mut self, cx: &mut Context<Self>) -> Option<MessageAnchor> {
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
let model = model_registry.default_model()?;
|
||||
let last_message_id = self.get_last_valid_message_id(cx)?;
|
||||
@@ -2322,7 +2007,7 @@ impl AssistantContext {
|
||||
// Compute which messages to cache, including the last one.
|
||||
self.mark_cache_anchors(&model.cache_configuration(), false, cx);
|
||||
|
||||
let request = self.to_completion_request(Some(&model), request_type, cx);
|
||||
let request = self.to_completion_request(Some(&model), cx);
|
||||
|
||||
let assistant_message = self
|
||||
.insert_message_after(last_message_id, Role::Assistant, MessageStatus::Pending, cx)
|
||||
@@ -2468,12 +2153,6 @@ impl AssistantContext {
|
||||
metadata.status = MessageStatus::Canceled;
|
||||
});
|
||||
Some(error.to_string())
|
||||
} else if error.is::<MaxMonthlySpendReachedError>() {
|
||||
cx.emit(ContextEvent::ShowMaxMonthlySpendReachedError);
|
||||
this.update_metadata(assistant_message_id, cx, |metadata| {
|
||||
metadata.status = MessageStatus::Canceled;
|
||||
});
|
||||
Some(error.to_string())
|
||||
} else {
|
||||
let error_message = error
|
||||
.chain()
|
||||
@@ -2524,6 +2203,7 @@ impl AssistantContext {
|
||||
StopReason::ToolUse => {}
|
||||
StopReason::EndTurn => {}
|
||||
StopReason::MaxTokens => {}
|
||||
StopReason::Refusal => {}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -2563,7 +2243,6 @@ impl AssistantContext {
|
||||
pub fn to_completion_request(
|
||||
&self,
|
||||
model: Option<&Arc<dyn LanguageModel>>,
|
||||
request_type: RequestType,
|
||||
cx: &App,
|
||||
) -> LanguageModelRequest {
|
||||
let buffer = self.buffer.read(cx);
|
||||
@@ -2587,8 +2266,7 @@ impl AssistantContext {
|
||||
tools: Vec::new(),
|
||||
tool_choice: None,
|
||||
stop: Vec::new(),
|
||||
temperature: model
|
||||
.and_then(|model| AssistantSettings::temperature_for_model(model, cx)),
|
||||
temperature: model.and_then(|model| AgentSettings::temperature_for_model(model, cx)),
|
||||
};
|
||||
for message in self.messages(cx) {
|
||||
if message.status != MessageStatus::Done {
|
||||
@@ -2644,25 +2322,6 @@ impl AssistantContext {
|
||||
}
|
||||
}
|
||||
|
||||
if let RequestType::SuggestEdits = request_type {
|
||||
if let Ok(preamble) = self.prompt_builder.generate_suggest_edits_prompt() {
|
||||
let last_elem_index = completion_request.messages.len();
|
||||
|
||||
completion_request
|
||||
.messages
|
||||
.push(LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![MessageContent::Text(preamble)],
|
||||
cache: false,
|
||||
});
|
||||
|
||||
// The preamble message should be sent right before the last actual user message.
|
||||
completion_request
|
||||
.messages
|
||||
.swap(last_elem_index, last_elem_index.saturating_sub(1));
|
||||
}
|
||||
}
|
||||
|
||||
completion_request
|
||||
}
|
||||
|
||||
@@ -2697,17 +2356,6 @@ impl AssistantContext {
|
||||
ranges.push(message.anchor_range.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let buffer = self.buffer.read(cx).text_snapshot();
|
||||
let mut updated = Vec::new();
|
||||
let mut removed = Vec::new();
|
||||
for range in ranges {
|
||||
self.reparse_patches_in_range(range, &buffer, &mut updated, &mut removed, cx);
|
||||
}
|
||||
|
||||
if !updated.is_empty() || !removed.is_empty() {
|
||||
cx.emit(ContextEvent::PatchesUpdated { removed, updated })
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_metadata(
|
||||
@@ -2980,12 +2628,12 @@ impl AssistantContext {
|
||||
return;
|
||||
};
|
||||
|
||||
if replace_old || (self.message_anchors.len() >= 2 && self.summary.is_none()) {
|
||||
if replace_old || (self.message_anchors.len() >= 2 && self.summary.is_pending()) {
|
||||
if !model.provider.is_authenticated(cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut request = self.to_completion_request(Some(&model.model), RequestType::Chat, cx);
|
||||
let mut request = self.to_completion_request(Some(&model.model), cx);
|
||||
request.messages.push(LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![
|
||||
@@ -2997,17 +2645,20 @@ impl AssistantContext {
|
||||
|
||||
// If there is no summary, it is set with `done: false` so that "Loading Summary…" can
|
||||
// be displayed.
|
||||
if self.summary.is_none() {
|
||||
self.summary = Some(ContextSummary {
|
||||
text: "".to_string(),
|
||||
done: false,
|
||||
timestamp: clock::Lamport::default(),
|
||||
});
|
||||
replace_old = true;
|
||||
match self.summary {
|
||||
ContextSummary::Pending | ContextSummary::Error => {
|
||||
self.summary = ContextSummary::Content(ContextSummaryContent {
|
||||
text: "".to_string(),
|
||||
done: false,
|
||||
timestamp: clock::Lamport::default(),
|
||||
});
|
||||
replace_old = true;
|
||||
}
|
||||
ContextSummary::Content(_) => {}
|
||||
}
|
||||
|
||||
self.summary_task = cx.spawn(async move |this, cx| {
|
||||
async move {
|
||||
let result = async {
|
||||
let stream = model.model.stream_completion_text(request, &cx);
|
||||
let mut messages = stream.await?;
|
||||
|
||||
@@ -3018,7 +2669,7 @@ impl AssistantContext {
|
||||
this.update(cx, |this, cx| {
|
||||
let version = this.version.clone();
|
||||
let timestamp = this.next_timestamp();
|
||||
let summary = this.summary.get_or_insert(ContextSummary::default());
|
||||
let summary = this.summary.content_or_set_empty();
|
||||
if !replaced && replace_old {
|
||||
summary.text.clear();
|
||||
replaced = true;
|
||||
@@ -3040,10 +2691,19 @@ impl AssistantContext {
|
||||
}
|
||||
}
|
||||
|
||||
this.read_with(cx, |this, _cx| {
|
||||
if let Some(summary) = this.summary.content() {
|
||||
if summary.text.is_empty() {
|
||||
bail!("Model generated an empty summary");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})??;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
let version = this.version.clone();
|
||||
let timestamp = this.next_timestamp();
|
||||
if let Some(summary) = this.summary.as_mut() {
|
||||
if let Some(summary) = this.summary.content_as_mut() {
|
||||
summary.done = true;
|
||||
summary.timestamp = timestamp;
|
||||
let operation = ContextOperation::UpdateSummary {
|
||||
@@ -3058,8 +2718,18 @@ impl AssistantContext {
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err()
|
||||
.await
|
||||
.await;
|
||||
|
||||
if let Err(err) = result {
|
||||
this.update(cx, |this, cx| {
|
||||
this.summary = ContextSummary::Error;
|
||||
cx.emit(ContextEvent::SummaryChanged);
|
||||
})
|
||||
.log_err();
|
||||
log::error!("Error generating context summary: {}", err);
|
||||
}
|
||||
|
||||
Some(())
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -3173,7 +2843,7 @@ impl AssistantContext {
|
||||
|
||||
let (old_path, summary) = this.read_with(cx, |this, _| {
|
||||
let path = this.path.clone();
|
||||
let summary = if let Some(summary) = this.summary.as_ref() {
|
||||
let summary = if let Some(summary) = this.summary.content() {
|
||||
if summary.done {
|
||||
Some(summary.text.clone())
|
||||
} else {
|
||||
@@ -3227,39 +2897,12 @@ impl AssistantContext {
|
||||
|
||||
pub fn set_custom_summary(&mut self, custom_summary: String, cx: &mut Context<Self>) {
|
||||
let timestamp = self.next_timestamp();
|
||||
let summary = self.summary.get_or_insert(ContextSummary::default());
|
||||
let summary = self.summary.content_or_set_empty();
|
||||
summary.timestamp = timestamp;
|
||||
summary.done = true;
|
||||
summary.text = custom_summary;
|
||||
cx.emit(ContextEvent::SummaryChanged);
|
||||
}
|
||||
|
||||
pub const DEFAULT_SUMMARY: SharedString = SharedString::new_static("New Text Thread");
|
||||
|
||||
pub fn summary_or_default(&self) -> SharedString {
|
||||
self.summary
|
||||
.as_ref()
|
||||
.map(|summary| summary.text.clone().into())
|
||||
.unwrap_or(Self::DEFAULT_SUMMARY)
|
||||
}
|
||||
}
|
||||
|
||||
fn trimmed_text_in_range(buffer: &BufferSnapshot, range: Range<text::Anchor>) -> String {
|
||||
let mut is_start = true;
|
||||
let mut content = buffer
|
||||
.text_for_range(range)
|
||||
.map(|mut chunk| {
|
||||
if is_start {
|
||||
chunk = chunk.trim_start_matches('\n');
|
||||
if !chunk.is_empty() {
|
||||
is_start = false;
|
||||
}
|
||||
}
|
||||
chunk
|
||||
})
|
||||
.collect::<String>();
|
||||
content.truncate(content.trim_end().len());
|
||||
content
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
@@ -3367,7 +3010,7 @@ impl SavedContext {
|
||||
let saved_context_json = serde_json::from_str::<serde_json::Value>(json)?;
|
||||
match saved_context_json
|
||||
.get("version")
|
||||
.ok_or_else(|| anyhow!("version not found"))?
|
||||
.context("version not found")?
|
||||
{
|
||||
serde_json::Value::String(version) => match version.as_str() {
|
||||
SavedContext::VERSION => {
|
||||
@@ -3388,9 +3031,9 @@ impl SavedContext {
|
||||
serde_json::from_value::<SavedContextV0_1_0>(saved_context_json)?;
|
||||
Ok(saved_context.upgrade())
|
||||
}
|
||||
_ => Err(anyhow!("unrecognized saved context version: {}", version)),
|
||||
_ => anyhow::bail!("unrecognized saved context version: {version:?}"),
|
||||
},
|
||||
_ => Err(anyhow!("version not found on saved context")),
|
||||
_ => anyhow::bail!("version not found on saved context"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3475,7 +3118,7 @@ impl SavedContext {
|
||||
|
||||
let timestamp = next_timestamp.tick();
|
||||
operations.push(ContextOperation::UpdateSummary {
|
||||
summary: ContextSummary {
|
||||
summary: ContextSummaryContent {
|
||||
text: self.summary,
|
||||
done: true,
|
||||
timestamp,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
AssistantContext, AssistantEdit, AssistantEditKind, CacheStatus, ContextEvent, ContextId,
|
||||
ContextOperation, InvokedSlashCommandId, MessageCacheMetadata, MessageId, MessageStatus,
|
||||
AssistantContext, CacheStatus, ContextEvent, ContextId, ContextOperation, ContextSummary,
|
||||
InvokedSlashCommandId, MessageCacheMetadata, MessageId, MessageStatus,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use assistant_slash_command::{
|
||||
@@ -16,7 +16,10 @@ use futures::{
|
||||
};
|
||||
use gpui::{App, Entity, SharedString, Task, TestAppContext, WeakEntity, prelude::*};
|
||||
use language::{Buffer, BufferSnapshot, LanguageRegistry, LspAdapterDelegate};
|
||||
use language_model::{LanguageModelCacheConfiguration, LanguageModelRegistry, Role};
|
||||
use language_model::{
|
||||
ConfiguredModel, LanguageModelCacheConfiguration, LanguageModelRegistry, Role,
|
||||
fake_provider::{FakeLanguageModel, FakeLanguageModelProvider},
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use pretty_assertions::assert_eq;
|
||||
use project::Project;
|
||||
@@ -32,13 +35,10 @@ use std::{
|
||||
rc::Rc,
|
||||
sync::{Arc, atomic::AtomicBool},
|
||||
};
|
||||
use text::{OffsetRangeExt as _, ReplicaId, ToOffset, network::Network};
|
||||
use text::{ReplicaId, ToOffset, network::Network};
|
||||
use ui::{IconName, Window};
|
||||
use unindent::Unindent;
|
||||
use util::{
|
||||
RandomCharIter,
|
||||
test::{generate_marked_text, marked_text_ranges},
|
||||
};
|
||||
use util::RandomCharIter;
|
||||
use workspace::Workspace;
|
||||
|
||||
#[gpui::test]
|
||||
@@ -664,401 +664,6 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
init_test(cx);
|
||||
cx.update_global(|settings_store: &mut SettingsStore, cx| {
|
||||
settings_store
|
||||
.set_user_settings(
|
||||
r#"{ "assistant": { "enable_experimental_live_diffs": true } }"#,
|
||||
cx,
|
||||
)
|
||||
.unwrap()
|
||||
})
|
||||
});
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs, [Path::new("/root")], cx).await;
|
||||
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
|
||||
|
||||
// Create a new context
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let context = cx.new(|cx| {
|
||||
AssistantContext::local(
|
||||
registry.clone(),
|
||||
Some(project),
|
||||
None,
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
// Insert an assistant message to simulate a response.
|
||||
let assistant_message_id = context.update(cx, |context, cx| {
|
||||
let user_message_id = context.messages(cx).next().unwrap().id;
|
||||
context
|
||||
.insert_message_after(user_message_id, Role::Assistant, MessageStatus::Done, cx)
|
||||
.unwrap()
|
||||
.id
|
||||
});
|
||||
|
||||
// No edit tags
|
||||
edit(
|
||||
&context,
|
||||
"
|
||||
|
||||
«one
|
||||
two
|
||||
»",
|
||||
cx,
|
||||
);
|
||||
expect_patches(
|
||||
&context,
|
||||
"
|
||||
|
||||
one
|
||||
two
|
||||
",
|
||||
&[],
|
||||
cx,
|
||||
);
|
||||
|
||||
// Partial edit step tag is added
|
||||
edit(
|
||||
&context,
|
||||
"
|
||||
|
||||
one
|
||||
two
|
||||
«
|
||||
<patch»",
|
||||
cx,
|
||||
);
|
||||
expect_patches(
|
||||
&context,
|
||||
"
|
||||
|
||||
one
|
||||
two
|
||||
|
||||
<patch",
|
||||
&[],
|
||||
cx,
|
||||
);
|
||||
|
||||
// The rest of the step tag is added. The unclosed
|
||||
// step is treated as incomplete.
|
||||
edit(
|
||||
&context,
|
||||
"
|
||||
|
||||
one
|
||||
two
|
||||
|
||||
<patch«>
|
||||
<edit>»",
|
||||
cx,
|
||||
);
|
||||
expect_patches(
|
||||
&context,
|
||||
"
|
||||
|
||||
one
|
||||
two
|
||||
|
||||
«<patch>
|
||||
<edit>»",
|
||||
&[&[]],
|
||||
cx,
|
||||
);
|
||||
|
||||
// The full patch is added
|
||||
edit(
|
||||
&context,
|
||||
"
|
||||
|
||||
one
|
||||
two
|
||||
|
||||
<patch>
|
||||
<edit>«
|
||||
<description>add a `two` function</description>
|
||||
<path>src/lib.rs</path>
|
||||
<operation>insert_after</operation>
|
||||
<old_text>fn one</old_text>
|
||||
<new_text>
|
||||
fn two() {}
|
||||
</new_text>
|
||||
</edit>
|
||||
</patch>
|
||||
|
||||
also,»",
|
||||
cx,
|
||||
);
|
||||
expect_patches(
|
||||
&context,
|
||||
"
|
||||
|
||||
one
|
||||
two
|
||||
|
||||
«<patch>
|
||||
<edit>
|
||||
<description>add a `two` function</description>
|
||||
<path>src/lib.rs</path>
|
||||
<operation>insert_after</operation>
|
||||
<old_text>fn one</old_text>
|
||||
<new_text>
|
||||
fn two() {}
|
||||
</new_text>
|
||||
</edit>
|
||||
</patch>
|
||||
»
|
||||
also,",
|
||||
&[&[AssistantEdit {
|
||||
path: "src/lib.rs".into(),
|
||||
kind: AssistantEditKind::InsertAfter {
|
||||
old_text: "fn one".into(),
|
||||
new_text: "fn two() {}".into(),
|
||||
description: Some("add a `two` function".into()),
|
||||
},
|
||||
}]],
|
||||
cx,
|
||||
);
|
||||
|
||||
// The step is manually edited.
|
||||
edit(
|
||||
&context,
|
||||
"
|
||||
|
||||
one
|
||||
two
|
||||
|
||||
<patch>
|
||||
<edit>
|
||||
<description>add a `two` function</description>
|
||||
<path>src/lib.rs</path>
|
||||
<operation>insert_after</operation>
|
||||
<old_text>«fn zero»</old_text>
|
||||
<new_text>
|
||||
fn two() {}
|
||||
</new_text>
|
||||
</edit>
|
||||
</patch>
|
||||
|
||||
also,",
|
||||
cx,
|
||||
);
|
||||
expect_patches(
|
||||
&context,
|
||||
"
|
||||
|
||||
one
|
||||
two
|
||||
|
||||
«<patch>
|
||||
<edit>
|
||||
<description>add a `two` function</description>
|
||||
<path>src/lib.rs</path>
|
||||
<operation>insert_after</operation>
|
||||
<old_text>fn zero</old_text>
|
||||
<new_text>
|
||||
fn two() {}
|
||||
</new_text>
|
||||
</edit>
|
||||
</patch>
|
||||
»
|
||||
also,",
|
||||
&[&[AssistantEdit {
|
||||
path: "src/lib.rs".into(),
|
||||
kind: AssistantEditKind::InsertAfter {
|
||||
old_text: "fn zero".into(),
|
||||
new_text: "fn two() {}".into(),
|
||||
description: Some("add a `two` function".into()),
|
||||
},
|
||||
}]],
|
||||
cx,
|
||||
);
|
||||
|
||||
// When setting the message role to User, the steps are cleared.
|
||||
context.update(cx, |context, cx| {
|
||||
context.cycle_message_roles(HashSet::from_iter([assistant_message_id]), cx);
|
||||
context.cycle_message_roles(HashSet::from_iter([assistant_message_id]), cx);
|
||||
});
|
||||
expect_patches(
|
||||
&context,
|
||||
"
|
||||
|
||||
one
|
||||
two
|
||||
|
||||
<patch>
|
||||
<edit>
|
||||
<description>add a `two` function</description>
|
||||
<path>src/lib.rs</path>
|
||||
<operation>insert_after</operation>
|
||||
<old_text>fn zero</old_text>
|
||||
<new_text>
|
||||
fn two() {}
|
||||
</new_text>
|
||||
</edit>
|
||||
</patch>
|
||||
|
||||
also,",
|
||||
&[],
|
||||
cx,
|
||||
);
|
||||
|
||||
// When setting the message role back to Assistant, the steps are reparsed.
|
||||
context.update(cx, |context, cx| {
|
||||
context.cycle_message_roles(HashSet::from_iter([assistant_message_id]), cx);
|
||||
});
|
||||
expect_patches(
|
||||
&context,
|
||||
"
|
||||
|
||||
one
|
||||
two
|
||||
|
||||
«<patch>
|
||||
<edit>
|
||||
<description>add a `two` function</description>
|
||||
<path>src/lib.rs</path>
|
||||
<operation>insert_after</operation>
|
||||
<old_text>fn zero</old_text>
|
||||
<new_text>
|
||||
fn two() {}
|
||||
</new_text>
|
||||
</edit>
|
||||
</patch>
|
||||
»
|
||||
also,",
|
||||
&[&[AssistantEdit {
|
||||
path: "src/lib.rs".into(),
|
||||
kind: AssistantEditKind::InsertAfter {
|
||||
old_text: "fn zero".into(),
|
||||
new_text: "fn two() {}".into(),
|
||||
description: Some("add a `two` function".into()),
|
||||
},
|
||||
}]],
|
||||
cx,
|
||||
);
|
||||
|
||||
// Ensure steps are re-parsed when deserializing.
|
||||
let serialized_context = context.read_with(cx, |context, cx| context.serialize(cx));
|
||||
let deserialized_context = cx.new(|cx| {
|
||||
AssistantContext::deserialize(
|
||||
serialized_context,
|
||||
Path::new("").into(),
|
||||
registry.clone(),
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
None,
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
expect_patches(
|
||||
&deserialized_context,
|
||||
"
|
||||
|
||||
one
|
||||
two
|
||||
|
||||
«<patch>
|
||||
<edit>
|
||||
<description>add a `two` function</description>
|
||||
<path>src/lib.rs</path>
|
||||
<operation>insert_after</operation>
|
||||
<old_text>fn zero</old_text>
|
||||
<new_text>
|
||||
fn two() {}
|
||||
</new_text>
|
||||
</edit>
|
||||
</patch>
|
||||
»
|
||||
also,",
|
||||
&[&[AssistantEdit {
|
||||
path: "src/lib.rs".into(),
|
||||
kind: AssistantEditKind::InsertAfter {
|
||||
old_text: "fn zero".into(),
|
||||
new_text: "fn two() {}".into(),
|
||||
description: Some("add a `two` function".into()),
|
||||
},
|
||||
}]],
|
||||
cx,
|
||||
);
|
||||
|
||||
fn edit(
|
||||
context: &Entity<AssistantContext>,
|
||||
new_text_marked_with_edits: &str,
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
context.update(cx, |context, cx| {
|
||||
context.buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit_via_marked_text(&new_text_marked_with_edits.unindent(), None, cx);
|
||||
});
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn expect_patches(
|
||||
context: &Entity<AssistantContext>,
|
||||
expected_marked_text: &str,
|
||||
expected_suggestions: &[&[AssistantEdit]],
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
let expected_marked_text = expected_marked_text.unindent();
|
||||
let (expected_text, _) = marked_text_ranges(&expected_marked_text, false);
|
||||
|
||||
let (buffer_text, ranges, patches) = context.update(cx, |context, cx| {
|
||||
context.buffer.read_with(cx, |buffer, _| {
|
||||
let ranges = context
|
||||
.patches
|
||||
.iter()
|
||||
.map(|entry| entry.range.to_offset(buffer))
|
||||
.collect::<Vec<_>>();
|
||||
(
|
||||
buffer.text(),
|
||||
ranges,
|
||||
context
|
||||
.patches
|
||||
.iter()
|
||||
.map(|step| step.edits.clone())
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
})
|
||||
});
|
||||
|
||||
assert_eq!(buffer_text, expected_text);
|
||||
|
||||
let actual_marked_text = generate_marked_text(&expected_text, &ranges, false);
|
||||
assert_eq!(actual_marked_text, expected_marked_text);
|
||||
|
||||
assert_eq!(
|
||||
patches
|
||||
.iter()
|
||||
.map(|patch| {
|
||||
patch
|
||||
.iter()
|
||||
.map(|edit| {
|
||||
let edit = edit.as_ref().unwrap();
|
||||
AssistantEdit {
|
||||
path: edit.path.clone(),
|
||||
kind: edit.kind.clone(),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
expected_suggestions
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_serialization(cx: &mut TestAppContext) {
|
||||
cx.update(init_test);
|
||||
@@ -1575,6 +1180,187 @@ fn test_mark_cache_anchors(cx: &mut App) {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_summarization(cx: &mut TestAppContext) {
|
||||
let (context, fake_model) = setup_context_editor_with_fake_model(cx);
|
||||
|
||||
// Initial state should be pending
|
||||
context.read_with(cx, |context, _| {
|
||||
assert!(matches!(context.summary(), ContextSummary::Pending));
|
||||
assert_eq!(context.summary().or_default(), ContextSummary::DEFAULT);
|
||||
});
|
||||
|
||||
let message_1 = context.read_with(cx, |context, _cx| context.message_anchors[0].clone());
|
||||
context.update(cx, |context, cx| {
|
||||
context
|
||||
.insert_message_after(message_1.id, Role::Assistant, MessageStatus::Done, cx)
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
// Send a message
|
||||
context.update(cx, |context, cx| {
|
||||
context.assist(cx);
|
||||
});
|
||||
|
||||
simulate_successful_response(&fake_model, cx);
|
||||
|
||||
// Should start generating summary when there are >= 2 messages
|
||||
context.read_with(cx, |context, _| {
|
||||
assert!(!context.summary().content().unwrap().done);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
fake_model.stream_last_completion_response("Brief".into());
|
||||
fake_model.stream_last_completion_response(" Introduction".into());
|
||||
fake_model.end_last_completion_stream();
|
||||
cx.run_until_parked();
|
||||
|
||||
// Summary should be set
|
||||
context.read_with(cx, |context, _| {
|
||||
assert_eq!(context.summary().or_default(), "Brief Introduction");
|
||||
});
|
||||
|
||||
// We should be able to manually set a summary
|
||||
context.update(cx, |context, cx| {
|
||||
context.set_custom_summary("Brief Intro".into(), cx);
|
||||
});
|
||||
|
||||
context.read_with(cx, |context, _| {
|
||||
assert_eq!(context.summary().or_default(), "Brief Intro");
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_thread_summary_error_set_manually(cx: &mut TestAppContext) {
|
||||
let (context, fake_model) = setup_context_editor_with_fake_model(cx);
|
||||
|
||||
test_summarize_error(&fake_model, &context, cx);
|
||||
|
||||
// Now we should be able to set a summary
|
||||
context.update(cx, |context, cx| {
|
||||
context.set_custom_summary("Brief Intro".into(), cx);
|
||||
});
|
||||
|
||||
context.read_with(cx, |context, _| {
|
||||
assert_eq!(context.summary().or_default(), "Brief Intro");
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_thread_summary_error_retry(cx: &mut TestAppContext) {
|
||||
let (context, fake_model) = setup_context_editor_with_fake_model(cx);
|
||||
|
||||
test_summarize_error(&fake_model, &context, cx);
|
||||
|
||||
// Sending another message should not trigger another summarize request
|
||||
context.update(cx, |context, cx| {
|
||||
context.assist(cx);
|
||||
});
|
||||
|
||||
simulate_successful_response(&fake_model, cx);
|
||||
|
||||
context.read_with(cx, |context, _| {
|
||||
// State is still Error, not Generating
|
||||
assert!(matches!(context.summary(), ContextSummary::Error));
|
||||
});
|
||||
|
||||
// But the summarize request can be invoked manually
|
||||
context.update(cx, |context, cx| {
|
||||
context.summarize(true, cx);
|
||||
});
|
||||
|
||||
context.read_with(cx, |context, _| {
|
||||
assert!(!context.summary().content().unwrap().done);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
fake_model.stream_last_completion_response("A successful summary".into());
|
||||
fake_model.end_last_completion_stream();
|
||||
cx.run_until_parked();
|
||||
|
||||
context.read_with(cx, |context, _| {
|
||||
assert_eq!(context.summary().or_default(), "A successful summary");
|
||||
});
|
||||
}
|
||||
|
||||
fn test_summarize_error(
|
||||
model: &Arc<FakeLanguageModel>,
|
||||
context: &Entity<AssistantContext>,
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
let message_1 = context.read_with(cx, |context, _cx| context.message_anchors[0].clone());
|
||||
context.update(cx, |context, cx| {
|
||||
context
|
||||
.insert_message_after(message_1.id, Role::Assistant, MessageStatus::Done, cx)
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
// Send a message
|
||||
context.update(cx, |context, cx| {
|
||||
context.assist(cx);
|
||||
});
|
||||
|
||||
simulate_successful_response(&model, cx);
|
||||
|
||||
context.read_with(cx, |context, _| {
|
||||
assert!(!context.summary().content().unwrap().done);
|
||||
});
|
||||
|
||||
// Simulate summary request ending
|
||||
cx.run_until_parked();
|
||||
model.end_last_completion_stream();
|
||||
cx.run_until_parked();
|
||||
|
||||
// State is set to Error and default message
|
||||
context.read_with(cx, |context, _| {
|
||||
assert_eq!(*context.summary(), ContextSummary::Error);
|
||||
assert_eq!(context.summary().or_default(), ContextSummary::DEFAULT);
|
||||
});
|
||||
}
|
||||
|
||||
fn setup_context_editor_with_fake_model(
|
||||
cx: &mut TestAppContext,
|
||||
) -> (Entity<AssistantContext>, Arc<FakeLanguageModel>) {
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.executor().clone()));
|
||||
|
||||
let fake_provider = Arc::new(FakeLanguageModelProvider);
|
||||
let fake_model = Arc::new(fake_provider.test_model());
|
||||
|
||||
cx.update(|cx| {
|
||||
init_test(cx);
|
||||
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
|
||||
registry.set_default_model(
|
||||
Some(ConfiguredModel {
|
||||
provider: fake_provider.clone(),
|
||||
model: fake_model.clone(),
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
});
|
||||
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let context = cx.new(|cx| {
|
||||
AssistantContext::local(
|
||||
registry,
|
||||
None,
|
||||
None,
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
(context, fake_model)
|
||||
}
|
||||
|
||||
fn simulate_successful_response(fake_model: &FakeLanguageModel, cx: &mut TestAppContext) {
|
||||
cx.run_until_parked();
|
||||
fake_model.stream_last_completion_response("Assistant response".into());
|
||||
fake_model.end_last_completion_stream();
|
||||
cx.run_until_parked();
|
||||
}
|
||||
|
||||
fn messages(context: &Entity<AssistantContext>, cx: &App) -> Vec<(MessageId, Role, Range<usize>)> {
|
||||
context
|
||||
.read(cx)
|
||||
@@ -1600,7 +1386,7 @@ fn init_test(cx: &mut App) {
|
||||
LanguageModelRegistry::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
language::init(cx);
|
||||
assistant_settings::init(cx);
|
||||
agent_settings::init(cx);
|
||||
Project::init_settings(cx);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::{
|
||||
AssistantContext, ContextEvent, ContextId, ContextOperation, ContextVersion, SavedContext,
|
||||
SavedContextMetadata,
|
||||
};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use anyhow::{Context as _, Result};
|
||||
use assistant_slash_command::{SlashCommandId, SlashCommandWorkingSet};
|
||||
use client::{Client, TypedEnvelope, proto, telemetry::Telemetry};
|
||||
use clock::ReplicaId;
|
||||
@@ -164,16 +164,18 @@ impl ContextStore {
|
||||
) -> Result<proto::OpenContextResponse> {
|
||||
let context_id = ContextId::from_proto(envelope.payload.context_id);
|
||||
let operations = this.update(&mut cx, |this, cx| {
|
||||
if this.project.read(cx).is_via_collab() {
|
||||
return Err(anyhow!("only the host contexts can be opened"));
|
||||
}
|
||||
anyhow::ensure!(
|
||||
!this.project.read(cx).is_via_collab(),
|
||||
"only the host contexts can be opened"
|
||||
);
|
||||
|
||||
let context = this
|
||||
.loaded_context_for_id(&context_id, cx)
|
||||
.context("context not found")?;
|
||||
if context.read(cx).replica_id() != ReplicaId::default() {
|
||||
return Err(anyhow!("context must be opened via the host"));
|
||||
}
|
||||
anyhow::ensure!(
|
||||
context.read(cx).replica_id() == ReplicaId::default(),
|
||||
"context must be opened via the host"
|
||||
);
|
||||
|
||||
anyhow::Ok(
|
||||
context
|
||||
@@ -193,9 +195,10 @@ impl ContextStore {
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<proto::CreateContextResponse> {
|
||||
let (context_id, operations) = this.update(&mut cx, |this, cx| {
|
||||
if this.project.read(cx).is_via_collab() {
|
||||
return Err(anyhow!("can only create contexts as the host"));
|
||||
}
|
||||
anyhow::ensure!(
|
||||
!this.project.read(cx).is_via_collab(),
|
||||
"can only create contexts as the host"
|
||||
);
|
||||
|
||||
let context = this.create(cx);
|
||||
let context_id = context.read(cx).id().clone();
|
||||
@@ -237,9 +240,10 @@ impl ContextStore {
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<proto::SynchronizeContextsResponse> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if this.project.read(cx).is_via_collab() {
|
||||
return Err(anyhow!("only the host can synchronize contexts"));
|
||||
}
|
||||
anyhow::ensure!(
|
||||
!this.project.read(cx).is_via_collab(),
|
||||
"only the host can synchronize contexts"
|
||||
);
|
||||
|
||||
let mut local_versions = Vec::new();
|
||||
for remote_version_proto in envelope.payload.contexts {
|
||||
@@ -370,7 +374,7 @@ impl ContextStore {
|
||||
) -> Task<Result<Entity<AssistantContext>>> {
|
||||
let project = self.project.read(cx);
|
||||
let Some(project_id) = project.remote_id() else {
|
||||
return Task::ready(Err(anyhow!("project was not remote")));
|
||||
return Task::ready(Err(anyhow::anyhow!("project was not remote")));
|
||||
};
|
||||
|
||||
let replica_id = project.replica_id();
|
||||
@@ -533,7 +537,7 @@ impl ContextStore {
|
||||
) -> Task<Result<Entity<AssistantContext>>> {
|
||||
let project = self.project.read(cx);
|
||||
let Some(project_id) = project.remote_id() else {
|
||||
return Task::ready(Err(anyhow!("project was not remote")));
|
||||
return Task::ready(Err(anyhow::anyhow!("project was not remote")));
|
||||
};
|
||||
|
||||
if let Some(context) = self.loaded_context_for_id(&context_id, cx) {
|
||||
@@ -648,7 +652,10 @@ impl ContextStore {
|
||||
if context.replica_id() == ReplicaId::default() {
|
||||
Some(proto::ContextMetadata {
|
||||
context_id: context.id().to_proto(),
|
||||
summary: context.summary().map(|summary| summary.text.clone()),
|
||||
summary: context
|
||||
.summary()
|
||||
.content()
|
||||
.map(|summary| summary.text.clone()),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
use std::sync::Arc;
|
||||
use std::{cmp::Reverse, sync::Arc};
|
||||
|
||||
use collections::{HashSet, IndexMap};
|
||||
use feature_flags::ZedProFeatureFlag;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
|
||||
use gpui::{
|
||||
Action, AnyElement, AnyView, App, Corner, DismissEvent, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, Subscription, Task, WeakEntity, action_with_deprecated_aliases,
|
||||
Action, AnyElement, AnyView, App, BackgroundExecutor, Corner, DismissEvent, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity,
|
||||
action_with_deprecated_aliases,
|
||||
};
|
||||
use language_model::{
|
||||
AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelProviderId,
|
||||
LanguageModelRegistry,
|
||||
};
|
||||
use ordered_float::OrderedFloat;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use proto::Plan;
|
||||
use ui::{ListItem, ListItemSpacing, PopoverMenu, PopoverMenuHandle, PopoverTrigger, prelude::*};
|
||||
@@ -322,6 +325,29 @@ struct GroupedModels {
|
||||
}
|
||||
|
||||
impl GroupedModels {
|
||||
pub fn new(other: Vec<ModelInfo>, recommended: Vec<ModelInfo>) -> Self {
|
||||
let recommended_ids: HashSet<_> = recommended.iter().map(|info| info.model.id()).collect();
|
||||
|
||||
let mut other_by_provider: IndexMap<_, Vec<ModelInfo>> = IndexMap::default();
|
||||
for model in other {
|
||||
if recommended_ids.contains(&model.model.id()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let provider = model.model.provider_id();
|
||||
if let Some(models) = other_by_provider.get_mut(&provider) {
|
||||
models.push(model);
|
||||
} else {
|
||||
other_by_provider.insert(provider, vec![model]);
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
recommended,
|
||||
other: other_by_provider,
|
||||
}
|
||||
}
|
||||
|
||||
fn entries(&self) -> Vec<LanguageModelPickerEntry> {
|
||||
let mut entries = Vec::new();
|
||||
|
||||
@@ -349,6 +375,20 @@ impl GroupedModels {
|
||||
}
|
||||
entries
|
||||
}
|
||||
|
||||
fn model_infos(&self) -> Vec<ModelInfo> {
|
||||
let other = self
|
||||
.other
|
||||
.values()
|
||||
.flat_map(|model| model.iter())
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
self.recommended
|
||||
.iter()
|
||||
.chain(&other)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
}
|
||||
|
||||
enum LanguageModelPickerEntry {
|
||||
@@ -356,6 +396,78 @@ enum LanguageModelPickerEntry {
|
||||
Separator(SharedString),
|
||||
}
|
||||
|
||||
struct ModelMatcher {
|
||||
models: Vec<ModelInfo>,
|
||||
bg_executor: BackgroundExecutor,
|
||||
candidates: Vec<StringMatchCandidate>,
|
||||
}
|
||||
|
||||
impl ModelMatcher {
|
||||
fn new(models: Vec<ModelInfo>, bg_executor: BackgroundExecutor) -> ModelMatcher {
|
||||
let candidates = Self::make_match_candidates(&models);
|
||||
Self {
|
||||
models,
|
||||
bg_executor,
|
||||
candidates,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fuzzy_search(&self, query: &str) -> Vec<ModelInfo> {
|
||||
let mut matches = self.bg_executor.block(match_strings(
|
||||
&self.candidates,
|
||||
&query,
|
||||
false,
|
||||
100,
|
||||
&Default::default(),
|
||||
self.bg_executor.clone(),
|
||||
));
|
||||
|
||||
let sorting_key = |mat: &StringMatch| {
|
||||
let candidate = &self.candidates[mat.candidate_id];
|
||||
(Reverse(OrderedFloat(mat.score)), candidate.id)
|
||||
};
|
||||
matches.sort_unstable_by_key(sorting_key);
|
||||
|
||||
let matched_models: Vec<_> = matches
|
||||
.into_iter()
|
||||
.map(|mat| self.models[mat.candidate_id].clone())
|
||||
.collect();
|
||||
|
||||
matched_models
|
||||
}
|
||||
|
||||
pub fn exact_search(&self, query: &str) -> Vec<ModelInfo> {
|
||||
self.models
|
||||
.iter()
|
||||
.filter(|m| {
|
||||
m.model
|
||||
.name()
|
||||
.0
|
||||
.to_lowercase()
|
||||
.contains(&query.to_lowercase())
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
fn make_match_candidates(model_infos: &Vec<ModelInfo>) -> Vec<StringMatchCandidate> {
|
||||
model_infos
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, model)| {
|
||||
StringMatchCandidate::new(
|
||||
index,
|
||||
&format!(
|
||||
"{}/{}",
|
||||
&model.model.provider_name().0,
|
||||
&model.model.name().0
|
||||
),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
type ListItem = AnyElement;
|
||||
|
||||
@@ -396,56 +508,45 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
) -> Task<()> {
|
||||
let all_models = self.all_models.clone();
|
||||
let current_index = self.selected_index;
|
||||
let bg_executor = cx.background_executor();
|
||||
|
||||
let language_model_registry = LanguageModelRegistry::global(cx);
|
||||
|
||||
let configured_providers = language_model_registry
|
||||
.read(cx)
|
||||
.providers()
|
||||
.iter()
|
||||
.into_iter()
|
||||
.filter(|provider| provider.is_authenticated(cx))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let configured_provider_ids = configured_providers
|
||||
.iter()
|
||||
.map(|provider| provider.id())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let recommended_models = all_models
|
||||
.recommended
|
||||
.iter()
|
||||
.filter(|m| configured_provider_ids.contains(&m.model.provider_id()))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let available_models = all_models
|
||||
.model_infos()
|
||||
.iter()
|
||||
.filter(|m| configured_provider_ids.contains(&m.model.provider_id()))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let matcher_rec = ModelMatcher::new(recommended_models, bg_executor.clone());
|
||||
let matcher_all = ModelMatcher::new(available_models, bg_executor.clone());
|
||||
|
||||
let recommended = matcher_rec.exact_search(&query);
|
||||
let all = matcher_all.fuzzy_search(&query);
|
||||
|
||||
let filtered_models = GroupedModels::new(all, recommended);
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let filtered_models = cx
|
||||
.background_spawn(async move {
|
||||
let matches = |info: &ModelInfo| {
|
||||
info.model
|
||||
.name()
|
||||
.0
|
||||
.to_lowercase()
|
||||
.contains(&query.to_lowercase())
|
||||
};
|
||||
|
||||
let recommended_models = all_models
|
||||
.recommended
|
||||
.iter()
|
||||
.filter(|r| {
|
||||
configured_providers.contains(&r.model.provider_id()) && matches(r)
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
let mut other_models = IndexMap::default();
|
||||
for (provider_id, models) in &all_models.other {
|
||||
if configured_providers.contains(&provider_id) {
|
||||
other_models.insert(
|
||||
provider_id.clone(),
|
||||
models
|
||||
.iter()
|
||||
.filter(|m| matches(m))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
}
|
||||
GroupedModels {
|
||||
recommended: recommended_models,
|
||||
other: other_models,
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.delegate.filtered_entries = filtered_models.entries();
|
||||
// Preserve selection focus
|
||||
@@ -607,3 +708,213 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use futures::{future::BoxFuture, stream::BoxStream};
|
||||
use gpui::{AsyncApp, TestAppContext, http_client};
|
||||
use language_model::{
|
||||
LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId,
|
||||
LanguageModelName, LanguageModelProviderId, LanguageModelProviderName,
|
||||
LanguageModelRequest, LanguageModelToolChoice,
|
||||
};
|
||||
use ui::IconName;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TestLanguageModel {
|
||||
name: LanguageModelName,
|
||||
id: LanguageModelId,
|
||||
provider_id: LanguageModelProviderId,
|
||||
provider_name: LanguageModelProviderName,
|
||||
}
|
||||
|
||||
impl TestLanguageModel {
|
||||
fn new(name: &str, provider: &str) -> Self {
|
||||
Self {
|
||||
name: LanguageModelName::from(name.to_string()),
|
||||
id: LanguageModelId::from(name.to_string()),
|
||||
provider_id: LanguageModelProviderId::from(provider.to_string()),
|
||||
provider_name: LanguageModelProviderName::from(provider.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LanguageModel for TestLanguageModel {
|
||||
fn id(&self) -> LanguageModelId {
|
||||
self.id.clone()
|
||||
}
|
||||
|
||||
fn name(&self) -> LanguageModelName {
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
fn provider_id(&self) -> LanguageModelProviderId {
|
||||
self.provider_id.clone()
|
||||
}
|
||||
|
||||
fn provider_name(&self) -> LanguageModelProviderName {
|
||||
self.provider_name.clone()
|
||||
}
|
||||
|
||||
fn supports_tools(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn supports_tool_choice(&self, _choice: LanguageModelToolChoice) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn supports_images(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn telemetry_id(&self) -> String {
|
||||
format!("{}/{}", self.provider_id.0, self.name.0)
|
||||
}
|
||||
|
||||
fn max_token_count(&self) -> usize {
|
||||
1000
|
||||
}
|
||||
|
||||
fn count_tokens(
|
||||
&self,
|
||||
_: LanguageModelRequest,
|
||||
_: &App,
|
||||
) -> BoxFuture<'static, http_client::Result<usize>> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn stream_completion(
|
||||
&self,
|
||||
_: LanguageModelRequest,
|
||||
_: &AsyncApp,
|
||||
) -> BoxFuture<
|
||||
'static,
|
||||
http_client::Result<
|
||||
BoxStream<
|
||||
'static,
|
||||
http_client::Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
|
||||
>,
|
||||
>,
|
||||
> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
fn create_models(model_specs: Vec<(&str, &str)>) -> Vec<ModelInfo> {
|
||||
model_specs
|
||||
.into_iter()
|
||||
.map(|(provider, name)| ModelInfo {
|
||||
model: Arc::new(TestLanguageModel::new(name, provider)),
|
||||
icon: IconName::Ai,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn assert_models_eq(result: Vec<ModelInfo>, expected: Vec<&str>) {
|
||||
assert_eq!(
|
||||
result.len(),
|
||||
expected.len(),
|
||||
"Number of models doesn't match"
|
||||
);
|
||||
|
||||
for (i, expected_name) in expected.iter().enumerate() {
|
||||
assert_eq!(
|
||||
result[i].model.telemetry_id(),
|
||||
*expected_name,
|
||||
"Model at position {} doesn't match expected model",
|
||||
i
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_exact_match(cx: &mut TestAppContext) {
|
||||
let models = create_models(vec![
|
||||
("zed", "Claude 3.7 Sonnet"),
|
||||
("zed", "Claude 3.7 Sonnet Thinking"),
|
||||
("zed", "gpt-4.1"),
|
||||
("zed", "gpt-4.1-nano"),
|
||||
("openai", "gpt-3.5-turbo"),
|
||||
("openai", "gpt-4.1"),
|
||||
("openai", "gpt-4.1-nano"),
|
||||
("ollama", "mistral"),
|
||||
("ollama", "deepseek"),
|
||||
]);
|
||||
let matcher = ModelMatcher::new(models, cx.background_executor.clone());
|
||||
|
||||
// The order of models should be maintained, case doesn't matter
|
||||
let results = matcher.exact_search("GPT-4.1");
|
||||
assert_models_eq(
|
||||
results,
|
||||
vec![
|
||||
"zed/gpt-4.1",
|
||||
"zed/gpt-4.1-nano",
|
||||
"openai/gpt-4.1",
|
||||
"openai/gpt-4.1-nano",
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_fuzzy_match(cx: &mut TestAppContext) {
|
||||
let models = create_models(vec![
|
||||
("zed", "Claude 3.7 Sonnet"),
|
||||
("zed", "Claude 3.7 Sonnet Thinking"),
|
||||
("zed", "gpt-4.1"),
|
||||
("zed", "gpt-4.1-nano"),
|
||||
("openai", "gpt-3.5-turbo"),
|
||||
("openai", "gpt-4.1"),
|
||||
("openai", "gpt-4.1-nano"),
|
||||
("ollama", "mistral"),
|
||||
("ollama", "deepseek"),
|
||||
]);
|
||||
let matcher = ModelMatcher::new(models, cx.background_executor.clone());
|
||||
|
||||
// Results should preserve models order whenever possible.
|
||||
// In the case below, `zed/gpt-4.1` and `openai/gpt-4.1` have identical
|
||||
// similarity scores, but `zed/gpt-4.1` was higher in the models list,
|
||||
// so it should appear first in the results.
|
||||
let results = matcher.fuzzy_search("41");
|
||||
assert_models_eq(
|
||||
results,
|
||||
vec![
|
||||
"zed/gpt-4.1",
|
||||
"openai/gpt-4.1",
|
||||
"zed/gpt-4.1-nano",
|
||||
"openai/gpt-4.1-nano",
|
||||
],
|
||||
);
|
||||
|
||||
// Model provider should be searchable as well
|
||||
let results = matcher.fuzzy_search("ol"); // meaning "ollama"
|
||||
assert_models_eq(results, vec!["ollama/mistral", "ollama/deepseek"]);
|
||||
|
||||
// Fuzzy search
|
||||
let results = matcher.fuzzy_search("z4n");
|
||||
assert_models_eq(results, vec!["zed/gpt-4.1-nano"]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_exclude_recommended_models(_cx: &mut TestAppContext) {
|
||||
let recommended_models = create_models(vec![("zed", "claude")]);
|
||||
let all_models = create_models(vec![
|
||||
("zed", "claude"), // Should be filtered out from "other"
|
||||
("zed", "gemini"),
|
||||
("copilot", "o3"),
|
||||
]);
|
||||
|
||||
let grouped_models = GroupedModels::new(all_models, recommended_models);
|
||||
|
||||
let actual_other_models = grouped_models
|
||||
.other
|
||||
.values()
|
||||
.flatten()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Recommended models should not appear in "other"
|
||||
assert_models_eq(actual_other_models, vec!["zed/gemini", "copilot/o3"]);
|
||||
}
|
||||
}
|
||||
@@ -1,957 +0,0 @@
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use collections::HashMap;
|
||||
use editor::ProposedChangesEditor;
|
||||
use futures::{TryFutureExt as _, future};
|
||||
use gpui::{App, AppContext as _, AsyncApp, Entity, SharedString};
|
||||
use language::{AutoindentMode, Buffer, BufferSnapshot};
|
||||
use project::{Project, ProjectPath};
|
||||
use std::{cmp, ops::Range, path::Path, sync::Arc};
|
||||
use text::{AnchorRangeExt as _, Bias, OffsetRangeExt as _, Point};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AssistantPatch {
|
||||
pub range: Range<language::Anchor>,
|
||||
pub title: SharedString,
|
||||
pub edits: Arc<[Result<AssistantEdit>]>,
|
||||
pub status: AssistantPatchStatus,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum AssistantPatchStatus {
|
||||
Pending,
|
||||
Ready,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct AssistantEdit {
|
||||
pub path: String,
|
||||
pub kind: AssistantEditKind,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum AssistantEditKind {
|
||||
Update {
|
||||
old_text: String,
|
||||
new_text: String,
|
||||
description: Option<String>,
|
||||
},
|
||||
Create {
|
||||
new_text: String,
|
||||
description: Option<String>,
|
||||
},
|
||||
InsertBefore {
|
||||
old_text: String,
|
||||
new_text: String,
|
||||
description: Option<String>,
|
||||
},
|
||||
InsertAfter {
|
||||
old_text: String,
|
||||
new_text: String,
|
||||
description: Option<String>,
|
||||
},
|
||||
Delete {
|
||||
old_text: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ResolvedPatch {
|
||||
pub edit_groups: HashMap<Entity<Buffer>, Vec<ResolvedEditGroup>>,
|
||||
pub errors: Vec<AssistantPatchResolutionError>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ResolvedEditGroup {
|
||||
pub context_range: Range<language::Anchor>,
|
||||
pub edits: Vec<ResolvedEdit>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ResolvedEdit {
|
||||
range: Range<language::Anchor>,
|
||||
new_text: String,
|
||||
description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct AssistantPatchResolutionError {
|
||||
pub edit_ix: usize,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
enum SearchDirection {
|
||||
Up,
|
||||
Left,
|
||||
Diagonal,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
struct SearchState {
|
||||
cost: u32,
|
||||
direction: SearchDirection,
|
||||
}
|
||||
|
||||
impl SearchState {
|
||||
fn new(cost: u32, direction: SearchDirection) -> Self {
|
||||
Self { cost, direction }
|
||||
}
|
||||
}
|
||||
|
||||
struct SearchMatrix {
|
||||
cols: usize,
|
||||
data: Vec<SearchState>,
|
||||
}
|
||||
|
||||
impl SearchMatrix {
|
||||
fn new(rows: usize, cols: usize) -> Self {
|
||||
SearchMatrix {
|
||||
cols,
|
||||
data: vec![SearchState::new(0, SearchDirection::Diagonal); rows * cols],
|
||||
}
|
||||
}
|
||||
|
||||
fn get(&self, row: usize, col: usize) -> SearchState {
|
||||
self.data[row * self.cols + col]
|
||||
}
|
||||
|
||||
fn set(&mut self, row: usize, col: usize, cost: SearchState) {
|
||||
self.data[row * self.cols + col] = cost;
|
||||
}
|
||||
}
|
||||
|
||||
impl ResolvedPatch {
|
||||
pub fn apply(&self, editor: &ProposedChangesEditor, cx: &mut App) {
|
||||
for (buffer, groups) in &self.edit_groups {
|
||||
let branch = editor.branch_buffer_for_base(buffer).unwrap();
|
||||
Self::apply_edit_groups(groups, &branch, cx);
|
||||
}
|
||||
editor.recalculate_all_buffer_diffs();
|
||||
}
|
||||
|
||||
fn apply_edit_groups(groups: &Vec<ResolvedEditGroup>, buffer: &Entity<Buffer>, cx: &mut App) {
|
||||
let mut edits = Vec::new();
|
||||
for group in groups {
|
||||
for suggestion in &group.edits {
|
||||
edits.push((suggestion.range.clone(), suggestion.new_text.clone()));
|
||||
}
|
||||
}
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(
|
||||
edits,
|
||||
Some(AutoindentMode::Block {
|
||||
original_indent_columns: Vec::new(),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl ResolvedEdit {
|
||||
pub fn try_merge(&mut self, other: &Self, buffer: &text::BufferSnapshot) -> bool {
|
||||
let range = &self.range;
|
||||
let other_range = &other.range;
|
||||
|
||||
// Don't merge if we don't contain the other suggestion.
|
||||
if range.start.cmp(&other_range.start, buffer).is_gt()
|
||||
|| range.end.cmp(&other_range.end, buffer).is_lt()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
let other_offset_range = other_range.to_offset(buffer);
|
||||
let offset_range = range.to_offset(buffer);
|
||||
|
||||
// If the other range is empty at the start of this edit's range, combine the new text
|
||||
if other_offset_range.is_empty() && other_offset_range.start == offset_range.start {
|
||||
self.new_text = format!("{}\n{}", other.new_text, self.new_text);
|
||||
self.range.start = other_range.start;
|
||||
|
||||
if let Some((description, other_description)) =
|
||||
self.description.as_mut().zip(other.description.as_ref())
|
||||
{
|
||||
*description = format!("{}\n{}", other_description, description)
|
||||
}
|
||||
} else {
|
||||
if let Some((description, other_description)) =
|
||||
self.description.as_mut().zip(other.description.as_ref())
|
||||
{
|
||||
description.push('\n');
|
||||
description.push_str(other_description);
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl AssistantEdit {
|
||||
pub fn new(
|
||||
path: Option<String>,
|
||||
operation: Option<String>,
|
||||
old_text: Option<String>,
|
||||
new_text: Option<String>,
|
||||
description: Option<String>,
|
||||
) -> Result<Self> {
|
||||
let path = path.ok_or_else(|| anyhow!("missing path"))?;
|
||||
let operation = operation.ok_or_else(|| anyhow!("missing operation"))?;
|
||||
|
||||
let kind = match operation.as_str() {
|
||||
"update" => AssistantEditKind::Update {
|
||||
old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,
|
||||
new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,
|
||||
description,
|
||||
},
|
||||
"insert_before" => AssistantEditKind::InsertBefore {
|
||||
old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,
|
||||
new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,
|
||||
description,
|
||||
},
|
||||
"insert_after" => AssistantEditKind::InsertAfter {
|
||||
old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,
|
||||
new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,
|
||||
description,
|
||||
},
|
||||
"delete" => AssistantEditKind::Delete {
|
||||
old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,
|
||||
},
|
||||
"create" => AssistantEditKind::Create {
|
||||
description,
|
||||
new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,
|
||||
},
|
||||
_ => Err(anyhow!("unknown operation {operation:?}"))?,
|
||||
};
|
||||
|
||||
Ok(Self { path, kind })
|
||||
}
|
||||
|
||||
pub async fn resolve(
|
||||
&self,
|
||||
project: Entity<Project>,
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<(Entity<Buffer>, ResolvedEdit)> {
|
||||
let path = self.path.clone();
|
||||
let kind = self.kind.clone();
|
||||
let buffer = project
|
||||
.update(&mut cx, |project, cx| {
|
||||
let project_path = project
|
||||
.find_project_path(Path::new(&path), cx)
|
||||
.or_else(|| {
|
||||
// If we couldn't find a project path for it, put it in the active worktree
|
||||
// so that when we create the buffer, it can be saved.
|
||||
let worktree = project
|
||||
.active_entry()
|
||||
.and_then(|entry_id| project.worktree_for_entry(entry_id, cx))
|
||||
.or_else(|| project.worktrees(cx).next())?;
|
||||
let worktree = worktree.read(cx);
|
||||
|
||||
Some(ProjectPath {
|
||||
worktree_id: worktree.id(),
|
||||
path: Arc::from(Path::new(&path)),
|
||||
})
|
||||
})
|
||||
.with_context(|| format!("worktree not found for {:?}", path))?;
|
||||
anyhow::Ok(project.open_buffer(project_path, cx))
|
||||
})??
|
||||
.await?;
|
||||
|
||||
let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?;
|
||||
let suggestion = cx
|
||||
.background_spawn(async move { kind.resolve(&snapshot) })
|
||||
.await;
|
||||
|
||||
Ok((buffer, suggestion))
|
||||
}
|
||||
}
|
||||
|
||||
impl AssistantEditKind {
|
||||
fn resolve(self, snapshot: &BufferSnapshot) -> ResolvedEdit {
|
||||
match self {
|
||||
Self::Update {
|
||||
old_text,
|
||||
new_text,
|
||||
description,
|
||||
} => {
|
||||
let range = Self::resolve_location(&snapshot, &old_text);
|
||||
ResolvedEdit {
|
||||
range,
|
||||
new_text,
|
||||
description,
|
||||
}
|
||||
}
|
||||
Self::Create {
|
||||
new_text,
|
||||
description,
|
||||
} => ResolvedEdit {
|
||||
range: text::Anchor::MIN..text::Anchor::MAX,
|
||||
description,
|
||||
new_text,
|
||||
},
|
||||
Self::InsertBefore {
|
||||
old_text,
|
||||
mut new_text,
|
||||
description,
|
||||
} => {
|
||||
let range = Self::resolve_location(&snapshot, &old_text);
|
||||
new_text.push('\n');
|
||||
ResolvedEdit {
|
||||
range: range.start..range.start,
|
||||
new_text,
|
||||
description,
|
||||
}
|
||||
}
|
||||
Self::InsertAfter {
|
||||
old_text,
|
||||
mut new_text,
|
||||
description,
|
||||
} => {
|
||||
let range = Self::resolve_location(&snapshot, &old_text);
|
||||
new_text.insert(0, '\n');
|
||||
ResolvedEdit {
|
||||
range: range.end..range.end,
|
||||
new_text,
|
||||
description,
|
||||
}
|
||||
}
|
||||
Self::Delete { old_text } => {
|
||||
let range = Self::resolve_location(&snapshot, &old_text);
|
||||
ResolvedEdit {
|
||||
range,
|
||||
new_text: String::new(),
|
||||
description: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_location(buffer: &text::BufferSnapshot, search_query: &str) -> Range<text::Anchor> {
|
||||
const INSERTION_COST: u32 = 3;
|
||||
const DELETION_COST: u32 = 10;
|
||||
const WHITESPACE_INSERTION_COST: u32 = 1;
|
||||
const WHITESPACE_DELETION_COST: u32 = 1;
|
||||
|
||||
let buffer_len = buffer.len();
|
||||
let query_len = search_query.len();
|
||||
let mut matrix = SearchMatrix::new(query_len + 1, buffer_len + 1);
|
||||
let mut leading_deletion_cost = 0_u32;
|
||||
for (row, query_byte) in search_query.bytes().enumerate() {
|
||||
let deletion_cost = if query_byte.is_ascii_whitespace() {
|
||||
WHITESPACE_DELETION_COST
|
||||
} else {
|
||||
DELETION_COST
|
||||
};
|
||||
|
||||
leading_deletion_cost = leading_deletion_cost.saturating_add(deletion_cost);
|
||||
matrix.set(
|
||||
row + 1,
|
||||
0,
|
||||
SearchState::new(leading_deletion_cost, SearchDirection::Diagonal),
|
||||
);
|
||||
|
||||
for (col, buffer_byte) in buffer.bytes_in_range(0..buffer.len()).flatten().enumerate() {
|
||||
let insertion_cost = if buffer_byte.is_ascii_whitespace() {
|
||||
WHITESPACE_INSERTION_COST
|
||||
} else {
|
||||
INSERTION_COST
|
||||
};
|
||||
|
||||
let up = SearchState::new(
|
||||
matrix.get(row, col + 1).cost.saturating_add(deletion_cost),
|
||||
SearchDirection::Up,
|
||||
);
|
||||
let left = SearchState::new(
|
||||
matrix.get(row + 1, col).cost.saturating_add(insertion_cost),
|
||||
SearchDirection::Left,
|
||||
);
|
||||
let diagonal = SearchState::new(
|
||||
if query_byte == *buffer_byte {
|
||||
matrix.get(row, col).cost
|
||||
} else {
|
||||
matrix
|
||||
.get(row, col)
|
||||
.cost
|
||||
.saturating_add(deletion_cost + insertion_cost)
|
||||
},
|
||||
SearchDirection::Diagonal,
|
||||
);
|
||||
matrix.set(row + 1, col + 1, up.min(left).min(diagonal));
|
||||
}
|
||||
}
|
||||
|
||||
// Traceback to find the best match
|
||||
let mut best_buffer_end = buffer_len;
|
||||
let mut best_cost = u32::MAX;
|
||||
for col in 1..=buffer_len {
|
||||
let cost = matrix.get(query_len, col).cost;
|
||||
if cost < best_cost {
|
||||
best_cost = cost;
|
||||
best_buffer_end = col;
|
||||
}
|
||||
}
|
||||
|
||||
let mut query_ix = query_len;
|
||||
let mut buffer_ix = best_buffer_end;
|
||||
while query_ix > 0 && buffer_ix > 0 {
|
||||
let current = matrix.get(query_ix, buffer_ix);
|
||||
match current.direction {
|
||||
SearchDirection::Diagonal => {
|
||||
query_ix -= 1;
|
||||
buffer_ix -= 1;
|
||||
}
|
||||
SearchDirection::Up => {
|
||||
query_ix -= 1;
|
||||
}
|
||||
SearchDirection::Left => {
|
||||
buffer_ix -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut start = buffer.offset_to_point(buffer.clip_offset(buffer_ix, Bias::Left));
|
||||
start.column = 0;
|
||||
let mut end = buffer.offset_to_point(buffer.clip_offset(best_buffer_end, Bias::Right));
|
||||
if end.column > 0 {
|
||||
end.column = buffer.line_len(end.row);
|
||||
}
|
||||
|
||||
buffer.anchor_after(start)..buffer.anchor_before(end)
|
||||
}
|
||||
}
|
||||
|
||||
impl AssistantPatch {
|
||||
pub async fn resolve(&self, project: Entity<Project>, cx: &mut AsyncApp) -> ResolvedPatch {
|
||||
let mut resolve_tasks = Vec::new();
|
||||
for (ix, edit) in self.edits.iter().enumerate() {
|
||||
if let Ok(edit) = edit.as_ref() {
|
||||
resolve_tasks.push(
|
||||
edit.resolve(project.clone(), cx.clone())
|
||||
.map_err(move |error| (ix, error)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let edits = future::join_all(resolve_tasks).await;
|
||||
let mut errors = Vec::new();
|
||||
let mut edits_by_buffer = HashMap::default();
|
||||
for entry in edits {
|
||||
match entry {
|
||||
Ok((buffer, edit)) => {
|
||||
edits_by_buffer
|
||||
.entry(buffer)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(edit);
|
||||
}
|
||||
Err((edit_ix, error)) => errors.push(AssistantPatchResolutionError {
|
||||
edit_ix,
|
||||
message: error.to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
// Expand the context ranges of each edit and group edits with overlapping context ranges.
|
||||
let mut edit_groups_by_buffer = HashMap::default();
|
||||
for (buffer, edits) in edits_by_buffer {
|
||||
if let Ok(snapshot) = buffer.update(cx, |buffer, _| buffer.text_snapshot()) {
|
||||
edit_groups_by_buffer.insert(buffer, Self::group_edits(edits, &snapshot));
|
||||
}
|
||||
}
|
||||
|
||||
ResolvedPatch {
|
||||
edit_groups: edit_groups_by_buffer,
|
||||
errors,
|
||||
}
|
||||
}
|
||||
|
||||
fn group_edits(
|
||||
mut edits: Vec<ResolvedEdit>,
|
||||
snapshot: &text::BufferSnapshot,
|
||||
) -> Vec<ResolvedEditGroup> {
|
||||
let mut edit_groups = Vec::<ResolvedEditGroup>::new();
|
||||
// Sort edits by their range so that earlier, larger ranges come first
|
||||
edits.sort_by(|a, b| a.range.cmp(&b.range, &snapshot));
|
||||
|
||||
// Merge overlapping edits
|
||||
edits.dedup_by(|a, b| b.try_merge(a, &snapshot));
|
||||
|
||||
// Create context ranges for each edit
|
||||
for edit in edits {
|
||||
let context_range = {
|
||||
let edit_point_range = edit.range.to_point(&snapshot);
|
||||
let start_row = edit_point_range.start.row.saturating_sub(5);
|
||||
let end_row = cmp::min(edit_point_range.end.row + 5, snapshot.max_point().row);
|
||||
let start = snapshot.anchor_before(Point::new(start_row, 0));
|
||||
let end = snapshot.anchor_after(Point::new(end_row, snapshot.line_len(end_row)));
|
||||
start..end
|
||||
};
|
||||
|
||||
if let Some(last_group) = edit_groups.last_mut() {
|
||||
if last_group
|
||||
.context_range
|
||||
.end
|
||||
.cmp(&context_range.start, &snapshot)
|
||||
.is_ge()
|
||||
{
|
||||
// Merge with the previous group if context ranges overlap
|
||||
last_group.context_range.end = context_range.end;
|
||||
last_group.edits.push(edit);
|
||||
} else {
|
||||
// Create a new group
|
||||
edit_groups.push(ResolvedEditGroup {
|
||||
context_range,
|
||||
edits: vec![edit],
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Create the first group
|
||||
edit_groups.push(ResolvedEditGroup {
|
||||
context_range,
|
||||
edits: vec![edit],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
edit_groups
|
||||
}
|
||||
|
||||
pub fn path_count(&self) -> usize {
|
||||
self.paths().count()
|
||||
}
|
||||
|
||||
pub fn paths(&self) -> impl '_ + Iterator<Item = &str> {
|
||||
let mut prev_path = None;
|
||||
self.edits.iter().filter_map(move |edit| {
|
||||
if let Ok(edit) = edit {
|
||||
let path = Some(edit.path.as_str());
|
||||
if path != prev_path {
|
||||
prev_path = path;
|
||||
return path;
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for AssistantPatch {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.range == other.range
|
||||
&& self.title == other.title
|
||||
&& Arc::ptr_eq(&self.edits, &other.edits)
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for AssistantPatch {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::App;
|
||||
use language::{
|
||||
Language, LanguageConfig, LanguageMatcher, language_settings::AllLanguageSettings,
|
||||
};
|
||||
use settings::SettingsStore;
|
||||
use ui::BorrowAppContext;
|
||||
use unindent::Unindent as _;
|
||||
use util::test::{generate_marked_text, marked_text_ranges};
|
||||
|
||||
#[gpui::test]
|
||||
fn test_resolve_location(cx: &mut App) {
|
||||
assert_location_resolution(
|
||||
concat!(
|
||||
" Lorem\n",
|
||||
"« ipsum\n",
|
||||
" dolor sit amet»\n",
|
||||
" consecteur",
|
||||
),
|
||||
"ipsum\ndolor",
|
||||
cx,
|
||||
);
|
||||
|
||||
assert_location_resolution(
|
||||
&"
|
||||
«fn foo1(a: usize) -> usize {
|
||||
40
|
||||
}»
|
||||
|
||||
fn foo2(b: usize) -> usize {
|
||||
42
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
"fn foo1(b: usize) {\n40\n}",
|
||||
cx,
|
||||
);
|
||||
|
||||
assert_location_resolution(
|
||||
&"
|
||||
fn main() {
|
||||
« Foo
|
||||
.bar()
|
||||
.baz()
|
||||
.qux()»
|
||||
}
|
||||
|
||||
fn foo2(b: usize) -> usize {
|
||||
42
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
"Foo.bar.baz.qux()",
|
||||
cx,
|
||||
);
|
||||
|
||||
assert_location_resolution(
|
||||
&"
|
||||
class Something {
|
||||
one() { return 1; }
|
||||
« two() { return 2222; }
|
||||
three() { return 333; }
|
||||
four() { return 4444; }
|
||||
five() { return 5555; }
|
||||
six() { return 6666; }
|
||||
» seven() { return 7; }
|
||||
eight() { return 8; }
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
&"
|
||||
two() { return 2222; }
|
||||
four() { return 4444; }
|
||||
five() { return 5555; }
|
||||
six() { return 6666; }
|
||||
"
|
||||
.unindent(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_resolve_edits(cx: &mut App) {
|
||||
init_test(cx);
|
||||
|
||||
assert_edits(
|
||||
"
|
||||
/// A person
|
||||
struct Person {
|
||||
name: String,
|
||||
age: usize,
|
||||
}
|
||||
|
||||
/// A dog
|
||||
struct Dog {
|
||||
weight: f32,
|
||||
}
|
||||
|
||||
impl Person {
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
vec![
|
||||
AssistantEditKind::Update {
|
||||
old_text: "
|
||||
name: String,
|
||||
"
|
||||
.unindent(),
|
||||
new_text: "
|
||||
first_name: String,
|
||||
last_name: String,
|
||||
"
|
||||
.unindent(),
|
||||
description: None,
|
||||
},
|
||||
AssistantEditKind::Update {
|
||||
old_text: "
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
new_text: "
|
||||
fn name(&self) -> String {
|
||||
format!(\"{} {}\", self.first_name, self.last_name)
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
description: None,
|
||||
},
|
||||
],
|
||||
"
|
||||
/// A person
|
||||
struct Person {
|
||||
first_name: String,
|
||||
last_name: String,
|
||||
age: usize,
|
||||
}
|
||||
|
||||
/// A dog
|
||||
struct Dog {
|
||||
weight: f32,
|
||||
}
|
||||
|
||||
impl Person {
|
||||
fn name(&self) -> String {
|
||||
format!(\"{} {}\", self.first_name, self.last_name)
|
||||
}
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
cx,
|
||||
);
|
||||
|
||||
// Ensure InsertBefore merges correctly with Update of the same text
|
||||
assert_edits(
|
||||
"
|
||||
fn foo() {
|
||||
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
vec![
|
||||
AssistantEditKind::InsertBefore {
|
||||
old_text: "
|
||||
fn foo() {"
|
||||
.unindent(),
|
||||
new_text: "
|
||||
fn bar() {
|
||||
qux();
|
||||
}"
|
||||
.unindent(),
|
||||
description: Some("implement bar".into()),
|
||||
},
|
||||
AssistantEditKind::Update {
|
||||
old_text: "
|
||||
fn foo() {
|
||||
|
||||
}"
|
||||
.unindent(),
|
||||
new_text: "
|
||||
fn foo() {
|
||||
bar();
|
||||
}"
|
||||
.unindent(),
|
||||
description: Some("call bar in foo".into()),
|
||||
},
|
||||
AssistantEditKind::InsertAfter {
|
||||
old_text: "
|
||||
fn foo() {
|
||||
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
new_text: "
|
||||
fn qux() {
|
||||
// todo
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
description: Some("implement qux".into()),
|
||||
},
|
||||
],
|
||||
"
|
||||
fn bar() {
|
||||
qux();
|
||||
}
|
||||
|
||||
fn foo() {
|
||||
bar();
|
||||
}
|
||||
|
||||
fn qux() {
|
||||
// todo
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
cx,
|
||||
);
|
||||
|
||||
// Correctly indent new text when replacing multiple adjacent indented blocks.
|
||||
assert_edits(
|
||||
"
|
||||
impl Numbers {
|
||||
fn one() {
|
||||
1
|
||||
}
|
||||
|
||||
fn two() {
|
||||
2
|
||||
}
|
||||
|
||||
fn three() {
|
||||
3
|
||||
}
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
vec![
|
||||
AssistantEditKind::Update {
|
||||
old_text: "
|
||||
fn one() {
|
||||
1
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
new_text: "
|
||||
fn one() {
|
||||
101
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
description: None,
|
||||
},
|
||||
AssistantEditKind::Update {
|
||||
old_text: "
|
||||
fn two() {
|
||||
2
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
new_text: "
|
||||
fn two() {
|
||||
102
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
description: None,
|
||||
},
|
||||
AssistantEditKind::Update {
|
||||
old_text: "
|
||||
fn three() {
|
||||
3
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
new_text: "
|
||||
fn three() {
|
||||
103
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
description: None,
|
||||
},
|
||||
],
|
||||
"
|
||||
impl Numbers {
|
||||
fn one() {
|
||||
101
|
||||
}
|
||||
|
||||
fn two() {
|
||||
102
|
||||
}
|
||||
|
||||
fn three() {
|
||||
103
|
||||
}
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
cx,
|
||||
);
|
||||
|
||||
assert_edits(
|
||||
"
|
||||
impl Person {
|
||||
fn set_name(&mut self, name: String) {
|
||||
self.name = name;
|
||||
}
|
||||
|
||||
fn name(&self) -> String {
|
||||
return self.name;
|
||||
}
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
vec![
|
||||
AssistantEditKind::Update {
|
||||
old_text: "self.name = name;".unindent(),
|
||||
new_text: "self._name = name;".unindent(),
|
||||
description: None,
|
||||
},
|
||||
AssistantEditKind::Update {
|
||||
old_text: "return self.name;\n".unindent(),
|
||||
new_text: "return self._name;\n".unindent(),
|
||||
description: None,
|
||||
},
|
||||
],
|
||||
"
|
||||
impl Person {
|
||||
fn set_name(&mut self, name: String) {
|
||||
self._name = name;
|
||||
}
|
||||
|
||||
fn name(&self) -> String {
|
||||
return self._name;
|
||||
}
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut App) {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
language::init(cx);
|
||||
cx.update_global::<SettingsStore, _>(|settings, cx| {
|
||||
settings.update_user_settings::<AllLanguageSettings>(cx, |_| {});
|
||||
});
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_location_resolution(text_with_expected_range: &str, query: &str, cx: &mut App) {
|
||||
let (text, _) = marked_text_ranges(text_with_expected_range, false);
|
||||
let buffer = cx.new(|cx| Buffer::local(text.clone(), cx));
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let range = AssistantEditKind::resolve_location(&snapshot, query).to_offset(&snapshot);
|
||||
let text_with_actual_range = generate_marked_text(&text, &[range], false);
|
||||
pretty_assertions::assert_eq!(text_with_actual_range, text_with_expected_range);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_edits(
|
||||
old_text: String,
|
||||
edits: Vec<AssistantEditKind>,
|
||||
new_text: String,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let buffer =
|
||||
cx.new(|cx| Buffer::local(old_text, cx).with_language(Arc::new(rust_lang()), cx));
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let resolved_edits = edits
|
||||
.into_iter()
|
||||
.map(|kind| kind.resolve(&snapshot))
|
||||
.collect();
|
||||
let edit_groups = AssistantPatch::group_edits(resolved_edits, &snapshot);
|
||||
ResolvedPatch::apply_edit_groups(&edit_groups, &buffer, cx);
|
||||
let actual_new_text = buffer.read(cx).text();
|
||||
pretty_assertions::assert_eq!(actual_new_text, new_text);
|
||||
}
|
||||
|
||||
fn rust_lang() -> Language {
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Some(language::tree_sitter_rust::LANGUAGE.into()),
|
||||
)
|
||||
.with_indents_query(
|
||||
r#"
|
||||
(call_expression) @indent
|
||||
(field_expression) @indent
|
||||
(_ "(" ")" @end) @indent
|
||||
(_ "{" "}" @end) @indent
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
@@ -278,8 +278,8 @@ impl CompletionProvider for SlashCommandCompletionProvider {
|
||||
buffer.anchor_after(Point::new(position.row, first_arg_start.start as u32));
|
||||
let arguments = call
|
||||
.arguments
|
||||
.iter()
|
||||
.filter_map(|argument| Some(line.get(argument.clone())?.to_string()))
|
||||
.into_iter()
|
||||
.filter_map(|argument| Some(line.get(argument)?.to_string()))
|
||||
.collect::<Vec<_>>();
|
||||
let argument_range = first_arg_start..buffer_position;
|
||||
(
|
||||
|
||||
@@ -338,7 +338,7 @@ where
|
||||
|
||||
let handle = self
|
||||
.active_context_editor
|
||||
.update(cx, |this, _| this.slash_menu_handle.clone())
|
||||
.read_with(cx, |this, _| this.slash_menu_handle.clone())
|
||||
.ok();
|
||||
PopoverMenu::new("model-switcher")
|
||||
.menu(move |_window, _cx| Some(picker_view.clone()))
|
||||
|
||||
@@ -9,6 +9,7 @@ use anyhow::Result;
|
||||
use futures::StreamExt;
|
||||
use futures::stream::{self, BoxStream};
|
||||
use gpui::{App, SharedString, Task, WeakEntity, Window};
|
||||
use language::HighlightId;
|
||||
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate, OffsetRangeExt};
|
||||
pub use language_model::Role;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -16,6 +17,7 @@ use std::{
|
||||
ops::Range,
|
||||
sync::{Arc, atomic::AtomicBool},
|
||||
};
|
||||
use ui::ActiveTheme;
|
||||
use workspace::{Workspace, ui::IconName};
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
@@ -325,6 +327,18 @@ impl SlashCommandLine {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_label_for_command(command_name: &str, arguments: &[&str], cx: &App) -> CodeLabel {
|
||||
let mut label = CodeLabel::default();
|
||||
label.push_str(command_name, None);
|
||||
label.push_str(" ", None);
|
||||
label.push_str(
|
||||
&arguments.join(" "),
|
||||
cx.theme().syntax().highlight_id("comment").map(HighlightId),
|
||||
);
|
||||
label.filter_range = 0..command_name.len();
|
||||
label
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
@@ -35,7 +35,6 @@ rope.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
smol.workspace = true
|
||||
terminal_view.workspace = true
|
||||
text.workspace = true
|
||||
toml.workspace = true
|
||||
ui.workspace = true
|
||||
@@ -45,6 +44,6 @@ worktree.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger.workspace = true
|
||||
pretty_assertions.workspace = true
|
||||
settings.workspace = true
|
||||
zlog.workspace = true
|
||||
|
||||
@@ -12,11 +12,6 @@ mod selection_command;
|
||||
mod streaming_example_command;
|
||||
mod symbols_command;
|
||||
mod tab_command;
|
||||
mod terminal_command;
|
||||
|
||||
use gpui::App;
|
||||
use language::{CodeLabel, HighlightId};
|
||||
use ui::ActiveTheme as _;
|
||||
|
||||
pub use crate::cargo_workspace_command::*;
|
||||
pub use crate::context_server_command::*;
|
||||
@@ -32,16 +27,5 @@ pub use crate::selection_command::*;
|
||||
pub use crate::streaming_example_command::*;
|
||||
pub use crate::symbols_command::*;
|
||||
pub use crate::tab_command::*;
|
||||
pub use crate::terminal_command::*;
|
||||
|
||||
pub fn create_label_for_command(command_name: &str, arguments: &[&str], cx: &App) -> CodeLabel {
|
||||
let mut label = CodeLabel::default();
|
||||
label.push_str(command_name, None);
|
||||
label.push_str(" ", None);
|
||||
label.push_str(
|
||||
&arguments.join(" "),
|
||||
cx.theme().syntax().highlight_id("comment").map(HighlightId),
|
||||
);
|
||||
label.filter_range = 0..command_name.len();
|
||||
label
|
||||
}
|
||||
use assistant_slash_command::create_label_for_command;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_slash_command::{
|
||||
AfterCompletion, ArgumentCompletion, SlashCommand, SlashCommandOutput,
|
||||
SlashCommandOutputSection, SlashCommandResult,
|
||||
@@ -84,9 +84,7 @@ impl SlashCommand for ContextServerSlashCommand {
|
||||
|
||||
if let Some(server) = self.store.read(cx).get_running_server(&server_id) {
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let Some(protocol) = server.client() else {
|
||||
return Err(anyhow!("Context server not initialized"));
|
||||
};
|
||||
let protocol = server.client().context("Context server not initialized")?;
|
||||
|
||||
let completion_result = protocol
|
||||
.completion(
|
||||
@@ -139,21 +137,16 @@ impl SlashCommand for ContextServerSlashCommand {
|
||||
let store = self.store.read(cx);
|
||||
if let Some(server) = store.get_running_server(&server_id) {
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let Some(protocol) = server.client() else {
|
||||
return Err(anyhow!("Context server not initialized"));
|
||||
};
|
||||
let protocol = server.client().context("Context server not initialized")?;
|
||||
let result = protocol.run_prompt(&prompt_name, prompt_args).await?;
|
||||
|
||||
// Check that there are only user roles
|
||||
if result
|
||||
.messages
|
||||
.iter()
|
||||
.any(|msg| !matches!(msg.role, context_server::types::Role::User))
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"Prompt contains non-user roles, which is not supported"
|
||||
));
|
||||
}
|
||||
anyhow::ensure!(
|
||||
result
|
||||
.messages
|
||||
.iter()
|
||||
.all(|msg| matches!(msg.role, context_server::types::Role::User)),
|
||||
"Prompt contains non-user roles, which is not supported"
|
||||
);
|
||||
|
||||
// Extract text from user messages into a single prompt string
|
||||
let mut prompt = result
|
||||
@@ -192,9 +185,7 @@ impl SlashCommand for ContextServerSlashCommand {
|
||||
}
|
||||
|
||||
fn completion_argument(prompt: &Prompt, arguments: &[String]) -> Result<(String, String)> {
|
||||
if arguments.is_empty() {
|
||||
return Err(anyhow!("No arguments given"));
|
||||
}
|
||||
anyhow::ensure!(!arguments.is_empty(), "No arguments given");
|
||||
|
||||
match &prompt.arguments {
|
||||
Some(args) if args.len() == 1 => {
|
||||
@@ -202,16 +193,16 @@ fn completion_argument(prompt: &Prompt, arguments: &[String]) -> Result<(String,
|
||||
let arg_value = arguments.join(" ");
|
||||
Ok((arg_name, arg_value))
|
||||
}
|
||||
Some(_) => Err(anyhow!("Prompt must have exactly one argument")),
|
||||
None => Err(anyhow!("Prompt has no arguments")),
|
||||
Some(_) => anyhow::bail!("Prompt must have exactly one argument"),
|
||||
None => anyhow::bail!("Prompt has no arguments"),
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_arguments(prompt: &Prompt, arguments: &[String]) -> Result<HashMap<String, String>> {
|
||||
match &prompt.arguments {
|
||||
Some(args) if args.len() > 1 => Err(anyhow!(
|
||||
"Prompt has more than one argument, which is not supported"
|
||||
)),
|
||||
Some(args) if args.len() > 1 => {
|
||||
anyhow::bail!("Prompt has more than one argument, which is not supported");
|
||||
}
|
||||
Some(args) if args.len() == 1 => {
|
||||
if !arguments.is_empty() {
|
||||
let mut map = HashMap::default();
|
||||
@@ -220,15 +211,15 @@ fn prompt_arguments(prompt: &Prompt, arguments: &[String]) -> Result<HashMap<Str
|
||||
} else if arguments.is_empty() && args[0].required == Some(false) {
|
||||
Ok(HashMap::default())
|
||||
} else {
|
||||
Err(anyhow!("Prompt expects argument but none given"))
|
||||
anyhow::bail!("Prompt expects argument but none given");
|
||||
}
|
||||
}
|
||||
Some(_) | None => {
|
||||
if arguments.is_empty() {
|
||||
Ok(HashMap::default())
|
||||
} else {
|
||||
Err(anyhow!("Prompt expects no arguments but some were given"))
|
||||
}
|
||||
anyhow::ensure!(
|
||||
arguments.is_empty(),
|
||||
"Prompt expects no arguments but some were given"
|
||||
);
|
||||
Ok(HashMap::default())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,10 +118,7 @@ impl SlashCommand for DeltaSlashCommand {
|
||||
}
|
||||
}
|
||||
|
||||
if !changes_detected {
|
||||
return Err(anyhow!("no new changes detected"));
|
||||
}
|
||||
|
||||
anyhow::ensure!(changes_detected, "no new changes detected");
|
||||
Ok(output.to_event_stream())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||
SlashCommandResult,
|
||||
@@ -189,7 +189,7 @@ impl SlashCommand for DiagnosticsSlashCommand {
|
||||
window.spawn(cx, async move |_| {
|
||||
task.await?
|
||||
.map(|output| output.to_event_stream())
|
||||
.ok_or_else(|| anyhow!("No diagnostics found"))
|
||||
.context("No diagnostics found")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Result, anyhow, bail};
|
||||
use anyhow::{Context as _, Result, anyhow, bail};
|
||||
use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||
SlashCommandResult,
|
||||
@@ -52,15 +52,16 @@ impl DocsSlashCommand {
|
||||
.is_none()
|
||||
{
|
||||
let index_provider_deps = maybe!({
|
||||
let workspace = workspace.clone().ok_or_else(|| anyhow!("no workspace"))?;
|
||||
let workspace = workspace
|
||||
.as_ref()
|
||||
.context("no workspace")?
|
||||
.upgrade()
|
||||
.ok_or_else(|| anyhow!("workspace was dropped"))?;
|
||||
.context("workspace dropped")?;
|
||||
let project = workspace.read(cx).project().clone();
|
||||
let fs = project.read(cx).fs().clone();
|
||||
let cargo_workspace_root = Self::path_to_cargo_toml(project, cx)
|
||||
.and_then(|path| path.parent().map(|path| path.to_path_buf()))
|
||||
.ok_or_else(|| anyhow!("no Cargo workspace root found"))?;
|
||||
.context("no Cargo workspace root found")?;
|
||||
|
||||
anyhow::Ok((fs, cargo_workspace_root))
|
||||
});
|
||||
@@ -78,10 +79,11 @@ impl DocsSlashCommand {
|
||||
.is_none()
|
||||
{
|
||||
let http_client = maybe!({
|
||||
let workspace = workspace.ok_or_else(|| anyhow!("no workspace"))?;
|
||||
let workspace = workspace
|
||||
.as_ref()
|
||||
.context("no workspace")?
|
||||
.upgrade()
|
||||
.ok_or_else(|| anyhow!("workspace was dropped"))?;
|
||||
.context("workspace was dropped")?;
|
||||
let project = workspace.read(cx).project().clone();
|
||||
anyhow::Ok(project.read(cx).client().http_client())
|
||||
});
|
||||
@@ -174,7 +176,7 @@ impl SlashCommand for DocsSlashCommand {
|
||||
let args = DocsSlashCommandArgs::parse(arguments);
|
||||
let store = args
|
||||
.provider()
|
||||
.ok_or_else(|| anyhow!("no docs provider specified"))
|
||||
.context("no docs provider specified")
|
||||
.and_then(|provider| IndexedDocsStore::try_global(provider, cx));
|
||||
cx.background_spawn(async move {
|
||||
fn build_completions(items: Vec<String>) -> Vec<ArgumentCompletion> {
|
||||
@@ -287,7 +289,7 @@ impl SlashCommand for DocsSlashCommand {
|
||||
let task = cx.background_spawn({
|
||||
let store = args
|
||||
.provider()
|
||||
.ok_or_else(|| anyhow!("no docs provider specified"))
|
||||
.context("no docs provider specified")
|
||||
.and_then(|provider| IndexedDocsStore::try_global(provider, cx));
|
||||
async move {
|
||||
let (provider, key) = match args.clone() {
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
use anyhow::{Context, Result, anyhow, bail};
|
||||
use anyhow::{Context as _, Result, anyhow, bail};
|
||||
use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||
SlashCommandResult,
|
||||
|
||||
@@ -230,7 +230,10 @@ fn collect_files(
|
||||
})
|
||||
.collect::<anyhow::Result<Vec<custom_path_matcher::PathMatcher>>>()
|
||||
else {
|
||||
return futures::stream::once(async { Err(anyhow!("invalid path")) }).boxed();
|
||||
return futures::stream::once(async {
|
||||
anyhow::bail!("invalid path");
|
||||
})
|
||||
.boxed();
|
||||
};
|
||||
|
||||
let project_handle = project.downgrade();
|
||||
@@ -584,9 +587,7 @@ mod test {
|
||||
use super::collect_files;
|
||||
|
||||
pub fn init_test(cx: &mut gpui::TestAppContext) {
|
||||
if std::env::var("RUST_LOG").is_ok() {
|
||||
env_logger::try_init().ok();
|
||||
}
|
||||
zlog::init_test();
|
||||
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
|
||||
@@ -37,7 +37,6 @@ buffer_diff = { workspace = true, features = ["test-support"] }
|
||||
collections = { workspace = true, features = ["test-support"] }
|
||||
clock = { workspace = true, features = ["test-support"] }
|
||||
ctor.workspace = true
|
||||
env_logger.workspace = true
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
language_model = { workspace = true, features = ["test-support"] }
|
||||
@@ -48,3 +47,4 @@ rand.workspace = true
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
text = { workspace = true, features = ["test-support"] }
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
zlog.workspace = true
|
||||
|
||||
@@ -49,6 +49,37 @@ impl ActionLog {
|
||||
is_created: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> &mut TrackedBuffer {
|
||||
let status = if is_created {
|
||||
if let Some(tracked) = self.tracked_buffers.remove(&buffer) {
|
||||
match tracked.status {
|
||||
TrackedBufferStatus::Created {
|
||||
existing_file_content,
|
||||
} => TrackedBufferStatus::Created {
|
||||
existing_file_content,
|
||||
},
|
||||
TrackedBufferStatus::Modified | TrackedBufferStatus::Deleted => {
|
||||
TrackedBufferStatus::Created {
|
||||
existing_file_content: Some(tracked.diff_base),
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.map_or(false, |file| file.disk_state().exists())
|
||||
{
|
||||
TrackedBufferStatus::Created {
|
||||
existing_file_content: Some(buffer.read(cx).as_rope().clone()),
|
||||
}
|
||||
} else {
|
||||
TrackedBufferStatus::Created {
|
||||
existing_file_content: None,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
TrackedBufferStatus::Modified
|
||||
};
|
||||
|
||||
let tracked_buffer = self
|
||||
.tracked_buffers
|
||||
.entry(buffer.clone())
|
||||
@@ -60,36 +91,21 @@ impl ActionLog {
|
||||
let text_snapshot = buffer.read(cx).text_snapshot();
|
||||
let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
|
||||
let (diff_update_tx, diff_update_rx) = mpsc::unbounded();
|
||||
let base_text;
|
||||
let status;
|
||||
let diff_base;
|
||||
let unreviewed_changes;
|
||||
if is_created {
|
||||
let existing_file_content = if buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.map_or(false, |file| file.disk_state().exists())
|
||||
{
|
||||
Some(text_snapshot.as_rope().clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
base_text = Rope::default();
|
||||
status = TrackedBufferStatus::Created {
|
||||
existing_file_content,
|
||||
};
|
||||
diff_base = Rope::default();
|
||||
unreviewed_changes = Patch::new(vec![Edit {
|
||||
old: 0..1,
|
||||
new: 0..text_snapshot.max_point().row + 1,
|
||||
}])
|
||||
} else {
|
||||
base_text = buffer.read(cx).as_rope().clone();
|
||||
status = TrackedBufferStatus::Modified;
|
||||
diff_base = buffer.read(cx).as_rope().clone();
|
||||
unreviewed_changes = Patch::default();
|
||||
}
|
||||
TrackedBuffer {
|
||||
buffer: buffer.clone(),
|
||||
base_text,
|
||||
diff_base,
|
||||
unreviewed_changes,
|
||||
snapshot: text_snapshot.clone(),
|
||||
status,
|
||||
@@ -184,7 +200,7 @@ impl ActionLog {
|
||||
.context("buffer not tracked")?;
|
||||
|
||||
let rebase = cx.background_spawn({
|
||||
let mut base_text = tracked_buffer.base_text.clone();
|
||||
let mut base_text = tracked_buffer.diff_base.clone();
|
||||
let old_snapshot = tracked_buffer.snapshot.clone();
|
||||
let new_snapshot = buffer_snapshot.clone();
|
||||
let unreviewed_changes = tracked_buffer.unreviewed_changes.clone();
|
||||
@@ -210,7 +226,7 @@ impl ActionLog {
|
||||
))
|
||||
})??;
|
||||
|
||||
let (new_base_text, new_base_text_rope) = rebase.await;
|
||||
let (new_base_text, new_diff_base) = rebase.await;
|
||||
let diff_snapshot = BufferDiff::update_diff(
|
||||
diff.clone(),
|
||||
buffer_snapshot.clone(),
|
||||
@@ -229,24 +245,23 @@ impl ActionLog {
|
||||
.background_spawn({
|
||||
let diff_snapshot = diff_snapshot.clone();
|
||||
let buffer_snapshot = buffer_snapshot.clone();
|
||||
let new_base_text_rope = new_base_text_rope.clone();
|
||||
let new_diff_base = new_diff_base.clone();
|
||||
async move {
|
||||
let mut unreviewed_changes = Patch::default();
|
||||
for hunk in diff_snapshot.hunks_intersecting_range(
|
||||
Anchor::MIN..Anchor::MAX,
|
||||
&buffer_snapshot,
|
||||
) {
|
||||
let old_range = new_base_text_rope
|
||||
let old_range = new_diff_base
|
||||
.offset_to_point(hunk.diff_base_byte_range.start)
|
||||
..new_base_text_rope
|
||||
.offset_to_point(hunk.diff_base_byte_range.end);
|
||||
..new_diff_base.offset_to_point(hunk.diff_base_byte_range.end);
|
||||
let new_range = hunk.range.start..hunk.range.end;
|
||||
unreviewed_changes.push(point_to_row_edit(
|
||||
Edit {
|
||||
old: old_range,
|
||||
new: new_range,
|
||||
},
|
||||
&new_base_text_rope,
|
||||
&new_diff_base,
|
||||
&buffer_snapshot.as_rope(),
|
||||
));
|
||||
}
|
||||
@@ -264,7 +279,7 @@ impl ActionLog {
|
||||
.tracked_buffers
|
||||
.get_mut(&buffer)
|
||||
.context("buffer not tracked")?;
|
||||
tracked_buffer.base_text = new_base_text_rope;
|
||||
tracked_buffer.diff_base = new_diff_base;
|
||||
tracked_buffer.snapshot = buffer_snapshot;
|
||||
tracked_buffer.unreviewed_changes = unreviewed_changes;
|
||||
cx.notify();
|
||||
@@ -283,7 +298,6 @@ impl ActionLog {
|
||||
/// Mark a buffer as edited, so we can refresh it in the context
|
||||
pub fn buffer_created(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
|
||||
self.edited_since_project_diagnostics_check = true;
|
||||
self.tracked_buffers.remove(&buffer);
|
||||
self.track_buffer_internal(buffer.clone(), true, cx);
|
||||
}
|
||||
|
||||
@@ -346,11 +360,11 @@ impl ActionLog {
|
||||
true
|
||||
} else {
|
||||
let old_range = tracked_buffer
|
||||
.base_text
|
||||
.diff_base
|
||||
.point_to_offset(Point::new(edit.old.start, 0))
|
||||
..tracked_buffer.base_text.point_to_offset(cmp::min(
|
||||
..tracked_buffer.diff_base.point_to_offset(cmp::min(
|
||||
Point::new(edit.old.end, 0),
|
||||
tracked_buffer.base_text.max_point(),
|
||||
tracked_buffer.diff_base.max_point(),
|
||||
));
|
||||
let new_range = tracked_buffer
|
||||
.snapshot
|
||||
@@ -359,7 +373,7 @@ impl ActionLog {
|
||||
Point::new(edit.new.end, 0),
|
||||
tracked_buffer.snapshot.max_point(),
|
||||
));
|
||||
tracked_buffer.base_text.replace(
|
||||
tracked_buffer.diff_base.replace(
|
||||
old_range,
|
||||
&tracked_buffer
|
||||
.snapshot
|
||||
@@ -417,7 +431,7 @@ impl ActionLog {
|
||||
}
|
||||
TrackedBufferStatus::Deleted => {
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.set_text(tracked_buffer.base_text.to_string(), cx)
|
||||
buffer.set_text(tracked_buffer.diff_base.to_string(), cx)
|
||||
});
|
||||
let save = self
|
||||
.project
|
||||
@@ -464,14 +478,14 @@ impl ActionLog {
|
||||
|
||||
if revert {
|
||||
let old_range = tracked_buffer
|
||||
.base_text
|
||||
.diff_base
|
||||
.point_to_offset(Point::new(edit.old.start, 0))
|
||||
..tracked_buffer.base_text.point_to_offset(cmp::min(
|
||||
..tracked_buffer.diff_base.point_to_offset(cmp::min(
|
||||
Point::new(edit.old.end, 0),
|
||||
tracked_buffer.base_text.max_point(),
|
||||
tracked_buffer.diff_base.max_point(),
|
||||
));
|
||||
let old_text = tracked_buffer
|
||||
.base_text
|
||||
.diff_base
|
||||
.chunks_in_range(old_range)
|
||||
.collect::<String>();
|
||||
edits_to_revert.push((new_range, old_text));
|
||||
@@ -492,7 +506,7 @@ impl ActionLog {
|
||||
TrackedBufferStatus::Deleted => false,
|
||||
_ => {
|
||||
tracked_buffer.unreviewed_changes.clear();
|
||||
tracked_buffer.base_text = tracked_buffer.snapshot.as_rope().clone();
|
||||
tracked_buffer.diff_base = tracked_buffer.snapshot.as_rope().clone();
|
||||
tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
|
||||
true
|
||||
}
|
||||
@@ -655,7 +669,7 @@ enum TrackedBufferStatus {
|
||||
|
||||
struct TrackedBuffer {
|
||||
buffer: Entity<Buffer>,
|
||||
base_text: Rope,
|
||||
diff_base: Rope,
|
||||
unreviewed_changes: Patch<u32>,
|
||||
status: TrackedBufferStatus,
|
||||
version: clock::Global,
|
||||
@@ -703,9 +717,7 @@ mod tests {
|
||||
|
||||
#[ctor::ctor]
|
||||
fn init_logger() {
|
||||
if std::env::var("RUST_LOG").is_ok() {
|
||||
env_logger::init();
|
||||
}
|
||||
zlog::init_test();
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
@@ -1094,6 +1106,86 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_overwriting_previously_edited_files(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/dir"),
|
||||
json!({
|
||||
"file1": "Lorem ipsum dolor"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let file_path = project
|
||||
.read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
|
||||
.unwrap();
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_buffer(file_path, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
cx.update(|cx| {
|
||||
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
|
||||
buffer.update(cx, |buffer, cx| buffer.append(" sit amet consecteur", cx));
|
||||
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
|
||||
});
|
||||
project
|
||||
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
unreviewed_hunks(&action_log, cx),
|
||||
vec![(
|
||||
buffer.clone(),
|
||||
vec![HunkStatus {
|
||||
range: Point::new(0, 0)..Point::new(0, 37),
|
||||
diff_status: DiffHunkStatusKind::Modified,
|
||||
old_text: "Lorem ipsum dolor".into(),
|
||||
}],
|
||||
)]
|
||||
);
|
||||
|
||||
cx.update(|cx| {
|
||||
action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx));
|
||||
buffer.update(cx, |buffer, cx| buffer.set_text("rewritten", cx));
|
||||
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
|
||||
});
|
||||
project
|
||||
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
unreviewed_hunks(&action_log, cx),
|
||||
vec![(
|
||||
buffer.clone(),
|
||||
vec![HunkStatus {
|
||||
range: Point::new(0, 0)..Point::new(0, 9),
|
||||
diff_status: DiffHunkStatusKind::Added,
|
||||
old_text: "".into(),
|
||||
}],
|
||||
)]
|
||||
);
|
||||
|
||||
action_log
|
||||
.update(cx, |log, cx| {
|
||||
log.reject_edits_in_ranges(buffer.clone(), vec![2..5], cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, _cx| buffer.text()),
|
||||
"Lorem ipsum dolor"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_deleting_files(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
@@ -1601,7 +1693,7 @@ mod tests {
|
||||
cx.run_until_parked();
|
||||
action_log.update(cx, |log, cx| {
|
||||
let tracked_buffer = log.tracked_buffers.get(&buffer).unwrap();
|
||||
let mut old_text = tracked_buffer.base_text.clone();
|
||||
let mut old_text = tracked_buffer.diff_base.clone();
|
||||
let new_text = buffer.read(cx).as_rope();
|
||||
for edit in tracked_buffer.unreviewed_changes.edits() {
|
||||
let old_start = old_text.point_to_offset(Point::new(edit.new.start, 0));
|
||||
|
||||