Compare commits
609 Commits
v0.204.2
...
slash-comm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aca84668b0 | ||
|
|
4fe31d7f40 | ||
|
|
9ae3de2732 | ||
|
|
21e75b8221 | ||
|
|
978951b79a | ||
|
|
6b980ecad3 | ||
|
|
d9c7f44b0b | ||
|
|
55e68553a4 | ||
|
|
9fe46dc8d2 | ||
|
|
aced13bc9f | ||
|
|
2859cbdba9 | ||
|
|
4443f61c16 | ||
|
|
f0f0beb42f | ||
|
|
6707ff3b50 | ||
|
|
93770e8314 | ||
|
|
f8c617303a | ||
|
|
e5f05a21ce | ||
|
|
f499504b13 | ||
|
|
504216cbbf | ||
|
|
3bf71c690f | ||
|
|
456ba32ea7 | ||
|
|
9aeb617a89 | ||
|
|
fd8bae9b72 | ||
|
|
f71c9122ca | ||
|
|
8441aa49b2 | ||
|
|
7b96e1cf1a | ||
|
|
86322a186f | ||
|
|
1b94d74dc3 | ||
|
|
db825c1141 | ||
|
|
f3abd1dab5 | ||
|
|
662ec9977f | ||
|
|
3ab5103de1 | ||
|
|
39bd03b92d | ||
|
|
1fffcb99ba | ||
|
|
e4f90b5da2 | ||
|
|
dc6fad9659 | ||
|
|
64c289a9a2 | ||
|
|
a08897ff30 | ||
|
|
d359a814f8 | ||
|
|
4c35274b6e | ||
|
|
bf48a95344 | ||
|
|
7c3a21f732 | ||
|
|
af630be7ca | ||
|
|
dbd8efe129 | ||
|
|
3afbe836a1 | ||
|
|
d8709f2107 | ||
|
|
df7bc8200d | ||
|
|
8575972a07 | ||
|
|
40c417f9c3 | ||
|
|
7c2cf86dd9 | ||
|
|
126ed6fbdd | ||
|
|
6f4381b39d | ||
|
|
6fbbdb3512 | ||
|
|
179fb21778 | ||
|
|
6584fb23e3 | ||
|
|
d8698dffe3 | ||
|
|
bf44dc5ff5 | ||
|
|
d85b6a1544 | ||
|
|
702e618bba | ||
|
|
1029d3c301 | ||
|
|
97f552876c | ||
|
|
63c081d456 | ||
|
|
6970ab2040 | ||
|
|
e42dfb4387 | ||
|
|
ec202a26c8 | ||
|
|
f17096879c | ||
|
|
fb343a7743 | ||
|
|
a49b2d5bf8 | ||
|
|
b5d57598b6 | ||
|
|
b9d9602074 | ||
|
|
cc19f66ee1 | ||
|
|
62f90fec77 | ||
|
|
86ebb1890d | ||
|
|
dd5099ac28 | ||
|
|
c95b88d546 | ||
|
|
c217f6bd36 | ||
|
|
3314de8175 | ||
|
|
6b907bd102 | ||
|
|
3cb933ddb1 | ||
|
|
cf5362ffd1 | ||
|
|
74ac5ece6a | ||
|
|
f107708de3 | ||
|
|
4940e53d23 | ||
|
|
ab79fa440d | ||
|
|
c9b7df4113 | ||
|
|
f2df49764e | ||
|
|
77cc55656e | ||
|
|
1c85995ed7 | ||
|
|
d1543f75b6 | ||
|
|
fc0b249136 | ||
|
|
01dbc68f82 | ||
|
|
e111acad33 | ||
|
|
c61409e577 | ||
|
|
1659fb81e7 | ||
|
|
dd6c653fe9 | ||
|
|
a13e84a108 | ||
|
|
1cac3e3e40 | ||
|
|
9abe5811a5 | ||
|
|
97bd2846e9 | ||
|
|
e9244d50a7 | ||
|
|
83e5a3033e | ||
|
|
94a4c0c352 | ||
|
|
0f8693386a | ||
|
|
ed269b4467 | ||
|
|
34ddf5466f | ||
|
|
a701388cb7 | ||
|
|
29afc0412e | ||
|
|
e65a9291ef | ||
|
|
a53faff412 | ||
|
|
074cb88036 | ||
|
|
67ebb1f795 | ||
|
|
ace617037f | ||
|
|
43061b6b16 | ||
|
|
e23e976e58 | ||
|
|
0266a995aa | ||
|
|
9741e9ab8b | ||
|
|
3f31fc2874 | ||
|
|
6c50fd6de9 | ||
|
|
df43a2d3b1 | ||
|
|
35749e99e5 | ||
|
|
e965c43703 | ||
|
|
14fc726cae | ||
|
|
4f95186b53 | ||
|
|
33f44009de | ||
|
|
9d895c5ea7 | ||
|
|
0811d48a7a | ||
|
|
d8cafdf937 | ||
|
|
95190a2034 | ||
|
|
49335d54be | ||
|
|
624e448492 | ||
|
|
bf9dd6bbef | ||
|
|
6af385235d | ||
|
|
cc19387853 | ||
|
|
5922f4adce | ||
|
|
cac920d992 | ||
|
|
773850f477 | ||
|
|
9c60bc3837 | ||
|
|
fbb4dcf2b1 | ||
|
|
2ccadc7f65 | ||
|
|
80989d6767 | ||
|
|
719013dae6 | ||
|
|
8af3f583c2 | ||
|
|
f1d80b715a | ||
|
|
42ef3e5d3d | ||
|
|
90ea252c82 | ||
|
|
6e5ff6d091 | ||
|
|
04216a88f3 | ||
|
|
3ae65153db | ||
|
|
ffc9060607 | ||
|
|
4fc4707cfc | ||
|
|
8662025d12 | ||
|
|
ceddd5752a | ||
|
|
20166727a6 | ||
|
|
6e80fca0d5 | ||
|
|
778ca84f85 | ||
|
|
ebdc0572c6 | ||
|
|
cda48a3a1c | ||
|
|
b7f9fd7d74 | ||
|
|
98ab118526 | ||
|
|
1e70a1a4ce | ||
|
|
163219af35 | ||
|
|
f96fd928d7 | ||
|
|
9aa5817b85 | ||
|
|
28cc39ad56 | ||
|
|
0da3f9ffda | ||
|
|
f2efe78feb | ||
|
|
ed7217ff46 | ||
|
|
f9fb389f86 | ||
|
|
632e569c5f | ||
|
|
0c71aa9f01 | ||
|
|
92a09ecf25 | ||
|
|
bad96776cd | ||
|
|
aa14980523 | ||
|
|
12aba6193e | ||
|
|
720971e47b | ||
|
|
0a10e3e264 | ||
|
|
77854f4627 | ||
|
|
5ce7eda8d2 | ||
|
|
6d7a4c441b | ||
|
|
cc85a48de5 | ||
|
|
4cd839e352 | ||
|
|
78098f6809 | ||
|
|
4d2ff6c899 | ||
|
|
6f5d1522cb | ||
|
|
682cf023ca | ||
|
|
72948e14ee | ||
|
|
a063a70cfb | ||
|
|
687e22b4c3 | ||
|
|
e13b88e4bd | ||
|
|
e1e9f78dc3 | ||
|
|
0fe696bc7c | ||
|
|
ead38fd1be | ||
|
|
fbdf5d4df4 | ||
|
|
837f282f1e | ||
|
|
bd3cccea15 | ||
|
|
d437bbaa0a | ||
|
|
114791e1a8 | ||
|
|
d6fcd404af | ||
|
|
7ad9ca9bcc | ||
|
|
a55dff7834 | ||
|
|
6db621a1ed | ||
|
|
948b4379df | ||
|
|
8db24dd8ad | ||
|
|
4aac5642c1 | ||
|
|
30b49cfbf5 | ||
|
|
c69912c76a | ||
|
|
7f14ab26dd | ||
|
|
5ee73d3e3c | ||
|
|
d5aa81a5b2 | ||
|
|
21855c15e4 | ||
|
|
1f9279a56f | ||
|
|
da71465437 | ||
|
|
bcc8149263 | ||
|
|
b1528601cc | ||
|
|
ee357e8987 | ||
|
|
0891a7142d | ||
|
|
94fe862fb6 | ||
|
|
4f91fab190 | ||
|
|
0e0f48d8e1 | ||
|
|
7980dbdaea | ||
|
|
a5683f3541 | ||
|
|
67984d5e49 | ||
|
|
d83d7d35cb | ||
|
|
6470443271 | ||
|
|
5b72dfff87 | ||
|
|
495a7b0a84 | ||
|
|
301e976465 | ||
|
|
daebc4052d | ||
|
|
ecc35fcd9a | ||
|
|
236006b6b3 | ||
|
|
39c4480841 | ||
|
|
48aac2a746 | ||
|
|
ae036f8ead | ||
|
|
de1de25712 | ||
|
|
18fc951135 | ||
|
|
40138e12a4 | ||
|
|
e7a5c81b07 | ||
|
|
d98175c0a6 | ||
|
|
bc7d804a42 | ||
|
|
50bb8a4ae6 | ||
|
|
b2b90b003d | ||
|
|
c0f56f500e | ||
|
|
a9fe18f4cb | ||
|
|
3c5e683fbe | ||
|
|
783ba389f7 | ||
|
|
e72021a26b | ||
|
|
f25ace6be0 | ||
|
|
c627543b46 | ||
|
|
f303a461c4 | ||
|
|
a9def8128f | ||
|
|
6580eac077 | ||
|
|
5c3c79d667 | ||
|
|
16fccb5c76 | ||
|
|
a25504edaf | ||
|
|
bc11844b2e | ||
|
|
10b99c6f55 | ||
|
|
17dea24533 | ||
|
|
17e55daf6f | ||
|
|
6b968e0118 | ||
|
|
0f66310192 | ||
|
|
26adc70ae6 | ||
|
|
a5fb290252 | ||
|
|
8fc7bd9ae8 | ||
|
|
7167be5889 | ||
|
|
d321cf93ba | ||
|
|
ce7b02e3a1 | ||
|
|
03f9cf4414 | ||
|
|
3c626f3758 | ||
|
|
4a1bab52f3 | ||
|
|
91b0f42382 | ||
|
|
523c042930 | ||
|
|
ed7bd5a8ed | ||
|
|
8ebe4fa149 | ||
|
|
6b646e3a14 | ||
|
|
e653cc90c5 | ||
|
|
0794de71e3 | ||
|
|
2b283e7c53 | ||
|
|
45a4277026 | ||
|
|
fa76b6ce06 | ||
|
|
a13e3a8af3 | ||
|
|
39370bceb2 | ||
|
|
53885c00d3 | ||
|
|
6f3e66d027 | ||
|
|
b3f9be6e9c | ||
|
|
4353b61155 | ||
|
|
e1b57f00a0 | ||
|
|
c5219e8fd2 | ||
|
|
5612a961b0 | ||
|
|
c53e5ba397 | ||
|
|
d5a99d079e | ||
|
|
9418a2f4bc | ||
|
|
880fff471c | ||
|
|
5f6ae2361f | ||
|
|
5d89b2ea26 | ||
|
|
0f7dbf57f5 | ||
|
|
b60f19f71e | ||
|
|
0a261ad8d0 | ||
|
|
28ed08340c | ||
|
|
74fe3b17f7 | ||
|
|
9112554262 | ||
|
|
3b79490e8f | ||
|
|
52c467ea3a | ||
|
|
831de8e48f | ||
|
|
bc528411df | ||
|
|
9ac511e47c | ||
|
|
afaed3af62 | ||
|
|
f78699eb71 | ||
|
|
3646aa6bba | ||
|
|
dc20a41e0d | ||
|
|
6a24ad7d39 | ||
|
|
8fefd793f0 | ||
|
|
f6e2a2a808 | ||
|
|
3cf6fa8f61 | ||
|
|
2759f541da | ||
|
|
809d3bfe00 | ||
|
|
0aad47493e | ||
|
|
271d67f7ad | ||
|
|
2e87387e53 | ||
|
|
15e75bdf04 | ||
|
|
3ac14e15bb | ||
|
|
9e7302520e | ||
|
|
1bf8332333 | ||
|
|
d8048f46ee | ||
|
|
edb804de5a | ||
|
|
691bfe71db | ||
|
|
1d5da68560 | ||
|
|
f07bc12aed | ||
|
|
4532765ae8 | ||
|
|
25a1827456 | ||
|
|
98865a3ff2 | ||
|
|
681a4adc42 | ||
|
|
5e502a32fb | ||
|
|
e9fbcf5abf | ||
|
|
c9e3b32366 | ||
|
|
e9abd5b28b | ||
|
|
a90abb1009 | ||
|
|
46d19d8a47 | ||
|
|
e484f49ee8 | ||
|
|
80dcabe95c | ||
|
|
e602cfadd3 | ||
|
|
d4adb51553 | ||
|
|
a0514af589 | ||
|
|
c88fdaf02d | ||
|
|
003163eb4f | ||
|
|
9e64b7b911 | ||
|
|
d4fd59f0a2 | ||
|
|
4e6e424fd7 | ||
|
|
dccbb47fbc | ||
|
|
b97843ea02 | ||
|
|
fbe06238e4 | ||
|
|
e0028fbef2 | ||
|
|
1bbf98aea6 | ||
|
|
8bac1bee7a | ||
|
|
55dc9ff7ca | ||
|
|
50bd8bc255 | ||
|
|
a2c71d3d20 | ||
|
|
79620454d0 | ||
|
|
271771c742 | ||
|
|
891a06c294 | ||
|
|
11041ef3b0 | ||
|
|
839c216620 | ||
|
|
18df6a81b4 | ||
|
|
f5c2e4b49e | ||
|
|
1d1bbf01a9 | ||
|
|
ffa23d25e3 | ||
|
|
782058647d | ||
|
|
be77682a3f | ||
|
|
8df616e28b | ||
|
|
89520ea221 | ||
|
|
de75e2d9f6 | ||
|
|
4e316c683b | ||
|
|
1afbfcb832 | ||
|
|
be7575536e | ||
|
|
30a29ab34e | ||
|
|
b9188e0fd3 | ||
|
|
df6f0bc2a7 | ||
|
|
4743fe8415 | ||
|
|
0f4bdca9e9 | ||
|
|
154b01c5fe | ||
|
|
b6944d0bae | ||
|
|
94fcbb400b | ||
|
|
2e97ef32c4 | ||
|
|
aa5b99dc11 | ||
|
|
3217bcb83e | ||
|
|
a3da66cec0 | ||
|
|
9e6f1d5a6e | ||
|
|
430ac5175f | ||
|
|
5f728efccf | ||
|
|
194a13ffb5 | ||
|
|
66f2fda625 | ||
|
|
e62dd2a0e5 | ||
|
|
c826ce6fc6 | ||
|
|
e5e308ba78 | ||
|
|
166b2352f3 | ||
|
|
f18b19a73e | ||
|
|
b09764c54a | ||
|
|
5f4f0a873e | ||
|
|
82e1e5b7ac | ||
|
|
530225a06a | ||
|
|
11212b80f9 | ||
|
|
e3e0522e32 | ||
|
|
fc0eb882f7 | ||
|
|
6b8ed5bf28 | ||
|
|
5fccde9b1b | ||
|
|
c58763a526 | ||
|
|
a6a2465954 | ||
|
|
439d31e2d4 | ||
|
|
df50b5c14a | ||
|
|
55d130a166 | ||
|
|
fcdab160f9 | ||
|
|
0a9023bce0 | ||
|
|
fb60f710e3 | ||
|
|
589e2c0fe4 | ||
|
|
21d8b19926 | ||
|
|
82686bf94c | ||
|
|
f562e7e157 | ||
|
|
202dcb122f | ||
|
|
b1aa2723e9 | ||
|
|
59a609c9fc | ||
|
|
ca05ff89f4 | ||
|
|
9f9e8063fc | ||
|
|
32c868ff7d | ||
|
|
ed46e2ca77 | ||
|
|
d85a6db6a3 | ||
|
|
4b1e78cd5c | ||
|
|
eaa1cb0ca3 | ||
|
|
ea473eea87 | ||
|
|
4912096599 | ||
|
|
3c69144128 | ||
|
|
96111c6ef3 | ||
|
|
f6d08fe59c | ||
|
|
86a2649944 | ||
|
|
f0b21508ec | ||
|
|
3968b9cd09 | ||
|
|
43f40c60fd | ||
|
|
824f695383 | ||
|
|
50326ddc35 | ||
|
|
52521efc7b | ||
|
|
4a7784cf67 | ||
|
|
f3b8c619e3 | ||
|
|
0f6dd84c98 | ||
|
|
405a8eaf78 | ||
|
|
c54e294965 | ||
|
|
86834887da | ||
|
|
a5c29176a3 | ||
|
|
574b943081 | ||
|
|
399118f461 | ||
|
|
5ca3b998f3 | ||
|
|
d74b8bcf4c | ||
|
|
28800c2a3b | ||
|
|
83d9f07547 | ||
|
|
c5ac1e6218 | ||
|
|
d48d6a7454 | ||
|
|
a2de91827d | ||
|
|
531f9ee236 | ||
|
|
64d362cbce | ||
|
|
5d561aa494 | ||
|
|
4ee2daeded | ||
|
|
c27d8e0c7a | ||
|
|
f6c5c68751 | ||
|
|
74e5b848ff | ||
|
|
ee399ebccf | ||
|
|
54c82f2732 | ||
|
|
e14a4ab90d | ||
|
|
0343b5ff06 | ||
|
|
26202e5af2 | ||
|
|
ee912366a3 | ||
|
|
673a98a277 | ||
|
|
5674445a61 | ||
|
|
53513cab23 | ||
|
|
e885a939ba | ||
|
|
a01a2ed0e0 | ||
|
|
af3bc45a26 | ||
|
|
173074f248 | ||
|
|
a7cb64c64d | ||
|
|
c6472fd7a8 | ||
|
|
c0710fa8ca | ||
|
|
f321d02207 | ||
|
|
1c09985fb3 | ||
|
|
d986077592 | ||
|
|
555b6ee4e5 | ||
|
|
6446963a0c | ||
|
|
ceb907e0dc | ||
|
|
3dbccc828e | ||
|
|
853e625259 | ||
|
|
0784bb8192 | ||
|
|
9046091164 | ||
|
|
6384966ab5 | ||
|
|
8b9c74726a | ||
|
|
63586ff2e4 | ||
|
|
35e5aa4e71 | ||
|
|
7ea94a32be | ||
|
|
6d6c3d648a | ||
|
|
53b2f37452 | ||
|
|
92b946e8e5 | ||
|
|
e9b4f59e0f | ||
|
|
989adde57b | ||
|
|
393d6787a3 | ||
|
|
4a582504d4 | ||
|
|
cfb2925169 | ||
|
|
14f4e867aa | ||
|
|
4d54ccf494 | ||
|
|
5b1c87b6a6 | ||
|
|
0fef17baa2 | ||
|
|
526196917b | ||
|
|
a598fbaa73 | ||
|
|
634ae72cad | ||
|
|
98edf1bf0b | ||
|
|
1090c47a90 | ||
|
|
be7b22b0dc | ||
|
|
f3e49e1b05 | ||
|
|
0adc6ddaad | ||
|
|
99b71677c6 | ||
|
|
1c27a6dbc2 | ||
|
|
256a91019a | ||
|
|
85aa458b9c | ||
|
|
37239fd66b | ||
|
|
2b1f7d5763 | ||
|
|
813a9bb0bc | ||
|
|
e40a950bc4 | ||
|
|
89e527c23b | ||
|
|
c50b561e1c | ||
|
|
13113ab311 | ||
|
|
01f181339f | ||
|
|
d046016ef5 | ||
|
|
e43ad858d8 | ||
|
|
ded6467604 | ||
|
|
53c5db4495 | ||
|
|
cd2ecbbd27 | ||
|
|
e71012a2f8 | ||
|
|
b9cf5886e4 | ||
|
|
174a0b1517 | ||
|
|
e4b754a19f | ||
|
|
5f20b905a5 | ||
|
|
4c758bd0b7 | ||
|
|
4b7595c94c | ||
|
|
2143c59fba | ||
|
|
2b3ca360c3 | ||
|
|
85f7bb6277 | ||
|
|
7377a898e8 | ||
|
|
8ebe812c24 | ||
|
|
7f1c7c1910 | ||
|
|
503284db45 | ||
|
|
2aa564eeb7 | ||
|
|
cba9ff55c7 | ||
|
|
a577128163 | ||
|
|
687c2c88c7 | ||
|
|
2a03b6b80c | ||
|
|
e68aa18fd4 | ||
|
|
592b013013 | ||
|
|
1142408675 | ||
|
|
8201f3d72f | ||
|
|
fcfc54c515 | ||
|
|
ffb85d7e81 | ||
|
|
405d7d7476 | ||
|
|
bdf44e55aa | ||
|
|
45ee1327a4 | ||
|
|
b60e705782 | ||
|
|
2bb50acb58 | ||
|
|
87f5e72fc0 | ||
|
|
11b7913956 | ||
|
|
ff2eebf522 | ||
|
|
c4d75ea6d5 | ||
|
|
d5d30b5c44 | ||
|
|
7655e22ff5 | ||
|
|
7a83a7fbd0 | ||
|
|
3cb3f01406 | ||
|
|
46aa05e240 | ||
|
|
a33af4e9c0 | ||
|
|
116c6549f6 | ||
|
|
da8c7a1256 | ||
|
|
2b04186b0f | ||
|
|
462293667b | ||
|
|
e5c0373011 | ||
|
|
a066794e8d | ||
|
|
f6b6d4a9fe | ||
|
|
238dab4a9c | ||
|
|
d1c6c9d035 | ||
|
|
d7f3d08c59 | ||
|
|
4db19a3a96 | ||
|
|
c4e8fe1fb7 | ||
|
|
4002602a89 | ||
|
|
6ae83b4740 | ||
|
|
eec6bfebbb | ||
|
|
9875969cba | ||
|
|
59502289e7 | ||
|
|
f764077020 | ||
|
|
9708c8d507 | ||
|
|
f205732074 | ||
|
|
aee21ca17f | ||
|
|
816c4817d0 | ||
|
|
0f9232a10d | ||
|
|
db367cc6bf | ||
|
|
2ce0641fe0 | ||
|
|
95ccce3095 | ||
|
|
14de161d06 | ||
|
|
b8c30f448f | ||
|
|
cb75c2aeb7 | ||
|
|
2c29eac29f | ||
|
|
a94b0931c7 | ||
|
|
441a934d84 | ||
|
|
b28c979aae | ||
|
|
22e31a0d41 | ||
|
|
c0b583c9ef | ||
|
|
6441099a67 | ||
|
|
611b96627b | ||
|
|
630340d659 | ||
|
|
acb3406eb8 | ||
|
|
fb3c991112 |
@@ -10,3 +10,15 @@
|
||||
# Here, we opted to use `[target.'cfg(all())']` instead of `[build]` because `[target.'**']` is guaranteed to be cumulative.
|
||||
[target.'cfg(all())']
|
||||
rustflags = ["-D", "warnings"]
|
||||
|
||||
# Use Mold on Linux, because it's faster than GNU ld and LLD.
|
||||
#
|
||||
# We no longer set this in the default `config.toml` so that developers can opt in to Wild, which
|
||||
# is faster than Mold, in their own ~/.cargo/config.toml.
|
||||
[target.x86_64-unknown-linux-gnu]
|
||||
linker = "clang"
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "clang"
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
|
||||
@@ -4,14 +4,9 @@ rustflags = ["-C", "symbol-mangling-version=v0", "--cfg", "tokio_unstable"]
|
||||
|
||||
[alias]
|
||||
xtask = "run --package xtask --"
|
||||
|
||||
[target.x86_64-unknown-linux-gnu]
|
||||
linker = "clang"
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "clang"
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
perf-test = ["test", "--profile", "release-fast", "--lib", "--bins", "--tests", "--all-features", "--config", "target.'cfg(true)'.runner='cargo run -p perf --release'", "--config", "target.'cfg(true)'.rustflags=[\"--cfg\", \"perf_enabled\"]"]
|
||||
# Keep similar flags here to share some ccache
|
||||
perf-compare = ["run", "--profile", "release-fast", "-p", "perf", "--config", "target.'cfg(true)'.rustflags=[\"--cfg\", \"perf_enabled\"]", "--", "compare"]
|
||||
|
||||
[target.'cfg(target_os = "windows")']
|
||||
rustflags = [
|
||||
|
||||
@@ -26,7 +26,7 @@ third-party = [
|
||||
# build of remote_server should not include scap / its x11 dependency
|
||||
{ name = "scap", git = "https://github.com/zed-industries/scap", rev = "808aa5c45b41e8f44729d02e38fd00a2fe2722e7" },
|
||||
# build of remote_server should not need to include on libalsa through rodio
|
||||
{ name = "rodio" },
|
||||
{ name = "rodio", git = "https://github.com/RustAudio/rodio" },
|
||||
]
|
||||
|
||||
[final-excludes]
|
||||
@@ -37,8 +37,6 @@ workspace-members = [
|
||||
"zed_glsl",
|
||||
"zed_html",
|
||||
"zed_proto",
|
||||
"zed_ruff",
|
||||
"slash_commands_example",
|
||||
"zed_snippets",
|
||||
"zed_test_extension",
|
||||
]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
name: Bug Report (Windows Alpha)
|
||||
description: Zed Windows Alpha Related Bugs
|
||||
name: Bug Report (Windows Beta)
|
||||
description: Zed Windows Beta Related Bugs
|
||||
type: "Bug"
|
||||
labels: ["windows"]
|
||||
title: "Windows Alpha: <a short description of the Windows bug>"
|
||||
title: "Windows Beta: <a short description of the Windows bug>"
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
48
.github/workflows/ci.yml
vendored
48
.github/workflows/ci.yml
vendored
@@ -373,6 +373,46 @@ jobs:
|
||||
if: always()
|
||||
run: rm -rf ./../.cargo
|
||||
|
||||
doctests:
|
||||
# Nextest currently doesn't support doctests, so run them separately and in parallel.
|
||||
timeout-minutes: 60
|
||||
name: (Linux) Run doctests
|
||||
needs: [job_spec]
|
||||
if: |
|
||||
github.repository_owner == 'zed-industries' &&
|
||||
needs.job_spec.outputs.run_tests == 'true'
|
||||
runs-on:
|
||||
- namespace-profile-16x32-ubuntu-2204
|
||||
steps:
|
||||
- name: Add Rust to the PATH
|
||||
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
# cache-provider: "buildjet"
|
||||
|
||||
- name: Install Linux dependencies
|
||||
run: ./script/linux
|
||||
|
||||
- name: Configure CI
|
||||
run: |
|
||||
mkdir -p ./../.cargo
|
||||
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
|
||||
|
||||
- name: Run doctests
|
||||
run: cargo test --workspace --doc --no-fail-fast
|
||||
|
||||
- name: Clean CI config file
|
||||
if: always()
|
||||
run: rm -rf ./../.cargo
|
||||
|
||||
build_remote_server:
|
||||
timeout-minutes: 60
|
||||
name: (Linux) Build Remote Server
|
||||
@@ -786,8 +826,9 @@ jobs:
|
||||
timeout-minutes: 120
|
||||
name: Create a Windows installer
|
||||
runs-on: [self-32vcpu-windows-2022]
|
||||
if: contains(github.event.pull_request.labels.*.name, 'run-bundling')
|
||||
# if: (startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling'))
|
||||
if: |
|
||||
( startsWith(github.ref, 'refs/tags/v')
|
||||
|| contains(github.event.pull_request.labels.*.name, 'run-bundling') )
|
||||
needs: [windows_tests]
|
||||
env:
|
||||
AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }}
|
||||
@@ -830,8 +871,7 @@ jobs:
|
||||
|
||||
- name: Upload Artifacts to release
|
||||
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
|
||||
# Re-enable when we are ready to publish windows preview releases
|
||||
if: ${{ !(contains(github.event.pull_request.labels.*.name, 'run-bundling')) && env.RELEASE_CHANNEL == 'preview' }} # upload only preview
|
||||
if: ${{ !(contains(github.event.pull_request.labels.*.name, 'run-bundling')) }}
|
||||
with:
|
||||
draft: true
|
||||
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
||||
|
||||
48
.github/workflows/community_champion_auto_labeler.yml
vendored
Normal file
48
.github/workflows/community_champion_auto_labeler.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
name: Community Champion Auto Labeler
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
label_community_champion:
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check if author is a community champion and apply label
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const communityChampionBody = `${{ secrets.COMMUNITY_CHAMPIONS }}`;
|
||||
|
||||
const communityChampions = communityChampionBody
|
||||
.split('\n')
|
||||
.map(handle => handle.trim().toLowerCase());
|
||||
|
||||
let author;
|
||||
if (context.eventName === 'issues') {
|
||||
author = context.payload.issue.user.login;
|
||||
} else if (context.eventName === 'pull_request_target') {
|
||||
author = context.payload.pull_request.user.login;
|
||||
}
|
||||
|
||||
if (!author || !communityChampions.includes(author.toLowerCase())) {
|
||||
return;
|
||||
}
|
||||
|
||||
const issueNumber = context.payload.issue?.number || context.payload.pull_request?.number;
|
||||
|
||||
try {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
labels: ['community champion']
|
||||
});
|
||||
|
||||
console.log(`Applied 'community champion' label to #${issueNumber} by ${author}`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to apply label: ${error.message}`);
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
# IF YOU UPDATE THE NAME OF ANY GITHUB SECRET, YOU MUST CHERRY PICK THE COMMIT
|
||||
# TO BOTH STABLE AND PREVIEW CHANNELS
|
||||
|
||||
name: Release Actions
|
||||
|
||||
on:
|
||||
@@ -13,9 +16,9 @@ jobs:
|
||||
id: get-release-url
|
||||
run: |
|
||||
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
|
||||
URL="https://zed.dev/releases/preview/latest"
|
||||
URL="https://zed.dev/releases/preview"
|
||||
else
|
||||
URL="https://zed.dev/releases/stable/latest"
|
||||
URL="https://zed.dev/releases/stable"
|
||||
fi
|
||||
|
||||
echo "URL=$URL" >> "$GITHUB_OUTPUT"
|
||||
@@ -36,7 +39,7 @@ jobs:
|
||||
content: ${{ steps.get-content.outputs.string }}
|
||||
|
||||
send_release_notes_email:
|
||||
if: github.repository_owner == 'zed-industries' && !github.event.release.prerelease
|
||||
if: false && github.repository_owner == 'zed-industries' && !github.event.release.prerelease
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
57
.github/workflows/congrats.yml
vendored
Normal file
57
.github/workflows/congrats.yml
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
name: Congratsbot
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
check-author:
|
||||
if: ${{ github.repository_owner == 'zed-industries' }}
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_congratulate: ${{ steps.check.outputs.should_congratulate }}
|
||||
steps:
|
||||
- name: Get PR info and check if author is external
|
||||
id: check
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.CONGRATSBOT_GITHUB_TOKEN }}
|
||||
script: |
|
||||
const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
commit_sha: context.sha
|
||||
});
|
||||
|
||||
if (prs.length === 0) {
|
||||
core.setOutput('should_congratulate', 'false');
|
||||
return;
|
||||
}
|
||||
|
||||
const mergedPR = prs.find(pr => pr.merged_at !== null) || prs[0];
|
||||
const prAuthor = mergedPR.user.login;
|
||||
|
||||
try {
|
||||
await github.rest.teams.getMembershipForUserInOrg({
|
||||
org: 'zed-industries',
|
||||
team_slug: 'staff',
|
||||
username: prAuthor
|
||||
});
|
||||
core.setOutput('should_congratulate', 'false');
|
||||
} catch (error) {
|
||||
if (error.status === 404) {
|
||||
core.setOutput('should_congratulate', 'true');
|
||||
} else {
|
||||
console.error(`Error checking team membership: ${error.message}`);
|
||||
core.setOutput('should_congratulate', 'false');
|
||||
}
|
||||
}
|
||||
|
||||
congrats:
|
||||
needs: check-author
|
||||
if: needs.check-author.outputs.should_congratulate == 'true'
|
||||
uses: withastro/automation/.github/workflows/congratsbot.yml@main
|
||||
with:
|
||||
EMOJIS: 🎉,🎊,🧑🚀,🥳,🙌,🚀,🦀,🔥,🚢
|
||||
secrets:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_CONGRATS }}
|
||||
36
.github/workflows/good_first_issue_notifier.yml
vendored
Normal file
36
.github/workflows/good_first_issue_notifier.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: Good First Issue Notifier
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
||||
jobs:
|
||||
handle-good-first-issue:
|
||||
if: github.event.label.name == 'good first issue' && github.repository_owner == 'zed-industries'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- name: Prepare Discord message
|
||||
id: prepare-message
|
||||
env:
|
||||
ISSUE_TITLE: ${{ github.event.issue.title }}
|
||||
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
ISSUE_URL: ${{ github.event.issue.html_url }}
|
||||
ISSUE_AUTHOR: ${{ github.event.issue.user.login }}
|
||||
run: |
|
||||
MESSAGE="[${ISSUE_TITLE} (#${ISSUE_NUMBER})](<${ISSUE_URL}>)"
|
||||
|
||||
{
|
||||
echo "message<<EOF"
|
||||
echo "$MESSAGE"
|
||||
echo "EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Discord Webhook Action
|
||||
uses: tsickert/discord-webhook@c840d45a03a323fbc3f7507ac7769dbd91bfb164 # v5.3.0
|
||||
with:
|
||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_GOOD_FIRST_ISSUE }}
|
||||
content: ${{ steps.prepare-message.outputs.message }}
|
||||
33
.github/workflows/issue_response.yml
vendored
33
.github/workflows/issue_response.yml
vendored
@@ -1,33 +0,0 @@
|
||||
name: Issue Response
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 12 * * 2"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
issue-response:
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: "20"
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: "script/issue_response/pnpm-lock.yaml"
|
||||
|
||||
- run: pnpm install --dir script/issue_response
|
||||
|
||||
- name: Run Issue Response
|
||||
run: pnpm run --dir script/issue_response start
|
||||
env:
|
||||
ISSUE_RESPONSE_GITHUB_TOKEN: ${{ secrets.ISSUE_RESPONSE_GITHUB_TOKEN }}
|
||||
SLACK_ISSUE_RESPONSE_WEBHOOK_URL: ${{ secrets.SLACK_ISSUE_RESPONSE_WEBHOOK_URL }}
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -20,6 +20,7 @@
|
||||
.venv
|
||||
.vscode
|
||||
.wrangler
|
||||
.perf-runs
|
||||
/assets/*licenses.*
|
||||
/crates/collab/seed.json
|
||||
/crates/theme/schemas/theme.json
|
||||
|
||||
2
.rules
2
.rules
@@ -59,7 +59,7 @@ Trying to update an entity while it's already being updated must be avoided as t
|
||||
|
||||
When `read_with`, `update`, or `update_in` are used with an async context, the closure's return value is wrapped in an `anyhow::Result`.
|
||||
|
||||
`WeakEntity<T>` is a weak handle. It has `read_with`, `update`, and `update_in` methods that work the same, but always return an `anyhow::Result` so that they can fail if the entity no longer exists. This can be useful to avoid memory leaks - if entities have mutually recursive handles to eachother they will never be dropped.
|
||||
`WeakEntity<T>` is a weak handle. It has `read_with`, `update`, and `update_in` methods that work the same, but always return an `anyhow::Result` so that they can fail if the entity no longer exists. This can be useful to avoid memory leaks - if entities have mutually recursive handles to each other they will never be dropped.
|
||||
|
||||
## Concurrency
|
||||
|
||||
|
||||
@@ -1,71 +1,75 @@
|
||||
# Contributing to Zed
|
||||
|
||||
Thanks for your interest in contributing to Zed, the collaborative platform that is also a code editor!
|
||||
Thank you for helping us make Zed better!
|
||||
|
||||
All activity in Zed forums is subject to our [Code of Conduct](https://zed.dev/code-of-conduct). Additionally, contributors must sign our [Contributor License Agreement](https://zed.dev/cla) before their contributions can be merged.
|
||||
All activity in Zed forums is subject to our [Code of
|
||||
Conduct](https://zed.dev/code-of-conduct). Additionally, contributors must sign
|
||||
our [Contributor License Agreement](https://zed.dev/cla) before their
|
||||
contributions can be merged.
|
||||
|
||||
## Contribution ideas
|
||||
|
||||
If you're looking for ideas about what to work on, check out:
|
||||
Zed is a large project with a number of priorities. We spend most of
|
||||
our time working on what we believe the product needs, but we also love working
|
||||
with the community to improve the product in ways we haven't thought of (or had time to get to yet!)
|
||||
|
||||
In particular we love PRs that are:
|
||||
|
||||
- Fixes to existing bugs and issues.
|
||||
- Small enhancements to existing features, particularly to make them work for more people.
|
||||
- Small extra features, like keybindings or actions you miss from other editors or extensions.
|
||||
- Work towards shipping larger features on our roadmap.
|
||||
|
||||
If you're looking for concrete ideas:
|
||||
|
||||
- Our [public roadmap](https://zed.dev/roadmap) contains a rough outline of our near-term priorities for Zed.
|
||||
- Our [top-ranking issues](https://github.com/zed-industries/zed/issues/5393) based on votes by the community.
|
||||
- Our [public roadmap](https://zed.dev/roadmap) contains a rough outline of our near-term priorities for Zed.
|
||||
|
||||
For adding themes or support for a new language to Zed, check out our [docs on developing extensions](https://zed.dev/docs/extensions/developing-extensions).
|
||||
## Sending changes
|
||||
|
||||
## Proposing changes
|
||||
The Zed culture values working code and synchronous conversations over long
|
||||
discussion threads.
|
||||
|
||||
The best way to propose a change is to [start a discussion on our GitHub repository](https://github.com/zed-industries/zed/discussions).
|
||||
The best way to get us to take a look at a proposed change is to send a pull
|
||||
request. We will get back to you (though this sometimes takes longer than we'd
|
||||
like, sorry).
|
||||
|
||||
First, write a short **problem statement**, which _clearly_ and _briefly_ describes the problem you want to solve independently from any specific solution. It doesn't need to be long or formal, but it's difficult to consider a solution in absence of a clear understanding of the problem.
|
||||
Although we will take a look, we tend to only merge about half the PRs that are
|
||||
submitted. If you'd like your PR to have the best chance of being merged:
|
||||
|
||||
Next, write a short **solution proposal**. How can the problem (or set of problems) you have stated above be addressed? What are the pros and cons of your approach? Again, keep it brief and informal. This isn't a specification, but rather a starting point for a conversation.
|
||||
- Include a clear description of what you're solving, and why it's important to you.
|
||||
- Include tests.
|
||||
- If it changes the UI, attach screenshots or screen recordings.
|
||||
|
||||
By effectively engaging with the Zed team and community early in your process, we're better positioned to give you feedback and understand your pull request once you open it. If the first thing we see from you is a big changeset, we're much less likely to respond to it in a timely manner.
|
||||
The internal advice for reviewers is as follows:
|
||||
|
||||
## Pair programming
|
||||
- If the fix/feature is obviously great, and the code is great. Hit merge.
|
||||
- If the fix/feature is obviously great, and the code is nearly great. Send PR comments, or offer to pair to get things perfect.
|
||||
- If the fix/feature is not obviously great, or the code needs rewriting from scratch. Close the PR with a thank you and some explanation.
|
||||
|
||||
We plan to set aside time each week to pair program with contributors on promising pull requests in Zed. This will be an experiment. We tend to prefer pairing over async code review on our team, and we'd like to see how well it works in an open source setting. If we're finding it difficult to get on the same page with async review, we may ask you to pair with us if you're open to it. The closer a contribution is to the goals outlined in our roadmap, the more likely we'll be to spend time pairing on it.
|
||||
If you need more feedback from us: the best way is to be responsive to
|
||||
Github comments, or to offer up time to pair with us.
|
||||
|
||||
## Mandatory PR contents
|
||||
If you are making a larger change, or need advice on how to finish the change
|
||||
you're making, please open the PR early. We would love to help you get
|
||||
things right, and it's often easier to see how to solve a problem before the
|
||||
diff gets too big.
|
||||
|
||||
Please ensure the PR contains
|
||||
## Things we will (probably) not merge
|
||||
|
||||
- Before & after screenshots, if there are visual adjustments introduced.
|
||||
Although there are few hard and fast rules, typically we don't merge:
|
||||
|
||||
Examples of visual adjustments: tree-sitter query updates, UI changes, etc.
|
||||
|
||||
- A disclosure of the AI assistance usage, if any was used.
|
||||
|
||||
Any kind of AI assistance must be disclosed in the PR, along with the extent to which AI assistance was used (e.g. docs only vs. code generation).
|
||||
|
||||
If the PR responses are being generated by an AI, disclose that as well.
|
||||
|
||||
As a small exception, trivial tab-completion doesn't need to be disclosed, as long as it's limited to single keywords or short phrases.
|
||||
|
||||
## Tips to improve the chances of your PR getting reviewed and merged
|
||||
|
||||
- Discuss your plans ahead of time with the team
|
||||
- Small, focused, incremental pull requests are much easier to review
|
||||
- Spend time explaining your changes in the pull request body
|
||||
- Add test coverage and documentation
|
||||
- Choose tasks that align with our roadmap
|
||||
- Pair with us and watch us code to learn the codebase
|
||||
- Low effort PRs, such as those that just re-arrange syntax, won't be merged without a compelling justification
|
||||
|
||||
## File icons
|
||||
|
||||
Zed's default icon theme consists of icons that are hand-designed to fit together in a cohesive manner.
|
||||
|
||||
We do not accept PRs for file icons that are just an off-the-shelf SVG taken from somewhere else.
|
||||
|
||||
### Adding new icons to the Zed icon theme
|
||||
|
||||
If you would like to add a new icon to the Zed icon theme, [open a Discussion](https://github.com/zed-industries/zed/discussions/new?category=ux-and-design) and we can work with you on getting an icon designed and added to Zed.
|
||||
- Anything that can be provided by an extension. For example a new language, or theme. For adding themes or support for a new language to Zed, check out our [docs on developing extensions](https://zed.dev/docs/extensions/developing-extensions).
|
||||
- New file icons. Zed's default icon theme consists of icons that are hand-designed to fit together in a cohesive manner, please don't submit PRs with off-the-shelf SVGs.
|
||||
- Giant refactorings.
|
||||
- Non-trivial changes with no tests.
|
||||
- Stylistic code changes that do not alter any app logic. Reducing allocations, removing `.unwrap()`s, fixing typos is great; making code "more readable" — maybe not so much.
|
||||
- Features where (in our subjective opinion) the extra complexity isn't worth it for the number of people who will benefit.
|
||||
- Anything that seems completely AI generated.
|
||||
|
||||
## Bird's-eye view of Zed
|
||||
|
||||
We suggest you keep the [zed glossary](docs/src/development/glossary.md) at your side when starting out. It lists and explains some of the structures and terms you will see throughout the codebase.
|
||||
We suggest you keep the [Zed glossary](docs/src/development/glossary.md) at your side when starting out. It lists and explains some of the structures and terms you will see throughout the codebase.
|
||||
|
||||
Zed is made up of several smaller crates - let's go over those you're most likely to interact with:
|
||||
|
||||
|
||||
2702
Cargo.lock
generated
2702
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
57
Cargo.toml
57
Cargo.toml
@@ -35,6 +35,7 @@ members = [
|
||||
"crates/cloud_api_client",
|
||||
"crates/cloud_api_types",
|
||||
"crates/cloud_llm_client",
|
||||
"crates/cloud_zeta2_prompt",
|
||||
"crates/collab",
|
||||
"crates/collab_ui",
|
||||
"crates/collections",
|
||||
@@ -52,10 +53,13 @@ members = [
|
||||
"crates/debugger_tools",
|
||||
"crates/debugger_ui",
|
||||
"crates/deepseek",
|
||||
"crates/denoise",
|
||||
"crates/diagnostics",
|
||||
"crates/docs_preprocessor",
|
||||
"crates/edit_prediction",
|
||||
"crates/edit_prediction_button",
|
||||
"crates/edit_prediction_context",
|
||||
"crates/zeta2_tools",
|
||||
"crates/editor",
|
||||
"crates/eval",
|
||||
"crates/explorer_command_injector",
|
||||
@@ -86,9 +90,8 @@ members = [
|
||||
"crates/image_viewer",
|
||||
"crates/inspector_ui",
|
||||
"crates/install_cli",
|
||||
"crates/jj",
|
||||
"crates/jj_ui",
|
||||
"crates/journal",
|
||||
"crates/json_schema_store",
|
||||
"crates/keymap_editor",
|
||||
"crates/language",
|
||||
"crates/language_extension",
|
||||
@@ -147,9 +150,9 @@ members = [
|
||||
"crates/semantic_version",
|
||||
"crates/session",
|
||||
"crates/settings",
|
||||
"crates/settings_macros",
|
||||
"crates/settings_profile_selector",
|
||||
"crates/settings_ui",
|
||||
"crates/settings_ui_macros",
|
||||
"crates/snippet",
|
||||
"crates/snippet_provider",
|
||||
"crates/snippets_ui",
|
||||
@@ -197,6 +200,7 @@ members = [
|
||||
"crates/zed_actions",
|
||||
"crates/zed_env_vars",
|
||||
"crates/zeta",
|
||||
"crates/zeta2",
|
||||
"crates/zeta_cli",
|
||||
"crates/zlog",
|
||||
"crates/zlog_settings",
|
||||
@@ -208,15 +212,14 @@ members = [
|
||||
"extensions/glsl",
|
||||
"extensions/html",
|
||||
"extensions/proto",
|
||||
"extensions/ruff",
|
||||
"extensions/slash-commands-example",
|
||||
"extensions/snippets",
|
||||
"extensions/test-extension",
|
||||
|
||||
#
|
||||
# Tooling
|
||||
#
|
||||
|
||||
"tooling/perf",
|
||||
"tooling/workspace-hack",
|
||||
"tooling/xtask",
|
||||
]
|
||||
@@ -267,6 +270,7 @@ clock = { path = "crates/clock" }
|
||||
cloud_api_client = { path = "crates/cloud_api_client" }
|
||||
cloud_api_types = { path = "crates/cloud_api_types" }
|
||||
cloud_llm_client = { path = "crates/cloud_llm_client" }
|
||||
cloud_zeta2_prompt = { path = "crates/cloud_zeta2_prompt" }
|
||||
collab = { path = "crates/collab" }
|
||||
collab_ui = { path = "crates/collab_ui" }
|
||||
collections = { path = "crates/collections" }
|
||||
@@ -277,6 +281,7 @@ context_server = { path = "crates/context_server" }
|
||||
copilot = { path = "crates/copilot" }
|
||||
crashes = { path = "crates/crashes" }
|
||||
credentials_provider = { path = "crates/credentials_provider" }
|
||||
crossbeam = "0.8.4"
|
||||
dap = { path = "crates/dap" }
|
||||
dap_adapters = { path = "crates/dap_adapters" }
|
||||
db = { path = "crates/db" }
|
||||
@@ -311,11 +316,12 @@ icons = { path = "crates/icons" }
|
||||
image_viewer = { path = "crates/image_viewer" }
|
||||
edit_prediction = { path = "crates/edit_prediction" }
|
||||
edit_prediction_button = { path = "crates/edit_prediction_button" }
|
||||
edit_prediction_context = { path = "crates/edit_prediction_context" }
|
||||
zeta2_tools = { path = "crates/zeta2_tools" }
|
||||
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" }
|
||||
json_schema_store = { path = "crates/json_schema_store" }
|
||||
keymap_editor = { path = "crates/keymap_editor" }
|
||||
language = { path = "crates/language" }
|
||||
language_extension = { path = "crates/language_extension" }
|
||||
@@ -350,6 +356,7 @@ outline = { path = "crates/outline" }
|
||||
outline_panel = { path = "crates/outline_panel" }
|
||||
panel = { path = "crates/panel" }
|
||||
paths = { path = "crates/paths" }
|
||||
perf = { path = "tooling/perf" }
|
||||
picker = { path = "crates/picker" }
|
||||
plugin = { path = "crates/plugin" }
|
||||
plugin_macros = { path = "crates/plugin_macros" }
|
||||
@@ -369,7 +376,7 @@ remote_server = { path = "crates/remote_server" }
|
||||
repl = { path = "crates/repl" }
|
||||
reqwest_client = { path = "crates/reqwest_client" }
|
||||
rich_text = { path = "crates/rich_text" }
|
||||
rodio = { version = "0.21.1", default-features = false }
|
||||
rodio = { git = "https://github.com/RustAudio/rodio" }
|
||||
rope = { path = "crates/rope" }
|
||||
rpc = { path = "crates/rpc" }
|
||||
rules_library = { path = "crates/rules_library" }
|
||||
@@ -377,8 +384,8 @@ search = { path = "crates/search" }
|
||||
semantic_version = { path = "crates/semantic_version" }
|
||||
session = { path = "crates/session" }
|
||||
settings = { path = "crates/settings" }
|
||||
settings_macros = { path = "crates/settings_macros" }
|
||||
settings_ui = { path = "crates/settings_ui" }
|
||||
settings_ui_macros = { path = "crates/settings_ui_macros" }
|
||||
snippet = { path = "crates/snippet" }
|
||||
snippet_provider = { path = "crates/snippet_provider" }
|
||||
snippets_ui = { path = "crates/snippets_ui" }
|
||||
@@ -426,6 +433,7 @@ zed = { path = "crates/zed" }
|
||||
zed_actions = { path = "crates/zed_actions" }
|
||||
zed_env_vars = { path = "crates/zed_env_vars" }
|
||||
zeta = { path = "crates/zeta" }
|
||||
zeta2 = { path = "crates/zeta2" }
|
||||
zlog = { path = "crates/zlog" }
|
||||
zlog_settings = { path = "crates/zlog_settings" }
|
||||
|
||||
@@ -433,9 +441,9 @@ zlog_settings = { path = "crates/zlog_settings" }
|
||||
# External crates
|
||||
#
|
||||
|
||||
agent-client-protocol = { version = "0.2.0-alpha.8", features = ["unstable"] }
|
||||
agent-client-protocol = { version = "0.4.3", features = ["unstable"] }
|
||||
aho-corasick = "1.1"
|
||||
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
|
||||
alacritty_terminal = "0.25.1-rc1"
|
||||
any_vec = "0.14"
|
||||
anyhow = "1.0.86"
|
||||
arrayvec = { version = "0.7.4", features = ["serde"] }
|
||||
@@ -464,13 +472,14 @@ backtrace = "0.3"
|
||||
base64 = "0.22"
|
||||
bincode = "1.2.1"
|
||||
bitflags = "2.6.0"
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "bfa594ea697d4b6326ea29f747525c85ecf933b9" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "bfa594ea697d4b6326ea29f747525c85ecf933b9" }
|
||||
blade-util = { git = "https://github.com/kvark/blade", rev = "bfa594ea697d4b6326ea29f747525c85ecf933b9" }
|
||||
blade-graphics = { version = "0.7.0" }
|
||||
blade-macros = { version = "0.3.0" }
|
||||
blade-util = { version = "0.3.0" }
|
||||
blake3 = "1.5.3"
|
||||
bytes = "1.0"
|
||||
cargo_metadata = "0.19"
|
||||
cargo_toml = "0.21"
|
||||
cfg-if = "1.0.3"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
ciborium = "0.2"
|
||||
circular-buffer = "1.0"
|
||||
@@ -503,6 +512,7 @@ futures-lite = "1.13"
|
||||
git2 = { version = "0.20.1", default-features = false }
|
||||
globset = "0.4"
|
||||
handlebars = "4.3"
|
||||
hashbrown = "0.15.3"
|
||||
heck = "0.5"
|
||||
heed = { version = "0.21.0", features = ["read-txn-no-tls"] }
|
||||
hex = "0.4.3"
|
||||
@@ -518,7 +528,6 @@ 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"
|
||||
@@ -539,6 +548,7 @@ nanoid = "0.4"
|
||||
nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
|
||||
nix = "0.29"
|
||||
num-format = "0.4.4"
|
||||
num-traits = "0.2"
|
||||
objc = "0.2"
|
||||
objc2-foundation = { version = "0.3", default-features = false, features = [
|
||||
"NSArray",
|
||||
@@ -580,6 +590,7 @@ pet-fs = { git = "https://github.com/microsoft/python-environment-tools.git", re
|
||||
pet-pixi = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
|
||||
pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
|
||||
pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
|
||||
pet-virtualenv = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
|
||||
portable-pty = "0.9.0"
|
||||
postage = { version = "0.5", features = ["futures-traits"] }
|
||||
pretty_assertions = { version = "1.3.0", features = ["unstable"] }
|
||||
@@ -608,16 +619,14 @@ runtimelib = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804
|
||||
"async-dispatcher-runtime",
|
||||
] }
|
||||
rust-embed = { version = "8.4", features = ["include-exclude"] }
|
||||
rustc-demangle = "0.1.23"
|
||||
rustc-hash = "2.1.0"
|
||||
rustls = { version = "0.23.26" }
|
||||
rustls-platform-verifier = "0.5.0"
|
||||
scap = { git = "https://github.com/zed-industries/scap", rev = "808aa5c45b41e8f44729d02e38fd00a2fe2722e7", default-features = false }
|
||||
schemars = { version = "1.0", features = ["indexmap2"] }
|
||||
semver = "1.0"
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
|
||||
serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
|
||||
serde = { version = "1.0.221", features = ["derive", "rc"] }
|
||||
serde_json = { version = "1.0.144", features = ["preserve_order", "raw_value"] }
|
||||
serde_json_lenient = { version = "0.2", features = [
|
||||
"preserve_order",
|
||||
"raw_value",
|
||||
@@ -625,10 +634,12 @@ serde_json_lenient = { version = "0.2", features = [
|
||||
serde_path_to_error = "0.1.17"
|
||||
serde_repr = "0.1"
|
||||
serde_urlencoded = "0.7"
|
||||
serde_with = "3.4.0"
|
||||
sha2 = "0.10"
|
||||
shellexpand = "2.1.0"
|
||||
shlex = "1.3.0"
|
||||
simplelog = "0.12.2"
|
||||
slotmap = "1.0.6"
|
||||
smallvec = { version = "1.6", features = ["union"] }
|
||||
smol = "2.0"
|
||||
sqlformat = "0.2"
|
||||
@@ -656,7 +667,7 @@ tokio = { version = "1" }
|
||||
tokio-tungstenite = { version = "0.26", features = ["__rustls-tls"] }
|
||||
toml = "0.8"
|
||||
tower-http = "0.4.4"
|
||||
tree-sitter = { version = "0.25.6", features = ["wasm"] }
|
||||
tree-sitter = { version = "0.25.10", features = ["wasm"] }
|
||||
tree-sitter-bash = "0.25.0"
|
||||
tree-sitter-c = "0.23"
|
||||
tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "5cb9b693cfd7bfacab1d9ff4acac1a4150700609" }
|
||||
@@ -673,7 +684,7 @@ tree-sitter-html = "0.23"
|
||||
tree-sitter-jsdoc = "0.23"
|
||||
tree-sitter-json = "0.24"
|
||||
tree-sitter-md = { git = "https://github.com/tree-sitter-grammars/tree-sitter-markdown", rev = "9a23c1a96c0513d8fc6520972beedd419a973539" }
|
||||
tree-sitter-python = { git = "https://github.com/zed-industries/tree-sitter-python", rev = "218fcbf3fda3d029225f3dec005cb497d111b35e" }
|
||||
tree-sitter-python = "0.25"
|
||||
tree-sitter-regex = "0.24"
|
||||
tree-sitter-ruby = "0.23"
|
||||
tree-sitter-rust = "0.24"
|
||||
@@ -704,6 +715,7 @@ windows-core = "0.61"
|
||||
wit-component = "0.221"
|
||||
workspace-hack = "0.1.0"
|
||||
yawc = "0.2.5"
|
||||
zeroize = "1.8"
|
||||
zstd = "0.11"
|
||||
|
||||
[workspace.dependencies.windows]
|
||||
@@ -730,6 +742,7 @@ features = [
|
||||
"Win32_Networking_WinSock",
|
||||
"Win32_Security",
|
||||
"Win32_Security_Credentials",
|
||||
"Win32_Security_Cryptography",
|
||||
"Win32_Storage_FileSystem",
|
||||
"Win32_System_Com",
|
||||
"Win32_System_Com_StructuredStorage",
|
||||
@@ -798,6 +811,7 @@ image_viewer = { codegen-units = 1 }
|
||||
edit_prediction_button = { codegen-units = 1 }
|
||||
install_cli = { codegen-units = 1 }
|
||||
journal = { codegen-units = 1 }
|
||||
json_schema_store = { codegen-units = 1 }
|
||||
lmstudio = { codegen-units = 1 }
|
||||
menu = { codegen-units = 1 }
|
||||
notifications = { codegen-units = 1 }
|
||||
@@ -849,6 +863,7 @@ todo = "deny"
|
||||
declare_interior_mutable_const = "deny"
|
||||
|
||||
redundant_clone = "deny"
|
||||
disallowed_methods = "deny"
|
||||
|
||||
# We currently do not restrict any style rules
|
||||
# as it slows down shipping code to Zed.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax = docker/dockerfile:1.2
|
||||
|
||||
FROM rust:1.89-bookworm as builder
|
||||
FROM rust:1.90-bookworm as builder
|
||||
WORKDIR app
|
||||
COPY . .
|
||||
|
||||
|
||||
11
assets/icons/linux.svg
Normal file
11
assets/icons/linux.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_3010_383)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.71141 7.06133C3.76141 6.47267 3.78341 5.88133 3.81608 5.29133C4.10416 0.190201 11.896 0.190202 12.1841 5.29133C12.2174 5.898 12.2441 6.50333 12.3067 7.10733C12.6951 7.94202 14.3637 11.6214 13.4134 12.006C13.1894 12.096 12.8041 11.7227 12.3694 11.052C12.207 11.9614 11.7273 12.8132 11.0587 13.4467C11.7441 13.68 12.3334 13.998 12.3334 14.3333C12.3334 14.9176 3.66675 14.9257 3.66675 14.3333C3.66675 13.998 4.25608 13.68 4.94141 13.4467C4.26191 12.803 3.82279 11.9657 3.62408 11.056C3.19075 11.724 2.80608 12.096 2.58341 12.006C1.626 11.6185 3.31478 7.90684 3.71141 7.06133Z" stroke="#7B7B7B" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6.11822 6.6L7.68822 7.89C7.85822 8.03 8.12822 8.03 8.29822 7.89L9.86822 6.6C10.1382 6.38 9.94822 6 9.56822 6H6.42822C6.04822 6 5.85822 6.38 6.12822 6.6H6.11822Z" fill="#7B7B7B"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3010_383">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
3
assets/icons/paperclip.svg
Normal file
3
assets/icons/paperclip.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.1645 4.45825L5.20344 9.52074C4.98225 9.74193 4.85798 10.0419 4.85798 10.3548C4.85798 10.6676 4.98225 10.9676 5.20344 11.1888C5.42464 11.41 5.72464 11.5342 6.03746 11.5342C6.35028 11.5342 6.65028 11.41 6.87148 11.1888L11.8326 6.12629C12.2749 5.68397 12.5234 5.08407 12.5234 4.45854C12.5234 3.83302 12.2749 3.23311 11.8326 2.7908C11.3902 2.34849 10.7903 2.1 10.1648 2.1C9.53928 2.1 8.93938 2.34849 8.49707 2.7908L3.55663 7.83265C3.22373 8.16017 2.95897 8.55037 2.77762 8.98072C2.59628 9.41108 2.50193 9.87308 2.50003 10.3401C2.49813 10.8071 2.58871 11.2698 2.76654 11.7017C2.94438 12.1335 3.20595 12.5258 3.53618 12.856C3.8664 13.1863 4.25873 13.4478 4.69055 13.6257C5.12237 13.8035 5.58513 13.8941 6.05213 13.8922C6.51913 13.8903 6.98114 13.7959 7.41149 13.6146C7.84185 13.4332 8.23204 13.1685 8.55957 12.8356L13.5 7.79373" stroke="#C4CAD4" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -250,7 +250,7 @@
|
||||
"alt-enter": "agent::ContinueWithBurnMode",
|
||||
"ctrl-y": "agent::AllowOnce",
|
||||
"ctrl-alt-y": "agent::AllowAlways",
|
||||
"ctrl-d": "agent::RejectOnce"
|
||||
"ctrl-alt-z": "agent::RejectOnce"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -462,8 +462,8 @@
|
||||
"ctrl-k ctrl-w": "workspace::CloseAllItemsAndPanes",
|
||||
"back": "pane::GoBack",
|
||||
"ctrl-alt--": "pane::GoBack",
|
||||
"ctrl-alt-_": "pane::GoForward",
|
||||
"forward": "pane::GoForward",
|
||||
"ctrl-alt-_": "pane::GoForward",
|
||||
"ctrl-alt-g": "search::SelectNextMatch",
|
||||
"f3": "search::SelectNextMatch",
|
||||
"ctrl-alt-shift-g": "search::SelectPreviousMatch",
|
||||
@@ -649,7 +649,9 @@
|
||||
"ctrl-k shift-up": "workspace::SwapPaneUp",
|
||||
"ctrl-k shift-down": "workspace::SwapPaneDown",
|
||||
"ctrl-shift-x": "zed::Extensions",
|
||||
"ctrl-shift-r": "task::Rerun",
|
||||
// All task parameters are captured and unchanged between reruns by default.
|
||||
// Use the `"reevaluate_context"` parameter to control this.
|
||||
"ctrl-shift-r": ["task::Rerun", { "reevaluate_context": false }],
|
||||
"ctrl-alt-r": "task::Rerun",
|
||||
"alt-t": "task::Rerun",
|
||||
"alt-shift-t": "task::Spawn",
|
||||
@@ -1073,6 +1075,12 @@
|
||||
"ctrl-backspace": "tab_switcher::CloseSelectedItem"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "StashList || (StashList > Picker > Editor)",
|
||||
"bindings": {
|
||||
"ctrl-shift-backspace": "stash_picker::DropStashItem"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Terminal",
|
||||
"bindings": {
|
||||
@@ -1132,6 +1140,13 @@
|
||||
"ctrl-enter": "menu::Confirm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ContextServerToolsModal",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "OnboardingAiConfigurationModal",
|
||||
"use_key_equivalents": true,
|
||||
|
||||
@@ -289,7 +289,7 @@
|
||||
"alt-enter": "agent::ContinueWithBurnMode",
|
||||
"cmd-y": "agent::AllowOnce",
|
||||
"cmd-alt-y": "agent::AllowAlways",
|
||||
"cmd-d": "agent::RejectOnce"
|
||||
"cmd-alt-z": "agent::RejectOnce"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -550,6 +550,8 @@
|
||||
"cmd-ctrl-left": "editor::SelectSmallerSyntaxNode", // Shrink selection
|
||||
"cmd-ctrl-right": "editor::SelectLargerSyntaxNode", // Expand selection
|
||||
"cmd-ctrl-up": "editor::SelectPreviousSyntaxNode", // Move selection up
|
||||
"ctrl-shift-right": "editor::SelectLargerSyntaxNode", // Expand selection (VSCode version)
|
||||
"ctrl-shift-left": "editor::SelectSmallerSyntaxNode", // Shrink selection (VSCode version)
|
||||
"cmd-ctrl-down": "editor::SelectNextSyntaxNode", // Move selection down
|
||||
"cmd-d": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch / find_under_expand
|
||||
"cmd-shift-l": "editor::SelectAllMatches", // Select all occurrences of current selection
|
||||
@@ -724,7 +726,9 @@
|
||||
"bindings": {
|
||||
"cmd-n": "workspace::NewFile",
|
||||
"cmd-shift-r": "task::Spawn",
|
||||
"cmd-alt-r": "task::Rerun",
|
||||
// All task parameters are captured and unchanged between reruns by default.
|
||||
// Use the `"reevaluate_context"` parameter to control this.
|
||||
"cmd-alt-r": ["task::Rerun", { "reevaluate_context": false }],
|
||||
"ctrl-alt-shift-r": ["task::Spawn", { "reveal_target": "center" }]
|
||||
// also possible to spawn tasks by name:
|
||||
// "foo-bar": ["task::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }]
|
||||
@@ -1144,6 +1148,13 @@
|
||||
"ctrl-backspace": "tab_switcher::CloseSelectedItem"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "StashList || (StashList > Picker > Editor)",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-shift-backspace": "stash_picker::DropStashItem"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Terminal",
|
||||
"use_key_equivalents": true,
|
||||
@@ -1235,6 +1246,13 @@
|
||||
"cmd-enter": "menu::Confirm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ContextServerToolsModal",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "OnboardingAiConfigurationModal",
|
||||
"use_key_equivalents": true,
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
"up": "menu::SelectPrevious",
|
||||
"enter": "menu::Confirm",
|
||||
"ctrl-enter": "menu::SecondaryConfirm",
|
||||
"ctrl-escape": "menu::Cancel",
|
||||
"ctrl-c": "menu::Cancel",
|
||||
"escape": "menu::Cancel",
|
||||
"shift-alt-enter": "menu::Restart",
|
||||
@@ -252,7 +251,7 @@
|
||||
"alt-enter": "agent::ContinueWithBurnMode",
|
||||
"ctrl-y": "agent::AllowOnce",
|
||||
"ctrl-alt-y": "agent::AllowAlways",
|
||||
"ctrl-d": "agent::RejectOnce"
|
||||
"ctrl-alt-z": "agent::RejectOnce"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -346,7 +345,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AcpThread > Editor",
|
||||
"context": "AcpThread > Editor && !use_modifier_to_send",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "agent::Chat",
|
||||
@@ -356,6 +355,17 @@
|
||||
"shift-tab": "agent::CycleModeSelector"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AcpThread > Editor && use_modifier_to_send",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-enter": "agent::Chat",
|
||||
"ctrl-shift-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
"shift-tab": "agent::CycleModeSelector"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ThreadHistory",
|
||||
"use_key_equivalents": true,
|
||||
@@ -465,8 +475,8 @@
|
||||
"ctrl-k ctrl-w": "workspace::CloseAllItemsAndPanes",
|
||||
"back": "pane::GoBack",
|
||||
"alt--": "pane::GoBack",
|
||||
"alt-=": "pane::GoForward",
|
||||
"forward": "pane::GoForward",
|
||||
"alt-=": "pane::GoForward",
|
||||
"f3": "search::SelectNextMatch",
|
||||
"shift-f3": "search::SelectPreviousMatch",
|
||||
"ctrl-shift-f": "project_search::ToggleFocus",
|
||||
@@ -497,8 +507,6 @@
|
||||
"shift-alt-down": "editor::DuplicateLineDown",
|
||||
"shift-alt-right": "editor::SelectLargerSyntaxNode", // Expand selection
|
||||
"shift-alt-left": "editor::SelectSmallerSyntaxNode", // Shrink selection
|
||||
"ctrl-shift-right": "editor::SelectLargerSyntaxNode", // Expand selection (VSCode version)
|
||||
"ctrl-shift-left": "editor::SelectSmallerSyntaxNode", // Shrink selection (VSCode version)
|
||||
"ctrl-shift-l": "editor::SelectAllMatches", // Select all occurrences of current selection
|
||||
"ctrl-f2": "editor::SelectAllMatches", // Select all occurrences of current word
|
||||
"ctrl-d": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch / find_under_expand
|
||||
@@ -610,8 +618,6 @@
|
||||
"shift-alt--": ["workspace::DecreaseActiveDockSize", { "px": 0 }],
|
||||
"shift-alt-=": ["workspace::IncreaseActiveDockSize", { "px": 0 }],
|
||||
"shift-alt-0": "workspace::ResetOpenDocksSize",
|
||||
"ctrl-shift-alt--": ["workspace::DecreaseOpenDocksSize", { "px": 0 }],
|
||||
"ctrl-shift-alt-=": ["workspace::IncreaseOpenDocksSize", { "px": 0 }],
|
||||
"ctrl-shift-f": "pane::DeploySearch",
|
||||
"ctrl-shift-h": ["pane::DeploySearch", { "replace_enabled": true }],
|
||||
"ctrl-shift-t": "pane::ReopenClosedItem",
|
||||
@@ -644,7 +650,9 @@
|
||||
"ctrl-k shift-up": "workspace::SwapPaneUp",
|
||||
"ctrl-k shift-down": "workspace::SwapPaneDown",
|
||||
"ctrl-shift-x": "zed::Extensions",
|
||||
"ctrl-shift-r": "task::Rerun",
|
||||
// All task parameters are captured and unchanged between reruns by default.
|
||||
// Use the `"reevaluate_context"` parameter to control this.
|
||||
"ctrl-shift-r": ["task::Rerun", { "reevaluate_context": false }],
|
||||
"alt-t": "task::Rerun",
|
||||
"shift-alt-t": "task::Spawn",
|
||||
"shift-alt-r": ["task::Spawn", { "reveal_target": "center" }],
|
||||
@@ -1092,6 +1100,13 @@
|
||||
"ctrl-backspace": "tab_switcher::CloseSelectedItem"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "StashList || (StashList > Picker > Editor)",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-shift-backspace": "stash_picker::DropStashItem"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Terminal",
|
||||
"use_key_equivalents": true,
|
||||
@@ -1106,6 +1121,7 @@
|
||||
"alt-f": ["terminal::SendText", "\u001bf"],
|
||||
"alt-.": ["terminal::SendText", "\u001b."],
|
||||
"ctrl-delete": ["terminal::SendText", "\u001bd"],
|
||||
"ctrl-n": "workspace::NewTerminal",
|
||||
// Overrides for conflicting keybindings
|
||||
"ctrl-b": ["terminal::SendKeystroke", "ctrl-b"],
|
||||
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"],
|
||||
@@ -1151,6 +1167,13 @@
|
||||
"ctrl-enter": "menu::Confirm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ContextServerToolsModal",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "OnboardingAiConfigurationModal",
|
||||
"use_key_equivalents": true,
|
||||
@@ -1235,8 +1258,8 @@
|
||||
"ctrl-1": "onboarding::ActivateBasicsPage",
|
||||
"ctrl-2": "onboarding::ActivateEditingPage",
|
||||
"ctrl-3": "onboarding::ActivateAISetupPage",
|
||||
"ctrl-escape": "onboarding::Finish",
|
||||
"alt-tab": "onboarding::SignIn",
|
||||
"ctrl-enter": "onboarding::Finish",
|
||||
"alt-shift-l": "onboarding::SignIn",
|
||||
"shift-alt-a": "onboarding::OpenAccount"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
// from the command palette.
|
||||
[
|
||||
{
|
||||
"context": "!GitPanel",
|
||||
"bindings": {
|
||||
"ctrl-g": "menu::Cancel"
|
||||
}
|
||||
|
||||
@@ -95,8 +95,8 @@
|
||||
"g g": "vim::StartOfDocument",
|
||||
"g h": "editor::Hover",
|
||||
"g B": "editor::BlameHover",
|
||||
"g t": "pane::ActivateNextItem",
|
||||
"g shift-t": "pane::ActivatePreviousItem",
|
||||
"g t": "vim::GoToTab",
|
||||
"g shift-t": "vim::GoToPreviousTab",
|
||||
"g d": "editor::GoToDefinition",
|
||||
"g shift-d": "editor::GoToDeclaration",
|
||||
"g y": "editor::GoToTypeDefinition",
|
||||
@@ -240,6 +240,7 @@
|
||||
"delete": "vim::DeleteRight",
|
||||
"g shift-j": "vim::JoinLinesNoWhitespace",
|
||||
"y": "vim::PushYank",
|
||||
"shift-y": "vim::YankToEndOfLine",
|
||||
"x": "vim::DeleteRight",
|
||||
"shift-x": "vim::DeleteLeft",
|
||||
"ctrl-a": "vim::Increment",
|
||||
@@ -325,6 +326,27 @@
|
||||
"\"": "vim::PushRegister"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == helix_select",
|
||||
"bindings": {
|
||||
"v": "vim::NormalBefore",
|
||||
";": "vim::HelixCollapseSelection",
|
||||
"~": "vim::ChangeCase",
|
||||
"ctrl-a": "vim::Increment",
|
||||
"ctrl-x": "vim::Decrement",
|
||||
"shift-j": "vim::JoinLines",
|
||||
"i": "vim::InsertBefore",
|
||||
"a": "vim::InsertAfter",
|
||||
"p": "vim::Paste",
|
||||
"u": "vim::Undo",
|
||||
"r": "vim::PushReplace",
|
||||
"s": "vim::Substitute",
|
||||
"ctrl-pageup": "pane::ActivatePreviousItem",
|
||||
"ctrl-pagedown": "pane::ActivateNextItem",
|
||||
".": "vim::Repeat",
|
||||
"alt-.": "vim::RepeatFind"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == insert",
|
||||
"bindings": {
|
||||
@@ -371,7 +393,7 @@
|
||||
"escape": "editor::Cancel",
|
||||
"shift-d": "vim::DeleteToEndOfLine",
|
||||
"shift-j": "vim::JoinLines",
|
||||
"shift-y": "vim::YankLine",
|
||||
"shift-y": "vim::YankToEndOfLine",
|
||||
"shift-i": "vim::InsertFirstNonWhitespace",
|
||||
"shift-a": "vim::InsertEndOfLine",
|
||||
"o": "vim::InsertLineBelow",
|
||||
@@ -396,10 +418,16 @@
|
||||
"bindings": {
|
||||
"i": "vim::HelixInsert",
|
||||
"a": "vim::HelixAppend",
|
||||
"ctrl-[": "editor::Cancel",
|
||||
"ctrl-[": "editor::Cancel"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "(vim_mode == helix_normal || vim_mode == helix_select) && !menu",
|
||||
"bindings": {
|
||||
";": "vim::HelixCollapseSelection",
|
||||
":": "command_palette::Toggle",
|
||||
"m": "vim::PushHelixMatch",
|
||||
"s": "vim::HelixSelectRegex",
|
||||
"]": ["vim::PushHelixNext", { "around": true }],
|
||||
"[": ["vim::PushHelixPrevious", { "around": true }],
|
||||
"left": "vim::WrappingLeft",
|
||||
@@ -407,6 +435,8 @@
|
||||
"h": "vim::WrappingLeft",
|
||||
"l": "vim::WrappingRight",
|
||||
"y": "vim::HelixYank",
|
||||
"p": "vim::HelixPaste",
|
||||
"shift-p": ["vim::HelixPaste", { "before": true }],
|
||||
"alt-;": "vim::OtherEnd",
|
||||
"ctrl-r": "vim::Redo",
|
||||
"f": ["vim::PushFindForward", { "before": false, "multiline": true }],
|
||||
@@ -416,9 +446,8 @@
|
||||
">": "vim::Indent",
|
||||
"<": "vim::Outdent",
|
||||
"=": "vim::AutoIndent",
|
||||
"g u": "vim::PushLowercase",
|
||||
"g shift-u": "vim::PushUppercase",
|
||||
"g ~": "vim::PushOppositeCase",
|
||||
"`": "vim::ConvertToLowerCase",
|
||||
"alt-`": "vim::ConvertToUpperCase",
|
||||
"g q": "vim::PushRewrap",
|
||||
"g w": "vim::PushRewrap",
|
||||
"insert": "vim::InsertBefore",
|
||||
@@ -855,10 +884,12 @@
|
||||
"/": "project_panel::NewSearchInDirectory",
|
||||
"d": "project_panel::NewDirectory",
|
||||
"enter": "project_panel::OpenPermanent",
|
||||
"escape": "project_panel::ToggleFocus",
|
||||
"escape": "vim::ToggleProjectPanelFocus",
|
||||
"h": "project_panel::CollapseSelectedEntry",
|
||||
"j": "menu::SelectNext",
|
||||
"k": "menu::SelectPrevious",
|
||||
"j": "vim::MenuSelectNext",
|
||||
"k": "vim::MenuSelectPrevious",
|
||||
"down": "vim::MenuSelectNext",
|
||||
"up": "vim::MenuSelectPrevious",
|
||||
"l": "project_panel::ExpandSelectedEntry",
|
||||
"shift-d": "project_panel::Delete",
|
||||
"shift-r": "project_panel::Rename",
|
||||
@@ -877,7 +908,22 @@
|
||||
"{": "project_panel::SelectPrevDirectory",
|
||||
"shift-g": "menu::SelectLast",
|
||||
"g g": "menu::SelectFirst",
|
||||
"-": "project_panel::SelectParent"
|
||||
"-": "project_panel::SelectParent",
|
||||
"ctrl-u": "project_panel::ScrollUp",
|
||||
"ctrl-d": "project_panel::ScrollDown",
|
||||
"z t": "project_panel::ScrollCursorTop",
|
||||
"z z": "project_panel::ScrollCursorCenter",
|
||||
"z b": "project_panel::ScrollCursorBottom",
|
||||
"0": ["vim::Number", 0],
|
||||
"1": ["vim::Number", 1],
|
||||
"2": ["vim::Number", 2],
|
||||
"3": ["vim::Number", 3],
|
||||
"4": ["vim::Number", 4],
|
||||
"5": ["vim::Number", 5],
|
||||
"6": ["vim::Number", 6],
|
||||
"7": ["vim::Number", 7],
|
||||
"8": ["vim::Number", 8],
|
||||
"9": ["vim::Number", 9]
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -29,7 +29,9 @@ Generate {{content_type}} based on the following prompt:
|
||||
|
||||
Match the indentation in the original file in the inserted {{content_type}}, don't include any indentation on blank lines.
|
||||
|
||||
Immediately start with the following format with no remarks:
|
||||
Return ONLY the {{content_type}} to insert. Do NOT include any XML tags like <document>, <insert_here>, or any surrounding markup from the input.
|
||||
|
||||
Respond with a code block containing the {{content_type}} to insert. Replace \{{INSERTED_CODE}} with your actual {{content_type}}:
|
||||
|
||||
```
|
||||
\{{INSERTED_CODE}}
|
||||
@@ -66,7 +68,9 @@ Only make changes that are necessary to fulfill the prompt, leave everything els
|
||||
|
||||
Start at the indentation level in the original file in the rewritten {{content_type}}. Don't stop until you've rewritten the entire section, even if you have no more changes to make, always write out the whole section with no unnecessary elisions.
|
||||
|
||||
Immediately start with the following format with no remarks:
|
||||
Return ONLY the rewritten {{content_type}}. Do NOT include any XML tags like <document>, <rewrite_this>, or any surrounding markup from the input.
|
||||
|
||||
Respond with a code block containing the rewritten {{content_type}}. Replace \{{REWRITTEN_CODE}} with your actual rewritten {{content_type}}:
|
||||
|
||||
```
|
||||
\{{REWRITTEN_CODE}}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"project_name": null,
|
||||
/// The displayed name of this project. If not set or empty, the root directory name
|
||||
/// will be displayed.
|
||||
"project_name": "",
|
||||
// The name of the Zed theme to use for the UI.
|
||||
//
|
||||
// `mode` is one of:
|
||||
@@ -72,8 +74,10 @@
|
||||
"ui_font_weight": 400,
|
||||
// The default font size for text in the UI
|
||||
"ui_font_size": 16,
|
||||
// The default font size for text in the agent panel. Falls back to the UI font size if unset.
|
||||
"agent_font_size": null,
|
||||
// The default font size for agent responses in the agent panel. Falls back to the UI font size if unset.
|
||||
"agent_ui_font_size": null,
|
||||
// The default font size for user messages in the agent panel. Falls back to the buffer font size if unset.
|
||||
"agent_buffer_font_size": 12,
|
||||
// How much to fade out unused code.
|
||||
"unnecessary_code_fade": 0.3,
|
||||
// Active pane styling settings.
|
||||
@@ -115,6 +119,7 @@
|
||||
// Whether to enable vim modes and key bindings.
|
||||
"vim_mode": false,
|
||||
// Whether to enable helix mode and key bindings.
|
||||
// Enabling this mode will automatically enable vim mode.
|
||||
"helix_mode": false,
|
||||
// Whether to show the informational hover box when moving the mouse
|
||||
// over symbols in the editor.
|
||||
@@ -311,7 +316,7 @@
|
||||
// bracket, brace, single or double quote characters.
|
||||
// For example, when you select text and type (, Zed will surround the text with ().
|
||||
"use_auto_surround": true,
|
||||
/// Whether indentation should be adjusted based on the context whilst typing.
|
||||
// Whether indentation should be adjusted based on the context whilst typing.
|
||||
"auto_indent": true,
|
||||
// Whether indentation of pasted content should be adjusted based on the context.
|
||||
"auto_indent_on_paste": true,
|
||||
@@ -362,6 +367,11 @@
|
||||
// - It is adjacent to an edge (start or end)
|
||||
// - It is adjacent to a whitespace (left or right)
|
||||
"show_whitespaces": "selection",
|
||||
// Visible characters used to render whitespace when show_whitespaces is enabled.
|
||||
"whitespace_map": {
|
||||
"space": "•",
|
||||
"tab": "→"
|
||||
},
|
||||
// Settings related to calls in Zed
|
||||
"calls": {
|
||||
// Join calls with the microphone live by default
|
||||
@@ -401,6 +411,39 @@
|
||||
// Whether to show the menus in the titlebar.
|
||||
"show_menus": false
|
||||
},
|
||||
"audio": {
|
||||
// Opt into the new audio system.
|
||||
"experimental.rodio_audio": false,
|
||||
// Requires 'rodio_audio: true'
|
||||
//
|
||||
// Automatically increase or decrease you microphone's volume. This affects how
|
||||
// loud you sound to others.
|
||||
//
|
||||
// Recommended: off (default)
|
||||
// Microphones are too quite in zed, until everyone is on experimental
|
||||
// audio and has auto speaker volume on this will make you very loud
|
||||
// compared to other speakers.
|
||||
"experimental.auto_microphone_volume": false,
|
||||
// Requires 'rodio_audio: true'
|
||||
//
|
||||
// Automatically increate or decrease the volume of other call members.
|
||||
// This only affects how things sound for you.
|
||||
"experimental.auto_speaker_volume": true,
|
||||
// Requires 'rodio_audio: true'
|
||||
//
|
||||
// Remove background noises. Works great for typing, cars, dogs, AC. Does
|
||||
// not work well on music.
|
||||
"experimental.denoise": true,
|
||||
// Requires 'rodio_audio: true'
|
||||
//
|
||||
// Use audio parameters compatible with the previous versions of
|
||||
// experimental audio and non-experimental audio. When this is false you
|
||||
// will sound strange to anyone not on the latest experimental audio. In
|
||||
// the future we will migrate by setting this to false
|
||||
//
|
||||
// You need to rejoin a call for this setting to apply
|
||||
"experimental.legacy_audio_compatible": true
|
||||
},
|
||||
// Scrollbar related settings
|
||||
"scrollbar": {
|
||||
// When to show the scrollbar in the editor.
|
||||
@@ -581,6 +624,7 @@
|
||||
// Toggle certain types of hints on and off, all switched on by default.
|
||||
"show_type_hints": true,
|
||||
"show_parameter_hints": true,
|
||||
"show_value_hints": true,
|
||||
// Corresponds to null/None LSP hint type value.
|
||||
"show_other_hints": true,
|
||||
// Whether to show a background for inlay hints.
|
||||
@@ -789,7 +833,7 @@
|
||||
"agent": {
|
||||
// Whether the agent is enabled.
|
||||
"enabled": true,
|
||||
/// What completion mode to start new threads in, if available. Can be 'normal' or 'burn'.
|
||||
// What completion mode to start new threads in, if available. Can be 'normal' or 'burn'.
|
||||
"preferred_completion_mode": "normal",
|
||||
// Whether to show the agent panel button in the status bar.
|
||||
"button": true,
|
||||
@@ -799,6 +843,8 @@
|
||||
"default_width": 640,
|
||||
// Default height when the agent panel is docked to the bottom.
|
||||
"default_height": 320,
|
||||
// The view to use by default (thread, or text_thread)
|
||||
"default_view": "thread",
|
||||
// The default model to use when creating new threads.
|
||||
"default_model": {
|
||||
// The provider to use.
|
||||
@@ -900,22 +946,22 @@
|
||||
|
||||
// Default: false
|
||||
"play_sound_when_agent_done": false,
|
||||
/// Whether to have edit cards in the agent panel expanded, showing a preview of the full diff.
|
||||
///
|
||||
/// Default: true
|
||||
// Whether to have edit cards in the agent panel expanded, showing a preview of the full diff.
|
||||
//
|
||||
// Default: true
|
||||
"expand_edit_card": true,
|
||||
/// Whether to have terminal cards in the agent panel expanded, showing the whole command output.
|
||||
///
|
||||
/// Default: true
|
||||
"expand_terminal_card": true
|
||||
},
|
||||
// The settings for slash commands.
|
||||
"slash_commands": {
|
||||
// Settings for the `/project` slash command.
|
||||
"project": {
|
||||
// Whether `/project` is enabled.
|
||||
"enabled": false
|
||||
}
|
||||
// Whether to have terminal cards in the agent panel expanded, showing the whole command output.
|
||||
//
|
||||
// Default: true
|
||||
"expand_terminal_card": true,
|
||||
// Whether to always use cmd-enter (or ctrl-enter on Linux or Windows) to send messages in the agent panel.
|
||||
//
|
||||
// Default: false
|
||||
"use_modifier_to_send": false,
|
||||
// Minimum number of lines to display in the agent message editor.
|
||||
//
|
||||
// Default: 4
|
||||
"message_editor_min_lines": 4
|
||||
},
|
||||
// Whether the screen sharing icon is shown in the os status bar.
|
||||
"show_call_status_icon": true,
|
||||
@@ -928,6 +974,7 @@
|
||||
//
|
||||
// This is typically customized on a per-language basis.
|
||||
"language_servers": ["..."],
|
||||
|
||||
// When to automatically save edited buffers. This setting can
|
||||
// take four values.
|
||||
//
|
||||
@@ -1199,6 +1246,9 @@
|
||||
// The minimum column number to show the inline blame information at
|
||||
"min_column": 0
|
||||
},
|
||||
"blame": {
|
||||
"show_avatar": true
|
||||
},
|
||||
// Control which information is shown in the branch picker.
|
||||
"branch_picker": {
|
||||
"show_author_name": true
|
||||
@@ -1259,7 +1309,13 @@
|
||||
// },
|
||||
// Whether edit predictions are enabled when editing text threads.
|
||||
// This setting has no effect if globally disabled.
|
||||
"enabled_in_text_threads": true
|
||||
"enabled_in_text_threads": true,
|
||||
|
||||
"copilot": {
|
||||
"enterprise_uri": null,
|
||||
"proxy": null,
|
||||
"proxy_no_verify": null
|
||||
}
|
||||
},
|
||||
// Settings specific to journaling
|
||||
"journal": {
|
||||
@@ -1273,6 +1329,8 @@
|
||||
},
|
||||
// Status bar-related settings.
|
||||
"status_bar": {
|
||||
// Whether to show the status bar.
|
||||
"experimental.show": true,
|
||||
// Whether to show the active language button in the status bar.
|
||||
"active_language_button": true,
|
||||
// Whether to show the cursor position button in the status bar.
|
||||
@@ -1382,7 +1440,7 @@
|
||||
// "line_height": {
|
||||
// "custom": 2
|
||||
// },
|
||||
"line_height": "comfortable",
|
||||
"line_height": "standard",
|
||||
// Activate the python virtual environment, if one is found, in the
|
||||
// terminal's working directory (as resolved by the working_directory
|
||||
// setting). Set this to "off" to disable this behavior.
|
||||
@@ -1402,7 +1460,7 @@
|
||||
//
|
||||
// The shell running in the terminal needs to be configured to emit the title.
|
||||
// Example: `echo -e "\e]2;New Title\007";`
|
||||
"breadcrumbs": true
|
||||
"breadcrumbs": false
|
||||
},
|
||||
// Scrollbar-related settings
|
||||
"scrollbar": {
|
||||
@@ -1482,7 +1540,7 @@
|
||||
// }
|
||||
//
|
||||
"file_types": {
|
||||
"JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json", "**/.vscode/**/*.json"],
|
||||
"JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json", "**/.vscode/**/*.json", "tsconfig*.json"],
|
||||
"Shell Script": [".env.*"]
|
||||
},
|
||||
// Settings for which version of Node.js and NPM to use when installing
|
||||
@@ -1508,6 +1566,14 @@
|
||||
"auto_install_extensions": {
|
||||
"html": true
|
||||
},
|
||||
// The capabilities granted to extensions.
|
||||
//
|
||||
// This list can be customized to restrict what extensions are able to do.
|
||||
"granted_extension_capabilities": [
|
||||
{ "kind": "process:exec", "command": "*", "args": ["**"] },
|
||||
{ "kind": "download_file", "host": "*", "path": ["**"] },
|
||||
{ "kind": "npm:install", "package": "*" }
|
||||
],
|
||||
// Controls how completions are processed for this language.
|
||||
"completions": {
|
||||
// Controls how words are completed.
|
||||
@@ -1687,6 +1753,11 @@
|
||||
"allow_rewrap": "anywhere"
|
||||
},
|
||||
"Python": {
|
||||
"formatter": {
|
||||
"language_server": {
|
||||
"name": "ruff"
|
||||
}
|
||||
},
|
||||
"debuggers": ["Debugpy"]
|
||||
},
|
||||
"Ruby": {
|
||||
@@ -1757,6 +1828,7 @@
|
||||
"anthropic": {
|
||||
"api_url": "https://api.anthropic.com"
|
||||
},
|
||||
"bedrock": {},
|
||||
"google": {
|
||||
"api_url": "https://generativelanguage.googleapis.com"
|
||||
},
|
||||
@@ -1778,14 +1850,30 @@
|
||||
},
|
||||
"mistral": {
|
||||
"api_url": "https://api.mistral.ai/v1"
|
||||
}
|
||||
},
|
||||
"vercel": {
|
||||
"api_url": "https://api.v0.dev/v1"
|
||||
},
|
||||
"x_ai": {
|
||||
"api_url": "https://api.x.ai/v1"
|
||||
},
|
||||
"zed.dev": {}
|
||||
},
|
||||
"session": {
|
||||
// Whether or not to restore unsaved buffers on restart.
|
||||
//
|
||||
// If this is true, user won't be prompted whether to save/discard
|
||||
// dirty files when closing the application.
|
||||
//
|
||||
// Default: true
|
||||
"restore_unsaved_buffers": true
|
||||
},
|
||||
// Zed's Prettier integration settings.
|
||||
// Allows to enable/disable formatting with Prettier
|
||||
// and configure default Prettier, used when no project-level Prettier installation is found.
|
||||
"prettier": {
|
||||
// // Whether to consider prettier formatter or not when attempting to format a file.
|
||||
// "allowed": false,
|
||||
"allowed": false
|
||||
//
|
||||
// // Use regular Prettier json configuration.
|
||||
// // If Prettier is allowed, Zed will use this for its Prettier instance for any applicable file, if
|
||||
@@ -1818,6 +1906,10 @@
|
||||
// }
|
||||
// }
|
||||
},
|
||||
// DAP Specific settings.
|
||||
"dap": {
|
||||
// Specify the DAP name as a key here.
|
||||
},
|
||||
// Common language server settings.
|
||||
"global_lsp_settings": {
|
||||
// Whether to show the LSP servers button in the status bar.
|
||||
@@ -1825,13 +1917,23 @@
|
||||
},
|
||||
// Jupyter settings
|
||||
"jupyter": {
|
||||
"enabled": true
|
||||
"enabled": true,
|
||||
"kernel_selections": {}
|
||||
// Specify the language name as the key and the kernel name as the value.
|
||||
// "kernel_selections": {
|
||||
// "python": "conda-base"
|
||||
// "typescript": "deno"
|
||||
// }
|
||||
},
|
||||
// REPL settings.
|
||||
"repl": {
|
||||
// Maximum number of columns to keep in REPL's scrollback buffer.
|
||||
// Clamped with [20, 512] range.
|
||||
"max_columns": 128,
|
||||
// Maximum number of lines to keep in REPL's scrollback buffer.
|
||||
// Clamped with [4, 256] range.
|
||||
"max_lines": 32
|
||||
},
|
||||
// Vim settings
|
||||
"vim": {
|
||||
"default_mode": "normal",
|
||||
@@ -1934,7 +2036,7 @@
|
||||
// Examples:
|
||||
// "profiles": {
|
||||
// "Presenting": {
|
||||
// "agent_font_size": 20.0,
|
||||
// "agent_ui_font_size": 20.0,
|
||||
// "buffer_font_size": 20.0,
|
||||
// "theme": "One Light",
|
||||
// "ui_font_size": 20.0
|
||||
@@ -1947,5 +2049,11 @@
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
"profiles": []
|
||||
"profiles": [],
|
||||
|
||||
// A map of log scopes to the desired log level.
|
||||
// Useful for filtering out noisy logs or enabling more verbose logging.
|
||||
//
|
||||
// Example: {"log": {"client": "warn"}}
|
||||
"log": {}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,11 @@
|
||||
// "args": ["--login"]
|
||||
// }
|
||||
// }
|
||||
"shell": "system"
|
||||
"shell": "system",
|
||||
// Whether to show the task line in the output of the spawned task, defaults to `true`.
|
||||
"show_summary": true,
|
||||
// Whether to show the command line in the output of the spawned task, defaults to `true`.
|
||||
"show_command": true
|
||||
// Represents the tags for inline runnable indicators, or spawning multiple tasks at once.
|
||||
// "tags": []
|
||||
}
|
||||
|
||||
BIN
assets/sounds/guest_joined_call.wav
Normal file
BIN
assets/sounds/guest_joined_call.wav
Normal file
Binary file not shown.
@@ -192,7 +192,7 @@
|
||||
"font_weight": null
|
||||
},
|
||||
"comment": {
|
||||
"color": "#abb5be8c",
|
||||
"color": "#5c6773ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
@@ -239,7 +239,7 @@
|
||||
"hint": {
|
||||
"color": "#628b80ff",
|
||||
"font_style": null,
|
||||
"font_weight": 700
|
||||
"font_weight": null
|
||||
},
|
||||
"keyword": {
|
||||
"color": "#ff8f3fff",
|
||||
@@ -583,7 +583,7 @@
|
||||
"font_weight": null
|
||||
},
|
||||
"comment": {
|
||||
"color": "#787b8099",
|
||||
"color": "#abb0b6ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
@@ -630,7 +630,7 @@
|
||||
"hint": {
|
||||
"color": "#8ca7c2ff",
|
||||
"font_style": null,
|
||||
"font_weight": 700
|
||||
"font_weight": null
|
||||
},
|
||||
"keyword": {
|
||||
"color": "#fa8d3eff",
|
||||
@@ -974,7 +974,7 @@
|
||||
"font_weight": null
|
||||
},
|
||||
"comment": {
|
||||
"color": "#b8cfe680",
|
||||
"color": "#5c6773ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
@@ -1021,7 +1021,7 @@
|
||||
"hint": {
|
||||
"color": "#7399a3ff",
|
||||
"font_style": null,
|
||||
"font_weight": 700
|
||||
"font_weight": null
|
||||
},
|
||||
"keyword": {
|
||||
"color": "#ffad65ff",
|
||||
|
||||
@@ -248,7 +248,7 @@
|
||||
"hint": {
|
||||
"color": "#8c957dff",
|
||||
"font_style": null,
|
||||
"font_weight": 700
|
||||
"font_weight": null
|
||||
},
|
||||
"keyword": {
|
||||
"color": "#fb4833ff",
|
||||
@@ -653,7 +653,7 @@
|
||||
"hint": {
|
||||
"color": "#8c957dff",
|
||||
"font_style": null,
|
||||
"font_weight": 700
|
||||
"font_weight": null
|
||||
},
|
||||
"keyword": {
|
||||
"color": "#fb4833ff",
|
||||
@@ -1058,7 +1058,7 @@
|
||||
"hint": {
|
||||
"color": "#8c957dff",
|
||||
"font_style": null,
|
||||
"font_weight": 700
|
||||
"font_weight": null
|
||||
},
|
||||
"keyword": {
|
||||
"color": "#fb4833ff",
|
||||
@@ -1463,7 +1463,7 @@
|
||||
"hint": {
|
||||
"color": "#677562ff",
|
||||
"font_style": null,
|
||||
"font_weight": 700
|
||||
"font_weight": null
|
||||
},
|
||||
"keyword": {
|
||||
"color": "#9d0006ff",
|
||||
@@ -1868,7 +1868,7 @@
|
||||
"hint": {
|
||||
"color": "#677562ff",
|
||||
"font_style": null,
|
||||
"font_weight": 700
|
||||
"font_weight": null
|
||||
},
|
||||
"keyword": {
|
||||
"color": "#9d0006ff",
|
||||
@@ -2273,7 +2273,7 @@
|
||||
"hint": {
|
||||
"color": "#677562ff",
|
||||
"font_style": null,
|
||||
"font_weight": 700
|
||||
"font_weight": null
|
||||
},
|
||||
"keyword": {
|
||||
"color": "#9d0006ff",
|
||||
|
||||
@@ -244,7 +244,7 @@
|
||||
"hint": {
|
||||
"color": "#788ca6ff",
|
||||
"font_style": null,
|
||||
"font_weight": 700
|
||||
"font_weight": null
|
||||
},
|
||||
"keyword": {
|
||||
"color": "#b477cfff",
|
||||
@@ -643,7 +643,7 @@
|
||||
"hint": {
|
||||
"color": "#7274a7ff",
|
||||
"font_style": null,
|
||||
"font_weight": 700
|
||||
"font_weight": null
|
||||
},
|
||||
"keyword": {
|
||||
"color": "#a449abff",
|
||||
|
||||
11
clippy.toml
11
clippy.toml
@@ -5,3 +5,14 @@ ignore-interior-mutability = [
|
||||
# and Hash impls do not use fields with interior mutability.
|
||||
"agent::context::AgentContextKey"
|
||||
]
|
||||
disallowed-methods = [
|
||||
{ path = "std::process::Command::spawn", reason = "Spawning `std::process::Command` can block the current thread for an unknown duration", replacement = "smol::process::Command::spawn" },
|
||||
{ path = "std::process::Command::output", reason = "Spawning `std::process::Command` can block the current thread for an unknown duration", replacement = "smol::process::Command::output" },
|
||||
{ path = "std::process::Command::status", reason = "Spawning `std::process::Command` can block the current thread for an unknown duration", replacement = "smol::process::Command::status" },
|
||||
]
|
||||
disallowed-types = [
|
||||
# { path = "std::collections::HashMap", replacement = "collections::HashMap" },
|
||||
# { path = "std::collections::HashSet", replacement = "collections::HashSet" },
|
||||
# { path = "indexmap::IndexSet", replacement = "collections::IndexSet" },
|
||||
# { path = "indexmap::IndexMap", replacement = "collections::IndexMap" },
|
||||
]
|
||||
|
||||
10
compose.yml
10
compose.yml
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
image: docker.io/library/postgres:15
|
||||
container_name: zed_postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
@@ -23,7 +23,7 @@ services:
|
||||
- ./.blob_store:/data
|
||||
|
||||
livekit_server:
|
||||
image: livekit/livekit-server
|
||||
image: docker.io/livekit/livekit-server
|
||||
container_name: livekit_server
|
||||
entrypoint: /livekit-server --config /livekit.yaml
|
||||
ports:
|
||||
@@ -34,7 +34,7 @@ services:
|
||||
- ./livekit.yaml:/livekit.yaml
|
||||
|
||||
postgrest_app:
|
||||
image: postgrest/postgrest
|
||||
image: docker.io/postgrest/postgrest
|
||||
container_name: postgrest_app
|
||||
ports:
|
||||
- 8081:8081
|
||||
@@ -47,7 +47,7 @@ services:
|
||||
- postgres
|
||||
|
||||
postgrest_llm:
|
||||
image: postgrest/postgrest
|
||||
image: docker.io/postgrest/postgrest
|
||||
container_name: postgrest_llm
|
||||
ports:
|
||||
- 8082:8082
|
||||
@@ -60,7 +60,7 @@ services:
|
||||
- postgres
|
||||
|
||||
stripe-mock:
|
||||
image: stripe/stripe-mock:v0.178.0
|
||||
image: docker.io/stripe/stripe-mock:v0.178.0
|
||||
ports:
|
||||
- 12111:12111
|
||||
- 12112:12112
|
||||
|
||||
@@ -45,7 +45,6 @@ url.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
watch.workspace = true
|
||||
which.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -3,16 +3,17 @@ mod diff;
|
||||
mod mention;
|
||||
mod terminal;
|
||||
|
||||
use ::terminal::terminal_settings::TerminalSettings;
|
||||
use agent_settings::AgentSettings;
|
||||
use collections::HashSet;
|
||||
pub use connection::*;
|
||||
pub use diff::*;
|
||||
use futures::future::Shared;
|
||||
use language::language_settings::FormatOnSave;
|
||||
pub use mention::*;
|
||||
use project::lsp_store::{FormatTrigger, LspFormatTarget};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings as _;
|
||||
use task::{Shell, ShellBuilder};
|
||||
pub use terminal::*;
|
||||
|
||||
use action_log::ActionLog;
|
||||
@@ -34,7 +35,7 @@ use std::rc::Rc;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::{fmt::Display, mem, path::PathBuf, sync::Arc};
|
||||
use ui::App;
|
||||
use util::{ResultExt, get_system_shell};
|
||||
use util::{ResultExt, get_default_system_shell};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -573,7 +574,7 @@ impl ToolCallContent {
|
||||
))),
|
||||
acp::ToolCallContent::Diff { diff } => Ok(Self::Diff(cx.new(|cx| {
|
||||
Diff::finalized(
|
||||
diff.path,
|
||||
diff.path.to_string_lossy().into_owned(),
|
||||
diff.old_text,
|
||||
diff.new_text,
|
||||
language_registry,
|
||||
@@ -786,8 +787,9 @@ pub struct AcpThread {
|
||||
token_usage: Option<TokenUsage>,
|
||||
prompt_capabilities: acp::PromptCapabilities,
|
||||
_observe_prompt_capabilities: Task<anyhow::Result<()>>,
|
||||
determine_shell: Shared<Task<String>>,
|
||||
terminals: HashMap<acp::TerminalId, Entity<Terminal>>,
|
||||
pending_terminal_output: HashMap<acp::TerminalId, Vec<Vec<u8>>>,
|
||||
pending_terminal_exit: HashMap<acp::TerminalId, acp::TerminalExitStatus>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -810,6 +812,126 @@ pub enum AcpThreadEvent {
|
||||
|
||||
impl EventEmitter<AcpThreadEvent> for AcpThread {}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum TerminalProviderEvent {
|
||||
Created {
|
||||
terminal_id: acp::TerminalId,
|
||||
label: String,
|
||||
cwd: Option<PathBuf>,
|
||||
output_byte_limit: Option<u64>,
|
||||
terminal: Entity<::terminal::Terminal>,
|
||||
},
|
||||
Output {
|
||||
terminal_id: acp::TerminalId,
|
||||
data: Vec<u8>,
|
||||
},
|
||||
TitleChanged {
|
||||
terminal_id: acp::TerminalId,
|
||||
title: String,
|
||||
},
|
||||
Exit {
|
||||
terminal_id: acp::TerminalId,
|
||||
status: acp::TerminalExitStatus,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum TerminalProviderCommand {
|
||||
WriteInput {
|
||||
terminal_id: acp::TerminalId,
|
||||
bytes: Vec<u8>,
|
||||
},
|
||||
Resize {
|
||||
terminal_id: acp::TerminalId,
|
||||
cols: u16,
|
||||
rows: u16,
|
||||
},
|
||||
Close {
|
||||
terminal_id: acp::TerminalId,
|
||||
},
|
||||
}
|
||||
|
||||
impl AcpThread {
|
||||
pub fn on_terminal_provider_event(
|
||||
&mut self,
|
||||
event: TerminalProviderEvent,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
TerminalProviderEvent::Created {
|
||||
terminal_id,
|
||||
label,
|
||||
cwd,
|
||||
output_byte_limit,
|
||||
terminal,
|
||||
} => {
|
||||
let entity = self.register_terminal_created(
|
||||
terminal_id.clone(),
|
||||
label,
|
||||
cwd,
|
||||
output_byte_limit,
|
||||
terminal,
|
||||
cx,
|
||||
);
|
||||
|
||||
if let Some(mut chunks) = self.pending_terminal_output.remove(&terminal_id) {
|
||||
for data in chunks.drain(..) {
|
||||
entity.update(cx, |term, cx| {
|
||||
term.inner().update(cx, |inner, cx| {
|
||||
inner.write_output(&data, cx);
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(_status) = self.pending_terminal_exit.remove(&terminal_id) {
|
||||
entity.update(cx, |_term, cx| {
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
TerminalProviderEvent::Output { terminal_id, data } => {
|
||||
if let Some(entity) = self.terminals.get(&terminal_id) {
|
||||
entity.update(cx, |term, cx| {
|
||||
term.inner().update(cx, |inner, cx| {
|
||||
inner.write_output(&data, cx);
|
||||
})
|
||||
});
|
||||
} else {
|
||||
self.pending_terminal_output
|
||||
.entry(terminal_id)
|
||||
.or_default()
|
||||
.push(data);
|
||||
}
|
||||
}
|
||||
TerminalProviderEvent::TitleChanged { terminal_id, title } => {
|
||||
if let Some(entity) = self.terminals.get(&terminal_id) {
|
||||
entity.update(cx, |term, cx| {
|
||||
term.inner().update(cx, |inner, cx| {
|
||||
inner.breadcrumb_text = title;
|
||||
cx.emit(::terminal::Event::BreadcrumbsChanged);
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
TerminalProviderEvent::Exit {
|
||||
terminal_id,
|
||||
status,
|
||||
} => {
|
||||
if let Some(entity) = self.terminals.get(&terminal_id) {
|
||||
entity.update(cx, |_term, cx| {
|
||||
cx.notify();
|
||||
});
|
||||
} else {
|
||||
self.pending_terminal_exit.insert(terminal_id, status);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
pub enum ThreadStatus {
|
||||
Idle,
|
||||
@@ -862,7 +984,7 @@ impl AcpThread {
|
||||
mut prompt_capabilities_rx: watch::Receiver<acp::PromptCapabilities>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let prompt_capabilities = *prompt_capabilities_rx.borrow();
|
||||
let prompt_capabilities = prompt_capabilities_rx.borrow().clone();
|
||||
let task = cx.spawn::<_, anyhow::Result<()>>(async move |this, cx| {
|
||||
loop {
|
||||
let caps = prompt_capabilities_rx.recv().await?;
|
||||
@@ -873,20 +995,6 @@ impl AcpThread {
|
||||
}
|
||||
});
|
||||
|
||||
let determine_shell = cx
|
||||
.background_spawn(async move {
|
||||
if cfg!(windows) {
|
||||
return get_system_shell();
|
||||
}
|
||||
|
||||
if which::which("bash").is_ok() {
|
||||
"bash".into()
|
||||
} else {
|
||||
get_system_shell()
|
||||
}
|
||||
})
|
||||
.shared();
|
||||
|
||||
Self {
|
||||
action_log,
|
||||
shared_buffers: Default::default(),
|
||||
@@ -901,12 +1009,13 @@ impl AcpThread {
|
||||
prompt_capabilities,
|
||||
_observe_prompt_capabilities: task,
|
||||
terminals: HashMap::default(),
|
||||
determine_shell,
|
||||
pending_terminal_output: HashMap::default(),
|
||||
pending_terminal_exit: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prompt_capabilities(&self) -> acp::PromptCapabilities {
|
||||
self.prompt_capabilities
|
||||
self.prompt_capabilities.clone()
|
||||
}
|
||||
|
||||
pub fn connection(&self) -> &Rc<dyn AgentConnection> {
|
||||
@@ -1127,9 +1236,33 @@ impl AcpThread {
|
||||
let update = update.into();
|
||||
let languages = self.project.read(cx).languages().clone();
|
||||
|
||||
let ix = self
|
||||
.index_for_tool_call(update.id())
|
||||
.context("Tool call not found")?;
|
||||
let ix = match self.index_for_tool_call(update.id()) {
|
||||
Some(ix) => ix,
|
||||
None => {
|
||||
// Tool call not found - create a failed tool call entry
|
||||
let failed_tool_call = ToolCall {
|
||||
id: update.id().clone(),
|
||||
label: cx.new(|cx| Markdown::new("Tool call not found".into(), None, None, cx)),
|
||||
kind: acp::ToolKind::Fetch,
|
||||
content: vec![ToolCallContent::ContentBlock(ContentBlock::new(
|
||||
acp::ContentBlock::Text(acp::TextContent {
|
||||
text: "Tool call not found".to_string(),
|
||||
annotations: None,
|
||||
meta: None,
|
||||
}),
|
||||
&languages,
|
||||
cx,
|
||||
))],
|
||||
status: ToolCallStatus::Failed,
|
||||
locations: Vec::new(),
|
||||
resolved_locations: Vec::new(),
|
||||
raw_input: None,
|
||||
raw_output: None,
|
||||
};
|
||||
self.push_entry(AgentThreadEntry::ToolCall(failed_tool_call), cx);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
let AgentThreadEntry::ToolCall(call) = &mut self.entries[ix] else {
|
||||
unreachable!()
|
||||
};
|
||||
@@ -1446,6 +1579,7 @@ impl AcpThread {
|
||||
vec![acp::ContentBlock::Text(acp::TextContent {
|
||||
text: message.to_string(),
|
||||
annotations: None,
|
||||
meta: None,
|
||||
})],
|
||||
cx,
|
||||
)
|
||||
@@ -1464,6 +1598,7 @@ impl AcpThread {
|
||||
let request = acp::PromptRequest {
|
||||
prompt: message.clone(),
|
||||
session_id: self.session_id.clone(),
|
||||
meta: None,
|
||||
};
|
||||
let git_store = self.project.read(cx).git_store().clone();
|
||||
|
||||
@@ -1555,7 +1690,8 @@ impl AcpThread {
|
||||
let canceled = matches!(
|
||||
result,
|
||||
Ok(Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::Cancelled
|
||||
stop_reason: acp::StopReason::Cancelled,
|
||||
meta: None,
|
||||
}))
|
||||
);
|
||||
|
||||
@@ -1571,6 +1707,7 @@ impl AcpThread {
|
||||
// Handle refusal - distinguish between user prompt and tool call refusals
|
||||
if let Ok(Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::Refusal,
|
||||
meta: _,
|
||||
})) = result
|
||||
{
|
||||
if let Some((user_msg_ix, _)) = this.last_user_message() {
|
||||
@@ -1768,20 +1905,26 @@ impl AcpThread {
|
||||
limit: Option<u32>,
|
||||
reuse_shared_snapshot: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<String>> {
|
||||
) -> Task<Result<String, acp::Error>> {
|
||||
// Args are 1-based, move to 0-based
|
||||
let line = line.unwrap_or_default().saturating_sub(1);
|
||||
let limit = limit.unwrap_or(u32::MAX);
|
||||
let project = self.project.clone();
|
||||
let action_log = self.action_log.clone();
|
||||
cx.spawn(async move |this, cx| {
|
||||
let load = project.update(cx, |project, cx| {
|
||||
let path = project
|
||||
.project_path_for_absolute_path(&path, cx)
|
||||
.context("invalid path")?;
|
||||
anyhow::Ok(project.open_buffer(path, cx))
|
||||
});
|
||||
let buffer = load??.await?;
|
||||
let load = project
|
||||
.update(cx, |project, cx| {
|
||||
let path = project
|
||||
.project_path_for_absolute_path(&path, cx)
|
||||
.ok_or_else(|| {
|
||||
acp::Error::resource_not_found(Some(path.display().to_string()))
|
||||
})?;
|
||||
Ok(project.open_buffer(path, cx))
|
||||
})
|
||||
.map_err(|e| acp::Error::internal_error().with_data(e.to_string()))
|
||||
.flatten()?;
|
||||
|
||||
let buffer = load.await?;
|
||||
|
||||
let snapshot = if reuse_shared_snapshot {
|
||||
this.read_with(cx, |this, _| {
|
||||
@@ -1808,15 +1951,17 @@ impl AcpThread {
|
||||
};
|
||||
|
||||
let max_point = snapshot.max_point();
|
||||
if line >= max_point.row {
|
||||
anyhow::bail!(
|
||||
let start_position = Point::new(line, 0);
|
||||
|
||||
if start_position > max_point {
|
||||
return Err(acp::Error::invalid_params().with_data(format!(
|
||||
"Attempting to read beyond the end of the file, line {}:{}",
|
||||
max_point.row + 1,
|
||||
max_point.column
|
||||
);
|
||||
)));
|
||||
}
|
||||
|
||||
let start = snapshot.anchor_before(Point::new(line, 0));
|
||||
let start = snapshot.anchor_before(start_position);
|
||||
let end = snapshot.anchor_before(Point::new(line.saturating_add(limit), 0));
|
||||
|
||||
project.update(cx, |project, cx| {
|
||||
@@ -1932,40 +2077,24 @@ impl AcpThread {
|
||||
|
||||
pub fn create_terminal(
|
||||
&self,
|
||||
mut command: String,
|
||||
command: String,
|
||||
args: Vec<String>,
|
||||
extra_env: Vec<acp::EnvVariable>,
|
||||
cwd: Option<PathBuf>,
|
||||
output_byte_limit: Option<u64>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Entity<Terminal>>> {
|
||||
for arg in args {
|
||||
command.push(' ');
|
||||
command.push_str(&arg);
|
||||
}
|
||||
|
||||
let shell_command = if cfg!(windows) {
|
||||
format!("$null | & {{{}}}", command.replace("\"", "'"))
|
||||
} else if let Some(cwd) = cwd.as_ref().and_then(|cwd| cwd.as_os_str().to_str()) {
|
||||
// Make sure once we're *inside* the shell, we cd into `cwd`
|
||||
format!("(cd {cwd}; {}) </dev/null", command)
|
||||
} else {
|
||||
format!("({}) </dev/null", command)
|
||||
};
|
||||
let args = vec!["-c".into(), shell_command];
|
||||
|
||||
let env = match &cwd {
|
||||
Some(dir) => self.project.update(cx, |project, cx| {
|
||||
project.directory_environment(dir.as_path().into(), cx)
|
||||
let shell = TerminalSettings::get_global(cx).shell.clone();
|
||||
project.directory_environment(&shell, dir.as_path().into(), cx)
|
||||
}),
|
||||
None => Task::ready(None).shared(),
|
||||
};
|
||||
|
||||
let env = cx.spawn(async move |_, _| {
|
||||
let mut env = env.await.unwrap_or_default();
|
||||
if cfg!(unix) {
|
||||
env.insert("PAGER".into(), "cat".into());
|
||||
}
|
||||
// Disables paging for `git` and hopefully other commands
|
||||
env.insert("PAGER".into(), "".into());
|
||||
for var in extra_env {
|
||||
env.insert(var.name, var.value);
|
||||
}
|
||||
@@ -1974,20 +2103,28 @@ impl AcpThread {
|
||||
|
||||
let project = self.project.clone();
|
||||
let language_registry = project.read(cx).languages().clone();
|
||||
let determine_shell = self.determine_shell.clone();
|
||||
|
||||
let terminal_id = acp::TerminalId(Uuid::new_v4().to_string().into());
|
||||
let terminal_task = cx.spawn({
|
||||
let terminal_id = terminal_id.clone();
|
||||
async move |_this, cx| {
|
||||
let program = determine_shell.await;
|
||||
let env = env.await;
|
||||
let shell = project
|
||||
.update(cx, |project, cx| {
|
||||
project
|
||||
.remote_client()
|
||||
.and_then(|r| r.read(cx).default_system_shell())
|
||||
})?
|
||||
.unwrap_or_else(|| get_default_system_shell());
|
||||
let (task_command, task_args) = ShellBuilder::new(&Shell::Program(shell))
|
||||
.redirect_stdin_to_dev_null()
|
||||
.build(Some(command.clone()), &args);
|
||||
let terminal = project
|
||||
.update(cx, |project, cx| {
|
||||
project.create_terminal_task(
|
||||
task::SpawnInTerminal {
|
||||
command: Some(program),
|
||||
args,
|
||||
command: Some(task_command),
|
||||
args: task_args,
|
||||
cwd: cwd.clone(),
|
||||
env,
|
||||
..Default::default()
|
||||
@@ -2000,7 +2137,7 @@ impl AcpThread {
|
||||
cx.new(|cx| {
|
||||
Terminal::new(
|
||||
terminal_id,
|
||||
command,
|
||||
&format!("{} {}", command, args.join(" ")),
|
||||
cwd,
|
||||
output_byte_limit.map(|l| l as usize),
|
||||
terminal,
|
||||
@@ -2064,6 +2201,32 @@ impl AcpThread {
|
||||
pub fn emit_load_error(&mut self, error: LoadError, cx: &mut Context<Self>) {
|
||||
cx.emit(AcpThreadEvent::LoadError(error));
|
||||
}
|
||||
|
||||
pub fn register_terminal_created(
|
||||
&mut self,
|
||||
terminal_id: acp::TerminalId,
|
||||
command_label: String,
|
||||
working_dir: Option<PathBuf>,
|
||||
output_byte_limit: Option<u64>,
|
||||
terminal: Entity<::terminal::Terminal>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Entity<Terminal> {
|
||||
let language_registry = self.project.read(cx).languages().clone();
|
||||
|
||||
let entity = cx.new(|cx| {
|
||||
Terminal::new(
|
||||
terminal_id.clone(),
|
||||
&command_label,
|
||||
working_dir.clone(),
|
||||
output_byte_limit.map(|l| l as usize),
|
||||
terminal,
|
||||
language_registry,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
self.terminals.insert(terminal_id.clone(), entity.clone());
|
||||
entity
|
||||
}
|
||||
}
|
||||
|
||||
fn markdown_for_raw_output(
|
||||
@@ -2140,6 +2303,145 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_terminal_output_buffered_before_created_renders(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs, [], cx).await;
|
||||
let connection = Rc::new(FakeAgentConnection::new());
|
||||
let thread = cx
|
||||
.update(|cx| connection.new_thread(project, std::path::Path::new(path!("/test")), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let terminal_id = acp::TerminalId(uuid::Uuid::new_v4().to_string().into());
|
||||
|
||||
// Send Output BEFORE Created - should be buffered by acp_thread
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.on_terminal_provider_event(
|
||||
TerminalProviderEvent::Output {
|
||||
terminal_id: terminal_id.clone(),
|
||||
data: b"hello buffered".to_vec(),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
// Create a display-only terminal and then send Created
|
||||
let lower = cx.new(|cx| {
|
||||
let builder = ::terminal::TerminalBuilder::new_display_only(
|
||||
::terminal::terminal_settings::CursorShape::default(),
|
||||
::terminal::terminal_settings::AlternateScroll::On,
|
||||
None,
|
||||
0,
|
||||
)
|
||||
.unwrap();
|
||||
builder.subscribe(cx)
|
||||
});
|
||||
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.on_terminal_provider_event(
|
||||
TerminalProviderEvent::Created {
|
||||
terminal_id: terminal_id.clone(),
|
||||
label: "Buffered Test".to_string(),
|
||||
cwd: None,
|
||||
output_byte_limit: None,
|
||||
terminal: lower.clone(),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
// After Created, buffered Output should have been flushed into the renderer
|
||||
let content = thread.read_with(cx, |thread, cx| {
|
||||
let term = thread.terminal(terminal_id.clone()).unwrap();
|
||||
term.read_with(cx, |t, cx| t.inner().read(cx).get_content())
|
||||
});
|
||||
|
||||
assert!(
|
||||
content.contains("hello buffered"),
|
||||
"expected buffered output to render, got: {content}"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_terminal_output_and_exit_buffered_before_created(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs, [], cx).await;
|
||||
let connection = Rc::new(FakeAgentConnection::new());
|
||||
let thread = cx
|
||||
.update(|cx| connection.new_thread(project, std::path::Path::new(path!("/test")), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let terminal_id = acp::TerminalId(uuid::Uuid::new_v4().to_string().into());
|
||||
|
||||
// Send Output BEFORE Created
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.on_terminal_provider_event(
|
||||
TerminalProviderEvent::Output {
|
||||
terminal_id: terminal_id.clone(),
|
||||
data: b"pre-exit data".to_vec(),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
// Send Exit BEFORE Created
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.on_terminal_provider_event(
|
||||
TerminalProviderEvent::Exit {
|
||||
terminal_id: terminal_id.clone(),
|
||||
status: acp::TerminalExitStatus {
|
||||
exit_code: Some(0),
|
||||
signal: None,
|
||||
meta: None,
|
||||
},
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
// Now create a display-only lower-level terminal and send Created
|
||||
let lower = cx.new(|cx| {
|
||||
let builder = ::terminal::TerminalBuilder::new_display_only(
|
||||
::terminal::terminal_settings::CursorShape::default(),
|
||||
::terminal::terminal_settings::AlternateScroll::On,
|
||||
None,
|
||||
0,
|
||||
)
|
||||
.unwrap();
|
||||
builder.subscribe(cx)
|
||||
});
|
||||
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.on_terminal_provider_event(
|
||||
TerminalProviderEvent::Created {
|
||||
terminal_id: terminal_id.clone(),
|
||||
label: "Buffered Exit Test".to_string(),
|
||||
cwd: None,
|
||||
output_byte_limit: None,
|
||||
terminal: lower.clone(),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
// Output should be present after Created (flushed from buffer)
|
||||
let content = thread.read_with(cx, |thread, cx| {
|
||||
let term = thread.terminal(terminal_id.clone()).unwrap();
|
||||
term.read_with(cx, |t, cx| t.inner().read(cx).get_content())
|
||||
});
|
||||
|
||||
assert!(
|
||||
content.contains("pre-exit data"),
|
||||
"expected pre-exit data to render, got: {content}"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_push_user_content_block(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
@@ -2159,6 +2461,7 @@ mod tests {
|
||||
acp::ContentBlock::Text(acp::TextContent {
|
||||
annotations: None,
|
||||
text: "Hello, ".to_string(),
|
||||
meta: None,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
@@ -2182,6 +2485,7 @@ mod tests {
|
||||
acp::ContentBlock::Text(acp::TextContent {
|
||||
annotations: None,
|
||||
text: "world!".to_string(),
|
||||
meta: None,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
@@ -2203,6 +2507,7 @@ mod tests {
|
||||
acp::ContentBlock::Text(acp::TextContent {
|
||||
annotations: None,
|
||||
text: "Assistant response".to_string(),
|
||||
meta: None,
|
||||
}),
|
||||
false,
|
||||
cx,
|
||||
@@ -2216,6 +2521,7 @@ mod tests {
|
||||
acp::ContentBlock::Text(acp::TextContent {
|
||||
annotations: None,
|
||||
text: "New user message".to_string(),
|
||||
meta: None,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
@@ -2261,6 +2567,7 @@ mod tests {
|
||||
})?;
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::EndTurn,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
.boxed_local()
|
||||
@@ -2331,6 +2638,7 @@ mod tests {
|
||||
.unwrap();
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::EndTurn,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
.boxed_local()
|
||||
@@ -2436,6 +2744,81 @@ mod tests {
|
||||
|
||||
assert_eq!(content, "two\nthree\n");
|
||||
|
||||
// Invalid
|
||||
let err = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.read_text_file(path!("/tmp/foo").into(), Some(6), Some(2), false, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"Invalid params: \"Attempting to read beyond the end of the file, line 5:0\""
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_reading_empty_file(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(path!("/tmp"), json!({"foo": ""})).await;
|
||||
let project = Project::test(fs.clone(), [], cx).await;
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project.find_or_create_worktree(path!("/tmp/foo"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let connection = Rc::new(FakeAgentConnection::new());
|
||||
|
||||
let thread = cx
|
||||
.update(|cx| connection.new_thread(project, Path::new(path!("/tmp")), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Whole file
|
||||
let content = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.read_text_file(path!("/tmp/foo").into(), None, None, false, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(content, "");
|
||||
|
||||
// Only start line
|
||||
let content = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.read_text_file(path!("/tmp/foo").into(), Some(1), None, false, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(content, "");
|
||||
|
||||
// Only limit
|
||||
let content = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.read_text_file(path!("/tmp/foo").into(), None, Some(2), false, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(content, "");
|
||||
|
||||
// Range
|
||||
let content = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.read_text_file(path!("/tmp/foo").into(), Some(1), Some(1), false, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(content, "");
|
||||
|
||||
// Invalid
|
||||
let err = thread
|
||||
.update(cx, |thread, cx| {
|
||||
@@ -2446,9 +2829,40 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"Attempting to read beyond the end of the file, line 5:0"
|
||||
"Invalid params: \"Attempting to read beyond the end of the file, line 1:0\""
|
||||
);
|
||||
}
|
||||
#[gpui::test]
|
||||
async fn test_reading_non_existing_file(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(path!("/tmp"), json!({})).await;
|
||||
let project = Project::test(fs.clone(), [], cx).await;
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project.find_or_create_worktree(path!("/tmp"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let connection = Rc::new(FakeAgentConnection::new());
|
||||
|
||||
let thread = cx
|
||||
.update(|cx| connection.new_thread(project, Path::new(path!("/tmp")), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Out of project file
|
||||
let err = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.read_text_file(path!("/foo").into(), None, None, false, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(err.code, acp::ErrorCode::RESOURCE_NOT_FOUND.code);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_succeeding_canceled_toolcall(cx: &mut TestAppContext) {
|
||||
@@ -2475,6 +2889,7 @@ mod tests {
|
||||
locations: vec![],
|
||||
raw_input: None,
|
||||
raw_output: None,
|
||||
meta: None,
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
@@ -2483,6 +2898,7 @@ mod tests {
|
||||
.unwrap();
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::EndTurn,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
.boxed_local()
|
||||
@@ -2531,6 +2947,7 @@ mod tests {
|
||||
status: Some(acp::ToolCallStatus::Completed),
|
||||
..Default::default()
|
||||
},
|
||||
meta: None,
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
@@ -2573,11 +2990,13 @@ mod tests {
|
||||
path: "/test/test.txt".into(),
|
||||
old_text: None,
|
||||
new_text: "foo".into(),
|
||||
meta: None,
|
||||
},
|
||||
}],
|
||||
locations: vec![],
|
||||
raw_input: None,
|
||||
raw_output: None,
|
||||
meta: None,
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
@@ -2586,6 +3005,7 @@ mod tests {
|
||||
.unwrap();
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::EndTurn,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
.boxed_local()
|
||||
@@ -2648,6 +3068,7 @@ mod tests {
|
||||
})?;
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::EndTurn,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
.boxed_local()
|
||||
@@ -2815,6 +3236,7 @@ mod tests {
|
||||
raw_output: Some(
|
||||
serde_json::json!({"result": "inappropriate content"}),
|
||||
),
|
||||
meta: None,
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
@@ -2824,10 +3246,12 @@ mod tests {
|
||||
// Now return refusal because of the tool result
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::Refusal,
|
||||
meta: None,
|
||||
})
|
||||
} else {
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::EndTurn,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2861,6 +3285,7 @@ mod tests {
|
||||
vec![acp::ContentBlock::Text(acp::TextContent {
|
||||
text: "Hello".into(),
|
||||
annotations: None,
|
||||
meta: None,
|
||||
})],
|
||||
cx,
|
||||
)
|
||||
@@ -2913,6 +3338,7 @@ mod tests {
|
||||
async move {
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::Refusal,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
.boxed_local()
|
||||
@@ -2920,6 +3346,7 @@ mod tests {
|
||||
async move {
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::EndTurn,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
.boxed_local()
|
||||
@@ -2981,6 +3408,7 @@ mod tests {
|
||||
if refuse_next.load(SeqCst) {
|
||||
return Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::Refusal,
|
||||
meta: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2999,6 +3427,7 @@ mod tests {
|
||||
})?;
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::EndTurn,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
.boxed_local()
|
||||
@@ -3154,6 +3583,7 @@ mod tests {
|
||||
image: true,
|
||||
audio: true,
|
||||
embedded_context: true,
|
||||
meta: None,
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
@@ -3185,6 +3615,7 @@ mod tests {
|
||||
} else {
|
||||
Task::ready(Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::EndTurn,
|
||||
meta: None,
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -3226,4 +3657,65 @@ mod tests {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_tool_call_not_found_creates_failed_entry(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs, [], cx).await;
|
||||
let connection = Rc::new(FakeAgentConnection::new());
|
||||
let thread = cx
|
||||
.update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Try to update a tool call that doesn't exist
|
||||
let nonexistent_id = acp::ToolCallId("nonexistent-tool-call".into());
|
||||
thread.update(cx, |thread, cx| {
|
||||
let result = thread.handle_session_update(
|
||||
acp::SessionUpdate::ToolCallUpdate(acp::ToolCallUpdate {
|
||||
id: nonexistent_id.clone(),
|
||||
fields: acp::ToolCallUpdateFields {
|
||||
status: Some(acp::ToolCallStatus::Completed),
|
||||
..Default::default()
|
||||
},
|
||||
meta: None,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
|
||||
// The update should succeed (not return an error)
|
||||
assert!(result.is_ok());
|
||||
|
||||
// There should now be exactly one entry in the thread
|
||||
assert_eq!(thread.entries.len(), 1);
|
||||
|
||||
// The entry should be a failed tool call
|
||||
if let AgentThreadEntry::ToolCall(tool_call) = &thread.entries[0] {
|
||||
assert_eq!(tool_call.id, nonexistent_id);
|
||||
assert!(matches!(tool_call.status, ToolCallStatus::Failed));
|
||||
assert_eq!(tool_call.kind, acp::ToolKind::Fetch);
|
||||
|
||||
// Check that the content contains the error message
|
||||
assert_eq!(tool_call.content.len(), 1);
|
||||
if let ToolCallContent::ContentBlock(content_block) = &tool_call.content[0] {
|
||||
match content_block {
|
||||
ContentBlock::Markdown { markdown } => {
|
||||
let markdown_text = markdown.read(cx).source();
|
||||
assert!(markdown_text.contains("Tool call not found"));
|
||||
}
|
||||
ContentBlock::Empty => panic!("Expected markdown content, got empty"),
|
||||
ContentBlock::ResourceLink { .. } => {
|
||||
panic!("Expected markdown content, got resource link")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
panic!("Expected ContentBlock, got: {:?}", tool_call.content[0]);
|
||||
}
|
||||
} else {
|
||||
panic!("Expected ToolCall entry, got: {:?}", thread.entries[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ pub trait AgentConnection {
|
||||
///
|
||||
/// If the agent does not support model selection, returns [None].
|
||||
/// This allows sharing the selector in UI components.
|
||||
fn model_selector(&self) -> Option<Rc<dyn AgentModelSelector>> {
|
||||
fn model_selector(&self, _session_id: &acp::SessionId) -> Option<Rc<dyn AgentModelSelector>> {
|
||||
None
|
||||
}
|
||||
|
||||
@@ -177,61 +177,48 @@ pub trait AgentModelSelector: 'static {
|
||||
/// If the session doesn't exist or the model is invalid, it returns an error.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `session_id`: The ID of the session (thread) to apply the model to.
|
||||
/// - `model`: The model to select (should be one from [list_models]).
|
||||
/// - `cx`: The GPUI app context.
|
||||
///
|
||||
/// # Returns
|
||||
/// A task resolving to `Ok(())` on success or an error.
|
||||
fn select_model(
|
||||
&self,
|
||||
session_id: acp::SessionId,
|
||||
model_id: AgentModelId,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<()>>;
|
||||
fn select_model(&self, model_id: acp::ModelId, cx: &mut App) -> Task<Result<()>>;
|
||||
|
||||
/// Retrieves the currently selected model for a specific session (thread).
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `session_id`: The ID of the session (thread) to query.
|
||||
/// - `cx`: The GPUI app context.
|
||||
///
|
||||
/// # Returns
|
||||
/// A task resolving to the selected model (always set) or an error (e.g., session not found).
|
||||
fn selected_model(
|
||||
&self,
|
||||
session_id: &acp::SessionId,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<AgentModelInfo>>;
|
||||
fn selected_model(&self, cx: &mut App) -> Task<Result<AgentModelInfo>>;
|
||||
|
||||
/// Whenever the model list is updated the receiver will be notified.
|
||||
fn watch(&self, cx: &mut App) -> watch::Receiver<()>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct AgentModelId(pub SharedString);
|
||||
|
||||
impl std::ops::Deref for AgentModelId {
|
||||
type Target = SharedString;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for AgentModelId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
/// Optional for agents that don't update their model list.
|
||||
fn watch(&self, _cx: &mut App) -> Option<watch::Receiver<()>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AgentModelInfo {
|
||||
pub id: AgentModelId,
|
||||
pub id: acp::ModelId,
|
||||
pub name: SharedString,
|
||||
pub description: Option<SharedString>,
|
||||
pub icon: Option<IconName>,
|
||||
}
|
||||
|
||||
impl From<acp::ModelInfo> for AgentModelInfo {
|
||||
fn from(info: acp::ModelInfo) -> Self {
|
||||
Self {
|
||||
id: info.model_id,
|
||||
name: info.name.into(),
|
||||
description: info.description.map(|desc| desc.into()),
|
||||
icon: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct AgentModelGroupName(pub SharedString);
|
||||
|
||||
@@ -354,6 +341,7 @@ mod test_support {
|
||||
image: true,
|
||||
audio: true,
|
||||
embedded_context: true,
|
||||
meta: None,
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
@@ -393,7 +381,10 @@ mod test_support {
|
||||
response_tx.replace(tx);
|
||||
cx.spawn(async move |_| {
|
||||
let stop_reason = rx.await?;
|
||||
Ok(acp::PromptResponse { stop_reason })
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason,
|
||||
meta: None,
|
||||
})
|
||||
})
|
||||
} else {
|
||||
for update in self.next_prompt_updates.lock().drain(..) {
|
||||
@@ -432,6 +423,7 @@ mod test_support {
|
||||
try_join_all(tasks).await?;
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::EndTurn,
|
||||
meta: None,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,12 +6,7 @@ use itertools::Itertools;
|
||||
use language::{
|
||||
Anchor, Buffer, Capability, LanguageRegistry, OffsetRangeExt as _, Point, Rope, TextBuffer,
|
||||
};
|
||||
use std::{
|
||||
cmp::Reverse,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc};
|
||||
use util::ResultExt;
|
||||
|
||||
pub enum Diff {
|
||||
@@ -21,7 +16,7 @@ pub enum Diff {
|
||||
|
||||
impl Diff {
|
||||
pub fn finalized(
|
||||
path: PathBuf,
|
||||
path: String,
|
||||
old_text: Option<String>,
|
||||
new_text: String,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
@@ -36,7 +31,7 @@ impl Diff {
|
||||
let buffer = new_buffer.clone();
|
||||
async move |_, cx| {
|
||||
let language = language_registry
|
||||
.language_for_file_path(&path)
|
||||
.load_language_for_file_path(Path::new(&path))
|
||||
.await
|
||||
.log_err();
|
||||
|
||||
@@ -152,12 +147,15 @@ impl Diff {
|
||||
let path = match self {
|
||||
Diff::Pending(PendingDiff {
|
||||
new_buffer: buffer, ..
|
||||
}) => buffer.read(cx).file().map(|file| file.path().as_ref()),
|
||||
Diff::Finalized(FinalizedDiff { path, .. }) => Some(path.as_path()),
|
||||
}) => buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.map(|file| file.path().display(file.path_style(cx))),
|
||||
Diff::Finalized(FinalizedDiff { path, .. }) => Some(path.as_str().into()),
|
||||
};
|
||||
format!(
|
||||
"Diff: {}\n```\n{}\n```\n",
|
||||
path.unwrap_or(Path::new("untitled")).display(),
|
||||
path.unwrap_or("untitled".into()),
|
||||
buffer_text
|
||||
)
|
||||
}
|
||||
@@ -244,8 +242,8 @@ impl PendingDiff {
|
||||
.new_buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.map(|file| file.path().as_ref())
|
||||
.unwrap_or(Path::new("untitled"))
|
||||
.map(|file| file.path().display(file.path_style(cx)))
|
||||
.unwrap_or("untitled".into())
|
||||
.into();
|
||||
|
||||
// Replace the buffer in the multibuffer with the snapshot
|
||||
@@ -348,7 +346,7 @@ impl PendingDiff {
|
||||
}
|
||||
|
||||
pub struct FinalizedDiff {
|
||||
path: PathBuf,
|
||||
path: String,
|
||||
base_text: Arc<String>,
|
||||
new_buffer: Entity<Buffer>,
|
||||
multibuffer: Entity<MultiBuffer>,
|
||||
|
||||
@@ -126,6 +126,39 @@ impl MentionUri {
|
||||
abs_path: None,
|
||||
line_range,
|
||||
})
|
||||
} else if let Some(name) = path.strip_prefix("/agent/symbol/") {
|
||||
let fragment = url
|
||||
.fragment()
|
||||
.context("Missing fragment for untitled buffer selection")?;
|
||||
let line_range = parse_line_range(fragment)?;
|
||||
let path =
|
||||
single_query_param(&url, "path")?.context("Missing path for symbol")?;
|
||||
Ok(Self::Symbol {
|
||||
name: name.to_string(),
|
||||
abs_path: path.into(),
|
||||
line_range,
|
||||
})
|
||||
} else if path.starts_with("/agent/file") {
|
||||
let path =
|
||||
single_query_param(&url, "path")?.context("Missing path for file")?;
|
||||
Ok(Self::File {
|
||||
abs_path: path.into(),
|
||||
})
|
||||
} else if path.starts_with("/agent/directory") {
|
||||
let path =
|
||||
single_query_param(&url, "path")?.context("Missing path for directory")?;
|
||||
Ok(Self::Directory {
|
||||
abs_path: path.into(),
|
||||
})
|
||||
} else if path.starts_with("/agent/selection") {
|
||||
let fragment = url.fragment().context("Missing fragment for selection")?;
|
||||
let line_range = parse_line_range(fragment)?;
|
||||
let path =
|
||||
single_query_param(&url, "path")?.context("Missing path for selection")?;
|
||||
Ok(Self::Selection {
|
||||
abs_path: Some(path.into()),
|
||||
line_range,
|
||||
})
|
||||
} else {
|
||||
bail!("invalid zed url: {:?}", input);
|
||||
}
|
||||
@@ -162,7 +195,7 @@ impl MentionUri {
|
||||
FileIcons::get_icon(abs_path, cx).unwrap_or_else(|| IconName::File.path().into())
|
||||
}
|
||||
MentionUri::PastedImage => IconName::Image.path().into(),
|
||||
MentionUri::Directory { .. } => FileIcons::get_folder_icon(false, cx)
|
||||
MentionUri::Directory { abs_path } => FileIcons::get_folder_icon(false, abs_path, cx)
|
||||
.unwrap_or_else(|| IconName::Folder.path().into()),
|
||||
MentionUri::Symbol { .. } => IconName::Code.path().into(),
|
||||
MentionUri::Thread { .. } => IconName::Thread.path().into(),
|
||||
@@ -180,20 +213,29 @@ impl MentionUri {
|
||||
pub fn to_uri(&self) -> Url {
|
||||
match self {
|
||||
MentionUri::File { abs_path } => {
|
||||
Url::from_file_path(abs_path).expect("mention path should be absolute")
|
||||
let mut url = Url::parse("zed:///").unwrap();
|
||||
url.set_path("/agent/file");
|
||||
url.query_pairs_mut()
|
||||
.append_pair("path", &abs_path.to_string_lossy());
|
||||
url
|
||||
}
|
||||
MentionUri::PastedImage => Url::parse("zed:///agent/pasted-image").unwrap(),
|
||||
MentionUri::Directory { abs_path } => {
|
||||
Url::from_directory_path(abs_path).expect("mention path should be absolute")
|
||||
let mut url = Url::parse("zed:///").unwrap();
|
||||
url.set_path("/agent/directory");
|
||||
url.query_pairs_mut()
|
||||
.append_pair("path", &abs_path.to_string_lossy());
|
||||
url
|
||||
}
|
||||
MentionUri::Symbol {
|
||||
abs_path,
|
||||
name,
|
||||
line_range,
|
||||
} => {
|
||||
let mut url =
|
||||
Url::from_file_path(abs_path).expect("mention path should be absolute");
|
||||
url.query_pairs_mut().append_pair("symbol", name);
|
||||
let mut url = Url::parse("zed:///").unwrap();
|
||||
url.set_path(&format!("/agent/symbol/{name}"));
|
||||
url.query_pairs_mut()
|
||||
.append_pair("path", &abs_path.to_string_lossy());
|
||||
url.set_fragment(Some(&format!(
|
||||
"L{}:{}",
|
||||
line_range.start() + 1,
|
||||
@@ -202,15 +244,16 @@ impl MentionUri {
|
||||
url
|
||||
}
|
||||
MentionUri::Selection {
|
||||
abs_path: path,
|
||||
abs_path,
|
||||
line_range,
|
||||
} => {
|
||||
let mut url = if let Some(path) = path {
|
||||
Url::from_file_path(path).expect("mention path should be absolute")
|
||||
let mut url = Url::parse("zed:///").unwrap();
|
||||
if let Some(abs_path) = abs_path {
|
||||
url.set_path("/agent/selection");
|
||||
url.query_pairs_mut()
|
||||
.append_pair("path", &abs_path.to_string_lossy());
|
||||
} else {
|
||||
let mut url = Url::parse("zed:///").unwrap();
|
||||
url.set_path("/agent/untitled-buffer");
|
||||
url
|
||||
};
|
||||
url.set_fragment(Some(&format!(
|
||||
"L{}:{}",
|
||||
@@ -295,37 +338,32 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_parse_file_uri() {
|
||||
let file_uri = uri!("file:///path/to/file.rs");
|
||||
let parsed = MentionUri::parse(file_uri).unwrap();
|
||||
let old_uri = uri!("file:///path/to/file.rs");
|
||||
let parsed = MentionUri::parse(old_uri).unwrap();
|
||||
match &parsed {
|
||||
MentionUri::File { abs_path } => {
|
||||
assert_eq!(abs_path.to_str().unwrap(), path!("/path/to/file.rs"));
|
||||
}
|
||||
_ => panic!("Expected File variant"),
|
||||
}
|
||||
assert_eq!(parsed.to_uri().to_string(), file_uri);
|
||||
let new_uri = parsed.to_uri().to_string();
|
||||
assert!(new_uri.starts_with("zed:///agent/file"));
|
||||
assert_eq!(MentionUri::parse(&new_uri).unwrap(), parsed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_directory_uri() {
|
||||
let file_uri = uri!("file:///path/to/dir/");
|
||||
let parsed = MentionUri::parse(file_uri).unwrap();
|
||||
let old_uri = uri!("file:///path/to/dir/");
|
||||
let parsed = MentionUri::parse(old_uri).unwrap();
|
||||
match &parsed {
|
||||
MentionUri::Directory { abs_path } => {
|
||||
assert_eq!(abs_path.to_str().unwrap(), path!("/path/to/dir/"));
|
||||
}
|
||||
_ => panic!("Expected Directory variant"),
|
||||
}
|
||||
assert_eq!(parsed.to_uri().to_string(), file_uri);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_directory_uri_with_slash() {
|
||||
let uri = MentionUri::Directory {
|
||||
abs_path: PathBuf::from(path!("/path/to/dir/")),
|
||||
};
|
||||
let expected = uri!("file:///path/to/dir/");
|
||||
assert_eq!(uri.to_uri().to_string(), expected);
|
||||
let new_uri = parsed.to_uri().to_string();
|
||||
assert!(new_uri.starts_with("zed:///agent/directory"));
|
||||
assert_eq!(MentionUri::parse(&new_uri).unwrap(), parsed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -333,14 +371,15 @@ mod tests {
|
||||
let uri = MentionUri::Directory {
|
||||
abs_path: PathBuf::from(path!("/path/to/dir")),
|
||||
};
|
||||
let expected = uri!("file:///path/to/dir/");
|
||||
assert_eq!(uri.to_uri().to_string(), expected);
|
||||
let uri_string = uri.to_uri().to_string();
|
||||
assert!(uri_string.starts_with("zed:///agent/directory"));
|
||||
assert_eq!(MentionUri::parse(&uri_string).unwrap(), uri);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_symbol_uri() {
|
||||
let symbol_uri = uri!("file:///path/to/file.rs?symbol=MySymbol#L10:20");
|
||||
let parsed = MentionUri::parse(symbol_uri).unwrap();
|
||||
let old_uri = uri!("file:///path/to/file.rs?symbol=MySymbol#L10:20");
|
||||
let parsed = MentionUri::parse(old_uri).unwrap();
|
||||
match &parsed {
|
||||
MentionUri::Symbol {
|
||||
abs_path: path,
|
||||
@@ -354,13 +393,15 @@ mod tests {
|
||||
}
|
||||
_ => panic!("Expected Symbol variant"),
|
||||
}
|
||||
assert_eq!(parsed.to_uri().to_string(), symbol_uri);
|
||||
let new_uri = parsed.to_uri().to_string();
|
||||
assert!(new_uri.starts_with("zed:///agent/symbol/MySymbol"));
|
||||
assert_eq!(MentionUri::parse(&new_uri).unwrap(), parsed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_selection_uri() {
|
||||
let selection_uri = uri!("file:///path/to/file.rs#L5:15");
|
||||
let parsed = MentionUri::parse(selection_uri).unwrap();
|
||||
let old_uri = uri!("file:///path/to/file.rs#L5:15");
|
||||
let parsed = MentionUri::parse(old_uri).unwrap();
|
||||
match &parsed {
|
||||
MentionUri::Selection {
|
||||
abs_path: path,
|
||||
@@ -375,7 +416,9 @@ mod tests {
|
||||
}
|
||||
_ => panic!("Expected Selection variant"),
|
||||
}
|
||||
assert_eq!(parsed.to_uri().to_string(), selection_uri);
|
||||
let new_uri = parsed.to_uri().to_string();
|
||||
assert!(new_uri.starts_with("zed:///agent/selection"));
|
||||
assert_eq!(MentionUri::parse(&new_uri).unwrap(), parsed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -28,7 +28,7 @@ pub struct TerminalOutput {
|
||||
impl Terminal {
|
||||
pub fn new(
|
||||
id: acp::TerminalId,
|
||||
command: String,
|
||||
command_label: &str,
|
||||
working_dir: Option<PathBuf>,
|
||||
output_byte_limit: Option<usize>,
|
||||
terminal: Entity<terminal::Terminal>,
|
||||
@@ -40,7 +40,7 @@ impl Terminal {
|
||||
id,
|
||||
command: cx.new(|cx| {
|
||||
Markdown::new(
|
||||
format!("```\n{}\n```", command).into(),
|
||||
format!("```\n{}\n```", command_label).into(),
|
||||
Some(language_registry.clone()),
|
||||
None,
|
||||
cx,
|
||||
@@ -75,6 +75,7 @@ impl Terminal {
|
||||
acp::TerminalExitStatus {
|
||||
exit_code: exit_status.as_ref().map(|e| e.exit_code()),
|
||||
signal: exit_status.and_then(|e| e.signal().map(Into::into)),
|
||||
meta: None,
|
||||
}
|
||||
})
|
||||
.shared(),
|
||||
@@ -105,7 +106,9 @@ impl Terminal {
|
||||
exit_status: Some(acp::TerminalExitStatus {
|
||||
exit_code: exit_status.as_ref().map(|e| e.exit_code()),
|
||||
signal: exit_status.and_then(|e| e.signal().map(Into::into)),
|
||||
meta: None,
|
||||
}),
|
||||
meta: None,
|
||||
}
|
||||
} else {
|
||||
let (current_content, original_len) = self.truncated_output(cx);
|
||||
@@ -114,6 +117,7 @@ impl Terminal {
|
||||
truncated: current_content.len() < original_len,
|
||||
output: current_content,
|
||||
exit_status: None,
|
||||
meta: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,7 @@ use language::{Anchor, Buffer, BufferEvent, DiskState, Point, ToPoint};
|
||||
use project::{Project, ProjectItem, lsp_store::OpenLspBufferHandle};
|
||||
use std::{cmp, ops::Range, sync::Arc};
|
||||
use text::{Edit, Patch, Rope};
|
||||
use util::{
|
||||
RangeExt, ResultExt as _,
|
||||
paths::{PathStyle, RemotePathBuf},
|
||||
};
|
||||
use util::{RangeExt, ResultExt as _};
|
||||
|
||||
/// Tracks actions performed by tools in a thread
|
||||
pub struct ActionLog {
|
||||
@@ -62,7 +59,13 @@ impl ActionLog {
|
||||
let file_path = buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.map(|file| RemotePathBuf::new(file.full_path(cx), PathStyle::Posix).to_proto())
|
||||
.map(|file| {
|
||||
let mut path = file.full_path(cx).to_string_lossy().into_owned();
|
||||
if file.path_style(cx).is_windows() {
|
||||
path = path.replace('\\', "/");
|
||||
}
|
||||
path
|
||||
})
|
||||
.unwrap_or_else(|| format!("buffer_{}", buffer.entity_id()));
|
||||
|
||||
let mut result = String::new();
|
||||
@@ -2301,7 +2304,7 @@ mod tests {
|
||||
.await;
|
||||
fs.set_head_for_repo(
|
||||
path!("/project/.git").as_ref(),
|
||||
&[("file.txt".into(), "a\nb\nc\nd\ne\nf\ng\nh\ni\nj".into())],
|
||||
&[("file.txt", "a\nb\nc\nd\ne\nf\ng\nh\ni\nj".into())],
|
||||
"0000000",
|
||||
);
|
||||
cx.run_until_parked();
|
||||
@@ -2384,7 +2387,7 @@ mod tests {
|
||||
// - Ignores the last line edit (j stays as j)
|
||||
fs.set_head_for_repo(
|
||||
path!("/project/.git").as_ref(),
|
||||
&[("file.txt".into(), "A\nb\nc\nf\nG\nh\ni\nj".into())],
|
||||
&[("file.txt", "A\nb\nc\nf\nG\nh\ni\nj".into())],
|
||||
"0000001",
|
||||
);
|
||||
cx.run_until_parked();
|
||||
@@ -2415,10 +2418,7 @@ mod tests {
|
||||
// Make another commit that accepts the NEW line but with different content
|
||||
fs.set_head_for_repo(
|
||||
path!("/project/.git").as_ref(),
|
||||
&[(
|
||||
"file.txt".into(),
|
||||
"A\nb\nc\nf\nGGG\nh\nDIFFERENT\ni\nj".into(),
|
||||
)],
|
||||
&[("file.txt", "A\nb\nc\nf\nGGG\nh\nDIFFERENT\ni\nj".into())],
|
||||
"0000002",
|
||||
);
|
||||
cx.run_until_parked();
|
||||
@@ -2444,7 +2444,7 @@ mod tests {
|
||||
// Final commit that accepts all remaining edits
|
||||
fs.set_head_for_repo(
|
||||
path!("/project/.git").as_ref(),
|
||||
&[("file.txt".into(), "A\nb\nc\nf\nGGG\nh\nNEW\ni\nJ".into())],
|
||||
&[("file.txt", "A\nb\nc\nf\nGGG\nh\nNEW\ni\nJ".into())],
|
||||
"0000003",
|
||||
);
|
||||
cx.run_until_parked();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage, VersionCheckType};
|
||||
use auto_update::{AutoUpdateStatus, AutoUpdater, DismissMessage, VersionCheckType};
|
||||
use editor::Editor;
|
||||
use extension_host::{ExtensionOperation, ExtensionStore};
|
||||
use futures::StreamExt;
|
||||
@@ -280,18 +280,13 @@ impl ActivityIndicator {
|
||||
});
|
||||
}
|
||||
|
||||
fn dismiss_error_message(
|
||||
&mut self,
|
||||
_: &DismissErrorMessage,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let error_dismissed = if let Some(updater) = &self.auto_updater {
|
||||
updater.update(cx, |updater, cx| updater.dismiss_error(cx))
|
||||
fn dismiss_message(&mut self, _: &DismissMessage, _: &mut Window, cx: &mut Context<Self>) {
|
||||
let dismissed = if let Some(updater) = &self.auto_updater {
|
||||
updater.update(cx, |updater, cx| updater.dismiss(cx))
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if error_dismissed {
|
||||
if dismissed {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -513,7 +508,7 @@ impl ActivityIndicator {
|
||||
on_click: Some(Arc::new(move |this, window, cx| {
|
||||
this.statuses
|
||||
.retain(|status| !downloading.contains(&status.name));
|
||||
this.dismiss_error_message(&DismissErrorMessage, window, cx)
|
||||
this.dismiss_message(&DismissMessage, window, cx)
|
||||
})),
|
||||
tooltip_message: None,
|
||||
});
|
||||
@@ -542,7 +537,7 @@ impl ActivityIndicator {
|
||||
on_click: Some(Arc::new(move |this, window, cx| {
|
||||
this.statuses
|
||||
.retain(|status| !checking_for_update.contains(&status.name));
|
||||
this.dismiss_error_message(&DismissErrorMessage, window, cx)
|
||||
this.dismiss_message(&DismissMessage, window, cx)
|
||||
})),
|
||||
tooltip_message: None,
|
||||
});
|
||||
@@ -650,13 +645,14 @@ impl ActivityIndicator {
|
||||
.and_then(|updater| match &updater.read(cx).status() {
|
||||
AutoUpdateStatus::Checking => Some(Content {
|
||||
icon: Some(
|
||||
Icon::new(IconName::Download)
|
||||
Icon::new(IconName::LoadCircle)
|
||||
.size(IconSize::Small)
|
||||
.with_rotate_animation(3)
|
||||
.into_any_element(),
|
||||
),
|
||||
message: "Checking for Zed updates…".to_string(),
|
||||
on_click: Some(Arc::new(|this, window, cx| {
|
||||
this.dismiss_error_message(&DismissErrorMessage, window, cx)
|
||||
this.dismiss_message(&DismissMessage, window, cx)
|
||||
})),
|
||||
tooltip_message: None,
|
||||
}),
|
||||
@@ -668,19 +664,20 @@ impl ActivityIndicator {
|
||||
),
|
||||
message: "Downloading Zed update…".to_string(),
|
||||
on_click: Some(Arc::new(|this, window, cx| {
|
||||
this.dismiss_error_message(&DismissErrorMessage, window, cx)
|
||||
this.dismiss_message(&DismissMessage, window, cx)
|
||||
})),
|
||||
tooltip_message: Some(Self::version_tooltip_message(version)),
|
||||
}),
|
||||
AutoUpdateStatus::Installing { version } => Some(Content {
|
||||
icon: Some(
|
||||
Icon::new(IconName::Download)
|
||||
Icon::new(IconName::LoadCircle)
|
||||
.size(IconSize::Small)
|
||||
.with_rotate_animation(3)
|
||||
.into_any_element(),
|
||||
),
|
||||
message: "Installing Zed update…".to_string(),
|
||||
on_click: Some(Arc::new(|this, window, cx| {
|
||||
this.dismiss_error_message(&DismissErrorMessage, window, cx)
|
||||
this.dismiss_message(&DismissMessage, window, cx)
|
||||
})),
|
||||
tooltip_message: Some(Self::version_tooltip_message(version)),
|
||||
}),
|
||||
@@ -690,17 +687,18 @@ impl ActivityIndicator {
|
||||
on_click: Some(Arc::new(move |_, _, cx| workspace::reload(cx))),
|
||||
tooltip_message: Some(Self::version_tooltip_message(version)),
|
||||
}),
|
||||
AutoUpdateStatus::Errored => Some(Content {
|
||||
AutoUpdateStatus::Errored { error } => Some(Content {
|
||||
icon: Some(
|
||||
Icon::new(IconName::Warning)
|
||||
.size(IconSize::Small)
|
||||
.into_any_element(),
|
||||
),
|
||||
message: "Auto update failed".to_string(),
|
||||
message: "Failed to update Zed".to_string(),
|
||||
on_click: Some(Arc::new(|this, window, cx| {
|
||||
this.dismiss_error_message(&DismissErrorMessage, window, cx)
|
||||
window.dispatch_action(Box::new(workspace::OpenLog), cx);
|
||||
this.dismiss_message(&DismissMessage, window, cx);
|
||||
})),
|
||||
tooltip_message: None,
|
||||
tooltip_message: Some(format!("{error}")),
|
||||
}),
|
||||
AutoUpdateStatus::Idle => None,
|
||||
})
|
||||
@@ -738,7 +736,7 @@ impl ActivityIndicator {
|
||||
})),
|
||||
message,
|
||||
on_click: Some(Arc::new(|this, window, cx| {
|
||||
this.dismiss_error_message(&Default::default(), window, cx)
|
||||
this.dismiss_message(&Default::default(), window, cx)
|
||||
})),
|
||||
tooltip_message: None,
|
||||
})
|
||||
@@ -777,7 +775,7 @@ impl Render for ActivityIndicator {
|
||||
let result = h_flex()
|
||||
.id("activity-indicator")
|
||||
.on_action(cx.listener(Self::show_error_message))
|
||||
.on_action(cx.listener(Self::dismiss_error_message));
|
||||
.on_action(cx.listener(Self::dismiss_message));
|
||||
let Some(content) = self.content_to_render(cx) else {
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -9,12 +9,14 @@ pub mod tool_use;
|
||||
|
||||
pub use context::{AgentContext, ContextId, ContextLoadResult};
|
||||
pub use context_store::ContextStore;
|
||||
use fs::Fs;
|
||||
use std::sync::Arc;
|
||||
pub use thread::{
|
||||
LastRestoreCheckpoint, Message, MessageCrease, MessageId, MessageSegment, Thread, ThreadError,
|
||||
ThreadEvent, ThreadFeedback, ThreadId, ThreadSummary, TokenUsageRatio,
|
||||
};
|
||||
pub use thread_store::{SerializedThread, TextThreadStore, ThreadStore};
|
||||
|
||||
pub fn init(cx: &mut gpui::App) {
|
||||
thread_store::init(cx);
|
||||
pub fn init(fs: Arc<dyn Fs>, cx: &mut gpui::App) {
|
||||
thread_store::init(fs, cx);
|
||||
}
|
||||
|
||||
@@ -49,10 +49,10 @@ impl AgentProfile {
|
||||
.unwrap_or_default(),
|
||||
};
|
||||
|
||||
update_settings_file::<AgentSettings>(fs, cx, {
|
||||
update_settings_file(fs, cx, {
|
||||
let id = id.clone();
|
||||
move |settings, _cx| {
|
||||
settings.create_profile(id, profile_settings).log_err();
|
||||
profile_settings.save_to_settings(id, settings).log_err();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ use futures::future;
|
||||
use futures::{FutureExt, future::Shared};
|
||||
use gpui::{App, AppContext as _, ElementId, Entity, SharedString, Task};
|
||||
use icons::IconName;
|
||||
use language::{Buffer, ParseStatus};
|
||||
use language::Buffer;
|
||||
use language_model::{LanguageModelImage, LanguageModelRequestMessage, MessageContent};
|
||||
use project::{Project, ProjectEntryId, ProjectPath, Worktree};
|
||||
use prompt_store::{PromptStore, UserPromptId};
|
||||
@@ -18,6 +18,7 @@ use std::path::PathBuf;
|
||||
use std::{ops::Range, path::Path, sync::Arc};
|
||||
use text::{Anchor, OffsetRangeExt as _};
|
||||
use util::markdown::MarkdownCodeBlock;
|
||||
use util::rel_path::RelPath;
|
||||
use util::{ResultExt as _, post_inc};
|
||||
|
||||
pub const RULES_ICON: IconName = IconName::Reader;
|
||||
@@ -158,7 +159,7 @@ pub struct FileContextHandle {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FileContext {
|
||||
pub handle: FileContextHandle,
|
||||
pub full_path: Arc<Path>,
|
||||
pub full_path: String,
|
||||
pub text: SharedString,
|
||||
pub is_outline: bool,
|
||||
}
|
||||
@@ -186,51 +187,24 @@ impl FileContextHandle {
|
||||
log::error!("file context missing path");
|
||||
return Task::ready(None);
|
||||
};
|
||||
let full_path: Arc<Path> = file.full_path(cx).into();
|
||||
let full_path = file.full_path(cx).to_string_lossy().into_owned();
|
||||
let rope = buffer_ref.as_rope().clone();
|
||||
let buffer = self.buffer.clone();
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
// For large files, use outline instead of full content
|
||||
if rope.len() > outline::AUTO_OUTLINE_SIZE {
|
||||
// Wait until the buffer has been fully parsed, so we can read its outline
|
||||
if let Ok(mut parse_status) =
|
||||
buffer.read_with(cx, |buffer, _| buffer.parse_status())
|
||||
{
|
||||
while *parse_status.borrow() != ParseStatus::Idle {
|
||||
parse_status.changed().await.log_err();
|
||||
}
|
||||
let buffer_content =
|
||||
outline::get_buffer_content_or_outline(buffer.clone(), Some(&full_path), &cx)
|
||||
.await
|
||||
.unwrap_or_else(|_| outline::BufferContent {
|
||||
text: rope.to_string(),
|
||||
is_outline: false,
|
||||
});
|
||||
|
||||
if let Ok(snapshot) = buffer.read_with(cx, |buffer, _| buffer.snapshot())
|
||||
&& let Some(outline) = snapshot.outline(None)
|
||||
{
|
||||
let items = outline
|
||||
.items
|
||||
.into_iter()
|
||||
.map(|item| item.to_point(&snapshot));
|
||||
|
||||
if let Ok(outline_text) =
|
||||
outline::render_outline(items, None, 0, usize::MAX).await
|
||||
{
|
||||
let context = AgentContext::File(FileContext {
|
||||
handle: self,
|
||||
full_path,
|
||||
text: outline_text.into(),
|
||||
is_outline: true,
|
||||
});
|
||||
return Some((context, vec![buffer]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to full content if we couldn't build an outline
|
||||
// (or didn't need to because the file was small enough)
|
||||
let context = AgentContext::File(FileContext {
|
||||
handle: self,
|
||||
full_path,
|
||||
text: rope.to_string().into(),
|
||||
is_outline: false,
|
||||
text: buffer_content.text.into(),
|
||||
is_outline: buffer_content.is_outline,
|
||||
});
|
||||
Some((context, vec![buffer]))
|
||||
})
|
||||
@@ -262,14 +236,14 @@ pub struct DirectoryContextHandle {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DirectoryContext {
|
||||
pub handle: DirectoryContextHandle,
|
||||
pub full_path: Arc<Path>,
|
||||
pub full_path: String,
|
||||
pub descendants: Vec<DirectoryContextDescendant>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DirectoryContextDescendant {
|
||||
/// Path within the directory.
|
||||
pub rel_path: Arc<Path>,
|
||||
pub rel_path: Arc<RelPath>,
|
||||
pub fenced_codeblock: SharedString,
|
||||
}
|
||||
|
||||
@@ -300,13 +274,16 @@ impl DirectoryContextHandle {
|
||||
}
|
||||
|
||||
let directory_path = entry.path.clone();
|
||||
let directory_full_path = worktree_ref.full_path(&directory_path).into();
|
||||
let directory_full_path = worktree_ref
|
||||
.full_path(&directory_path)
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
let file_paths = collect_files_in_path(worktree_ref, &directory_path);
|
||||
let descendants_future = future::join_all(file_paths.into_iter().map(|path| {
|
||||
let worktree_ref = worktree.read(cx);
|
||||
let worktree_id = worktree_ref.id();
|
||||
let full_path = worktree_ref.full_path(&path);
|
||||
let full_path = worktree_ref.full_path(&path).to_string_lossy().into_owned();
|
||||
|
||||
let rel_path = path
|
||||
.strip_prefix(&directory_path)
|
||||
@@ -387,7 +364,7 @@ pub struct SymbolContextHandle {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SymbolContext {
|
||||
pub handle: SymbolContextHandle,
|
||||
pub full_path: Arc<Path>,
|
||||
pub full_path: String,
|
||||
pub line_range: Range<Point>,
|
||||
pub text: SharedString,
|
||||
}
|
||||
@@ -426,7 +403,7 @@ impl SymbolContextHandle {
|
||||
log::error!("symbol context's file has no path");
|
||||
return Task::ready(None);
|
||||
};
|
||||
let full_path = file.full_path(cx).into();
|
||||
let full_path = file.full_path(cx).to_string_lossy().into_owned();
|
||||
let line_range = self.enclosing_range.to_point(&buffer_ref.snapshot());
|
||||
let text = self.text(cx);
|
||||
let buffer = self.buffer.clone();
|
||||
@@ -460,7 +437,7 @@ pub struct SelectionContextHandle {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SelectionContext {
|
||||
pub handle: SelectionContextHandle,
|
||||
pub full_path: Arc<Path>,
|
||||
pub full_path: String,
|
||||
pub line_range: Range<Point>,
|
||||
pub text: SharedString,
|
||||
}
|
||||
@@ -499,7 +476,7 @@ impl SelectionContextHandle {
|
||||
let text = self.text(cx);
|
||||
let buffer = self.buffer.clone();
|
||||
let context = AgentContext::Selection(SelectionContext {
|
||||
full_path: full_path.into(),
|
||||
full_path: full_path.to_string_lossy().into_owned(),
|
||||
line_range: self.line_range(cx),
|
||||
text,
|
||||
handle: self,
|
||||
@@ -729,7 +706,7 @@ impl Display for RulesContext {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ImageContext {
|
||||
pub project_path: Option<ProjectPath>,
|
||||
pub full_path: Option<Arc<Path>>,
|
||||
pub full_path: Option<String>,
|
||||
pub original_image: Arc<gpui::Image>,
|
||||
// TODO: handle this elsewhere and remove `ignore-interior-mutability` opt-out in clippy.toml
|
||||
// needed due to a false positive of `clippy::mutable_key_type`.
|
||||
@@ -995,7 +972,7 @@ pub fn load_context(
|
||||
})
|
||||
}
|
||||
|
||||
fn collect_files_in_path(worktree: &Worktree, path: &Path) -> Vec<Arc<Path>> {
|
||||
fn collect_files_in_path(worktree: &Worktree, path: &RelPath) -> Vec<Arc<RelPath>> {
|
||||
let mut files = Vec::new();
|
||||
|
||||
for entry in worktree.child_entries(path) {
|
||||
@@ -1009,14 +986,17 @@ fn collect_files_in_path(worktree: &Worktree, path: &Path) -> Vec<Arc<Path>> {
|
||||
files
|
||||
}
|
||||
|
||||
fn codeblock_tag(full_path: &Path, line_range: Option<Range<Point>>) -> String {
|
||||
fn codeblock_tag(full_path: &str, line_range: Option<Range<Point>>) -> String {
|
||||
let mut result = String::new();
|
||||
|
||||
if let Some(extension) = full_path.extension().and_then(|ext| ext.to_str()) {
|
||||
if let Some(extension) = Path::new(full_path)
|
||||
.extension()
|
||||
.and_then(|ext| ext.to_str())
|
||||
{
|
||||
let _ = write!(result, "{} ", extension);
|
||||
}
|
||||
|
||||
let _ = write!(result, "{}", full_path.display());
|
||||
let _ = write!(result, "{}", full_path);
|
||||
|
||||
if let Some(range) = line_range {
|
||||
if range.start.row == range.end.row {
|
||||
|
||||
@@ -14,7 +14,10 @@ use futures::{self, FutureExt};
|
||||
use gpui::{App, Context, Entity, EventEmitter, Image, SharedString, Task, WeakEntity};
|
||||
use language::{Buffer, File as _};
|
||||
use language_model::LanguageModelImage;
|
||||
use project::{Project, ProjectItem, ProjectPath, Symbol, image_store::is_image_file};
|
||||
use project::{
|
||||
Project, ProjectItem, ProjectPath, Symbol, image_store::is_image_file,
|
||||
lsp_store::SymbolLocation,
|
||||
};
|
||||
use prompt_store::UserPromptId;
|
||||
use ref_cast::RefCast as _;
|
||||
use std::{
|
||||
@@ -309,7 +312,7 @@ impl ContextStore {
|
||||
let item = image_item.read(cx);
|
||||
this.insert_image(
|
||||
Some(item.project_path(cx)),
|
||||
Some(item.file.full_path(cx).into()),
|
||||
Some(item.file.full_path(cx).to_string_lossy().into_owned()),
|
||||
item.image.clone(),
|
||||
remove_if_exists,
|
||||
cx,
|
||||
@@ -325,7 +328,7 @@ impl ContextStore {
|
||||
fn insert_image(
|
||||
&mut self,
|
||||
project_path: Option<ProjectPath>,
|
||||
full_path: Option<Arc<Path>>,
|
||||
full_path: Option<String>,
|
||||
image: Arc<Image>,
|
||||
remove_if_exists: bool,
|
||||
cx: &mut Context<ContextStore>,
|
||||
@@ -500,7 +503,7 @@ impl ContextStore {
|
||||
let Some(context_path) = buffer.project_path(cx) else {
|
||||
return false;
|
||||
};
|
||||
if context_path != symbol.path {
|
||||
if symbol.path != SymbolLocation::InProject(context_path) {
|
||||
return false;
|
||||
}
|
||||
let context_range = context.range.to_point_utf16(&buffer.snapshot());
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use crate::{
|
||||
ThreadId,
|
||||
thread_store::{SerializedThreadMetadata, ThreadStore},
|
||||
};
|
||||
use crate::{ThreadId, thread_store::SerializedThreadMetadata};
|
||||
use anyhow::{Context as _, Result};
|
||||
use assistant_context::SavedContextMetadata;
|
||||
use chrono::{DateTime, Utc};
|
||||
@@ -61,7 +58,6 @@ enum SerializedRecentOpen {
|
||||
}
|
||||
|
||||
pub struct HistoryStore {
|
||||
thread_store: Entity<ThreadStore>,
|
||||
context_store: Entity<assistant_context::ContextStore>,
|
||||
recently_opened_entries: VecDeque<HistoryEntryId>,
|
||||
_subscriptions: Vec<gpui::Subscription>,
|
||||
@@ -70,15 +66,11 @@ pub struct HistoryStore {
|
||||
|
||||
impl HistoryStore {
|
||||
pub fn new(
|
||||
thread_store: Entity<ThreadStore>,
|
||||
context_store: Entity<assistant_context::ContextStore>,
|
||||
initial_recent_entries: impl IntoIterator<Item = HistoryEntryId>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let subscriptions = vec![
|
||||
cx.observe(&thread_store, |_, _, cx| cx.notify()),
|
||||
cx.observe(&context_store, |_, _, cx| cx.notify()),
|
||||
];
|
||||
let subscriptions = vec![cx.observe(&context_store, |_, _, cx| cx.notify())];
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let entries = Self::load_recently_opened_entries(cx).await.log_err()?;
|
||||
@@ -96,7 +88,6 @@ impl HistoryStore {
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
thread_store,
|
||||
context_store,
|
||||
recently_opened_entries: initial_recent_entries.into_iter().collect(),
|
||||
_subscriptions: subscriptions,
|
||||
@@ -112,13 +103,6 @@ impl HistoryStore {
|
||||
return history_entries;
|
||||
}
|
||||
|
||||
history_entries.extend(
|
||||
self.thread_store
|
||||
.read(cx)
|
||||
.reverse_chronological_threads()
|
||||
.cloned()
|
||||
.map(HistoryEntry::Thread),
|
||||
);
|
||||
history_entries.extend(
|
||||
self.context_store
|
||||
.read(cx)
|
||||
@@ -141,22 +125,6 @@ impl HistoryStore {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let thread_entries = self
|
||||
.thread_store
|
||||
.read(cx)
|
||||
.reverse_chronological_threads()
|
||||
.flat_map(|thread| {
|
||||
self.recently_opened_entries
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(index, entry)| match entry {
|
||||
HistoryEntryId::Thread(id) if &thread.id == id => {
|
||||
Some((index, HistoryEntry::Thread(thread.clone())))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
});
|
||||
|
||||
let context_entries =
|
||||
self.context_store
|
||||
.read(cx)
|
||||
@@ -173,8 +141,7 @@ impl HistoryStore {
|
||||
})
|
||||
});
|
||||
|
||||
thread_entries
|
||||
.chain(context_entries)
|
||||
context_entries
|
||||
// optimization to halt iteration early
|
||||
.take(self.recently_opened_entries.len())
|
||||
.sorted_unstable_by_key(|(index, _)| *index)
|
||||
@@ -188,7 +155,7 @@ impl HistoryStore {
|
||||
.iter()
|
||||
.filter_map(|entry| match entry {
|
||||
HistoryEntryId::Context(path) => path.file_name().map(|file| {
|
||||
SerializedRecentOpen::ContextName(file.to_string_lossy().to_string())
|
||||
SerializedRecentOpen::ContextName(file.to_string_lossy().into_owned())
|
||||
}),
|
||||
HistoryEntryId::Thread(id) => Some(SerializedRecentOpen::Thread(id.to_string())),
|
||||
})
|
||||
|
||||
@@ -234,7 +234,6 @@ impl MessageSegment {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ProjectSnapshot {
|
||||
pub worktree_snapshots: Vec<WorktreeSnapshot>,
|
||||
pub unsaved_buffer_paths: Vec<String>,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
}
|
||||
|
||||
@@ -2857,27 +2856,11 @@ impl Thread {
|
||||
.map(|worktree| Self::worktree_snapshot(worktree, git_store.clone(), cx))
|
||||
.collect();
|
||||
|
||||
cx.spawn(async move |_, cx| {
|
||||
cx.spawn(async move |_, _| {
|
||||
let worktree_snapshots = futures::future::join_all(worktree_snapshots).await;
|
||||
|
||||
let mut unsaved_buffers = Vec::new();
|
||||
cx.update(|app_cx| {
|
||||
let buffer_store = project.read(app_cx).buffer_store();
|
||||
for buffer_handle in buffer_store.read(app_cx).buffers() {
|
||||
let buffer = buffer_handle.read(app_cx);
|
||||
if buffer.is_dirty()
|
||||
&& let Some(file) = buffer.file()
|
||||
{
|
||||
let path = file.path().to_string_lossy().to_string();
|
||||
unsaved_buffers.push(path);
|
||||
}
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
|
||||
Arc::new(ProjectSnapshot {
|
||||
worktree_snapshots,
|
||||
unsaved_buffer_paths: unsaved_buffers,
|
||||
timestamp: Utc::now(),
|
||||
})
|
||||
})
|
||||
@@ -2892,7 +2875,7 @@ impl Thread {
|
||||
// Get worktree path and snapshot
|
||||
let worktree_info = cx.update(|app_cx| {
|
||||
let worktree = worktree.read(app_cx);
|
||||
let path = worktree.abs_path().to_string_lossy().to_string();
|
||||
let path = worktree.abs_path().to_string_lossy().into_owned();
|
||||
let snapshot = worktree.snapshot();
|
||||
(path, snapshot)
|
||||
});
|
||||
@@ -3272,9 +3255,10 @@ mod tests {
|
||||
|
||||
// Test-specific constants
|
||||
const TEST_RATE_LIMIT_RETRY_SECS: u64 = 30;
|
||||
use agent_settings::{AgentProfileId, AgentSettings, LanguageModelParameters};
|
||||
use agent_settings::{AgentProfileId, AgentSettings};
|
||||
use assistant_tool::ToolRegistry;
|
||||
use assistant_tools;
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use futures::future::BoxFuture;
|
||||
use futures::stream::BoxStream;
|
||||
@@ -3289,7 +3273,7 @@ mod tests {
|
||||
use project::{FakeFs, Project};
|
||||
use prompt_store::PromptBuilder;
|
||||
use serde_json::json;
|
||||
use settings::{Settings, SettingsStore};
|
||||
use settings::{LanguageModelParameters, Settings, SettingsStore};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use theme::ThemeSettings;
|
||||
@@ -3298,9 +3282,10 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_message_with_context(cx: &mut TestAppContext) {
|
||||
init_test_settings(cx);
|
||||
let fs = init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(
|
||||
&fs,
|
||||
cx,
|
||||
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
|
||||
)
|
||||
@@ -3375,9 +3360,10 @@ fn main() {{
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_only_include_new_contexts(cx: &mut TestAppContext) {
|
||||
init_test_settings(cx);
|
||||
let fs = init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(
|
||||
&fs,
|
||||
cx,
|
||||
json!({
|
||||
"file1.rs": "fn function1() {}\n",
|
||||
@@ -3531,9 +3517,10 @@ fn main() {{
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_message_without_files(cx: &mut TestAppContext) {
|
||||
init_test_settings(cx);
|
||||
let fs = init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(
|
||||
&fs,
|
||||
cx,
|
||||
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
|
||||
)
|
||||
@@ -3610,9 +3597,10 @@ fn main() {{
|
||||
#[gpui::test]
|
||||
#[ignore] // turn this test on when project_notifications tool is re-enabled
|
||||
async fn test_stale_buffer_notification(cx: &mut TestAppContext) {
|
||||
init_test_settings(cx);
|
||||
let fs = init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(
|
||||
&fs,
|
||||
cx,
|
||||
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
|
||||
)
|
||||
@@ -3738,9 +3726,10 @@ fn main() {{
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_storing_profile_setting_per_thread(cx: &mut TestAppContext) {
|
||||
init_test_settings(cx);
|
||||
let fs = init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(
|
||||
&fs,
|
||||
cx,
|
||||
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
|
||||
)
|
||||
@@ -3760,9 +3749,10 @@ fn main() {{
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_serializing_thread_profile(cx: &mut TestAppContext) {
|
||||
init_test_settings(cx);
|
||||
let fs = init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(
|
||||
&fs,
|
||||
cx,
|
||||
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
|
||||
)
|
||||
@@ -3803,9 +3793,10 @@ fn main() {{
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_temperature_setting(cx: &mut TestAppContext) {
|
||||
init_test_settings(cx);
|
||||
let fs = init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(
|
||||
&fs,
|
||||
cx,
|
||||
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
|
||||
)
|
||||
@@ -3897,9 +3888,9 @@ fn main() {{
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_thread_summary(cx: &mut TestAppContext) {
|
||||
init_test_settings(cx);
|
||||
let fs = init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
let project = create_test_project(&fs, cx, json!({})).await;
|
||||
|
||||
let (_, _thread_store, thread, _context_store, model) =
|
||||
setup_test_environment(cx, project.clone()).await;
|
||||
@@ -3982,9 +3973,9 @@ fn main() {{
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_thread_summary_error_set_manually(cx: &mut TestAppContext) {
|
||||
init_test_settings(cx);
|
||||
let fs = init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
let project = create_test_project(&fs, cx, json!({})).await;
|
||||
|
||||
let (_, _thread_store, thread, _context_store, model) =
|
||||
setup_test_environment(cx, project.clone()).await;
|
||||
@@ -4004,9 +3995,9 @@ fn main() {{
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_thread_summary_error_retry(cx: &mut TestAppContext) {
|
||||
init_test_settings(cx);
|
||||
let fs = init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
let project = create_test_project(&fs, cx, json!({})).await;
|
||||
|
||||
let (_, _thread_store, thread, _context_store, model) =
|
||||
setup_test_environment(cx, project.clone()).await;
|
||||
@@ -4158,9 +4149,9 @@ fn main() {{
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_retry_on_overloaded_error(cx: &mut TestAppContext) {
|
||||
init_test_settings(cx);
|
||||
let fs = init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
let project = create_test_project(&fs, cx, json!({})).await;
|
||||
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Enable Burn Mode to allow retries
|
||||
@@ -4236,9 +4227,9 @@ fn main() {{
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_retry_on_internal_server_error(cx: &mut TestAppContext) {
|
||||
init_test_settings(cx);
|
||||
let fs = init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
let project = create_test_project(&fs, cx, json!({})).await;
|
||||
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Enable Burn Mode to allow retries
|
||||
@@ -4318,9 +4309,9 @@ fn main() {{
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_exponential_backoff_on_retries(cx: &mut TestAppContext) {
|
||||
init_test_settings(cx);
|
||||
let fs = init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
let project = create_test_project(&fs, cx, json!({})).await;
|
||||
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Enable Burn Mode to allow retries
|
||||
@@ -4438,9 +4429,9 @@ fn main() {{
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_max_retries_exceeded(cx: &mut TestAppContext) {
|
||||
init_test_settings(cx);
|
||||
let fs = init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
let project = create_test_project(&fs, cx, json!({})).await;
|
||||
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Enable Burn Mode to allow retries
|
||||
@@ -4529,9 +4520,9 @@ fn main() {{
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_retry_message_removed_on_retry(cx: &mut TestAppContext) {
|
||||
init_test_settings(cx);
|
||||
let fs = init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
let project = create_test_project(&fs, cx, json!({})).await;
|
||||
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Enable Burn Mode to allow retries
|
||||
@@ -4702,9 +4693,9 @@ fn main() {{
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_successful_completion_clears_retry_state(cx: &mut TestAppContext) {
|
||||
init_test_settings(cx);
|
||||
let fs = init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
let project = create_test_project(&fs, cx, json!({})).await;
|
||||
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Enable Burn Mode to allow retries
|
||||
@@ -4868,9 +4859,9 @@ fn main() {{
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_rate_limit_retry_single_attempt(cx: &mut TestAppContext) {
|
||||
init_test_settings(cx);
|
||||
let fs = init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
let project = create_test_project(&fs, cx, json!({})).await;
|
||||
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Enable Burn Mode to allow retries
|
||||
@@ -5053,9 +5044,9 @@ fn main() {{
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_ui_only_messages_not_sent_to_model(cx: &mut TestAppContext) {
|
||||
init_test_settings(cx);
|
||||
let fs = init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
let project = create_test_project(&fs, cx, json!({})).await;
|
||||
let (_, _, thread, _, model) = setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Insert a regular user message
|
||||
@@ -5153,9 +5144,9 @@ fn main() {{
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_no_retry_without_burn_mode(cx: &mut TestAppContext) {
|
||||
init_test_settings(cx);
|
||||
let fs = init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
let project = create_test_project(&fs, cx, json!({})).await;
|
||||
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Ensure we're in Normal mode (not Burn mode)
|
||||
@@ -5226,9 +5217,9 @@ fn main() {{
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_retry_canceled_on_stop(cx: &mut TestAppContext) {
|
||||
init_test_settings(cx);
|
||||
let fs = init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
let project = create_test_project(&fs, cx, json!({})).await;
|
||||
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Enable Burn Mode to allow retries
|
||||
@@ -5334,7 +5325,8 @@ fn main() {{
|
||||
cx.run_until_parked();
|
||||
}
|
||||
|
||||
fn init_test_settings(cx: &mut TestAppContext) {
|
||||
fn init_test_settings(cx: &mut TestAppContext) -> Arc<dyn Fs> {
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
@@ -5342,7 +5334,7 @@ fn main() {{
|
||||
Project::init_settings(cx);
|
||||
AgentSettings::register(cx);
|
||||
prompt_store::init(cx);
|
||||
thread_store::init(cx);
|
||||
thread_store::init(fs.clone(), cx);
|
||||
workspace::init_settings(cx);
|
||||
language_model::init_settings(cx);
|
||||
ThemeSettings::register(cx);
|
||||
@@ -5356,16 +5348,17 @@ fn main() {{
|
||||
));
|
||||
assistant_tools::init(http_client, cx);
|
||||
});
|
||||
fs
|
||||
}
|
||||
|
||||
// Helper to create a test project with test files
|
||||
async fn create_test_project(
|
||||
fs: &Arc<dyn Fs>,
|
||||
cx: &mut TestAppContext,
|
||||
files: serde_json::Value,
|
||||
) -> Entity<Project> {
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(path!("/test"), files).await;
|
||||
Project::test(fs, [path!("/test").as_ref()], cx).await
|
||||
fs.as_fake().insert_tree(path!("/test"), files).await;
|
||||
Project::test(fs.clone(), [path!("/test").as_ref()], cx).await
|
||||
}
|
||||
|
||||
async fn setup_test_environment(
|
||||
|
||||
@@ -10,6 +10,7 @@ use assistant_tool::{Tool, ToolId, ToolWorkingSet};
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::HashMap;
|
||||
use context_server::ContextServerId;
|
||||
use fs::{Fs, RemoveOptions};
|
||||
use futures::{
|
||||
FutureExt as _, StreamExt as _,
|
||||
channel::{mpsc, oneshot},
|
||||
@@ -39,7 +40,7 @@ use std::{
|
||||
rc::Rc,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use util::{ResultExt as _, rel_path::RelPath};
|
||||
|
||||
use zed_env_vars::ZED_STATELESS;
|
||||
|
||||
@@ -85,8 +86,8 @@ const RULES_FILE_NAMES: [&str; 9] = [
|
||||
"GEMINI.md",
|
||||
];
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
ThreadsDatabase::init(cx);
|
||||
pub fn init(fs: Arc<dyn Fs>, cx: &mut App) {
|
||||
ThreadsDatabase::init(fs, cx);
|
||||
}
|
||||
|
||||
/// A system prompt shared by all threads created by this ThreadStore
|
||||
@@ -234,7 +235,7 @@ impl ThreadStore {
|
||||
if items.iter().any(|(path, _, _)| {
|
||||
RULES_FILE_NAMES
|
||||
.iter()
|
||||
.any(|name| path.as_ref() == Path::new(name))
|
||||
.any(|name| path.as_ref() == RelPath::unix(name).unwrap())
|
||||
}) {
|
||||
self.enqueue_system_prompt_reload();
|
||||
}
|
||||
@@ -327,7 +328,7 @@ impl ThreadStore {
|
||||
cx: &mut App,
|
||||
) -> Task<(WorktreeContext, Option<RulesLoadingError>)> {
|
||||
let tree = worktree.read(cx);
|
||||
let root_name = tree.root_name().into();
|
||||
let root_name = tree.root_name_str().into();
|
||||
let abs_path = tree.abs_path();
|
||||
|
||||
let mut context = WorktreeContext {
|
||||
@@ -367,7 +368,7 @@ impl ThreadStore {
|
||||
.into_iter()
|
||||
.filter_map(|name| {
|
||||
worktree
|
||||
.entry_for_path(name)
|
||||
.entry_for_path(RelPath::unix(name).unwrap())
|
||||
.filter(|entry| entry.is_file())
|
||||
.map(|entry| entry.path.clone())
|
||||
})
|
||||
@@ -869,13 +870,13 @@ impl ThreadsDatabase {
|
||||
GlobalThreadsDatabase::global(cx).0.clone()
|
||||
}
|
||||
|
||||
fn init(cx: &mut App) {
|
||||
fn init(fs: Arc<dyn Fs>, cx: &mut App) {
|
||||
let executor = cx.background_executor().clone();
|
||||
let database_future = executor
|
||||
.spawn({
|
||||
let executor = executor.clone();
|
||||
let threads_dir = paths::data_dir().join("threads");
|
||||
async move { ThreadsDatabase::new(threads_dir, executor) }
|
||||
async move { ThreadsDatabase::new(fs, threads_dir, executor).await }
|
||||
})
|
||||
.then(|result| future::ready(result.map(Arc::new).map_err(Arc::new)))
|
||||
.boxed()
|
||||
@@ -884,13 +885,17 @@ impl ThreadsDatabase {
|
||||
cx.set_global(GlobalThreadsDatabase(database_future));
|
||||
}
|
||||
|
||||
pub fn new(threads_dir: PathBuf, executor: BackgroundExecutor) -> Result<Self> {
|
||||
std::fs::create_dir_all(&threads_dir)?;
|
||||
pub async fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
threads_dir: PathBuf,
|
||||
executor: BackgroundExecutor,
|
||||
) -> Result<Self> {
|
||||
fs.create_dir(&threads_dir).await?;
|
||||
|
||||
let sqlite_path = threads_dir.join("threads.db");
|
||||
let mdb_path = threads_dir.join("threads-db.1.mdb");
|
||||
|
||||
let needs_migration_from_heed = mdb_path.exists();
|
||||
let needs_migration_from_heed = fs.is_file(&mdb_path).await;
|
||||
|
||||
let connection = if *ZED_STATELESS {
|
||||
Connection::open_memory(Some("THREAD_FALLBACK_DB"))
|
||||
@@ -932,7 +937,14 @@ impl ThreadsDatabase {
|
||||
.spawn(async move {
|
||||
log::info!("Starting threads.db migration");
|
||||
Self::migrate_from_heed(&mdb_path, db_connection, executor_clone)?;
|
||||
std::fs::remove_dir_all(mdb_path)?;
|
||||
fs.remove_dir(
|
||||
&mdb_path,
|
||||
RemoveOptions {
|
||||
recursive: true,
|
||||
ignore_if_not_exists: true,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
log::info!("threads.db migrated to sqlite");
|
||||
Ok::<(), anyhow::Error>(())
|
||||
})
|
||||
|
||||
@@ -6,7 +6,6 @@ use crate::{HistoryStore, TerminalHandle, ThreadEnvironment, TitleUpdated, Token
|
||||
use acp_thread::{AcpThread, AgentModelSelector};
|
||||
use action_log::ActionLog;
|
||||
use agent_client_protocol as acp;
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use collections::{HashSet, IndexMap};
|
||||
use fs::Fs;
|
||||
@@ -21,13 +20,14 @@ use project::{Project, ProjectItem, ProjectPath, Worktree};
|
||||
use prompt_store::{
|
||||
ProjectContext, PromptId, PromptStore, RulesFileContext, UserRulesContext, WorktreeContext,
|
||||
};
|
||||
use settings::update_settings_file;
|
||||
use settings::{LanguageModelSelection, update_settings_file};
|
||||
use std::any::Any;
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use util::ResultExt;
|
||||
use util::rel_path::RelPath;
|
||||
|
||||
const RULES_FILE_NAMES: [&str; 9] = [
|
||||
".rules",
|
||||
@@ -57,7 +57,7 @@ struct Session {
|
||||
|
||||
pub struct LanguageModels {
|
||||
/// Access language model by ID
|
||||
models: HashMap<acp_thread::AgentModelId, Arc<dyn LanguageModel>>,
|
||||
models: HashMap<acp::ModelId, Arc<dyn LanguageModel>>,
|
||||
/// Cached list for returning language model information
|
||||
model_list: acp_thread::AgentModelList,
|
||||
refresh_models_rx: watch::Receiver<()>,
|
||||
@@ -133,10 +133,7 @@ impl LanguageModels {
|
||||
self.refresh_models_rx.clone()
|
||||
}
|
||||
|
||||
pub fn model_from_id(
|
||||
&self,
|
||||
model_id: &acp_thread::AgentModelId,
|
||||
) -> Option<Arc<dyn LanguageModel>> {
|
||||
pub fn model_from_id(&self, model_id: &acp::ModelId) -> Option<Arc<dyn LanguageModel>> {
|
||||
self.models.get(model_id).cloned()
|
||||
}
|
||||
|
||||
@@ -147,12 +144,13 @@ impl LanguageModels {
|
||||
acp_thread::AgentModelInfo {
|
||||
id: Self::model_id(model),
|
||||
name: model.name().0,
|
||||
description: None,
|
||||
icon: Some(provider.icon()),
|
||||
}
|
||||
}
|
||||
|
||||
fn model_id(model: &Arc<dyn LanguageModel>) -> acp_thread::AgentModelId {
|
||||
acp_thread::AgentModelId(format!("{}/{}", model.provider_id().0, model.id().0).into())
|
||||
fn model_id(model: &Arc<dyn LanguageModel>) -> acp::ModelId {
|
||||
acp::ModelId(format!("{}/{}", model.provider_id().0, model.id().0).into())
|
||||
}
|
||||
|
||||
fn authenticate_all_language_model_providers(cx: &mut App) -> Task<()> {
|
||||
@@ -166,33 +164,41 @@ impl LanguageModels {
|
||||
cx.background_spawn(async move {
|
||||
for (provider_id, provider_name, authenticate_task) in authenticate_all_providers {
|
||||
if let Err(err) = authenticate_task.await {
|
||||
if matches!(err, language_model::AuthenticateError::CredentialsNotFound) {
|
||||
// Since we're authenticating these providers in the
|
||||
// background for the purposes of populating the
|
||||
// language selector, we don't care about providers
|
||||
// where the credentials are not found.
|
||||
} else {
|
||||
// Some providers have noisy failure states that we
|
||||
// don't want to spam the logs with every time the
|
||||
// language model selector is initialized.
|
||||
//
|
||||
// Ideally these should have more clear failure modes
|
||||
// that we know are safe to ignore here, like what we do
|
||||
// with `CredentialsNotFound` above.
|
||||
match provider_id.0.as_ref() {
|
||||
"lmstudio" | "ollama" => {
|
||||
// LM Studio and Ollama both make fetch requests to the local APIs to determine if they are "authenticated".
|
||||
//
|
||||
// These fail noisily, so we don't log them.
|
||||
}
|
||||
"copilot_chat" => {
|
||||
// Copilot Chat returns an error if Copilot is not enabled, so we don't log those errors.
|
||||
}
|
||||
_ => {
|
||||
log::error!(
|
||||
"Failed to authenticate provider: {}: {err}",
|
||||
provider_name.0
|
||||
);
|
||||
match err {
|
||||
language_model::AuthenticateError::CredentialsNotFound => {
|
||||
// Since we're authenticating these providers in the
|
||||
// background for the purposes of populating the
|
||||
// language selector, we don't care about providers
|
||||
// where the credentials are not found.
|
||||
}
|
||||
language_model::AuthenticateError::ConnectionRefused => {
|
||||
// Not logging connection refused errors as they are mostly from LM Studio's noisy auth failures.
|
||||
// LM Studio only has one auth method (endpoint call) which fails for users who haven't enabled it.
|
||||
// TODO: Better manage LM Studio auth logic to avoid these noisy failures.
|
||||
}
|
||||
_ => {
|
||||
// Some providers have noisy failure states that we
|
||||
// don't want to spam the logs with every time the
|
||||
// language model selector is initialized.
|
||||
//
|
||||
// Ideally these should have more clear failure modes
|
||||
// that we know are safe to ignore here, like what we do
|
||||
// with `CredentialsNotFound` above.
|
||||
match provider_id.0.as_ref() {
|
||||
"lmstudio" | "ollama" => {
|
||||
// LM Studio and Ollama both make fetch requests to the local APIs to determine if they are "authenticated".
|
||||
//
|
||||
// These fail noisily, so we don't log them.
|
||||
}
|
||||
"copilot_chat" => {
|
||||
// Copilot Chat returns an error if Copilot is not enabled, so we don't log those errors.
|
||||
}
|
||||
_ => {
|
||||
log::error!(
|
||||
"Failed to authenticate provider: {}: {err}",
|
||||
provider_name.0
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -429,7 +435,7 @@ impl NativeAgent {
|
||||
cx: &mut App,
|
||||
) -> Task<(WorktreeContext, Option<RulesLoadingError>)> {
|
||||
let tree = worktree.read(cx);
|
||||
let root_name = tree.root_name().into();
|
||||
let root_name = tree.root_name_str().into();
|
||||
let abs_path = tree.abs_path();
|
||||
|
||||
let mut context = WorktreeContext {
|
||||
@@ -469,7 +475,7 @@ impl NativeAgent {
|
||||
.into_iter()
|
||||
.filter_map(|name| {
|
||||
worktree
|
||||
.entry_for_path(name)
|
||||
.entry_for_path(RelPath::unix(name).unwrap())
|
||||
.filter(|entry| entry.is_file())
|
||||
.map(|entry| entry.path.clone())
|
||||
})
|
||||
@@ -553,7 +559,7 @@ impl NativeAgent {
|
||||
if items.iter().any(|(path, _, _)| {
|
||||
RULES_FILE_NAMES
|
||||
.iter()
|
||||
.any(|name| path.as_ref() == Path::new(name))
|
||||
.any(|name| path.as_ref() == RelPath::unix(name).unwrap())
|
||||
}) {
|
||||
self.project_context_needs_refresh.send(()).ok();
|
||||
}
|
||||
@@ -747,6 +753,7 @@ impl NativeAgentConnection {
|
||||
acp::ContentBlock::Text(acp::TextContent {
|
||||
text,
|
||||
annotations: None,
|
||||
meta: None,
|
||||
}),
|
||||
false,
|
||||
cx,
|
||||
@@ -759,6 +766,7 @@ impl NativeAgentConnection {
|
||||
acp::ContentBlock::Text(acp::TextContent {
|
||||
text,
|
||||
annotations: None,
|
||||
meta: None,
|
||||
}),
|
||||
true,
|
||||
cx,
|
||||
@@ -804,7 +812,10 @@ impl NativeAgentConnection {
|
||||
}
|
||||
ThreadEvent::Stop(stop_reason) => {
|
||||
log::debug!("Assistant message complete: {:?}", stop_reason);
|
||||
return Ok(acp::PromptResponse { stop_reason });
|
||||
return Ok(acp::PromptResponse {
|
||||
stop_reason,
|
||||
meta: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -818,15 +829,21 @@ impl NativeAgentConnection {
|
||||
log::debug!("Response stream completed");
|
||||
anyhow::Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::EndTurn,
|
||||
meta: None,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl AgentModelSelector for NativeAgentConnection {
|
||||
struct NativeAgentModelSelector {
|
||||
session_id: acp::SessionId,
|
||||
connection: NativeAgentConnection,
|
||||
}
|
||||
|
||||
impl acp_thread::AgentModelSelector for NativeAgentModelSelector {
|
||||
fn list_models(&self, cx: &mut App) -> Task<Result<acp_thread::AgentModelList>> {
|
||||
log::debug!("NativeAgentConnection::list_models called");
|
||||
let list = self.0.read(cx).models.model_list.clone();
|
||||
let list = self.connection.0.read(cx).models.model_list.clone();
|
||||
Task::ready(if list.is_empty() {
|
||||
Err(anyhow::anyhow!("No models available"))
|
||||
} else {
|
||||
@@ -834,24 +851,24 @@ impl AgentModelSelector for NativeAgentConnection {
|
||||
})
|
||||
}
|
||||
|
||||
fn select_model(
|
||||
&self,
|
||||
session_id: acp::SessionId,
|
||||
model_id: acp_thread::AgentModelId,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<()>> {
|
||||
log::debug!("Setting model for session {}: {}", session_id, model_id);
|
||||
fn select_model(&self, model_id: acp::ModelId, cx: &mut App) -> Task<Result<()>> {
|
||||
log::debug!(
|
||||
"Setting model for session {}: {}",
|
||||
self.session_id,
|
||||
model_id
|
||||
);
|
||||
let Some(thread) = self
|
||||
.connection
|
||||
.0
|
||||
.read(cx)
|
||||
.sessions
|
||||
.get(&session_id)
|
||||
.get(&self.session_id)
|
||||
.map(|session| session.thread.clone())
|
||||
else {
|
||||
return Task::ready(Err(anyhow!("Session not found")));
|
||||
};
|
||||
|
||||
let Some(model) = self.0.read(cx).models.model_from_id(&model_id) else {
|
||||
let Some(model) = self.connection.0.read(cx).models.model_from_id(&model_id) else {
|
||||
return Task::ready(Err(anyhow!("Invalid model ID {}", model_id)));
|
||||
};
|
||||
|
||||
@@ -859,29 +876,32 @@ impl AgentModelSelector for NativeAgentConnection {
|
||||
thread.set_model(model.clone(), cx);
|
||||
});
|
||||
|
||||
update_settings_file::<AgentSettings>(
|
||||
self.0.read(cx).fs.clone(),
|
||||
update_settings_file(
|
||||
self.connection.0.read(cx).fs.clone(),
|
||||
cx,
|
||||
move |settings, _cx| {
|
||||
settings.set_model(model);
|
||||
let provider = model.provider_id().0.to_string();
|
||||
let model = model.id().0.to_string();
|
||||
settings
|
||||
.agent
|
||||
.get_or_insert_default()
|
||||
.set_model(LanguageModelSelection {
|
||||
provider: provider.into(),
|
||||
model,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn selected_model(
|
||||
&self,
|
||||
session_id: &acp::SessionId,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<acp_thread::AgentModelInfo>> {
|
||||
let session_id = session_id.clone();
|
||||
|
||||
fn selected_model(&self, cx: &mut App) -> Task<Result<acp_thread::AgentModelInfo>> {
|
||||
let Some(thread) = self
|
||||
.connection
|
||||
.0
|
||||
.read(cx)
|
||||
.sessions
|
||||
.get(&session_id)
|
||||
.get(&self.session_id)
|
||||
.map(|session| session.thread.clone())
|
||||
else {
|
||||
return Task::ready(Err(anyhow!("Session not found")));
|
||||
@@ -898,8 +918,8 @@ impl AgentModelSelector for NativeAgentConnection {
|
||||
)))
|
||||
}
|
||||
|
||||
fn watch(&self, cx: &mut App) -> watch::Receiver<()> {
|
||||
self.0.read(cx).models.watch()
|
||||
fn watch(&self, cx: &mut App) -> Option<watch::Receiver<()>> {
|
||||
Some(self.connection.0.read(cx).models.watch())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -955,8 +975,11 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn model_selector(&self) -> Option<Rc<dyn AgentModelSelector>> {
|
||||
Some(Rc::new(self.clone()) as Rc<dyn AgentModelSelector>)
|
||||
fn model_selector(&self, session_id: &acp::SessionId) -> Option<Rc<dyn AgentModelSelector>> {
|
||||
Some(Rc::new(NativeAgentModelSelector {
|
||||
session_id: session_id.clone(),
|
||||
connection: self.clone(),
|
||||
}) as Rc<dyn AgentModelSelector>)
|
||||
}
|
||||
|
||||
fn prompt(
|
||||
@@ -1179,16 +1202,14 @@ mod tests {
|
||||
use crate::HistoryEntryId;
|
||||
|
||||
use super::*;
|
||||
use acp_thread::{
|
||||
AgentConnection, AgentModelGroupName, AgentModelId, AgentModelInfo, MentionUri,
|
||||
};
|
||||
use acp_thread::{AgentConnection, AgentModelGroupName, AgentModelInfo, MentionUri};
|
||||
use fs::FakeFs;
|
||||
use gpui::TestAppContext;
|
||||
use indoc::indoc;
|
||||
use indoc::formatdoc;
|
||||
use language_model::fake_provider::FakeLanguageModel;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use util::path;
|
||||
use util::{path, rel_path::rel_path};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_maintaining_project_context(cx: &mut TestAppContext) {
|
||||
@@ -1238,14 +1259,17 @@ mod tests {
|
||||
fs.insert_file("/a/.rules", Vec::new()).await;
|
||||
cx.run_until_parked();
|
||||
agent.read_with(cx, |agent, cx| {
|
||||
let rules_entry = worktree.read(cx).entry_for_path(".rules").unwrap();
|
||||
let rules_entry = worktree
|
||||
.read(cx)
|
||||
.entry_for_path(rel_path(".rules"))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
agent.project_context.read(cx).worktrees,
|
||||
vec![WorktreeContext {
|
||||
root_name: "a".into(),
|
||||
abs_path: Path::new("/a").into(),
|
||||
rules_file: Some(RulesFileContext {
|
||||
path_in_worktree: Path::new(".rules").into(),
|
||||
path_in_worktree: rel_path(".rules").into(),
|
||||
text: "".into(),
|
||||
project_entry_id: rules_entry.id.to_usize()
|
||||
})
|
||||
@@ -1275,7 +1299,25 @@ mod tests {
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let models = cx.update(|cx| connection.list_models(cx)).await.unwrap();
|
||||
// Create a thread/session
|
||||
let acp_thread = cx
|
||||
.update(|cx| {
|
||||
Rc::new(connection.clone()).new_thread(project.clone(), Path::new("/a"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let session_id = cx.update(|cx| acp_thread.read(cx).session_id().clone());
|
||||
|
||||
let models = cx
|
||||
.update(|cx| {
|
||||
connection
|
||||
.model_selector(&session_id)
|
||||
.unwrap()
|
||||
.list_models(cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let acp_thread::AgentModelList::Grouped(models) = models else {
|
||||
panic!("Unexpected model group");
|
||||
@@ -1285,8 +1327,9 @@ mod tests {
|
||||
IndexMap::from_iter([(
|
||||
AgentModelGroupName("Fake".into()),
|
||||
vec![AgentModelInfo {
|
||||
id: AgentModelId("fake/fake".into()),
|
||||
id: acp::ModelId("fake/fake".into()),
|
||||
name: "Fake".into(),
|
||||
description: None,
|
||||
icon: Some(ui::IconName::ZedAssistant),
|
||||
}]
|
||||
)])
|
||||
@@ -1343,8 +1386,9 @@ mod tests {
|
||||
let session_id = cx.update(|cx| acp_thread.read(cx).session_id().clone());
|
||||
|
||||
// Select a model
|
||||
let model_id = AgentModelId("fake/fake".into());
|
||||
cx.update(|cx| connection.select_model(session_id.clone(), model_id.clone(), cx))
|
||||
let selector = connection.model_selector(&session_id).unwrap();
|
||||
let model_id = acp::ModelId("fake/fake".into());
|
||||
cx.update(|cx| selector.select_model(model_id.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -1441,6 +1485,7 @@ mod tests {
|
||||
mime_type: None,
|
||||
size: None,
|
||||
title: None,
|
||||
meta: None,
|
||||
}),
|
||||
" mean?".into(),
|
||||
],
|
||||
@@ -1457,13 +1502,17 @@ mod tests {
|
||||
summary_model.end_last_completion_stream();
|
||||
|
||||
send.await.unwrap();
|
||||
let uri = MentionUri::File {
|
||||
abs_path: path!("/a/b.md").into(),
|
||||
}
|
||||
.to_uri();
|
||||
acp_thread.read_with(cx, |thread, cx| {
|
||||
assert_eq!(
|
||||
thread.to_markdown(cx),
|
||||
indoc! {"
|
||||
formatdoc! {"
|
||||
## User
|
||||
|
||||
What does [@b.md](file:///a/b.md) mean?
|
||||
What does [@b.md]({uri}) mean?
|
||||
|
||||
## Assistant
|
||||
|
||||
@@ -1499,10 +1548,10 @@ mod tests {
|
||||
acp_thread.read_with(cx, |thread, cx| {
|
||||
assert_eq!(
|
||||
thread.to_markdown(cx),
|
||||
indoc! {"
|
||||
formatdoc! {"
|
||||
## User
|
||||
|
||||
What does [@b.md](file:///a/b.md) mean?
|
||||
What does [@b.md]({uri}) mean?
|
||||
|
||||
## Assistant
|
||||
|
||||
|
||||
@@ -422,7 +422,7 @@ mod tests {
|
||||
use agent::MessageSegment;
|
||||
use agent::context::LoadedContext;
|
||||
use client::Client;
|
||||
use fs::FakeFs;
|
||||
use fs::{FakeFs, Fs};
|
||||
use gpui::AppContext;
|
||||
use gpui::TestAppContext;
|
||||
use http_client::FakeHttpClient;
|
||||
@@ -430,7 +430,7 @@ mod tests {
|
||||
use project::Project;
|
||||
use settings::SettingsStore;
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
fn init_test(fs: Arc<dyn Fs>, cx: &mut TestAppContext) {
|
||||
env_logger::try_init().ok();
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
@@ -441,7 +441,7 @@ mod tests {
|
||||
let http_client = FakeHttpClient::with_404_response();
|
||||
let clock = Arc::new(clock::FakeSystemClock::new());
|
||||
let client = Client::new(clock, http_client, cx);
|
||||
agent::init(cx);
|
||||
agent::init(fs, cx);
|
||||
agent_settings::init(cx);
|
||||
language_model::init(client, cx);
|
||||
});
|
||||
@@ -449,8 +449,8 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_retrieving_old_thread(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
init_test(fs.clone(), cx);
|
||||
let project = Project::test(fs, [], cx).await;
|
||||
|
||||
// Save a thread using the old agent.
|
||||
|
||||
@@ -262,7 +262,7 @@ impl HistoryStore {
|
||||
.iter()
|
||||
.filter_map(|entry| match entry {
|
||||
HistoryEntryId::TextThread(path) => path.file_name().map(|file| {
|
||||
SerializedRecentOpen::TextThread(file.to_string_lossy().to_string())
|
||||
SerializedRecentOpen::TextThread(file.to_string_lossy().into_owned())
|
||||
}),
|
||||
HistoryEntryId::AcpThread(id) => {
|
||||
Some(SerializedRecentOpen::AcpThread(id.to_string()))
|
||||
|
||||
@@ -48,16 +48,15 @@ The one exception to this is if the user references something you don't know abo
|
||||
## Code Block Formatting
|
||||
|
||||
Whenever you mention a code block, you MUST use ONLY use the following format:
|
||||
|
||||
```path/to/Something.blah#L123-456
|
||||
(code goes here)
|
||||
```
|
||||
The `#L123-456` means the line number range 123 through 456, and the path/to/Something.blah
|
||||
is a path in the project. (If there is no valid path in the project, then you can use
|
||||
/dev/null/path.extension for its path.) This is the ONLY valid way to format code blocks, because the Markdown parser
|
||||
does not understand the more common ```language syntax, or bare ``` blocks. It only
|
||||
understands this path-based syntax, and if the path is missing, then it will error and you will have to do it over again.
|
||||
|
||||
The `#L123-456` means the line number range 123 through 456, and the path/to/Something.blah is a path in the project. (If there is no valid path in the project, then you can use /dev/null/path.extension for its path.) This is the ONLY valid way to format code blocks, because the Markdown parser does not understand the more common ```language syntax, or bare ``` blocks. It only understands this path-based syntax, and if the path is missing, then it will error and you will have to do it over again.
|
||||
Just to be really clear about this, if you ever find yourself writing three backticks followed by a language name, STOP!
|
||||
You have made a mistake. You can only ever put paths after triple backticks!
|
||||
|
||||
<example>
|
||||
Based on all the information I've gathered, here's a summary of how this system works:
|
||||
1. The README file is loaded into the system.
|
||||
@@ -74,6 +73,7 @@ This is the last header in the README.
|
||||
```
|
||||
4. Finally, it passes this information on to the next process.
|
||||
</example>
|
||||
|
||||
<example>
|
||||
In Markdown, hash marks signify headings. For example:
|
||||
```/dev/null/example.md#L1-3
|
||||
@@ -82,6 +82,7 @@ In Markdown, hash marks signify headings. For example:
|
||||
### Level 3 heading
|
||||
```
|
||||
</example>
|
||||
|
||||
Here are examples of ways you must never render code blocks:
|
||||
<bad_example_do_not_do_this>
|
||||
In Markdown, hash marks signify headings. For example:
|
||||
@@ -91,7 +92,9 @@ In Markdown, hash marks signify headings. For example:
|
||||
### Level 3 heading
|
||||
```
|
||||
</bad_example_do_not_do_this>
|
||||
|
||||
This example is unacceptable because it does not include the path.
|
||||
|
||||
<bad_example_do_not_do_this>
|
||||
In Markdown, hash marks signify headings. For example:
|
||||
```markdown
|
||||
@@ -101,14 +104,15 @@ In Markdown, hash marks signify headings. For example:
|
||||
```
|
||||
</bad_example_do_not_do_this>
|
||||
This example is unacceptable because it has the language instead of the path.
|
||||
|
||||
<bad_example_do_not_do_this>
|
||||
In Markdown, hash marks signify headings. For example:
|
||||
# Level 1 heading
|
||||
## Level 2 heading
|
||||
### Level 3 heading
|
||||
</bad_example_do_not_do_this>
|
||||
This example is unacceptable because it uses indentation to mark the code block
|
||||
instead of backticks with a path.
|
||||
This example is unacceptable because it uses indentation to mark the code block instead of backticks with a path.
|
||||
|
||||
<bad_example_do_not_do_this>
|
||||
In Markdown, hash marks signify headings. For example:
|
||||
```markdown
|
||||
|
||||
@@ -1299,6 +1299,7 @@ async fn test_cancellation(cx: &mut TestAppContext) {
|
||||
status: Some(acp::ToolCallStatus::Completed),
|
||||
..
|
||||
},
|
||||
meta: None,
|
||||
},
|
||||
)) if Some(&id) == echo_id.as_ref() => {
|
||||
echo_completed = true;
|
||||
@@ -1849,8 +1850,18 @@ async fn test_agent_connection(cx: &mut TestAppContext) {
|
||||
.unwrap();
|
||||
let connection = NativeAgentConnection(agent.clone());
|
||||
|
||||
// Create a thread using new_thread
|
||||
let connection_rc = Rc::new(connection.clone());
|
||||
let acp_thread = cx
|
||||
.update(|cx| connection_rc.new_thread(project, cwd, cx))
|
||||
.await
|
||||
.expect("new_thread should succeed");
|
||||
|
||||
// Get the session_id from the AcpThread
|
||||
let session_id = acp_thread.read_with(cx, |thread, _| thread.session_id().clone());
|
||||
|
||||
// Test model_selector returns Some
|
||||
let selector_opt = connection.model_selector();
|
||||
let selector_opt = connection.model_selector(&session_id);
|
||||
assert!(
|
||||
selector_opt.is_some(),
|
||||
"agent2 should always support ModelSelector"
|
||||
@@ -1867,23 +1878,16 @@ async fn test_agent_connection(cx: &mut TestAppContext) {
|
||||
};
|
||||
assert!(!listed_models.is_empty(), "should have at least one model");
|
||||
assert_eq!(
|
||||
listed_models[&AgentModelGroupName("Fake".into())][0].id.0,
|
||||
listed_models[&AgentModelGroupName("Fake".into())][0]
|
||||
.id
|
||||
.0
|
||||
.as_ref(),
|
||||
"fake/fake"
|
||||
);
|
||||
|
||||
// Create a thread using new_thread
|
||||
let connection_rc = Rc::new(connection.clone());
|
||||
let acp_thread = cx
|
||||
.update(|cx| connection_rc.new_thread(project, cwd, cx))
|
||||
.await
|
||||
.expect("new_thread should succeed");
|
||||
|
||||
// Get the session_id from the AcpThread
|
||||
let session_id = acp_thread.read_with(cx, |thread, _| thread.session_id().clone());
|
||||
|
||||
// Test selected_model returns the default
|
||||
let model = cx
|
||||
.update(|cx| selector.selected_model(&session_id, cx))
|
||||
.update(|cx| selector.selected_model(cx))
|
||||
.await
|
||||
.expect("selected_model should succeed");
|
||||
let model = cx
|
||||
@@ -1926,6 +1930,7 @@ async fn test_agent_connection(cx: &mut TestAppContext) {
|
||||
acp::PromptRequest {
|
||||
session_id: session_id.clone(),
|
||||
prompt: vec!["ghi".into()],
|
||||
meta: None,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
@@ -1990,6 +1995,7 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
|
||||
locations: vec![],
|
||||
raw_input: Some(json!({})),
|
||||
raw_output: None,
|
||||
meta: None,
|
||||
}
|
||||
);
|
||||
let update = expect_tool_call_update_fields(&mut events).await;
|
||||
@@ -2003,6 +2009,7 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
|
||||
raw_input: Some(json!({ "content": "Thinking hard!" })),
|
||||
..Default::default()
|
||||
},
|
||||
meta: None,
|
||||
}
|
||||
);
|
||||
let update = expect_tool_call_update_fields(&mut events).await;
|
||||
@@ -2014,6 +2021,7 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
|
||||
status: Some(acp::ToolCallStatus::InProgress),
|
||||
..Default::default()
|
||||
},
|
||||
meta: None,
|
||||
}
|
||||
);
|
||||
let update = expect_tool_call_update_fields(&mut events).await;
|
||||
@@ -2025,6 +2033,7 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
|
||||
content: Some(vec!["Thinking hard!".into()]),
|
||||
..Default::default()
|
||||
},
|
||||
meta: None,
|
||||
}
|
||||
);
|
||||
let update = expect_tool_call_update_fields(&mut events).await;
|
||||
@@ -2037,6 +2046,7 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
|
||||
raw_output: Some("Finished thinking.".into()),
|
||||
..Default::default()
|
||||
},
|
||||
meta: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -614,6 +614,7 @@ impl Thread {
|
||||
fn prompt_capabilities(model: Option<&dyn LanguageModel>) -> acp::PromptCapabilities {
|
||||
let image = model.map_or(true, |model| model.supports_images());
|
||||
acp::PromptCapabilities {
|
||||
meta: None,
|
||||
image,
|
||||
audio: false,
|
||||
embedded_context: true,
|
||||
@@ -728,6 +729,7 @@ impl Thread {
|
||||
stream
|
||||
.0
|
||||
.unbounded_send(Ok(ThreadEvent::ToolCall(acp::ToolCall {
|
||||
meta: None,
|
||||
id: acp::ToolCallId(tool_use.id.to_string().into()),
|
||||
title: tool_use.name.to_string(),
|
||||
kind: acp::ToolKind::Other,
|
||||
@@ -877,27 +879,11 @@ impl Thread {
|
||||
.map(|worktree| Self::worktree_snapshot(worktree, git_store.clone(), cx))
|
||||
.collect();
|
||||
|
||||
cx.spawn(async move |_, cx| {
|
||||
cx.spawn(async move |_, _| {
|
||||
let worktree_snapshots = futures::future::join_all(worktree_snapshots).await;
|
||||
|
||||
let mut unsaved_buffers = Vec::new();
|
||||
cx.update(|app_cx| {
|
||||
let buffer_store = project.read(app_cx).buffer_store();
|
||||
for buffer_handle in buffer_store.read(app_cx).buffers() {
|
||||
let buffer = buffer_handle.read(app_cx);
|
||||
if buffer.is_dirty()
|
||||
&& let Some(file) = buffer.file()
|
||||
{
|
||||
let path = file.path().to_string_lossy().to_string();
|
||||
unsaved_buffers.push(path);
|
||||
}
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
|
||||
Arc::new(ProjectSnapshot {
|
||||
worktree_snapshots,
|
||||
unsaved_buffer_paths: unsaved_buffers,
|
||||
timestamp: Utc::now(),
|
||||
})
|
||||
})
|
||||
@@ -912,7 +898,7 @@ impl Thread {
|
||||
// Get worktree path and snapshot
|
||||
let worktree_info = cx.update(|app_cx| {
|
||||
let worktree = worktree.read(app_cx);
|
||||
let path = worktree.abs_path().to_string_lossy().to_string();
|
||||
let path = worktree.abs_path().to_string_lossy().into_owned();
|
||||
let snapshot = worktree.snapshot();
|
||||
(path, snapshot)
|
||||
});
|
||||
@@ -2333,6 +2319,7 @@ impl ThreadEventStream {
|
||||
input: serde_json::Value,
|
||||
) -> acp::ToolCall {
|
||||
acp::ToolCall {
|
||||
meta: None,
|
||||
id: acp::ToolCallId(id.to_string().into()),
|
||||
title,
|
||||
kind,
|
||||
@@ -2352,6 +2339,7 @@ impl ThreadEventStream {
|
||||
self.0
|
||||
.unbounded_send(Ok(ThreadEvent::ToolCallUpdate(
|
||||
acp::ToolCallUpdate {
|
||||
meta: None,
|
||||
id: acp::ToolCallId(tool_use_id.to_string().into()),
|
||||
fields,
|
||||
}
|
||||
@@ -2437,6 +2425,7 @@ impl ToolCallEventStream {
|
||||
.unbounded_send(Ok(ThreadEvent::ToolCallAuthorization(
|
||||
ToolCallAuthorization {
|
||||
tool_call: acp::ToolCallUpdate {
|
||||
meta: None,
|
||||
id: acp::ToolCallId(self.tool_use_id.to_string().into()),
|
||||
fields: acp::ToolCallUpdateFields {
|
||||
title: Some(title.into()),
|
||||
@@ -2448,16 +2437,19 @@ impl ToolCallEventStream {
|
||||
id: acp::PermissionOptionId("always_allow".into()),
|
||||
name: "Always Allow".into(),
|
||||
kind: acp::PermissionOptionKind::AllowAlways,
|
||||
meta: None,
|
||||
},
|
||||
acp::PermissionOption {
|
||||
id: acp::PermissionOptionId("allow".into()),
|
||||
name: "Allow".into(),
|
||||
kind: acp::PermissionOptionKind::AllowOnce,
|
||||
meta: None,
|
||||
},
|
||||
acp::PermissionOption {
|
||||
id: acp::PermissionOptionId("deny".into()),
|
||||
name: "Deny".into(),
|
||||
kind: acp::PermissionOptionKind::RejectOnce,
|
||||
meta: None,
|
||||
},
|
||||
],
|
||||
response: response_tx,
|
||||
@@ -2469,8 +2461,11 @@ impl ToolCallEventStream {
|
||||
"always_allow" => {
|
||||
if let Some(fs) = fs.clone() {
|
||||
cx.update(|cx| {
|
||||
update_settings_file::<AgentSettings>(fs, cx, |settings, _| {
|
||||
settings.set_always_allow_tool_actions(true);
|
||||
update_settings_file(fs, cx, |settings, _| {
|
||||
settings
|
||||
.agent
|
||||
.get_or_insert_default()
|
||||
.set_always_allow_tool_actions(true);
|
||||
});
|
||||
})?;
|
||||
}
|
||||
@@ -2611,17 +2606,21 @@ impl From<UserMessageContent> for acp::ContentBlock {
|
||||
UserMessageContent::Text(text) => acp::ContentBlock::Text(acp::TextContent {
|
||||
text,
|
||||
annotations: None,
|
||||
meta: None,
|
||||
}),
|
||||
UserMessageContent::Image(image) => acp::ContentBlock::Image(acp::ImageContent {
|
||||
data: image.source.to_string(),
|
||||
mime_type: "image/png".to_string(),
|
||||
meta: None,
|
||||
annotations: None,
|
||||
uri: None,
|
||||
}),
|
||||
UserMessageContent::Mention { uri, content } => {
|
||||
acp::ContentBlock::Resource(acp::EmbeddedResource {
|
||||
meta: None,
|
||||
resource: acp::EmbeddedResourceResource::TextResourceContents(
|
||||
acp::TextResourceContents {
|
||||
meta: None,
|
||||
mime_type: None,
|
||||
text: content,
|
||||
uri: uri.to_uri().to_string(),
|
||||
|
||||
@@ -9,14 +9,14 @@ use std::sync::Arc;
|
||||
use util::markdown::MarkdownInlineCode;
|
||||
|
||||
/// Copies a file or directory in the project, and returns confirmation that the copy succeeded.
|
||||
/// Directory contents will be copied recursively (like `cp -r`).
|
||||
/// Directory contents will be copied recursively.
|
||||
///
|
||||
/// This tool should be used when it's desirable to create a copy of a file or directory without modifying the original.
|
||||
/// It's much more efficient than doing this by separately reading and then writing the file or directory's contents, so this tool should be preferred over that approach whenever copying is the goal.
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct CopyPathToolInput {
|
||||
/// The source path of the file or directory to copy.
|
||||
/// If a directory is specified, its contents will be copied recursively (like `cp -r`).
|
||||
/// If a directory is specified, its contents will be copied recursively.
|
||||
///
|
||||
/// <example>
|
||||
/// If the project has the following files:
|
||||
@@ -84,9 +84,7 @@ impl AgentTool for CopyPathTool {
|
||||
.and_then(|project_path| project.entry_for_path(&project_path, cx))
|
||||
{
|
||||
Some(entity) => match project.find_project_path(&input.destination_path, cx) {
|
||||
Some(project_path) => {
|
||||
project.copy_entry(entity.id, None, project_path.path, cx)
|
||||
}
|
||||
Some(project_path) => project.copy_entry(entity.id, project_path, cx),
|
||||
None => Task::ready(Err(anyhow!(
|
||||
"Destination path {} was outside the project.",
|
||||
input.destination_path
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::{AgentTool, ToolCallEventStream};
|
||||
|
||||
/// Creates a new directory at the specified path within the project. Returns confirmation that the directory was created.
|
||||
///
|
||||
/// This tool creates a directory and all necessary parent directories (similar to `mkdir -p`). It should be used whenever you need to create new directories within the project.
|
||||
/// This tool creates a directory and all necessary parent directories. It should be used whenever you need to create new directories within the project.
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct CreateDirectoryToolInput {
|
||||
/// The path of the new directory.
|
||||
|
||||
@@ -6,7 +6,7 @@ use language::{DiagnosticSeverity, OffsetRangeExt};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{fmt::Write, path::Path, sync::Arc};
|
||||
use std::{fmt::Write, sync::Arc};
|
||||
use ui::SharedString;
|
||||
use util::markdown::MarkdownInlineCode;
|
||||
|
||||
@@ -147,9 +147,7 @@ impl AgentTool for DiagnosticsTool {
|
||||
has_diagnostics = true;
|
||||
output.push_str(&format!(
|
||||
"{}: {} error(s), {} warning(s)\n",
|
||||
Path::new(worktree.read(cx).root_name())
|
||||
.join(project_path.path)
|
||||
.display(),
|
||||
worktree.read(cx).absolutize(&project_path.path).display(),
|
||||
summary.error_count,
|
||||
summary.warning_count
|
||||
));
|
||||
|
||||
@@ -17,10 +17,12 @@ use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
use smol::stream::StreamExt as _;
|
||||
use std::ffi::OsStr;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use ui::SharedString;
|
||||
use util::ResultExt;
|
||||
use util::rel_path::RelPath;
|
||||
|
||||
const DEFAULT_UI_TEXT: &str = "Editing file";
|
||||
|
||||
@@ -148,12 +150,11 @@ impl EditFileTool {
|
||||
|
||||
// If any path component matches the local settings folder, then this could affect
|
||||
// the editor in ways beyond the project source, so prompt.
|
||||
let local_settings_folder = paths::local_settings_folder_relative_path();
|
||||
let local_settings_folder = paths::local_settings_folder_name();
|
||||
let path = Path::new(&input.path);
|
||||
if path
|
||||
.components()
|
||||
.any(|component| component.as_os_str() == local_settings_folder.as_os_str())
|
||||
{
|
||||
if path.components().any(|component| {
|
||||
component.as_os_str() == <_ as AsRef<OsStr>>::as_ref(&local_settings_folder)
|
||||
}) {
|
||||
return event_stream.authorize(
|
||||
format!("{} (local settings)", input.display_description),
|
||||
cx,
|
||||
@@ -162,6 +163,7 @@ impl EditFileTool {
|
||||
|
||||
// It's also possible that the global config dir is configured to be inside the project,
|
||||
// so check for that edge case too.
|
||||
// TODO this is broken when remoting
|
||||
if let Ok(canonical_path) = std::fs::canonicalize(&input.path)
|
||||
&& canonical_path.starts_with(paths::config_dir())
|
||||
{
|
||||
@@ -216,9 +218,7 @@ impl AgentTool for EditFileTool {
|
||||
.read(cx)
|
||||
.short_full_path_for_project_path(&project_path, cx)
|
||||
})
|
||||
.unwrap_or(Path::new(&input.path).into())
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
.unwrap_or(input.path.to_string_lossy().into_owned())
|
||||
.into(),
|
||||
Err(raw_input) => {
|
||||
if let Some(input) =
|
||||
@@ -235,9 +235,7 @@ impl AgentTool for EditFileTool {
|
||||
.read(cx)
|
||||
.short_full_path_for_project_path(&project_path, cx)
|
||||
})
|
||||
.unwrap_or(Path::new(&input.path).into())
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
.unwrap_or(input.path)
|
||||
.into();
|
||||
}
|
||||
|
||||
@@ -274,6 +272,7 @@ impl AgentTool for EditFileTool {
|
||||
locations: Some(vec![acp::ToolCallLocation {
|
||||
path: abs_path,
|
||||
line: None,
|
||||
meta: None,
|
||||
}]),
|
||||
..Default::default()
|
||||
});
|
||||
@@ -353,7 +352,7 @@ impl AgentTool for EditFileTool {
|
||||
}).ok();
|
||||
if let Some(abs_path) = abs_path.clone() {
|
||||
event_stream.update_fields(ToolCallUpdateFields {
|
||||
locations: Some(vec![ToolCallLocation { path: abs_path, line }]),
|
||||
locations: Some(vec![ToolCallLocation { path: abs_path, line, meta: None }]),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
@@ -477,7 +476,7 @@ impl AgentTool for EditFileTool {
|
||||
) -> Result<()> {
|
||||
event_stream.update_diff(cx.new(|cx| {
|
||||
Diff::finalized(
|
||||
output.input_path,
|
||||
output.input_path.to_string_lossy().into_owned(),
|
||||
Some(output.old_text.to_string()),
|
||||
output.new_text,
|
||||
self.language_registry.clone(),
|
||||
@@ -541,10 +540,12 @@ fn resolve_path(
|
||||
let file_name = input
|
||||
.path
|
||||
.file_name()
|
||||
.and_then(|file_name| file_name.to_str())
|
||||
.and_then(|file_name| RelPath::unix(file_name).ok())
|
||||
.context("Can't create file: invalid filename")?;
|
||||
|
||||
let new_file_path = parent_project_path.map(|parent| ProjectPath {
|
||||
path: Arc::from(parent.path.join(file_name)),
|
||||
path: parent.path.join(file_name),
|
||||
..parent
|
||||
});
|
||||
|
||||
@@ -564,7 +565,7 @@ mod tests {
|
||||
use prompt_store::ProjectContext;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use util::path;
|
||||
use util::{path, rel_path::rel_path};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_edit_nonexistent_file(cx: &mut TestAppContext) {
|
||||
@@ -613,13 +614,13 @@ mod tests {
|
||||
let mode = &EditFileMode::Create;
|
||||
|
||||
let result = test_resolve_path(mode, "root/new.txt", cx);
|
||||
assert_resolved_path_eq(result.await, "new.txt");
|
||||
assert_resolved_path_eq(result.await, rel_path("new.txt"));
|
||||
|
||||
let result = test_resolve_path(mode, "new.txt", cx);
|
||||
assert_resolved_path_eq(result.await, "new.txt");
|
||||
assert_resolved_path_eq(result.await, rel_path("new.txt"));
|
||||
|
||||
let result = test_resolve_path(mode, "dir/new.txt", cx);
|
||||
assert_resolved_path_eq(result.await, "dir/new.txt");
|
||||
assert_resolved_path_eq(result.await, rel_path("dir/new.txt"));
|
||||
|
||||
let result = test_resolve_path(mode, "root/dir/subdir/existing.txt", cx);
|
||||
assert_eq!(
|
||||
@@ -641,10 +642,10 @@ mod tests {
|
||||
let path_with_root = "root/dir/subdir/existing.txt";
|
||||
let path_without_root = "dir/subdir/existing.txt";
|
||||
let result = test_resolve_path(mode, path_with_root, cx);
|
||||
assert_resolved_path_eq(result.await, path_without_root);
|
||||
assert_resolved_path_eq(result.await, rel_path(path_without_root));
|
||||
|
||||
let result = test_resolve_path(mode, path_without_root, cx);
|
||||
assert_resolved_path_eq(result.await, path_without_root);
|
||||
assert_resolved_path_eq(result.await, rel_path(path_without_root));
|
||||
|
||||
let result = test_resolve_path(mode, "root/nonexistent.txt", cx);
|
||||
assert_eq!(
|
||||
@@ -689,14 +690,10 @@ mod tests {
|
||||
cx.update(|cx| resolve_path(&input, project, cx))
|
||||
}
|
||||
|
||||
fn assert_resolved_path_eq(path: anyhow::Result<ProjectPath>, expected: &str) {
|
||||
let actual = path
|
||||
.expect("Should return valid path")
|
||||
.path
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.replace("\\", "/"); // Naive Windows paths normalization
|
||||
assert_eq!(actual, expected);
|
||||
#[track_caller]
|
||||
fn assert_resolved_path_eq(path: anyhow::Result<ProjectPath>, expected: &RelPath) {
|
||||
let actual = path.expect("Should return valid path").path;
|
||||
assert_eq!(actual.as_ref(), expected);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -790,14 +787,11 @@ mod tests {
|
||||
// First, test with format_on_save enabled
|
||||
cx.update(|cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<language::language_settings::AllLanguageSettings>(
|
||||
cx,
|
||||
|settings| {
|
||||
settings.defaults.format_on_save = Some(FormatOnSave::On);
|
||||
settings.defaults.formatter =
|
||||
Some(language::language_settings::SelectedFormatter::Auto);
|
||||
},
|
||||
);
|
||||
store.update_user_settings(cx, |settings| {
|
||||
settings.project.all_languages.defaults.format_on_save = Some(FormatOnSave::On);
|
||||
settings.project.all_languages.defaults.formatter =
|
||||
Some(language::language_settings::SelectedFormatter::Auto);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -852,12 +846,10 @@ mod tests {
|
||||
// Next, test with format_on_save disabled
|
||||
cx.update(|cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<language::language_settings::AllLanguageSettings>(
|
||||
cx,
|
||||
|settings| {
|
||||
settings.defaults.format_on_save = Some(FormatOnSave::Off);
|
||||
},
|
||||
);
|
||||
store.update_user_settings(cx, |settings| {
|
||||
settings.project.all_languages.defaults.format_on_save =
|
||||
Some(FormatOnSave::Off);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -934,12 +926,13 @@ mod tests {
|
||||
// First, test with remove_trailing_whitespace_on_save enabled
|
||||
cx.update(|cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<language::language_settings::AllLanguageSettings>(
|
||||
cx,
|
||||
|settings| {
|
||||
settings.defaults.remove_trailing_whitespace_on_save = Some(true);
|
||||
},
|
||||
);
|
||||
store.update_user_settings(cx, |settings| {
|
||||
settings
|
||||
.project
|
||||
.all_languages
|
||||
.defaults
|
||||
.remove_trailing_whitespace_on_save = Some(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -990,12 +983,13 @@ mod tests {
|
||||
// Next, test with remove_trailing_whitespace_on_save disabled
|
||||
cx.update(|cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<language::language_settings::AllLanguageSettings>(
|
||||
cx,
|
||||
|settings| {
|
||||
settings.defaults.remove_trailing_whitespace_on_save = Some(false);
|
||||
},
|
||||
);
|
||||
store.update_user_settings(cx, |settings| {
|
||||
settings
|
||||
.project
|
||||
.all_languages
|
||||
.defaults
|
||||
.remove_trailing_whitespace_on_save = Some(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1410,8 +1404,8 @@ mod tests {
|
||||
// Parent directory references - find_project_path resolves these
|
||||
(
|
||||
"project/../other",
|
||||
false,
|
||||
"Path with .. is resolved by find_project_path",
|
||||
true,
|
||||
"Path with .. that goes outside of root directory",
|
||||
),
|
||||
(
|
||||
"project/./src/file.rs",
|
||||
@@ -1439,16 +1433,18 @@ mod tests {
|
||||
)
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
if should_confirm {
|
||||
stream_rx.expect_authorization().await;
|
||||
} else {
|
||||
auth.await.unwrap();
|
||||
assert!(
|
||||
stream_rx.try_next().is_err(),
|
||||
"Failed for case: {} - path: {} - expected no confirmation but got one",
|
||||
description,
|
||||
path
|
||||
);
|
||||
auth.await.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,6 +138,7 @@ impl AgentTool for FindPathTool {
|
||||
mime_type: None,
|
||||
size: None,
|
||||
title: None,
|
||||
meta: None,
|
||||
}),
|
||||
})
|
||||
.collect(),
|
||||
@@ -155,10 +156,14 @@ impl AgentTool for FindPathTool {
|
||||
}
|
||||
|
||||
fn search_paths(glob: &str, project: Entity<Project>, cx: &mut App) -> Task<Result<Vec<PathBuf>>> {
|
||||
let path_matcher = match PathMatcher::new([
|
||||
// Sometimes models try to search for "". In this case, return all paths in the project.
|
||||
if glob.is_empty() { "*" } else { glob },
|
||||
]) {
|
||||
let path_style = project.read(cx).path_style(cx);
|
||||
let path_matcher = match PathMatcher::new(
|
||||
[
|
||||
// Sometimes models try to search for "". In this case, return all paths in the project.
|
||||
if glob.is_empty() { "*" } else { glob },
|
||||
],
|
||||
path_style,
|
||||
) {
|
||||
Ok(matcher) => matcher,
|
||||
Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {err}"))),
|
||||
};
|
||||
@@ -172,9 +177,8 @@ fn search_paths(glob: &str, project: Entity<Project>, cx: &mut App) -> Task<Resu
|
||||
let mut results = Vec::new();
|
||||
for snapshot in snapshots {
|
||||
for entry in snapshot.entries(false, 0) {
|
||||
let root_name = PathBuf::from(snapshot.root_name());
|
||||
if path_matcher.is_match(root_name.join(&entry.path)) {
|
||||
results.push(snapshot.abs_path().join(entry.path.as_ref()));
|
||||
if path_matcher.is_match(snapshot.root_name().join(&entry.path).as_std_path()) {
|
||||
results.push(snapshot.absolutize(&entry.path));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,12 +110,15 @@ impl AgentTool for GrepTool {
|
||||
const CONTEXT_LINES: u32 = 2;
|
||||
const MAX_ANCESTOR_LINES: u32 = 10;
|
||||
|
||||
let path_style = self.project.read(cx).path_style(cx);
|
||||
|
||||
let include_matcher = match PathMatcher::new(
|
||||
input
|
||||
.include_pattern
|
||||
.as_ref()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>(),
|
||||
path_style,
|
||||
) {
|
||||
Ok(matcher) => matcher,
|
||||
Err(error) => {
|
||||
@@ -132,7 +135,7 @@ impl AgentTool for GrepTool {
|
||||
.iter()
|
||||
.chain(global_settings.private_files.sources().iter());
|
||||
|
||||
match PathMatcher::new(exclude_patterns) {
|
||||
match PathMatcher::new(exclude_patterns, path_style) {
|
||||
Ok(matcher) => matcher,
|
||||
Err(error) => {
|
||||
return Task::ready(Err(anyhow!("invalid exclude pattern: {error}")));
|
||||
@@ -261,10 +264,8 @@ impl AgentTool for GrepTool {
|
||||
let end_row = range.end.row;
|
||||
output.push_str("\n### ");
|
||||
|
||||
if let Some(parent_symbols) = &parent_symbols {
|
||||
for symbol in parent_symbols {
|
||||
write!(output, "{} › ", symbol.text)?;
|
||||
}
|
||||
for symbol in parent_symbols {
|
||||
write!(output, "{} › ", symbol.text)?;
|
||||
}
|
||||
|
||||
if range.start.row == end_row {
|
||||
@@ -310,7 +311,7 @@ mod tests {
|
||||
use super::*;
|
||||
use gpui::{TestAppContext, UpdateGlobal};
|
||||
use language::{Language, LanguageConfig, LanguageMatcher};
|
||||
use project::{FakeFs, Project, WorktreeSettings};
|
||||
use project::{FakeFs, Project};
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use unindent::Unindent;
|
||||
@@ -829,19 +830,21 @@ mod tests {
|
||||
|
||||
cx.update(|cx| {
|
||||
use gpui::UpdateGlobal;
|
||||
use project::WorktreeSettings;
|
||||
use settings::SettingsStore;
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<WorktreeSettings>(cx, |settings| {
|
||||
settings.file_scan_exclusions = Some(vec![
|
||||
store.update_user_settings(cx, |settings| {
|
||||
settings.project.worktree.file_scan_exclusions = Some(vec![
|
||||
"**/.secretdir".to_string(),
|
||||
"**/.mymetadata".to_string(),
|
||||
]);
|
||||
settings.private_files = Some(vec![
|
||||
"**/.mysecrets".to_string(),
|
||||
"**/*.privatekey".to_string(),
|
||||
"**/*.mysensitive".to_string(),
|
||||
]);
|
||||
settings.project.worktree.private_files = Some(
|
||||
vec![
|
||||
"**/.mysecrets".to_string(),
|
||||
"**/*.privatekey".to_string(),
|
||||
"**/*.mysensitive".to_string(),
|
||||
]
|
||||
.into(),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1064,10 +1067,11 @@ mod tests {
|
||||
// Set global settings
|
||||
cx.update(|cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<WorktreeSettings>(cx, |settings| {
|
||||
settings.file_scan_exclusions =
|
||||
store.update_user_settings(cx, |settings| {
|
||||
settings.project.worktree.file_scan_exclusions =
|
||||
Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]);
|
||||
settings.private_files = Some(vec!["**/.env".to_string()]);
|
||||
settings.project.worktree.private_files =
|
||||
Some(vec!["**/.env".to_string()].into());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,12 +2,12 @@ use crate::{AgentTool, ToolCallEventStream};
|
||||
use agent_client_protocol::ToolKind;
|
||||
use anyhow::{Result, anyhow};
|
||||
use gpui::{App, Entity, SharedString, Task};
|
||||
use project::{Project, WorktreeSettings};
|
||||
use project::{Project, ProjectPath, WorktreeSettings};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
use std::fmt::Write;
|
||||
use std::{path::Path, sync::Arc};
|
||||
use std::sync::Arc;
|
||||
use util::markdown::MarkdownInlineCode;
|
||||
|
||||
/// Lists files and directories in a given path. Prefer the `grep` or `find_path` tools when searching the codebase.
|
||||
@@ -86,13 +86,13 @@ impl AgentTool for ListDirectoryTool {
|
||||
.read(cx)
|
||||
.worktrees(cx)
|
||||
.filter_map(|worktree| {
|
||||
worktree.read(cx).root_entry().and_then(|entry| {
|
||||
if entry.is_dir() {
|
||||
entry.path.to_str()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
let worktree = worktree.read(cx);
|
||||
let root_entry = worktree.root_entry()?;
|
||||
if root_entry.is_dir() {
|
||||
Some(root_entry.path.display(worktree.path_style()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
@@ -143,7 +143,7 @@ impl AgentTool for ListDirectoryTool {
|
||||
}
|
||||
|
||||
let worktree_snapshot = worktree.read(cx).snapshot();
|
||||
let worktree_root_name = worktree.read(cx).root_name().to_string();
|
||||
let worktree_root_name = worktree.read(cx).root_name();
|
||||
|
||||
let Some(entry) = worktree_snapshot.entry_for_path(&project_path.path) else {
|
||||
return Task::ready(Err(anyhow!("Path not found: {}", input.path)));
|
||||
@@ -165,25 +165,17 @@ impl AgentTool for ListDirectoryTool {
|
||||
continue;
|
||||
}
|
||||
|
||||
if self
|
||||
.project
|
||||
.read(cx)
|
||||
.find_project_path(&entry.path, cx)
|
||||
.map(|project_path| {
|
||||
let worktree_settings = WorktreeSettings::get(Some((&project_path).into()), cx);
|
||||
|
||||
worktree_settings.is_path_excluded(&project_path.path)
|
||||
|| worktree_settings.is_path_private(&project_path.path)
|
||||
})
|
||||
.unwrap_or(false)
|
||||
let project_path: ProjectPath = (worktree_snapshot.id(), entry.path.clone()).into();
|
||||
if worktree_settings.is_path_excluded(&project_path.path)
|
||||
|| worktree_settings.is_path_private(&project_path.path)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let full_path = Path::new(&worktree_root_name)
|
||||
let full_path = worktree_root_name
|
||||
.join(&entry.path)
|
||||
.display()
|
||||
.to_string();
|
||||
.display(worktree_snapshot.path_style())
|
||||
.into_owned();
|
||||
if entry.is_dir() {
|
||||
folders.push(full_path);
|
||||
} else {
|
||||
@@ -214,7 +206,7 @@ mod tests {
|
||||
use super::*;
|
||||
use gpui::{TestAppContext, UpdateGlobal};
|
||||
use indoc::indoc;
|
||||
use project::{FakeFs, Project, WorktreeSettings};
|
||||
use project::{FakeFs, Project};
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use util::path;
|
||||
@@ -421,17 +413,20 @@ mod tests {
|
||||
// Configure settings explicitly
|
||||
cx.update(|cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<WorktreeSettings>(cx, |settings| {
|
||||
settings.file_scan_exclusions = Some(vec![
|
||||
store.update_user_settings(cx, |settings| {
|
||||
settings.project.worktree.file_scan_exclusions = Some(vec![
|
||||
"**/.secretdir".to_string(),
|
||||
"**/.mymetadata".to_string(),
|
||||
"**/.hidden_subdir".to_string(),
|
||||
]);
|
||||
settings.private_files = Some(vec![
|
||||
"**/.mysecrets".to_string(),
|
||||
"**/*.privatekey".to_string(),
|
||||
"**/*.mysensitive".to_string(),
|
||||
]);
|
||||
settings.project.worktree.private_files = Some(
|
||||
vec![
|
||||
"**/.mysecrets".to_string(),
|
||||
"**/*.privatekey".to_string(),
|
||||
"**/*.mysensitive".to_string(),
|
||||
]
|
||||
.into(),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -565,10 +560,11 @@ mod tests {
|
||||
// Set global settings
|
||||
cx.update(|cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<WorktreeSettings>(cx, |settings| {
|
||||
settings.file_scan_exclusions =
|
||||
store.update_user_settings(cx, |settings| {
|
||||
settings.project.worktree.file_scan_exclusions =
|
||||
Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]);
|
||||
settings.private_files = Some(vec!["**/.env".to_string()]);
|
||||
settings.project.worktree.private_files =
|
||||
Some(vec!["**/.env".to_string()].into());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -98,7 +98,7 @@ impl AgentTool for MovePathTool {
|
||||
.and_then(|project_path| project.entry_for_path(&project_path, cx))
|
||||
{
|
||||
Some(entity) => match project.find_project_path(&input.destination_path, cx) {
|
||||
Some(project_path) => project.rename_entry(entity.id, project_path.path, cx),
|
||||
Some(project_path) => project.rename_entry(entity.id, project_path, cx),
|
||||
None => Task::ready(Err(anyhow!(
|
||||
"Destination path {} was outside the project.",
|
||||
input.destination_path
|
||||
|
||||
@@ -104,7 +104,7 @@ mod tests {
|
||||
async fn test_to_absolute_path(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp directory");
|
||||
let temp_path = temp_dir.path().to_string_lossy().to_string();
|
||||
let temp_path = temp_dir.path().to_string_lossy().into_owned();
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
|
||||
@@ -82,12 +82,12 @@ impl AgentTool for ReadFileTool {
|
||||
{
|
||||
match (input.start_line, input.end_line) {
|
||||
(Some(start), Some(end)) => {
|
||||
format!("Read file `{}` (lines {}-{})", path.display(), start, end,)
|
||||
format!("Read file `{path}` (lines {}-{})", start, end,)
|
||||
}
|
||||
(Some(start), None) => {
|
||||
format!("Read file `{}` (from line {})", path.display(), start)
|
||||
format!("Read file `{path}` (from line {})", start)
|
||||
}
|
||||
_ => format!("Read file `{}`", path.display()),
|
||||
_ => format!("Read file `{path}`"),
|
||||
}
|
||||
.into()
|
||||
} else {
|
||||
@@ -147,8 +147,9 @@ impl AgentTool for ReadFileTool {
|
||||
|
||||
event_stream.update_fields(ToolCallUpdateFields {
|
||||
locations: Some(vec![acp::ToolCallLocation {
|
||||
path: abs_path,
|
||||
path: abs_path.clone(),
|
||||
line: input.start_line.map(|line| line.saturating_sub(1)),
|
||||
meta: None,
|
||||
}]),
|
||||
..Default::default()
|
||||
});
|
||||
@@ -200,7 +201,6 @@ impl AgentTool for ReadFileTool {
|
||||
// Check if specific line ranges are provided
|
||||
let result = if input.start_line.is_some() || input.end_line.is_some() {
|
||||
let result = buffer.read_with(cx, |buffer, _cx| {
|
||||
let text = buffer.text();
|
||||
// .max(1) because despite instructions to be 1-indexed, sometimes the model passes 0.
|
||||
let start = input.start_line.unwrap_or(1).max(1);
|
||||
let start_row = start - 1;
|
||||
@@ -209,13 +209,13 @@ impl AgentTool for ReadFileTool {
|
||||
anchor = Some(buffer.anchor_before(Point::new(start_row, column)));
|
||||
}
|
||||
|
||||
let lines = text.split('\n').skip(start_row as usize);
|
||||
if let Some(end) = input.end_line {
|
||||
let count = end.saturating_sub(start).saturating_add(1); // Ensure at least 1 line
|
||||
itertools::intersperse(lines.take(count as usize), "\n").collect::<String>()
|
||||
} else {
|
||||
itertools::intersperse(lines, "\n").collect::<String>()
|
||||
let mut end_row = input.end_line.unwrap_or(u32::MAX);
|
||||
if end_row <= start_row {
|
||||
end_row = start_row + 1; // read at least one lines
|
||||
}
|
||||
let start = buffer.anchor_before(Point::new(start_row, 0));
|
||||
let end = buffer.anchor_before(Point::new(end_row, 0));
|
||||
buffer.text_for_range(start..end).collect::<String>()
|
||||
})?;
|
||||
|
||||
action_log.update(cx, |log, cx| {
|
||||
@@ -225,38 +225,33 @@ impl AgentTool for ReadFileTool {
|
||||
Ok(result.into())
|
||||
} else {
|
||||
// No line ranges specified, so check file size to see if it's too big.
|
||||
let file_size = buffer.read_with(cx, |buffer, _cx| buffer.text().len())?;
|
||||
let buffer_content = outline::get_buffer_content_or_outline(
|
||||
buffer.clone(),
|
||||
Some(&abs_path.to_string_lossy()),
|
||||
cx,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if file_size <= outline::AUTO_OUTLINE_SIZE {
|
||||
// File is small enough, so return its contents.
|
||||
let result = buffer.read_with(cx, |buffer, _cx| buffer.text())?;
|
||||
action_log.update(cx, |log, cx| {
|
||||
log.buffer_read(buffer.clone(), cx);
|
||||
})?;
|
||||
|
||||
action_log.update(cx, |log, cx| {
|
||||
log.buffer_read(buffer.clone(), cx);
|
||||
})?;
|
||||
|
||||
Ok(result.into())
|
||||
} else {
|
||||
// File is too big, so return the outline
|
||||
// and a suggestion to read again with line numbers.
|
||||
let outline =
|
||||
outline::file_outline(project.clone(), file_path, action_log, None, cx)
|
||||
.await?;
|
||||
if buffer_content.is_outline {
|
||||
Ok(formatdoc! {"
|
||||
This file was too big to read all at once.
|
||||
|
||||
Here is an outline of its symbols:
|
||||
|
||||
{outline}
|
||||
{}
|
||||
|
||||
Using the line numbers in this outline, you can call this tool again
|
||||
while specifying the start_line and end_line fields to see the
|
||||
implementations of symbols in the outline.
|
||||
|
||||
Alternatively, you can fall back to the `grep` tool (if available)
|
||||
to search the file for specific content."
|
||||
to search the file for specific content.", buffer_content.text
|
||||
}
|
||||
.into())
|
||||
} else {
|
||||
Ok(buffer_content.text.into())
|
||||
}
|
||||
};
|
||||
|
||||
@@ -452,7 +447,7 @@ mod test {
|
||||
tool.run(input, ToolCallEventStream::test().0, cx)
|
||||
})
|
||||
.await;
|
||||
assert_eq!(result.unwrap(), "Line 2\nLine 3\nLine 4".into());
|
||||
assert_eq!(result.unwrap(), "Line 2\nLine 3\nLine 4\n".into());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -482,7 +477,7 @@ mod test {
|
||||
tool.clone().run(input, ToolCallEventStream::test().0, cx)
|
||||
})
|
||||
.await;
|
||||
assert_eq!(result.unwrap(), "Line 1\nLine 2".into());
|
||||
assert_eq!(result.unwrap(), "Line 1\nLine 2\n".into());
|
||||
|
||||
// end_line of 0 should result in at least 1 line
|
||||
let result = cx
|
||||
@@ -495,7 +490,7 @@ mod test {
|
||||
tool.clone().run(input, ToolCallEventStream::test().0, cx)
|
||||
})
|
||||
.await;
|
||||
assert_eq!(result.unwrap(), "Line 1".into());
|
||||
assert_eq!(result.unwrap(), "Line 1\n".into());
|
||||
|
||||
// when start_line > end_line, should still return at least 1 line
|
||||
let result = cx
|
||||
@@ -508,7 +503,7 @@ mod test {
|
||||
tool.clone().run(input, ToolCallEventStream::test().0, cx)
|
||||
})
|
||||
.await;
|
||||
assert_eq!(result.unwrap(), "Line 3".into());
|
||||
assert_eq!(result.unwrap(), "Line 3\n".into());
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
@@ -594,19 +589,21 @@ mod test {
|
||||
|
||||
cx.update(|cx| {
|
||||
use gpui::UpdateGlobal;
|
||||
use project::WorktreeSettings;
|
||||
use settings::SettingsStore;
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<WorktreeSettings>(cx, |settings| {
|
||||
settings.file_scan_exclusions = Some(vec![
|
||||
store.update_user_settings(cx, |settings| {
|
||||
settings.project.worktree.file_scan_exclusions = Some(vec![
|
||||
"**/.secretdir".to_string(),
|
||||
"**/.mymetadata".to_string(),
|
||||
]);
|
||||
settings.private_files = Some(vec![
|
||||
"**/.mysecrets".to_string(),
|
||||
"**/*.privatekey".to_string(),
|
||||
"**/*.mysensitive".to_string(),
|
||||
]);
|
||||
settings.project.worktree.private_files = Some(
|
||||
vec![
|
||||
"**/.mysecrets".to_string(),
|
||||
"**/*.privatekey".to_string(),
|
||||
"**/*.mysensitive".to_string(),
|
||||
]
|
||||
.into(),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -810,10 +807,11 @@ mod test {
|
||||
// Set global settings
|
||||
cx.update(|cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<WorktreeSettings>(cx, |settings| {
|
||||
settings.file_scan_exclusions =
|
||||
store.update_user_settings(cx, |settings| {
|
||||
settings.project.worktree.file_scan_exclusions =
|
||||
Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]);
|
||||
settings.private_files = Some(vec!["**/.env".to_string()]);
|
||||
settings.project.worktree.private_files =
|
||||
Some(vec!["**/.env".to_string()].into());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -82,7 +82,7 @@ impl AgentTool for TerminalTool {
|
||||
.into(),
|
||||
}
|
||||
} else {
|
||||
"Run terminal command".into()
|
||||
"".into()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -122,6 +122,7 @@ fn emit_update(response: &WebSearchResponse, event_stream: &ToolCallEventStream)
|
||||
mime_type: None,
|
||||
annotations: None,
|
||||
size: None,
|
||||
meta: None,
|
||||
}),
|
||||
})
|
||||
.collect(),
|
||||
|
||||
@@ -23,6 +23,7 @@ action_log.workspace = true
|
||||
agent-client-protocol.workspace = true
|
||||
agent_settings.workspace = true
|
||||
anyhow.workspace = true
|
||||
async-trait.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
env_logger = { workspace = true, optional = true }
|
||||
@@ -46,6 +47,8 @@ task.workspace = true
|
||||
tempfile.workspace = true
|
||||
thiserror.workspace = true
|
||||
ui.workspace = true
|
||||
terminal.workspace = true
|
||||
uuid.workspace = true
|
||||
util.workspace = true
|
||||
watch.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
@@ -9,6 +9,7 @@ use futures::io::BufReader;
|
||||
use project::Project;
|
||||
use project::agent_server_store::AgentServerCommand;
|
||||
use serde::Deserialize;
|
||||
use task::Shell;
|
||||
use util::ResultExt as _;
|
||||
|
||||
use std::path::PathBuf;
|
||||
@@ -19,7 +20,9 @@ use thiserror::Error;
|
||||
use anyhow::{Context as _, Result};
|
||||
use gpui::{App, AppContext as _, AsyncApp, Entity, SharedString, Task, WeakEntity};
|
||||
|
||||
use acp_thread::{AcpThread, AuthRequired, LoadError};
|
||||
use acp_thread::{AcpThread, AuthRequired, LoadError, TerminalProviderEvent};
|
||||
use terminal::TerminalBuilder;
|
||||
use terminal::terminal_settings::{AlternateScroll, CursorShape};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("Unsupported version")]
|
||||
@@ -44,6 +47,7 @@ pub struct AcpConnection {
|
||||
pub struct AcpSession {
|
||||
thread: WeakEntity<AcpThread>,
|
||||
suppress_abort_err: bool,
|
||||
models: Option<Rc<RefCell<acp::SessionModelState>>>,
|
||||
session_modes: Option<Rc<RefCell<acp::SessionModeState>>>,
|
||||
}
|
||||
|
||||
@@ -156,9 +160,12 @@ impl AcpConnection {
|
||||
fs: acp::FileSystemCapability {
|
||||
read_text_file: true,
|
||||
write_text_file: true,
|
||||
meta: None,
|
||||
},
|
||||
terminal: true,
|
||||
meta: None,
|
||||
},
|
||||
meta: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -226,6 +233,7 @@ impl AgentConnection for AcpConnection {
|
||||
.map(|(name, value)| acp::EnvVariable {
|
||||
name: name.clone(),
|
||||
value: value.clone(),
|
||||
meta: None,
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
@@ -243,7 +251,7 @@ impl AgentConnection for AcpConnection {
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let response = conn
|
||||
.new_session(acp::NewSessionRequest { mcp_servers, cwd })
|
||||
.new_session(acp::NewSessionRequest { mcp_servers, cwd, meta: None })
|
||||
.await
|
||||
.map_err(|err| {
|
||||
if err.code == acp::ErrorCode::AUTH_REQUIRED.code {
|
||||
@@ -260,6 +268,7 @@ impl AgentConnection for AcpConnection {
|
||||
})?;
|
||||
|
||||
let modes = response.modes.map(|modes| Rc::new(RefCell::new(modes)));
|
||||
let models = response.models.map(|models| Rc::new(RefCell::new(models)));
|
||||
|
||||
if let Some(default_mode) = default_mode {
|
||||
if let Some(modes) = modes.as_ref() {
|
||||
@@ -277,6 +286,7 @@ impl AgentConnection for AcpConnection {
|
||||
let result = conn.set_session_mode(acp::SetSessionModeRequest {
|
||||
session_id,
|
||||
mode_id: default_mode,
|
||||
meta: None,
|
||||
})
|
||||
.await.log_err();
|
||||
|
||||
@@ -316,15 +326,17 @@ impl AgentConnection for AcpConnection {
|
||||
action_log,
|
||||
session_id.clone(),
|
||||
// ACP doesn't currently support per-session prompt capabilities or changing capabilities dynamically.
|
||||
watch::Receiver::constant(self.agent_capabilities.prompt_capabilities),
|
||||
watch::Receiver::constant(self.agent_capabilities.prompt_capabilities.clone()),
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
|
||||
|
||||
let session = AcpSession {
|
||||
thread: thread.downgrade(),
|
||||
suppress_abort_err: false,
|
||||
session_modes: modes
|
||||
session_modes: modes,
|
||||
models,
|
||||
};
|
||||
sessions.borrow_mut().insert(session_id, session);
|
||||
|
||||
@@ -339,13 +351,13 @@ impl AgentConnection for AcpConnection {
|
||||
fn authenticate(&self, method_id: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>> {
|
||||
let conn = self.connection.clone();
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let result = conn
|
||||
.authenticate(acp::AuthenticateRequest {
|
||||
method_id: method_id.clone(),
|
||||
})
|
||||
.await?;
|
||||
conn.authenticate(acp::AuthenticateRequest {
|
||||
method_id: method_id.clone(),
|
||||
meta: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(result)
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -371,6 +383,10 @@ impl AgentConnection for AcpConnection {
|
||||
match result {
|
||||
Ok(response) => Ok(response),
|
||||
Err(err) => {
|
||||
if err.code == acp::ErrorCode::AUTH_REQUIRED.code {
|
||||
return Err(anyhow!(acp::Error::auth_required()));
|
||||
}
|
||||
|
||||
if err.code != ErrorCode::INTERNAL_ERROR.code {
|
||||
anyhow::bail!(err)
|
||||
}
|
||||
@@ -396,6 +412,7 @@ impl AgentConnection for AcpConnection {
|
||||
{
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::Cancelled,
|
||||
meta: None,
|
||||
})
|
||||
} else {
|
||||
Err(anyhow!(details))
|
||||
@@ -415,6 +432,7 @@ impl AgentConnection for AcpConnection {
|
||||
let conn = self.connection.clone();
|
||||
let params = acp::CancelNotification {
|
||||
session_id: session_id.clone(),
|
||||
meta: None,
|
||||
};
|
||||
cx.foreground_executor()
|
||||
.spawn(async move { conn.cancel(params).await })
|
||||
@@ -443,6 +461,27 @@ impl AgentConnection for AcpConnection {
|
||||
}
|
||||
}
|
||||
|
||||
fn model_selector(
|
||||
&self,
|
||||
session_id: &acp::SessionId,
|
||||
) -> Option<Rc<dyn acp_thread::AgentModelSelector>> {
|
||||
let sessions = self.sessions.clone();
|
||||
let sessions_ref = sessions.borrow();
|
||||
let Some(session) = sessions_ref.get(session_id) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
if let Some(models) = session.models.as_ref() {
|
||||
Some(Rc::new(AcpModelSelector::new(
|
||||
session_id.clone(),
|
||||
self.connection.clone(),
|
||||
models.clone(),
|
||||
)) as _)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
|
||||
self
|
||||
}
|
||||
@@ -478,6 +517,7 @@ impl acp_thread::AgentSessionModes for AcpSessionModes {
|
||||
.set_session_mode(acp::SetSessionModeRequest {
|
||||
session_id,
|
||||
mode_id,
|
||||
meta: None,
|
||||
})
|
||||
.await;
|
||||
|
||||
@@ -492,11 +532,88 @@ impl acp_thread::AgentSessionModes for AcpSessionModes {
|
||||
}
|
||||
}
|
||||
|
||||
struct AcpModelSelector {
|
||||
session_id: acp::SessionId,
|
||||
connection: Rc<acp::ClientSideConnection>,
|
||||
state: Rc<RefCell<acp::SessionModelState>>,
|
||||
}
|
||||
|
||||
impl AcpModelSelector {
|
||||
fn new(
|
||||
session_id: acp::SessionId,
|
||||
connection: Rc<acp::ClientSideConnection>,
|
||||
state: Rc<RefCell<acp::SessionModelState>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
session_id,
|
||||
connection,
|
||||
state,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl acp_thread::AgentModelSelector for AcpModelSelector {
|
||||
fn list_models(&self, _cx: &mut App) -> Task<Result<acp_thread::AgentModelList>> {
|
||||
Task::ready(Ok(acp_thread::AgentModelList::Flat(
|
||||
self.state
|
||||
.borrow()
|
||||
.available_models
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(acp_thread::AgentModelInfo::from)
|
||||
.collect(),
|
||||
)))
|
||||
}
|
||||
|
||||
fn select_model(&self, model_id: acp::ModelId, cx: &mut App) -> Task<Result<()>> {
|
||||
let connection = self.connection.clone();
|
||||
let session_id = self.session_id.clone();
|
||||
let old_model_id;
|
||||
{
|
||||
let mut state = self.state.borrow_mut();
|
||||
old_model_id = state.current_model_id.clone();
|
||||
state.current_model_id = model_id.clone();
|
||||
};
|
||||
let state = self.state.clone();
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let result = connection
|
||||
.set_session_model(acp::SetSessionModelRequest {
|
||||
session_id,
|
||||
model_id,
|
||||
meta: None,
|
||||
})
|
||||
.await;
|
||||
|
||||
if result.is_err() {
|
||||
state.borrow_mut().current_model_id = old_model_id;
|
||||
}
|
||||
|
||||
result?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn selected_model(&self, _cx: &mut App) -> Task<Result<acp_thread::AgentModelInfo>> {
|
||||
let state = self.state.borrow();
|
||||
Task::ready(
|
||||
state
|
||||
.available_models
|
||||
.iter()
|
||||
.find(|m| m.model_id == state.current_model_id)
|
||||
.cloned()
|
||||
.map(acp_thread::AgentModelInfo::from)
|
||||
.ok_or_else(|| anyhow::anyhow!("Model not found")),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct ClientDelegate {
|
||||
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
|
||||
cx: AsyncApp,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl acp::Client for ClientDelegate {
|
||||
async fn request_permission(
|
||||
&self,
|
||||
@@ -526,13 +643,16 @@ impl acp::Client for ClientDelegate {
|
||||
|
||||
let outcome = task.await;
|
||||
|
||||
Ok(acp::RequestPermissionResponse { outcome })
|
||||
Ok(acp::RequestPermissionResponse {
|
||||
outcome,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
|
||||
async fn write_text_file(
|
||||
&self,
|
||||
arguments: acp::WriteTextFileRequest,
|
||||
) -> Result<(), acp::Error> {
|
||||
) -> Result<acp::WriteTextFileResponse, acp::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
let task = self
|
||||
.session_thread(&arguments.session_id)?
|
||||
@@ -542,7 +662,7 @@ impl acp::Client for ClientDelegate {
|
||||
|
||||
task.await?;
|
||||
|
||||
Ok(())
|
||||
Ok(Default::default())
|
||||
}
|
||||
|
||||
async fn read_text_file(
|
||||
@@ -558,7 +678,10 @@ impl acp::Client for ClientDelegate {
|
||||
|
||||
let content = task.await?;
|
||||
|
||||
Ok(acp::ReadTextFileResponse { content })
|
||||
Ok(acp::ReadTextFileResponse {
|
||||
content,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
|
||||
async fn session_notification(
|
||||
@@ -580,10 +703,100 @@ impl acp::Client for ClientDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
// Clone so we can inspect meta both before and after handing off to the thread
|
||||
let update_clone = notification.update.clone();
|
||||
|
||||
// Pre-handle: if a ToolCall carries terminal_info, create/register a display-only terminal.
|
||||
if let acp::SessionUpdate::ToolCall(tc) = &update_clone {
|
||||
if let Some(meta) = &tc.meta {
|
||||
if let Some(terminal_info) = meta.get("terminal_info") {
|
||||
if let Some(id_str) = terminal_info.get("terminal_id").and_then(|v| v.as_str())
|
||||
{
|
||||
let terminal_id = acp::TerminalId(id_str.into());
|
||||
let cwd = terminal_info
|
||||
.get("cwd")
|
||||
.and_then(|v| v.as_str().map(PathBuf::from));
|
||||
|
||||
// Create a minimal display-only lower-level terminal and register it.
|
||||
let _ = session.thread.update(&mut self.cx.clone(), |thread, cx| {
|
||||
let builder = TerminalBuilder::new_display_only(
|
||||
CursorShape::default(),
|
||||
AlternateScroll::On,
|
||||
None,
|
||||
0,
|
||||
)?;
|
||||
let lower = cx.new(|cx| builder.subscribe(cx));
|
||||
thread.on_terminal_provider_event(
|
||||
TerminalProviderEvent::Created {
|
||||
terminal_id: terminal_id.clone(),
|
||||
label: tc.title.clone(),
|
||||
cwd,
|
||||
output_byte_limit: None,
|
||||
terminal: lower,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
anyhow::Ok(())
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Forward the update to the acp_thread as usual.
|
||||
session.thread.update(&mut self.cx.clone(), |thread, cx| {
|
||||
thread.handle_session_update(notification.update, cx)
|
||||
thread.handle_session_update(notification.update.clone(), cx)
|
||||
})??;
|
||||
|
||||
// Post-handle: stream terminal output/exit if present on ToolCallUpdate meta.
|
||||
if let acp::SessionUpdate::ToolCallUpdate(tcu) = &update_clone {
|
||||
if let Some(meta) = &tcu.meta {
|
||||
if let Some(term_out) = meta.get("terminal_output") {
|
||||
if let Some(id_str) = term_out.get("terminal_id").and_then(|v| v.as_str()) {
|
||||
let terminal_id = acp::TerminalId(id_str.into());
|
||||
if let Some(s) = term_out.get("data").and_then(|v| v.as_str()) {
|
||||
let data = s.as_bytes().to_vec();
|
||||
let _ = session.thread.update(&mut self.cx.clone(), |thread, cx| {
|
||||
thread.on_terminal_provider_event(
|
||||
TerminalProviderEvent::Output {
|
||||
terminal_id: terminal_id.clone(),
|
||||
data,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// terminal_exit
|
||||
if let Some(term_exit) = meta.get("terminal_exit") {
|
||||
if let Some(id_str) = term_exit.get("terminal_id").and_then(|v| v.as_str()) {
|
||||
let terminal_id = acp::TerminalId(id_str.into());
|
||||
let status = acp::TerminalExitStatus {
|
||||
exit_code: term_exit
|
||||
.get("exit_code")
|
||||
.and_then(|v| v.as_u64())
|
||||
.map(|i| i as u32),
|
||||
signal: term_exit
|
||||
.get("signal")
|
||||
.and_then(|v| v.as_str().map(|s| s.to_string())),
|
||||
meta: None,
|
||||
};
|
||||
let _ = session.thread.update(&mut self.cx.clone(), |thread, cx| {
|
||||
thread.on_terminal_provider_event(
|
||||
TerminalProviderEvent::Exit {
|
||||
terminal_id: terminal_id.clone(),
|
||||
status,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -591,42 +804,100 @@ impl acp::Client for ClientDelegate {
|
||||
&self,
|
||||
args: acp::CreateTerminalRequest,
|
||||
) -> Result<acp::CreateTerminalResponse, acp::Error> {
|
||||
let terminal = self
|
||||
.session_thread(&args.session_id)?
|
||||
.update(&mut self.cx.clone(), |thread, cx| {
|
||||
thread.create_terminal(
|
||||
args.command,
|
||||
args.args,
|
||||
args.env,
|
||||
args.cwd,
|
||||
args.output_byte_limit,
|
||||
let thread = self.session_thread(&args.session_id)?;
|
||||
let project = thread.read_with(&self.cx, |thread, _cx| thread.project().clone())?;
|
||||
|
||||
let mut env = if let Some(dir) = &args.cwd {
|
||||
project
|
||||
.update(&mut self.cx.clone(), |project, cx| {
|
||||
project.directory_environment(&task::Shell::System, dir.clone().into(), cx)
|
||||
})?
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
for var in args.env {
|
||||
env.insert(var.name, var.value);
|
||||
}
|
||||
|
||||
// Use remote shell or default system shell, as appropriate
|
||||
let shell = project
|
||||
.update(&mut self.cx.clone(), |project, cx| {
|
||||
project
|
||||
.remote_client()
|
||||
.and_then(|r| r.read(cx).default_system_shell())
|
||||
.map(Shell::Program)
|
||||
})?
|
||||
.unwrap_or(task::Shell::System);
|
||||
let (task_command, task_args) = task::ShellBuilder::new(&shell)
|
||||
.redirect_stdin_to_dev_null()
|
||||
.build(Some(args.command.clone()), &args.args);
|
||||
|
||||
let terminal_entity = project
|
||||
.update(&mut self.cx.clone(), |project, cx| {
|
||||
project.create_terminal_task(
|
||||
task::SpawnInTerminal {
|
||||
command: Some(task_command),
|
||||
args: task_args,
|
||||
cwd: args.cwd.clone(),
|
||||
env,
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await?;
|
||||
Ok(
|
||||
terminal.read_with(&self.cx, |terminal, _| acp::CreateTerminalResponse {
|
||||
terminal_id: terminal.id().clone(),
|
||||
})?,
|
||||
)
|
||||
|
||||
// Register with renderer
|
||||
let terminal_entity = thread.update(&mut self.cx.clone(), |thread, cx| {
|
||||
thread.register_terminal_created(
|
||||
acp::TerminalId(uuid::Uuid::new_v4().to_string().into()),
|
||||
format!("{} {}", args.command, args.args.join(" ")),
|
||||
args.cwd.clone(),
|
||||
args.output_byte_limit,
|
||||
terminal_entity,
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
let terminal_id =
|
||||
terminal_entity.read_with(&self.cx, |terminal, _| terminal.id().clone())?;
|
||||
Ok(acp::CreateTerminalResponse {
|
||||
terminal_id,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
|
||||
async fn kill_terminal(&self, args: acp::KillTerminalRequest) -> Result<(), acp::Error> {
|
||||
async fn kill_terminal_command(
|
||||
&self,
|
||||
args: acp::KillTerminalCommandRequest,
|
||||
) -> Result<acp::KillTerminalCommandResponse, acp::Error> {
|
||||
self.session_thread(&args.session_id)?
|
||||
.update(&mut self.cx.clone(), |thread, cx| {
|
||||
thread.kill_terminal(args.terminal_id, cx)
|
||||
})??;
|
||||
|
||||
Ok(())
|
||||
Ok(Default::default())
|
||||
}
|
||||
|
||||
async fn release_terminal(&self, args: acp::ReleaseTerminalRequest) -> Result<(), acp::Error> {
|
||||
async fn ext_method(&self, _args: acp::ExtRequest) -> Result<acp::ExtResponse, acp::Error> {
|
||||
Err(acp::Error::method_not_found())
|
||||
}
|
||||
|
||||
async fn ext_notification(&self, _args: acp::ExtNotification) -> Result<(), acp::Error> {
|
||||
Err(acp::Error::method_not_found())
|
||||
}
|
||||
|
||||
async fn release_terminal(
|
||||
&self,
|
||||
args: acp::ReleaseTerminalRequest,
|
||||
) -> Result<acp::ReleaseTerminalResponse, acp::Error> {
|
||||
self.session_thread(&args.session_id)?
|
||||
.update(&mut self.cx.clone(), |thread, cx| {
|
||||
thread.release_terminal(args.terminal_id, cx)
|
||||
})??;
|
||||
|
||||
Ok(())
|
||||
Ok(Default::default())
|
||||
}
|
||||
|
||||
async fn terminal_output(
|
||||
@@ -655,7 +926,10 @@ impl acp::Client for ClientDelegate {
|
||||
})??
|
||||
.await;
|
||||
|
||||
Ok(acp::WaitForTerminalExitResponse { exit_status })
|
||||
Ok(acp::WaitForTerminalExitResponse {
|
||||
exit_status,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
mod acp;
|
||||
mod claude;
|
||||
mod codex;
|
||||
mod custom;
|
||||
mod gemini;
|
||||
|
||||
@@ -8,6 +9,7 @@ pub mod e2e_tests;
|
||||
|
||||
pub use claude::*;
|
||||
use client::ProxySettings;
|
||||
pub use codex::*;
|
||||
use collections::HashMap;
|
||||
pub use custom::*;
|
||||
use fs::Fs;
|
||||
@@ -99,6 +101,9 @@ pub fn load_proxy_env(cx: &mut App) -> HashMap<String, String> {
|
||||
|
||||
if let Some(no_proxy) = read_no_proxy_from_env() {
|
||||
env.insert("NO_PROXY".to_owned(), no_proxy);
|
||||
} else if proxy_url.is_some() {
|
||||
// We sometimes need local MCP servers that we don't want to proxy
|
||||
env.insert("NO_PROXY".to_owned(), "localhost,127.0.0.1".to_owned());
|
||||
}
|
||||
|
||||
env
|
||||
|
||||
@@ -45,8 +45,13 @@ impl AgentServer for ClaudeCode {
|
||||
}
|
||||
|
||||
fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
|
||||
update_settings_file::<AllAgentServersSettings>(fs, cx, |settings, _| {
|
||||
settings.claude.get_or_insert_default().default_mode = mode_id.map(|m| m.to_string())
|
||||
update_settings_file(fs, cx, |settings, _| {
|
||||
settings
|
||||
.agent_servers
|
||||
.get_or_insert_default()
|
||||
.claude
|
||||
.get_or_insert_default()
|
||||
.default_mode = mode_id.map(|m| m.to_string())
|
||||
});
|
||||
}
|
||||
|
||||
@@ -57,7 +62,7 @@ impl AgentServer for ClaudeCode {
|
||||
cx: &mut App,
|
||||
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
|
||||
let name = self.name();
|
||||
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().to_string());
|
||||
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned());
|
||||
let is_remote = delegate.project.read(cx).is_via_remote_server();
|
||||
let store = delegate.store.downgrade();
|
||||
let extra_env = load_proxy_env(cx);
|
||||
|
||||
80
crates/agent_servers/src/codex.rs
Normal file
80
crates/agent_servers/src/codex.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use std::rc::Rc;
|
||||
use std::{any::Any, path::Path};
|
||||
|
||||
use crate::{AgentServer, AgentServerDelegate, load_proxy_env};
|
||||
use acp_thread::AgentConnection;
|
||||
use anyhow::{Context as _, Result};
|
||||
use gpui::{App, SharedString, Task};
|
||||
use project::agent_server_store::CODEX_NAME;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Codex;
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
|
||||
crate::common_e2e_tests!(async |_, _, _| Codex, allow_option_id = "proceed_once");
|
||||
}
|
||||
|
||||
impl AgentServer for Codex {
|
||||
fn telemetry_id(&self) -> &'static str {
|
||||
"codex"
|
||||
}
|
||||
|
||||
fn name(&self) -> SharedString {
|
||||
"Codex".into()
|
||||
}
|
||||
|
||||
fn logo(&self) -> ui::IconName {
|
||||
ui::IconName::AiOpenAi
|
||||
}
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
root_dir: Option<&Path>,
|
||||
delegate: AgentServerDelegate,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
|
||||
let name = self.name();
|
||||
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned());
|
||||
let is_remote = delegate.project.read(cx).is_via_remote_server();
|
||||
let store = delegate.store.downgrade();
|
||||
let extra_env = load_proxy_env(cx);
|
||||
let default_mode = self.default_mode(cx);
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let (command, root_dir, login) = store
|
||||
.update(cx, |store, cx| {
|
||||
let agent = store
|
||||
.get_external_agent(&CODEX_NAME.into())
|
||||
.context("Codex is not registered")?;
|
||||
anyhow::Ok(agent.get_command(
|
||||
root_dir.as_deref(),
|
||||
extra_env,
|
||||
delegate.status_tx,
|
||||
// For now, report that there are no updates.
|
||||
// (A future PR will use the GitHub Releases API to fetch them.)
|
||||
delegate.new_version_available,
|
||||
&mut cx.to_async(),
|
||||
))
|
||||
})??
|
||||
.await?;
|
||||
|
||||
let connection = crate::acp::connect(
|
||||
name,
|
||||
command,
|
||||
root_dir.as_ref(),
|
||||
default_mode,
|
||||
is_remote,
|
||||
cx,
|
||||
)
|
||||
.await?;
|
||||
Ok((connection, login))
|
||||
})
|
||||
}
|
||||
|
||||
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -49,8 +49,14 @@ impl crate::AgentServer for CustomAgentServer {
|
||||
|
||||
fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
|
||||
let name = self.name();
|
||||
update_settings_file::<AllAgentServersSettings>(fs, cx, move |settings, _| {
|
||||
settings.custom.get_mut(&name).unwrap().default_mode = mode_id.map(|m| m.to_string())
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
settings
|
||||
.agent_servers
|
||||
.get_or_insert_default()
|
||||
.custom
|
||||
.get_mut(&name)
|
||||
.unwrap()
|
||||
.default_mode = mode_id.map(|m| m.to_string())
|
||||
});
|
||||
}
|
||||
|
||||
@@ -61,7 +67,7 @@ impl crate::AgentServer for CustomAgentServer {
|
||||
cx: &mut App,
|
||||
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
|
||||
let name = self.name();
|
||||
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().to_string());
|
||||
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned());
|
||||
let is_remote = delegate.project.read(cx).is_via_remote_server();
|
||||
let default_mode = self.default_mode(cx);
|
||||
let store = delegate.store.downgrade();
|
||||
|
||||
@@ -83,6 +83,7 @@ where
|
||||
acp::ContentBlock::Text(acp::TextContent {
|
||||
text: "Read the file ".into(),
|
||||
annotations: None,
|
||||
meta: None,
|
||||
}),
|
||||
acp::ContentBlock::ResourceLink(acp::ResourceLink {
|
||||
uri: "foo.rs".into(),
|
||||
@@ -92,10 +93,12 @@ where
|
||||
mime_type: None,
|
||||
size: None,
|
||||
title: None,
|
||||
meta: None,
|
||||
}),
|
||||
acp::ContentBlock::Text(acp::TextContent {
|
||||
text: " and tell me what the content of the println! is".into(),
|
||||
annotations: None,
|
||||
meta: None,
|
||||
}),
|
||||
],
|
||||
cx,
|
||||
@@ -480,6 +483,13 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
|
||||
default_mode: None,
|
||||
}),
|
||||
gemini: Some(crate::gemini::tests::local_command().into()),
|
||||
codex: Some(BuiltinAgentServerSettings {
|
||||
path: Some("codex-acp".into()),
|
||||
args: None,
|
||||
env: None,
|
||||
ignore_system_version: None,
|
||||
default_mode: None,
|
||||
}),
|
||||
custom: collections::HashMap::default(),
|
||||
},
|
||||
cx,
|
||||
|
||||
@@ -31,7 +31,7 @@ impl AgentServer for Gemini {
|
||||
cx: &mut App,
|
||||
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
|
||||
let name = self.name();
|
||||
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().to_string());
|
||||
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned());
|
||||
let is_remote = delegate.project.read(cx).is_via_remote_server();
|
||||
let store = delegate.store.downgrade();
|
||||
let mut extra_env = load_proxy_env(cx);
|
||||
@@ -39,8 +39,13 @@ impl AgentServer for Gemini {
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
extra_env.insert("SURFACE".to_owned(), "zed".to_owned());
|
||||
if let Some(api_key) = cx.update(GoogleLanguageModelProvider::api_key)?.await.ok() {
|
||||
extra_env.insert("GEMINI_API_KEY".into(), api_key.key);
|
||||
|
||||
if let Some(api_key) = cx
|
||||
.update(GoogleLanguageModelProvider::api_key_for_gemini_cli)?
|
||||
.await
|
||||
.ok()
|
||||
{
|
||||
extra_env.insert("GEMINI_API_KEY".into(), api_key);
|
||||
}
|
||||
let (command, root_dir, login) = store
|
||||
.update(cx, |store, cx| {
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
use agent_client_protocol as acp;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::AgentServerCommand;
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use gpui::{App, SharedString};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
AllAgentServersSettings::register(cx);
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, SettingsUi, SettingsKey)]
|
||||
#[settings_key(key = "agent_servers")]
|
||||
pub struct AllAgentServersSettings {
|
||||
pub gemini: Option<BuiltinAgentServerSettings>,
|
||||
pub claude: Option<BuiltinAgentServerSettings>,
|
||||
|
||||
/// Custom agent servers configured by the user
|
||||
#[serde(flatten)]
|
||||
pub custom: HashMap<SharedString, CustomAgentServerSettings>,
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
|
||||
pub struct BuiltinAgentServerSettings {
|
||||
/// Absolute path to a binary to be used when launching this agent.
|
||||
///
|
||||
/// This can be used to run a specific binary without automatic downloads or searching `$PATH`.
|
||||
#[serde(rename = "command")]
|
||||
pub path: Option<PathBuf>,
|
||||
/// If a binary is specified in `command`, it will be passed these arguments.
|
||||
pub args: Option<Vec<String>>,
|
||||
/// If a binary is specified in `command`, it will be passed these environment variables.
|
||||
pub env: Option<HashMap<String, String>>,
|
||||
/// Whether to skip searching `$PATH` for an agent server binary when
|
||||
/// launching this agent.
|
||||
///
|
||||
/// This has no effect if a `command` is specified. Otherwise, when this is
|
||||
/// `false`, Zed will search `$PATH` for an agent server binary and, if one
|
||||
/// is found, use it for threads with this agent. If no agent binary is
|
||||
/// found on `$PATH`, Zed will automatically install and use its own binary.
|
||||
/// When this is `true`, Zed will not search `$PATH`, and will always use
|
||||
/// its own binary.
|
||||
///
|
||||
/// Default: true
|
||||
pub ignore_system_version: Option<bool>,
|
||||
/// The default mode for new threads.
|
||||
///
|
||||
/// Note: Not all agents support modes.
|
||||
///
|
||||
/// Default: None
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub default_mode: Option<acp::SessionModeId>,
|
||||
}
|
||||
|
||||
impl BuiltinAgentServerSettings {
|
||||
pub(crate) fn custom_command(self) -> Option<AgentServerCommand> {
|
||||
self.path.map(|path| AgentServerCommand {
|
||||
path,
|
||||
args: self.args.unwrap_or_default(),
|
||||
env: self.env,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AgentServerCommand> for BuiltinAgentServerSettings {
|
||||
fn from(value: AgentServerCommand) -> Self {
|
||||
BuiltinAgentServerSettings {
|
||||
path: Some(value.path),
|
||||
args: Some(value.args),
|
||||
env: value.env,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
|
||||
pub struct CustomAgentServerSettings {
|
||||
#[serde(flatten)]
|
||||
pub command: AgentServerCommand,
|
||||
/// The default mode for new threads.
|
||||
///
|
||||
/// Note: Not all agents support modes.
|
||||
///
|
||||
/// Default: None
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub default_mode: Option<acp::SessionModeId>,
|
||||
}
|
||||
|
||||
impl settings::Settings for AllAgentServersSettings {
|
||||
type FileContent = Self;
|
||||
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
|
||||
let mut settings = AllAgentServersSettings::default();
|
||||
|
||||
for AllAgentServersSettings {
|
||||
gemini,
|
||||
claude,
|
||||
custom,
|
||||
} in sources.defaults_and_customizations()
|
||||
{
|
||||
if gemini.is_some() {
|
||||
settings.gemini = gemini.clone();
|
||||
}
|
||||
if claude.is_some() {
|
||||
settings.claude = claude.clone();
|
||||
}
|
||||
|
||||
// Merge custom agents
|
||||
for (name, config) in custom {
|
||||
// Skip built-in agent names to avoid conflicts
|
||||
if name != "gemini" && name != "claude" {
|
||||
settings.custom.insert(name.clone(), config.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(settings)
|
||||
}
|
||||
|
||||
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
|
||||
}
|
||||
@@ -15,11 +15,15 @@ path = "src/agent_settings.rs"
|
||||
anyhow.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
convert_case.workspace = true
|
||||
fs.workspace = true
|
||||
gpui.workspace = true
|
||||
language_model.workspace = true
|
||||
project.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
settings.workspace = true
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Result, bail};
|
||||
use collections::IndexMap;
|
||||
use gpui::SharedString;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use convert_case::{Case, Casing as _};
|
||||
use fs::Fs;
|
||||
use gpui::{App, SharedString};
|
||||
use settings::{
|
||||
AgentProfileContent, ContextServerPresetContent, Settings as _, SettingsContent,
|
||||
update_settings_file,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::{AgentProfileId, AgentSettings};
|
||||
|
||||
pub mod builtin_profiles {
|
||||
use super::AgentProfileId;
|
||||
@@ -17,24 +25,66 @@ pub mod builtin_profiles {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct AgentProfileId(pub Arc<str>);
|
||||
|
||||
impl AgentProfileId {
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct AgentProfile {
|
||||
id: AgentProfileId,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for AgentProfileId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
pub type AvailableProfiles = IndexMap<AgentProfileId, SharedString>;
|
||||
|
||||
impl Default for AgentProfileId {
|
||||
fn default() -> Self {
|
||||
Self("write".into())
|
||||
impl AgentProfile {
|
||||
pub fn new(id: AgentProfileId) -> Self {
|
||||
Self { id }
|
||||
}
|
||||
|
||||
pub fn id(&self) -> &AgentProfileId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
/// Saves a new profile to the settings.
|
||||
pub fn create(
|
||||
name: String,
|
||||
base_profile_id: Option<AgentProfileId>,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &App,
|
||||
) -> AgentProfileId {
|
||||
let id = AgentProfileId(name.to_case(Case::Kebab).into());
|
||||
|
||||
let base_profile =
|
||||
base_profile_id.and_then(|id| AgentSettings::get_global(cx).profiles.get(&id).cloned());
|
||||
|
||||
let profile_settings = AgentProfileSettings {
|
||||
name: name.into(),
|
||||
tools: base_profile
|
||||
.as_ref()
|
||||
.map(|profile| profile.tools.clone())
|
||||
.unwrap_or_default(),
|
||||
enable_all_context_servers: base_profile
|
||||
.as_ref()
|
||||
.map(|profile| profile.enable_all_context_servers)
|
||||
.unwrap_or_default(),
|
||||
context_servers: base_profile
|
||||
.map(|profile| profile.context_servers)
|
||||
.unwrap_or_default(),
|
||||
};
|
||||
|
||||
update_settings_file(fs, cx, {
|
||||
let id = id.clone();
|
||||
move |settings, _cx| {
|
||||
profile_settings.save_to_settings(id, settings).log_err();
|
||||
}
|
||||
});
|
||||
|
||||
id
|
||||
}
|
||||
|
||||
/// Returns a map of AgentProfileIds to their names
|
||||
pub fn available_profiles(cx: &App) -> AvailableProfiles {
|
||||
let mut profiles = AvailableProfiles::default();
|
||||
for (id, profile) in AgentSettings::get_global(cx).profiles.iter() {
|
||||
profiles.insert(id.clone(), profile.name.clone());
|
||||
}
|
||||
profiles
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,9 +110,71 @@ impl AgentProfileSettings {
|
||||
.get(server_id)
|
||||
.is_some_and(|preset| preset.tools.get(tool_name) == Some(&true))
|
||||
}
|
||||
|
||||
pub fn save_to_settings(
|
||||
&self,
|
||||
profile_id: AgentProfileId,
|
||||
content: &mut SettingsContent,
|
||||
) -> Result<()> {
|
||||
let profiles = content
|
||||
.agent
|
||||
.get_or_insert_default()
|
||||
.profiles
|
||||
.get_or_insert_default();
|
||||
if profiles.contains_key(&profile_id.0) {
|
||||
bail!("profile with ID '{profile_id}' already exists");
|
||||
}
|
||||
|
||||
profiles.insert(
|
||||
profile_id.0,
|
||||
AgentProfileContent {
|
||||
name: self.name.clone().into(),
|
||||
tools: self.tools.clone(),
|
||||
enable_all_context_servers: Some(self.enable_all_context_servers),
|
||||
context_servers: self
|
||||
.context_servers
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|(server_id, preset)| {
|
||||
(
|
||||
server_id,
|
||||
ContextServerPresetContent {
|
||||
tools: preset.tools,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AgentProfileContent> for AgentProfileSettings {
|
||||
fn from(content: AgentProfileContent) -> Self {
|
||||
Self {
|
||||
name: content.name.into(),
|
||||
tools: content.tools,
|
||||
enable_all_context_servers: content.enable_all_context_servers.unwrap_or_default(),
|
||||
context_servers: content
|
||||
.context_servers
|
||||
.into_iter()
|
||||
.map(|(server_id, preset)| (server_id, preset.into()))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ContextServerPreset {
|
||||
pub tools: IndexMap<Arc<str>, bool>,
|
||||
}
|
||||
|
||||
impl From<settings::ContextServerPresetContent> for ContextServerPreset {
|
||||
fn from(content: settings::ContextServerPresetContent) -> Self {
|
||||
Self {
|
||||
tools: content.tools,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,16 @@ mod agent_profile;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Result, bail};
|
||||
use collections::IndexMap;
|
||||
use gpui::{App, Pixels, SharedString};
|
||||
use gpui::{App, Pixels, px};
|
||||
use language_model::LanguageModel;
|
||||
use schemars::{JsonSchema, json_schema};
|
||||
use project::DisableAiSettings;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
|
||||
use std::borrow::Cow;
|
||||
use settings::{
|
||||
DefaultAgentView, DockPosition, LanguageModelParameters, LanguageModelSelection,
|
||||
NotifyWhenAgentWaiting, Settings, SettingsContent,
|
||||
};
|
||||
|
||||
pub use crate::agent_profile::*;
|
||||
|
||||
@@ -22,37 +24,11 @@ pub fn init(cx: &mut App) {
|
||||
AgentSettings::register(cx);
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
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 {
|
||||
#[default]
|
||||
PrimaryScreen,
|
||||
AllScreens,
|
||||
Never,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AgentSettings {
|
||||
pub enabled: bool,
|
||||
pub button: bool,
|
||||
pub dock: AgentDockPosition,
|
||||
pub dock: DockPosition,
|
||||
pub default_width: Pixels,
|
||||
pub default_height: Pixels,
|
||||
pub default_model: Option<LanguageModelSelection>,
|
||||
@@ -60,9 +36,8 @@ pub struct AgentSettings {
|
||||
pub commit_message_model: Option<LanguageModelSelection>,
|
||||
pub thread_summary_model: Option<LanguageModelSelection>,
|
||||
pub inline_alternatives: Vec<LanguageModelSelection>,
|
||||
pub using_outdated_settings_version: bool,
|
||||
pub default_profile: AgentProfileId,
|
||||
pub default_view: DefaultView,
|
||||
pub default_view: DefaultAgentView,
|
||||
pub profiles: IndexMap<AgentProfileId, AgentProfileSettings>,
|
||||
pub always_allow_tool_actions: bool,
|
||||
pub notify_when_agent_waiting: NotifyWhenAgentWaiting,
|
||||
@@ -75,76 +50,30 @@ pub struct AgentSettings {
|
||||
pub expand_edit_card: bool,
|
||||
pub expand_terminal_card: bool,
|
||||
pub use_modifier_to_send: bool,
|
||||
pub message_editor_min_lines: usize,
|
||||
}
|
||||
|
||||
impl AgentSettings {
|
||||
pub fn enabled(&self, cx: &App) -> bool {
|
||||
self.enabled && !DisableAiSettings::get_global(cx).disable_ai
|
||||
}
|
||||
|
||||
pub fn temperature_for_model(model: &Arc<dyn LanguageModel>, cx: &App) -> Option<f32> {
|
||||
let settings = Self::get_global(cx);
|
||||
settings
|
||||
.model_parameters
|
||||
.iter()
|
||||
.rfind(|setting| setting.matches(model))
|
||||
.and_then(|m| m.temperature)
|
||||
}
|
||||
|
||||
pub fn set_inline_assistant_model(&mut self, provider: String, model: String) {
|
||||
self.inline_assistant_model = Some(LanguageModelSelection {
|
||||
provider: provider.into(),
|
||||
model,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn set_commit_message_model(&mut self, provider: String, model: String) {
|
||||
self.commit_message_model = Some(LanguageModelSelection {
|
||||
provider: provider.into(),
|
||||
model,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn set_thread_summary_model(&mut self, provider: String, model: String) {
|
||||
self.thread_summary_model = Some(LanguageModelSelection {
|
||||
provider: provider.into(),
|
||||
model,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct LanguageModelParameters {
|
||||
pub provider: Option<LanguageModelProviderSetting>,
|
||||
pub model: Option<SharedString>,
|
||||
pub temperature: Option<f32>,
|
||||
}
|
||||
|
||||
impl LanguageModelParameters {
|
||||
pub fn matches(&self, model: &Arc<dyn LanguageModel>) -> bool {
|
||||
if let Some(provider) = &self.provider
|
||||
&& provider.0 != model.provider_id().0
|
||||
{
|
||||
return false;
|
||||
for setting in settings.model_parameters.iter().rev() {
|
||||
if let Some(provider) = &setting.provider
|
||||
&& provider.0 != model.provider_id().0
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if let Some(setting_model) = &setting.model
|
||||
&& *setting_model != model.id().0
|
||||
{
|
||||
continue;
|
||||
}
|
||||
return setting.temperature;
|
||||
}
|
||||
if let Some(setting_model) = &self.model
|
||||
&& *setting_model != model.id().0
|
||||
{
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl AgentSettingsContent {
|
||||
pub fn set_dock(&mut self, dock: AgentDockPosition) {
|
||||
self.dock = Some(dock);
|
||||
}
|
||||
|
||||
pub fn set_model(&mut self, language_model: Arc<dyn LanguageModel>) {
|
||||
let model = language_model.id().0.to_string();
|
||||
let provider = language_model.provider_id().0.to_string();
|
||||
|
||||
self.default_model = Some(LanguageModelSelection {
|
||||
provider: provider.into(),
|
||||
model,
|
||||
});
|
||||
return None;
|
||||
}
|
||||
|
||||
pub fn set_inline_assistant_model(&mut self, provider: String, model: String) {
|
||||
@@ -168,158 +97,9 @@ impl AgentSettingsContent {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn set_always_allow_tool_actions(&mut self, allow: bool) {
|
||||
self.always_allow_tool_actions = Some(allow);
|
||||
pub fn set_message_editor_max_lines(&self) -> usize {
|
||||
self.message_editor_min_lines * 2
|
||||
}
|
||||
|
||||
pub fn set_play_sound_when_agent_done(&mut self, allow: bool) {
|
||||
self.play_sound_when_agent_done = Some(allow);
|
||||
}
|
||||
|
||||
pub fn set_single_file_review(&mut self, allow: bool) {
|
||||
self.single_file_review = Some(allow);
|
||||
}
|
||||
|
||||
pub fn set_use_modifier_to_send(&mut self, always_use: bool) {
|
||||
self.use_modifier_to_send = Some(always_use);
|
||||
}
|
||||
|
||||
pub fn set_profile(&mut self, profile_id: AgentProfileId) {
|
||||
self.default_profile = Some(profile_id);
|
||||
}
|
||||
|
||||
pub fn create_profile(
|
||||
&mut self,
|
||||
profile_id: AgentProfileId,
|
||||
profile_settings: AgentProfileSettings,
|
||||
) -> Result<()> {
|
||||
let profiles = self.profiles.get_or_insert_default();
|
||||
if profiles.contains_key(&profile_id) {
|
||||
bail!("profile with ID '{profile_id}' already exists");
|
||||
}
|
||||
|
||||
profiles.insert(
|
||||
profile_id,
|
||||
AgentProfileContent {
|
||||
name: profile_settings.name.into(),
|
||||
tools: profile_settings.tools,
|
||||
enable_all_context_servers: Some(profile_settings.enable_all_context_servers),
|
||||
context_servers: profile_settings
|
||||
.context_servers
|
||||
.into_iter()
|
||||
.map(|(server_id, preset)| {
|
||||
(
|
||||
server_id,
|
||||
ContextServerPresetContent {
|
||||
tools: preset.tools,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default, SettingsUi, SettingsKey)]
|
||||
#[settings_key(key = "agent", fallback_key = "assistant")]
|
||||
pub struct AgentSettingsContent {
|
||||
/// Whether the Agent is enabled.
|
||||
///
|
||||
/// Default: true
|
||||
enabled: Option<bool>,
|
||||
/// Whether to show the agent panel button in the status bar.
|
||||
///
|
||||
/// Default: true
|
||||
button: Option<bool>,
|
||||
/// Where to dock the agent panel.
|
||||
///
|
||||
/// Default: 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 agent panel is docked to the bottom.
|
||||
///
|
||||
/// Default: 320
|
||||
default_height: Option<f32>,
|
||||
/// The default model to use when creating new chats and for other features when a specific model is not specified.
|
||||
default_model: Option<LanguageModelSelection>,
|
||||
/// Model to use for the inline assistant. Defaults to default_model when not specified.
|
||||
inline_assistant_model: Option<LanguageModelSelection>,
|
||||
/// Model to use for generating git commit messages. Defaults to default_model when not specified.
|
||||
commit_message_model: Option<LanguageModelSelection>,
|
||||
/// Model to use for generating thread summaries. Defaults to default_model when not specified.
|
||||
thread_summary_model: Option<LanguageModelSelection>,
|
||||
/// Additional models with which to generate alternatives when performing inline assists.
|
||||
inline_alternatives: Option<Vec<LanguageModelSelection>>,
|
||||
/// 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
|
||||
/// that you allow it, always choose to allow it.
|
||||
///
|
||||
/// This setting has no effect on external agents that support permission modes, such as Claude Code.
|
||||
///
|
||||
/// Set `agent_servers.claude.default_mode` to `bypassPermissions`, to disable all permission requests when using Claude Code.
|
||||
///
|
||||
/// Default: false
|
||||
always_allow_tool_actions: Option<bool>,
|
||||
/// Where to show a popup notification when the agent is waiting for user input.
|
||||
///
|
||||
/// 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
|
||||
stream_edits: Option<bool>,
|
||||
/// Whether to display agent edits in single-file editors in addition to the review multibuffer pane.
|
||||
///
|
||||
/// Default: true
|
||||
single_file_review: Option<bool>,
|
||||
/// Additional parameters for language model requests. When making a request
|
||||
/// to a model, parameters will be taken from the last entry in this list
|
||||
/// that matches the model's provider and name. In each entry, both provider
|
||||
/// and model are optional, so that you can specify parameters for either
|
||||
/// one.
|
||||
///
|
||||
/// Default: []
|
||||
#[serde(default)]
|
||||
model_parameters: Vec<LanguageModelParameters>,
|
||||
/// What completion mode to enable for new threads
|
||||
///
|
||||
/// Default: normal
|
||||
preferred_completion_mode: Option<CompletionMode>,
|
||||
/// Whether to show thumb buttons for feedback in the agent panel.
|
||||
///
|
||||
/// Default: true
|
||||
enable_feedback: Option<bool>,
|
||||
/// Whether to have edit cards in the agent panel expanded, showing a preview of the full diff.
|
||||
///
|
||||
/// Default: true
|
||||
expand_edit_card: Option<bool>,
|
||||
/// Whether to have terminal cards in the agent panel expanded, showing the whole command output.
|
||||
///
|
||||
/// Default: true
|
||||
expand_terminal_card: Option<bool>,
|
||||
/// Whether to always use cmd-enter (or ctrl-enter on Linux or Windows) to send messages in the agent panel.
|
||||
///
|
||||
/// Default: false
|
||||
use_modifier_to_send: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
|
||||
@@ -340,202 +120,80 @@ impl From<CompletionMode> for cloud_llm_client::CompletionMode {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct LanguageModelSelection {
|
||||
pub provider: LanguageModelProviderSetting,
|
||||
pub model: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct LanguageModelProviderSetting(pub String);
|
||||
|
||||
impl JsonSchema for LanguageModelProviderSetting {
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"LanguageModelProviderSetting".into()
|
||||
}
|
||||
|
||||
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
json_schema!({
|
||||
"enum": [
|
||||
"amazon-bedrock",
|
||||
"anthropic",
|
||||
"copilot_chat",
|
||||
"deepseek",
|
||||
"google",
|
||||
"lmstudio",
|
||||
"mistral",
|
||||
"ollama",
|
||||
"openai",
|
||||
"openrouter",
|
||||
"vercel",
|
||||
"x_ai",
|
||||
"zed.dev"
|
||||
]
|
||||
})
|
||||
impl From<settings::CompletionMode> for CompletionMode {
|
||||
fn from(value: settings::CompletionMode) -> Self {
|
||||
match value {
|
||||
settings::CompletionMode::Normal => CompletionMode::Normal,
|
||||
settings::CompletionMode::Burn => CompletionMode::Burn,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for LanguageModelProviderSetting {
|
||||
fn from(provider: String) -> Self {
|
||||
Self(provider)
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct AgentProfileId(pub Arc<str>);
|
||||
|
||||
impl AgentProfileId {
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for LanguageModelProviderSetting {
|
||||
fn from(provider: &str) -> Self {
|
||||
Self(provider.to_string())
|
||||
impl std::fmt::Display for AgentProfileId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct AgentProfileContent {
|
||||
pub name: Arc<str>,
|
||||
#[serde(default)]
|
||||
pub tools: IndexMap<Arc<str>, bool>,
|
||||
/// Whether all context servers are enabled by default.
|
||||
pub enable_all_context_servers: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub context_servers: IndexMap<Arc<str>, ContextServerPresetContent>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ContextServerPresetContent {
|
||||
pub tools: IndexMap<Arc<str>, bool>,
|
||||
impl Default for AgentProfileId {
|
||||
fn default() -> Self {
|
||||
Self("write".into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Settings for AgentSettings {
|
||||
const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]);
|
||||
|
||||
type FileContent = AgentSettingsContent;
|
||||
|
||||
fn load(
|
||||
sources: SettingsSources<Self::FileContent>,
|
||||
_: &mut gpui::App,
|
||||
) -> anyhow::Result<Self> {
|
||||
let mut settings = AgentSettings::default();
|
||||
|
||||
for value in sources.defaults_and_customizations() {
|
||||
merge(&mut settings.enabled, value.enabled);
|
||||
merge(&mut settings.button, value.button);
|
||||
merge(&mut settings.dock, value.dock);
|
||||
merge(
|
||||
&mut settings.default_width,
|
||||
value.default_width.map(Into::into),
|
||||
);
|
||||
merge(
|
||||
&mut settings.default_height,
|
||||
value.default_height.map(Into::into),
|
||||
);
|
||||
settings.default_model = value
|
||||
.default_model
|
||||
.clone()
|
||||
.or(settings.default_model.take());
|
||||
settings.inline_assistant_model = value
|
||||
.inline_assistant_model
|
||||
.clone()
|
||||
.or(settings.inline_assistant_model.take());
|
||||
settings.commit_message_model = value
|
||||
.clone()
|
||||
.commit_message_model
|
||||
.or(settings.commit_message_model.take());
|
||||
settings.thread_summary_model = value
|
||||
.clone()
|
||||
.thread_summary_model
|
||||
.or(settings.thread_summary_model.take());
|
||||
merge(
|
||||
&mut settings.inline_alternatives,
|
||||
value.inline_alternatives.clone(),
|
||||
);
|
||||
merge(
|
||||
&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.clone());
|
||||
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);
|
||||
merge(&mut settings.expand_edit_card, value.expand_edit_card);
|
||||
merge(
|
||||
&mut settings.expand_terminal_card,
|
||||
value.expand_terminal_card,
|
||||
);
|
||||
merge(
|
||||
&mut settings.use_modifier_to_send,
|
||||
value.use_modifier_to_send,
|
||||
);
|
||||
|
||||
settings
|
||||
.model_parameters
|
||||
.extend_from_slice(&value.model_parameters);
|
||||
|
||||
if let Some(profiles) = value.profiles.as_ref() {
|
||||
settings
|
||||
.profiles
|
||||
.extend(profiles.into_iter().map(|(id, profile)| {
|
||||
(
|
||||
id.clone(),
|
||||
AgentProfileSettings {
|
||||
name: profile.name.clone().into(),
|
||||
tools: profile.tools.clone(),
|
||||
enable_all_context_servers: profile
|
||||
.enable_all_context_servers
|
||||
.unwrap_or_default(),
|
||||
context_servers: profile
|
||||
.context_servers
|
||||
.iter()
|
||||
.map(|(context_server_id, preset)| {
|
||||
(
|
||||
context_server_id.clone(),
|
||||
ContextServerPreset {
|
||||
tools: preset.tools.clone(),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
)
|
||||
}));
|
||||
}
|
||||
fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
|
||||
let agent = content.agent.clone().unwrap();
|
||||
Self {
|
||||
enabled: agent.enabled.unwrap(),
|
||||
button: agent.button.unwrap(),
|
||||
dock: agent.dock.unwrap(),
|
||||
default_width: px(agent.default_width.unwrap()),
|
||||
default_height: px(agent.default_height.unwrap()),
|
||||
default_model: Some(agent.default_model.unwrap()),
|
||||
inline_assistant_model: agent.inline_assistant_model,
|
||||
commit_message_model: agent.commit_message_model,
|
||||
thread_summary_model: agent.thread_summary_model,
|
||||
inline_alternatives: agent.inline_alternatives.unwrap_or_default(),
|
||||
default_profile: AgentProfileId(agent.default_profile.unwrap()),
|
||||
default_view: agent.default_view.unwrap(),
|
||||
profiles: agent
|
||||
.profiles
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|(key, val)| (AgentProfileId(key), val.into()))
|
||||
.collect(),
|
||||
always_allow_tool_actions: agent.always_allow_tool_actions.unwrap(),
|
||||
notify_when_agent_waiting: agent.notify_when_agent_waiting.unwrap(),
|
||||
play_sound_when_agent_done: agent.play_sound_when_agent_done.unwrap(),
|
||||
stream_edits: agent.stream_edits.unwrap(),
|
||||
single_file_review: agent.single_file_review.unwrap(),
|
||||
model_parameters: agent.model_parameters,
|
||||
preferred_completion_mode: agent.preferred_completion_mode.unwrap().into(),
|
||||
enable_feedback: agent.enable_feedback.unwrap(),
|
||||
expand_edit_card: agent.expand_edit_card.unwrap(),
|
||||
expand_terminal_card: agent.expand_terminal_card.unwrap(),
|
||||
use_modifier_to_send: agent.use_modifier_to_send.unwrap(),
|
||||
message_editor_min_lines: agent.message_editor_min_lines.unwrap(),
|
||||
}
|
||||
|
||||
debug_assert!(
|
||||
!sources.default.always_allow_tool_actions.unwrap_or(false),
|
||||
"For security, agent.always_allow_tool_actions should always be false in default.json. If it's true, that is a bug that should be fixed!"
|
||||
);
|
||||
|
||||
// For security reasons, only trust the user's global settings for whether to always allow tool actions.
|
||||
// If this could be overridden locally, an attacker could (e.g. by committing to source control and
|
||||
// convincing you to switch branches) modify your project-local settings to disable the agent's safety checks.
|
||||
settings.always_allow_tool_actions = sources
|
||||
.user
|
||||
.and_then(|setting| setting.always_allow_tool_actions)
|
||||
.unwrap_or(false);
|
||||
|
||||
Ok(settings)
|
||||
}
|
||||
|
||||
fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
|
||||
fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) {
|
||||
if let Some(b) = vscode
|
||||
.read_value("chat.agent.enabled")
|
||||
.and_then(|b| b.as_bool())
|
||||
{
|
||||
current.enabled = Some(b);
|
||||
current.button = Some(b);
|
||||
current.agent.get_or_insert_default().enabled = Some(b);
|
||||
current.agent.get_or_insert_default().button = Some(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn merge<T>(target: &mut T, value: Option<T>) {
|
||||
if let Some(value) = value {
|
||||
*target = value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,6 @@ gpui.workspace = true
|
||||
html_to_markdown.workspace = true
|
||||
http_client.workspace = true
|
||||
indoc.workspace = true
|
||||
inventory.workspace = true
|
||||
itertools.workspace = true
|
||||
jsonschema.workspace = true
|
||||
language.workspace = true
|
||||
@@ -81,7 +80,6 @@ serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_json_lenient.workspace = true
|
||||
settings.workspace = true
|
||||
shlex.workspace = true
|
||||
smol.workspace = true
|
||||
streaming_diff.workspace = true
|
||||
task.workspace = true
|
||||
@@ -98,7 +96,6 @@ ui_input.workspace = true
|
||||
url.workspace = true
|
||||
urlencoding.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
watch.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::cell::RefCell;
|
||||
use std::ops::Range;
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
@@ -13,7 +14,7 @@ use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{App, Entity, Task, WeakEntity};
|
||||
use language::{Buffer, CodeLabel, HighlightId};
|
||||
use lsp::CompletionContext;
|
||||
use project::lsp_store::CompletionDocumentation;
|
||||
use project::lsp_store::{CompletionDocumentation, SymbolLocation};
|
||||
use project::{
|
||||
Completion, CompletionDisplayOptions, CompletionIntent, CompletionResponse, Project,
|
||||
ProjectPath, Symbol, WorktreeId,
|
||||
@@ -22,6 +23,7 @@ use prompt_store::PromptStore;
|
||||
use rope::Point;
|
||||
use text::{Anchor, ToPoint as _};
|
||||
use ui::prelude::*;
|
||||
use util::rel_path::RelPath;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::AgentPanel;
|
||||
@@ -68,7 +70,7 @@ pub struct ContextPickerCompletionProvider {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
history_store: Entity<HistoryStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
|
||||
prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
|
||||
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
|
||||
}
|
||||
|
||||
@@ -78,7 +80,7 @@ impl ContextPickerCompletionProvider {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
history_store: Entity<HistoryStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
|
||||
prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
|
||||
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
@@ -187,7 +189,7 @@ impl ContextPickerCompletionProvider {
|
||||
|
||||
pub(crate) fn completion_for_path(
|
||||
project_path: ProjectPath,
|
||||
path_prefix: &str,
|
||||
path_prefix: &RelPath,
|
||||
is_recent: bool,
|
||||
is_directory: bool,
|
||||
source_range: Range<Anchor>,
|
||||
@@ -195,10 +197,12 @@ impl ContextPickerCompletionProvider {
|
||||
project: Entity<Project>,
|
||||
cx: &mut App,
|
||||
) -> Option<Completion> {
|
||||
let path_style = project.read(cx).path_style(cx);
|
||||
let (file_name, directory) =
|
||||
crate::context_picker::file_context_picker::extract_file_name_and_directory(
|
||||
&project_path.path,
|
||||
path_prefix,
|
||||
path_style,
|
||||
);
|
||||
|
||||
let label =
|
||||
@@ -250,7 +254,15 @@ impl ContextPickerCompletionProvider {
|
||||
|
||||
let label = CodeLabel::plain(symbol.name.clone(), None);
|
||||
|
||||
let abs_path = project.read(cx).absolute_path(&symbol.path, cx)?;
|
||||
let abs_path = match &symbol.path {
|
||||
SymbolLocation::InProject(project_path) => {
|
||||
project.read(cx).absolute_path(&project_path, cx)?
|
||||
}
|
||||
SymbolLocation::OutsideProject {
|
||||
abs_path,
|
||||
signature: _,
|
||||
} => PathBuf::from(abs_path.as_ref()),
|
||||
};
|
||||
let uri = MentionUri::Symbol {
|
||||
abs_path,
|
||||
name: symbol.name.clone(),
|
||||
@@ -600,7 +612,7 @@ impl ContextPickerCompletionProvider {
|
||||
}),
|
||||
);
|
||||
|
||||
if self.prompt_capabilities.get().embedded_context {
|
||||
if self.prompt_capabilities.borrow().embedded_context {
|
||||
const RECENT_COUNT: usize = 2;
|
||||
let threads = self
|
||||
.history_store
|
||||
@@ -622,7 +634,7 @@ impl ContextPickerCompletionProvider {
|
||||
workspace: &Entity<Workspace>,
|
||||
cx: &mut App,
|
||||
) -> Vec<ContextPickerEntry> {
|
||||
let embedded_context = self.prompt_capabilities.get().embedded_context;
|
||||
let embedded_context = self.prompt_capabilities.borrow().embedded_context;
|
||||
let mut entries = if embedded_context {
|
||||
vec![
|
||||
ContextPickerEntry::Mode(ContextPickerMode::File),
|
||||
@@ -694,7 +706,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
ContextCompletion::try_parse(
|
||||
line,
|
||||
offset_to_line,
|
||||
self.prompt_capabilities.get().embedded_context,
|
||||
self.prompt_capabilities.borrow().embedded_context,
|
||||
)
|
||||
});
|
||||
let Some(state) = state else {
|
||||
@@ -896,7 +908,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
ContextCompletion::try_parse(
|
||||
line,
|
||||
offset_to_line,
|
||||
self.prompt_capabilities.get().embedded_context,
|
||||
self.prompt_capabilities.borrow().embedded_context,
|
||||
)
|
||||
.map(|completion| {
|
||||
completion.source_range().start <= offset_to_line + position.column as usize
|
||||
@@ -1035,6 +1047,12 @@ impl SlashCommandCompletion {
|
||||
let mut argument = None;
|
||||
let mut command = None;
|
||||
if let Some((command_text, args)) = last_command.split_once(char::is_whitespace) {
|
||||
// If the args start with '@', treat this as a mention completion, not a slash command argument.
|
||||
// Early-return None to let MentionCompletion::try_parse handle it.
|
||||
if args.trim_start().starts_with('@') {
|
||||
return None;
|
||||
}
|
||||
|
||||
if !args.is_empty() {
|
||||
argument = Some(args.trim_end().to_string());
|
||||
}
|
||||
@@ -1108,6 +1126,12 @@ impl MentionCompletion {
|
||||
match rest_of_line[mode_text.len()..].find(|c: char| !c.is_whitespace()) {
|
||||
Some(whitespace_count) => {
|
||||
if let Some(argument_text) = parts.next() {
|
||||
// If mode wasn't recognized but we have an argument, don't suggest completions
|
||||
// (e.g. '@something word')
|
||||
if mode.is_none() && !argument_text.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
argument = Some(argument_text.to_string());
|
||||
end += whitespace_count + argument_text.len();
|
||||
}
|
||||
@@ -1196,6 +1220,14 @@ mod tests {
|
||||
assert_eq!(SlashCommandCompletion::try_parse("Lorem/", 0), None);
|
||||
|
||||
assert_eq!(SlashCommandCompletion::try_parse("/ ", 0), None);
|
||||
|
||||
// Slash commands should not consume @ symbols that could be mention triggers
|
||||
assert_eq!(SlashCommandCompletion::try_parse("/init @", 0), None);
|
||||
|
||||
assert_eq!(SlashCommandCompletion::try_parse("/help @file", 0), None);
|
||||
|
||||
// @ with whitespace before it should also not be consumed
|
||||
assert_eq!(SlashCommandCompletion::try_parse("/command @", 0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1265,6 +1297,17 @@ mod tests {
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
MentionCompletion::try_parse(true, "Lorem @main ", 0),
|
||||
Some(MentionCompletion {
|
||||
source_range: 6..12,
|
||||
mode: None,
|
||||
argument: Some("main".to_string()),
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(MentionCompletion::try_parse(true, "Lorem @main m", 0), None);
|
||||
|
||||
assert_eq!(MentionCompletion::try_parse(true, "test@", 0), None);
|
||||
|
||||
// Allowed non-file mentions
|
||||
@@ -1279,14 +1322,9 @@ mod tests {
|
||||
);
|
||||
|
||||
// Disallowed non-file mentions
|
||||
|
||||
assert_eq!(
|
||||
MentionCompletion::try_parse(false, "Lorem @symbol main", 0),
|
||||
Some(MentionCompletion {
|
||||
source_range: 6..18,
|
||||
mode: None,
|
||||
argument: Some("main".to_string()),
|
||||
})
|
||||
None
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
ops::Range,
|
||||
rc::Rc,
|
||||
};
|
||||
use std::{cell::RefCell, ops::Range, rc::Rc};
|
||||
|
||||
use acp_thread::{AcpThread, AgentThreadEntry};
|
||||
use agent_client_protocol::{self as acp, ToolCallId};
|
||||
@@ -30,7 +26,7 @@ pub struct EntryViewState {
|
||||
history_store: Entity<HistoryStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
entries: Vec<Entry>,
|
||||
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
|
||||
prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
|
||||
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
|
||||
agent_name: SharedString,
|
||||
}
|
||||
@@ -41,7 +37,7 @@ impl EntryViewState {
|
||||
project: Entity<Project>,
|
||||
history_store: Entity<HistoryStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
|
||||
prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
|
||||
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
|
||||
agent_name: SharedString,
|
||||
) -> Self {
|
||||
@@ -207,7 +203,7 @@ impl EntryViewState {
|
||||
self.entries.drain(range);
|
||||
}
|
||||
|
||||
pub fn agent_font_size_changed(&mut self, cx: &mut App) {
|
||||
pub fn agent_ui_font_size_changed(&mut self, cx: &mut App) {
|
||||
for entry in self.entries.iter() {
|
||||
match entry {
|
||||
Entry::UserMessage { .. } | Entry::AssistantMessage { .. } => {}
|
||||
@@ -391,7 +387,7 @@ fn diff_editor_text_style_refinement(cx: &mut App) -> TextStyleRefinement {
|
||||
font_size: Some(
|
||||
TextSize::Small
|
||||
.rems(cx)
|
||||
.to_pixels(ThemeSettings::get_global(cx).agent_font_size(cx))
|
||||
.to_pixels(ThemeSettings::get_global(cx).agent_ui_font_size(cx))
|
||||
.into(),
|
||||
),
|
||||
..Default::default()
|
||||
@@ -448,11 +444,13 @@ mod tests {
|
||||
path: "/project/hello.txt".into(),
|
||||
old_text: Some("hi world".into()),
|
||||
new_text: "hello world".into(),
|
||||
meta: None,
|
||||
},
|
||||
}],
|
||||
locations: vec![],
|
||||
raw_input: None,
|
||||
raw_output: None,
|
||||
meta: None,
|
||||
};
|
||||
let connection = Rc::new(StubAgentConnection::new());
|
||||
let thread = cx
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,8 +5,8 @@ use fs::Fs;
|
||||
use gpui::{Context, Entity, FocusHandle, WeakEntity, Window, prelude::*};
|
||||
use std::{rc::Rc, sync::Arc};
|
||||
use ui::{
|
||||
Button, ContextMenu, ContextMenuEntry, KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip,
|
||||
prelude::*,
|
||||
Button, ContextMenu, ContextMenuEntry, DocumentationEdge, DocumentationSide, KeyBinding,
|
||||
PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*,
|
||||
};
|
||||
|
||||
use crate::{CycleModeSelector, ToggleProfileSelector};
|
||||
@@ -91,7 +91,7 @@ impl ModeSelector {
|
||||
.toggleable(IconPosition::End, is_selected);
|
||||
|
||||
let entry = if let Some(description) = &mode.description {
|
||||
entry.documentation_aside(ui::DocumentationSide::Left, {
|
||||
entry.documentation_aside(DocumentationSide::Left, DocumentationEdge::Bottom, {
|
||||
let description = description.clone();
|
||||
|
||||
move |cx| {
|
||||
@@ -107,13 +107,15 @@ impl ModeSelector {
|
||||
.text_sm()
|
||||
.text_color(Color::Muted.color(cx))
|
||||
.child("Hold")
|
||||
.child(div().pt_0p5().children(ui::render_modifiers(
|
||||
&gpui::Modifiers::secondary_key(),
|
||||
PlatformStyle::platform(),
|
||||
None,
|
||||
Some(ui::TextSize::Default.rems(cx).into()),
|
||||
true,
|
||||
)))
|
||||
.child(h_flex().flex_shrink_0().children(
|
||||
ui::render_modifiers(
|
||||
&gpui::Modifiers::secondary_key(),
|
||||
PlatformStyle::platform(),
|
||||
None,
|
||||
Some(ui::TextSize::Default.rems(cx).into()),
|
||||
true,
|
||||
),
|
||||
))
|
||||
.child(div().map(|this| {
|
||||
if is_default {
|
||||
this.child("to also unset as default")
|
||||
@@ -223,6 +225,10 @@ impl Render for ModeSelector {
|
||||
)
|
||||
.anchor(gpui::Corner::BottomRight)
|
||||
.with_handle(self.menu_handle.clone())
|
||||
.offset(gpui::Point {
|
||||
x: px(0.0),
|
||||
y: px(-2.0),
|
||||
})
|
||||
.menu(move |window, cx| {
|
||||
Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
|
||||
})
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::{cmp::Reverse, rc::Rc, sync::Arc};
|
||||
|
||||
use acp_thread::{AgentModelInfo, AgentModelList, AgentModelSelector};
|
||||
use agent_client_protocol as acp;
|
||||
use anyhow::Result;
|
||||
use collections::IndexMap;
|
||||
use futures::FutureExt;
|
||||
@@ -10,20 +9,19 @@ use gpui::{Action, AsyncWindowContext, BackgroundExecutor, DismissEvent, Task, W
|
||||
use ordered_float::OrderedFloat;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use ui::{
|
||||
AnyElement, App, Context, IntoElement, ListItem, ListItemSpacing, SharedString, Window,
|
||||
prelude::*, rems,
|
||||
AnyElement, App, Context, DocumentationAside, DocumentationEdge, DocumentationSide,
|
||||
IntoElement, ListItem, ListItemSpacing, SharedString, Window, prelude::*, rems,
|
||||
};
|
||||
use util::ResultExt;
|
||||
|
||||
pub type AcpModelSelector = Picker<AcpModelPickerDelegate>;
|
||||
|
||||
pub fn acp_model_selector(
|
||||
session_id: acp::SessionId,
|
||||
selector: Rc<dyn AgentModelSelector>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<AcpModelSelector>,
|
||||
) -> AcpModelSelector {
|
||||
let delegate = AcpModelPickerDelegate::new(session_id, selector, window, cx);
|
||||
let delegate = AcpModelPickerDelegate::new(selector, window, cx);
|
||||
Picker::list(delegate, window, cx)
|
||||
.show_scrollbar(true)
|
||||
.width(rems(20.))
|
||||
@@ -36,61 +34,63 @@ enum AcpModelPickerEntry {
|
||||
}
|
||||
|
||||
pub struct AcpModelPickerDelegate {
|
||||
session_id: acp::SessionId,
|
||||
selector: Rc<dyn AgentModelSelector>,
|
||||
filtered_entries: Vec<AcpModelPickerEntry>,
|
||||
models: Option<AgentModelList>,
|
||||
selected_index: usize,
|
||||
selected_description: Option<(usize, SharedString)>,
|
||||
selected_model: Option<AgentModelInfo>,
|
||||
_refresh_models_task: Task<()>,
|
||||
}
|
||||
|
||||
impl AcpModelPickerDelegate {
|
||||
fn new(
|
||||
session_id: acp::SessionId,
|
||||
selector: Rc<dyn AgentModelSelector>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<AcpModelSelector>,
|
||||
) -> Self {
|
||||
let mut rx = selector.watch(cx);
|
||||
let refresh_models_task = cx.spawn_in(window, {
|
||||
let session_id = session_id.clone();
|
||||
async move |this, cx| {
|
||||
async fn refresh(
|
||||
this: &WeakEntity<Picker<AcpModelPickerDelegate>>,
|
||||
session_id: &acp::SessionId,
|
||||
cx: &mut AsyncWindowContext,
|
||||
) -> Result<()> {
|
||||
let (models_task, selected_model_task) = this.update(cx, |this, cx| {
|
||||
(
|
||||
this.delegate.selector.list_models(cx),
|
||||
this.delegate.selector.selected_model(session_id, cx),
|
||||
)
|
||||
})?;
|
||||
let rx = selector.watch(cx);
|
||||
let refresh_models_task = {
|
||||
cx.spawn_in(window, {
|
||||
async move |this, cx| {
|
||||
async fn refresh(
|
||||
this: &WeakEntity<Picker<AcpModelPickerDelegate>>,
|
||||
cx: &mut AsyncWindowContext,
|
||||
) -> Result<()> {
|
||||
let (models_task, selected_model_task) = this.update(cx, |this, cx| {
|
||||
(
|
||||
this.delegate.selector.list_models(cx),
|
||||
this.delegate.selector.selected_model(cx),
|
||||
)
|
||||
})?;
|
||||
|
||||
let (models, selected_model) = futures::join!(models_task, selected_model_task);
|
||||
let (models, selected_model) =
|
||||
futures::join!(models_task, selected_model_task);
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.delegate.models = models.ok();
|
||||
this.delegate.selected_model = selected_model.ok();
|
||||
this.refresh(window, cx)
|
||||
})
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.delegate.models = models.ok();
|
||||
this.delegate.selected_model = selected_model.ok();
|
||||
this.refresh(window, cx)
|
||||
})
|
||||
}
|
||||
|
||||
refresh(&this, cx).await.log_err();
|
||||
if let Some(mut rx) = rx {
|
||||
while let Ok(()) = rx.recv().await {
|
||||
refresh(&this, cx).await.log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
refresh(&this, &session_id, cx).await.log_err();
|
||||
while let Ok(()) = rx.recv().await {
|
||||
refresh(&this, &session_id, cx).await.log_err();
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
};
|
||||
|
||||
Self {
|
||||
session_id,
|
||||
selector,
|
||||
filtered_entries: Vec::new(),
|
||||
models: None,
|
||||
selected_model: None,
|
||||
selected_index: 0,
|
||||
selected_description: None,
|
||||
_refresh_models_task: refresh_models_task,
|
||||
}
|
||||
}
|
||||
@@ -182,7 +182,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
self.filtered_entries.get(self.selected_index)
|
||||
{
|
||||
self.selector
|
||||
.select_model(self.session_id.clone(), model_info.id.clone(), cx)
|
||||
.select_model(model_info.id.clone(), cx)
|
||||
.detach_and_log_err(cx);
|
||||
self.selected_model = Some(model_info.clone());
|
||||
let current_index = self.selected_index;
|
||||
@@ -233,31 +233,46 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
};
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.start_slot::<Icon>(model_info.icon.map(|icon| {
|
||||
Icon::new(icon)
|
||||
.color(model_icon_color)
|
||||
.size(IconSize::Small)
|
||||
}))
|
||||
div()
|
||||
.id(("model-picker-menu-child", ix))
|
||||
.when_some(model_info.description.clone(), |this, description| {
|
||||
this
|
||||
.on_hover(cx.listener(move |menu, hovered, _, cx| {
|
||||
if *hovered {
|
||||
menu.delegate.selected_description = Some((ix, description.clone()));
|
||||
} else if matches!(menu.delegate.selected_description, Some((id, _)) if id == ix) {
|
||||
menu.delegate.selected_description = None;
|
||||
}
|
||||
cx.notify();
|
||||
}))
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.pl_0p5()
|
||||
.gap_1p5()
|
||||
.w(px(240.))
|
||||
.child(Label::new(model_info.name.clone()).truncate()),
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.start_slot::<Icon>(model_info.icon.map(|icon| {
|
||||
Icon::new(icon)
|
||||
.color(model_icon_color)
|
||||
.size(IconSize::Small)
|
||||
}))
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.pl_0p5()
|
||||
.gap_1p5()
|
||||
.w(px(240.))
|
||||
.child(Label::new(model_info.name.clone()).truncate()),
|
||||
)
|
||||
.end_slot(div().pr_3().when(is_selected, |this| {
|
||||
this.child(
|
||||
Icon::new(IconName::Check)
|
||||
.color(Color::Accent)
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
})),
|
||||
)
|
||||
.end_slot(div().pr_3().when(is_selected, |this| {
|
||||
this.child(
|
||||
Icon::new(IconName::Check)
|
||||
.color(Color::Accent)
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
}))
|
||||
.into_any_element(),
|
||||
.into_any_element()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -292,6 +307,21 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
.into_any(),
|
||||
)
|
||||
}
|
||||
|
||||
fn documentation_aside(
|
||||
&self,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<ui::DocumentationAside> {
|
||||
self.selected_description.as_ref().map(|(_, description)| {
|
||||
let description = description.clone();
|
||||
DocumentationAside::new(
|
||||
DocumentationSide::Left,
|
||||
DocumentationEdge::Bottom,
|
||||
Rc::new(move |_| Label::new(description.clone()).into_any_element()),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn info_list_to_picker_entries(
|
||||
@@ -371,6 +401,7 @@ async fn fuzzy_search(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use agent_client_protocol as acp;
|
||||
use gpui::TestAppContext;
|
||||
|
||||
use super::*;
|
||||
@@ -383,8 +414,9 @@ mod tests {
|
||||
models
|
||||
.into_iter()
|
||||
.map(|model| acp_thread::AgentModelInfo {
|
||||
id: acp_thread::AgentModelId(model.to_string().into()),
|
||||
id: acp::ModelId(model.to_string().into()),
|
||||
name: model.to_string().into(),
|
||||
description: None,
|
||||
icon: None,
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use acp_thread::AgentModelSelector;
|
||||
use agent_client_protocol as acp;
|
||||
use gpui::{Entity, FocusHandle};
|
||||
use picker::popover_menu::PickerPopoverMenu;
|
||||
use ui::{
|
||||
@@ -20,7 +19,6 @@ pub struct AcpModelSelectorPopover {
|
||||
|
||||
impl AcpModelSelectorPopover {
|
||||
pub(crate) fn new(
|
||||
session_id: acp::SessionId,
|
||||
selector: Rc<dyn AgentModelSelector>,
|
||||
menu_handle: PopoverMenuHandle<AcpModelSelector>,
|
||||
focus_handle: FocusHandle,
|
||||
@@ -28,7 +26,7 @@ impl AcpModelSelectorPopover {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
Self {
|
||||
selector: cx.new(move |cx| acp_model_selector(session_id, selector, window, cx)),
|
||||
selector: cx.new(move |cx| acp_model_selector(selector, window, cx)),
|
||||
menu_handle,
|
||||
focus_handle,
|
||||
}
|
||||
|
||||
@@ -5,15 +5,15 @@ use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
|
||||
use editor::{Editor, EditorEvent};
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{
|
||||
App, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Stateful, Task,
|
||||
App, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Task,
|
||||
UniformListScrollHandle, WeakEntity, Window, uniform_list,
|
||||
};
|
||||
use std::{fmt::Display, ops::Range};
|
||||
use text::Bias;
|
||||
use time::{OffsetDateTime, UtcOffset};
|
||||
use ui::{
|
||||
HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Scrollbar, ScrollbarState,
|
||||
Tooltip, prelude::*,
|
||||
HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Tooltip, WithScrollbar,
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
pub struct AcpThreadHistory {
|
||||
@@ -26,8 +26,6 @@ pub struct AcpThreadHistory {
|
||||
|
||||
visible_items: Vec<ListItemType>,
|
||||
|
||||
scrollbar_visibility: bool,
|
||||
scrollbar_state: ScrollbarState,
|
||||
local_timezone: UtcOffset,
|
||||
|
||||
_update_task: Task<()>,
|
||||
@@ -70,7 +68,7 @@ impl AcpThreadHistory {
|
||||
) -> Self {
|
||||
let search_editor = cx.new(|cx| {
|
||||
let mut editor = Editor::single_line(window, cx);
|
||||
editor.set_placeholder_text("Search threads...", cx);
|
||||
editor.set_placeholder_text("Search threads...", window, cx);
|
||||
editor
|
||||
});
|
||||
|
||||
@@ -90,7 +88,6 @@ impl AcpThreadHistory {
|
||||
});
|
||||
|
||||
let scroll_handle = UniformListScrollHandle::default();
|
||||
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
|
||||
|
||||
let mut this = Self {
|
||||
history_store,
|
||||
@@ -99,8 +96,6 @@ impl AcpThreadHistory {
|
||||
hovered_index: None,
|
||||
visible_items: Default::default(),
|
||||
search_editor,
|
||||
scrollbar_visibility: true,
|
||||
scrollbar_state,
|
||||
local_timezone: UtcOffset::from_whole_seconds(
|
||||
chrono::Local::now().offset().local_minus_utc(),
|
||||
)
|
||||
@@ -339,43 +334,6 @@ impl AcpThreadHistory {
|
||||
task.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn render_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
|
||||
if !(self.scrollbar_visibility || self.scrollbar_state.is_dragging()) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(
|
||||
div()
|
||||
.occlude()
|
||||
.id("thread-history-scroll")
|
||||
.h_full()
|
||||
.bg(cx.theme().colors().panel_background.opacity(0.8))
|
||||
.border_l_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.absolute()
|
||||
.right_1()
|
||||
.top_0()
|
||||
.bottom_0()
|
||||
.w_4()
|
||||
.pl_1()
|
||||
.cursor_default()
|
||||
.on_mouse_move(cx.listener(|_, _, _window, cx| {
|
||||
cx.notify();
|
||||
cx.stop_propagation()
|
||||
}))
|
||||
.on_hover(|_, _window, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_any_mouse_down(|_, _window, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_scroll_wheel(cx.listener(|_, _, _window, cx| {
|
||||
cx.notify();
|
||||
}))
|
||||
.children(Scrollbar::vertical(self.scrollbar_state.clone())),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_list_items(
|
||||
&mut self,
|
||||
range: Range<usize>,
|
||||
@@ -491,7 +449,7 @@ impl Focusable for AcpThreadHistory {
|
||||
}
|
||||
|
||||
impl Render for AcpThreadHistory {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.key_context("ThreadHistory")
|
||||
.size_full()
|
||||
@@ -542,22 +500,24 @@ impl Render for AcpThreadHistory {
|
||||
),
|
||||
)
|
||||
} else {
|
||||
view.pr_5()
|
||||
.child(
|
||||
uniform_list(
|
||||
"thread-history",
|
||||
self.visible_items.len(),
|
||||
cx.processor(|this, range: Range<usize>, window, cx| {
|
||||
this.render_list_items(range, window, cx)
|
||||
}),
|
||||
)
|
||||
.p_1()
|
||||
.track_scroll(self.scroll_handle.clone())
|
||||
.flex_grow(),
|
||||
view.child(
|
||||
uniform_list(
|
||||
"thread-history",
|
||||
self.visible_items.len(),
|
||||
cx.processor(|this, range: Range<usize>, window, cx| {
|
||||
this.render_list_items(range, window, cx)
|
||||
}),
|
||||
)
|
||||
.when_some(self.render_scrollbar(cx), |div, scrollbar| {
|
||||
div.child(scrollbar)
|
||||
})
|
||||
.p_1()
|
||||
.pr_4()
|
||||
.track_scroll(self.scroll_handle.clone())
|
||||
.flex_grow(),
|
||||
)
|
||||
.vertical_scrollbar_for(
|
||||
self.scroll_handle.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
mod add_llm_provider_modal;
|
||||
mod configure_context_server_modal;
|
||||
mod configure_context_server_tools_modal;
|
||||
mod manage_profiles_modal;
|
||||
mod tool_picker;
|
||||
|
||||
@@ -8,12 +9,13 @@ use std::{ops::Range, sync::Arc};
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::Result;
|
||||
use assistant_tool::{ToolSource, ToolWorkingSet};
|
||||
use cloud_llm_client::Plan;
|
||||
use cloud_llm_client::{Plan, PlanV1, PlanV2};
|
||||
use collections::HashMap;
|
||||
use context_server::ContextServerId;
|
||||
use editor::{Editor, SelectionEffects, scroll::Autoscroll};
|
||||
use extension::ExtensionManifest;
|
||||
use extension_host::ExtensionStore;
|
||||
use feature_flags::{CodexAcpFeatureFlag, FeatureFlagAppExt as _};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
Action, AnyView, App, AsyncWindowContext, Corner, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
@@ -25,30 +27,25 @@ use language_model::{
|
||||
};
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
use project::{
|
||||
agent_server_store::{
|
||||
AgentServerCommand, AgentServerStore, AllAgentServersSettings, CLAUDE_CODE_NAME,
|
||||
CustomAgentServerSettings, GEMINI_NAME,
|
||||
},
|
||||
agent_server_store::{AgentServerStore, CLAUDE_CODE_NAME, CODEX_NAME, GEMINI_NAME},
|
||||
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
|
||||
project_settings::{ContextServerSettings, ProjectSettings},
|
||||
};
|
||||
use settings::{Settings, SettingsStore, update_settings_file};
|
||||
use ui::{
|
||||
Chip, CommonAnimationExt, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex,
|
||||
Indicator, PopoverMenu, Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip,
|
||||
prelude::*,
|
||||
Indicator, PopoverMenu, Switch, SwitchColor, SwitchField, Tooltip, WithScrollbar, prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{Workspace, create_and_open_local_file};
|
||||
use zed_actions::ExtensionCategoryFilter;
|
||||
|
||||
pub(crate) use configure_context_server_modal::ConfigureContextServerModal;
|
||||
pub(crate) use configure_context_server_tools_modal::ConfigureContextServerToolsModal;
|
||||
pub(crate) use manage_profiles_modal::ManageProfilesModal;
|
||||
|
||||
use crate::{
|
||||
AddContextServer, ExternalAgent, NewExternalAgentThread,
|
||||
AddContextServer,
|
||||
agent_configuration::add_llm_provider_modal::{AddLlmProviderModal, LlmCompatibleProvider},
|
||||
placeholder_command,
|
||||
};
|
||||
|
||||
pub struct AgentConfiguration {
|
||||
@@ -64,7 +61,6 @@ pub struct AgentConfiguration {
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
_registry_subscription: Subscription,
|
||||
scroll_handle: ScrollHandle,
|
||||
scrollbar_state: ScrollbarState,
|
||||
_check_for_gemini: Task<()>,
|
||||
}
|
||||
|
||||
@@ -101,9 +97,6 @@ impl AgentConfiguration {
|
||||
cx.subscribe(&context_server_store, |_, _, _, cx| cx.notify())
|
||||
.detach();
|
||||
|
||||
let scroll_handle = ScrollHandle::new();
|
||||
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
|
||||
|
||||
let mut this = Self {
|
||||
fs,
|
||||
language_registry,
|
||||
@@ -116,8 +109,7 @@ impl AgentConfiguration {
|
||||
expanded_provider_configurations: HashMap::default(),
|
||||
tools,
|
||||
_registry_subscription: registry_subscription,
|
||||
scroll_handle,
|
||||
scrollbar_state,
|
||||
scroll_handle: ScrollHandle::new(),
|
||||
_check_for_gemini: Task::ready(()),
|
||||
};
|
||||
this.build_provider_configuration_views(window, cx);
|
||||
@@ -206,9 +198,8 @@ impl AgentConfiguration {
|
||||
.when(is_expanded, |this| this.mb_2())
|
||||
.child(
|
||||
div()
|
||||
.opacity(0.6)
|
||||
.px_2()
|
||||
.child(Divider::horizontal().color(DividerColor::Border)),
|
||||
.child(Divider::horizontal().color(DividerColor::BorderFaded)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -233,7 +224,7 @@ impl AgentConfiguration {
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
Icon::new(provider.icon())
|
||||
.size(IconSize::Small)
|
||||
@@ -280,13 +271,28 @@ impl AgentConfiguration {
|
||||
*is_expanded = !*is_expanded;
|
||||
}
|
||||
})),
|
||||
)
|
||||
.when(provider.is_authenticated(cx), |parent| {
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.px_2()
|
||||
.gap_1()
|
||||
.when(is_expanded, |parent| match configuration_view {
|
||||
Some(configuration_view) => parent.child(configuration_view),
|
||||
None => parent.child(Label::new(format!(
|
||||
"No configuration view for {provider_name}",
|
||||
))),
|
||||
})
|
||||
.when(is_expanded && provider.is_authenticated(cx), |parent| {
|
||||
parent.child(
|
||||
Button::new(
|
||||
SharedString::from(format!("new-thread-{provider_id}")),
|
||||
"Start New Thread",
|
||||
)
|
||||
.full_width()
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon(IconName::Thread)
|
||||
.icon_size(IconSize::Small)
|
||||
@@ -303,17 +309,6 @@ impl AgentConfiguration {
|
||||
)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.px_2()
|
||||
.when(is_expanded, |parent| match configuration_view {
|
||||
Some(configuration_view) => parent.child(configuration_view),
|
||||
None => parent.child(Label::new(format!(
|
||||
"No configuration view for {provider_name}",
|
||||
))),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_provider_configuration_section(
|
||||
@@ -347,6 +342,8 @@ impl AgentConfiguration {
|
||||
PopoverMenu::new("add-provider-popover")
|
||||
.trigger(
|
||||
Button::new("add-provider", "Add Provider")
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon(IconName::Plus)
|
||||
.icon_size(IconSize::Small)
|
||||
@@ -419,8 +416,8 @@ impl AgentConfiguration {
|
||||
always_allow_tool_actions,
|
||||
move |state, _window, cx| {
|
||||
let allow = state == &ToggleState::Selected;
|
||||
update_settings_file::<AgentSettings>(fs.clone(), cx, move |settings, _| {
|
||||
settings.set_always_allow_tool_actions(allow);
|
||||
update_settings_file(fs.clone(), cx, move |settings, _| {
|
||||
settings.agent.get_or_insert_default().set_always_allow_tool_actions(allow);
|
||||
});
|
||||
},
|
||||
)
|
||||
@@ -437,8 +434,11 @@ impl AgentConfiguration {
|
||||
single_file_review,
|
||||
move |state, _window, cx| {
|
||||
let allow = state == &ToggleState::Selected;
|
||||
update_settings_file::<AgentSettings>(fs.clone(), cx, move |settings, _| {
|
||||
settings.set_single_file_review(allow);
|
||||
update_settings_file(fs.clone(), cx, move |settings, _| {
|
||||
settings
|
||||
.agent
|
||||
.get_or_insert_default()
|
||||
.set_single_file_review(allow);
|
||||
});
|
||||
},
|
||||
)
|
||||
@@ -457,8 +457,8 @@ impl AgentConfiguration {
|
||||
play_sound_when_agent_done,
|
||||
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);
|
||||
update_settings_file(fs.clone(), cx, move |settings, _| {
|
||||
settings.agent.get_or_insert_default().set_play_sound_when_agent_done(allow);
|
||||
});
|
||||
},
|
||||
)
|
||||
@@ -477,8 +477,8 @@ impl AgentConfiguration {
|
||||
use_modifier_to_send,
|
||||
move |state, _window, cx| {
|
||||
let allow = state == &ToggleState::Selected;
|
||||
update_settings_file::<AgentSettings>(fs.clone(), cx, move |settings, _| {
|
||||
settings.set_use_modifier_to_send(allow);
|
||||
update_settings_file(fs.clone(), cx, move |settings, _| {
|
||||
settings.agent.get_or_insert_default().set_use_modifier_to_send(allow);
|
||||
});
|
||||
},
|
||||
)
|
||||
@@ -515,11 +515,15 @@ impl AgentConfiguration {
|
||||
.blend(cx.theme().colors().text_accent.opacity(0.2));
|
||||
|
||||
let (plan_name, label_color, bg_color) = match plan {
|
||||
Plan::ZedFree | Plan::ZedFreeV2 => ("Free", Color::Default, free_chip_bg),
|
||||
Plan::ZedProTrial | Plan::ZedProTrialV2 => {
|
||||
Plan::V1(PlanV1::ZedFree) | Plan::V2(PlanV2::ZedFree) => {
|
||||
("Free", Color::Default, free_chip_bg)
|
||||
}
|
||||
Plan::V1(PlanV1::ZedProTrial) | Plan::V2(PlanV2::ZedProTrial) => {
|
||||
("Pro Trial", Color::Accent, pro_chip_bg)
|
||||
}
|
||||
Plan::ZedPro | Plan::ZedProV2 => ("Pro", Color::Accent, pro_chip_bg),
|
||||
Plan::V1(PlanV1::ZedPro) | Plan::V2(PlanV2::ZedPro) => {
|
||||
("Pro", Color::Accent, pro_chip_bg)
|
||||
}
|
||||
};
|
||||
|
||||
Chip::new(plan_name.to_string())
|
||||
@@ -531,10 +535,6 @@ impl AgentConfiguration {
|
||||
}
|
||||
}
|
||||
|
||||
fn card_item_bg_color(&self, cx: &mut Context<Self>) -> Hsla {
|
||||
cx.theme().colors().background.opacity(0.25)
|
||||
}
|
||||
|
||||
fn card_item_border_color(&self, cx: &mut Context<Self>) -> Hsla {
|
||||
cx.theme().colors().border.opacity(0.6)
|
||||
}
|
||||
@@ -544,61 +544,46 @@ impl AgentConfiguration {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let context_server_ids = self.context_server_store.read(cx).configured_server_ids();
|
||||
let mut context_server_ids = self
|
||||
.context_server_store
|
||||
.read(cx)
|
||||
.server_ids(cx)
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
v_flex()
|
||||
.p(DynamicSpacing::Base16.rems(cx))
|
||||
.pr(DynamicSpacing::Base20.rems(cx))
|
||||
.gap_2()
|
||||
.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(
|
||||
"All context servers connected through the Model Context Protocol.",
|
||||
)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
// Sort context servers: ones without mcp-server- prefix first, then prefixed ones
|
||||
context_server_ids.sort_by(|a, b| {
|
||||
const MCP_PREFIX: &str = "mcp-server-";
|
||||
match (a.0.strip_prefix(MCP_PREFIX), b.0.strip_prefix(MCP_PREFIX)) {
|
||||
// If one has mcp-server- prefix and other doesn't, non-mcp comes first
|
||||
(Some(_), None) => std::cmp::Ordering::Greater,
|
||||
(None, Some(_)) => std::cmp::Ordering::Less,
|
||||
// If both have same prefix status, sort by appropriate key
|
||||
(Some(a), Some(b)) => a.cmp(b),
|
||||
(None, None) => a.0.cmp(&b.0),
|
||||
}
|
||||
});
|
||||
|
||||
let add_server_popover = PopoverMenu::new("add-server-popover")
|
||||
.trigger(
|
||||
Button::new("add-server", "Add Server")
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon(IconName::Plus)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.label_size(LabelSize::Small),
|
||||
)
|
||||
.children(
|
||||
context_server_ids.into_iter().map(|context_server_id| {
|
||||
self.render_context_server(context_server_id, window, cx)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
h_flex().w_full().child(
|
||||
Button::new("add-context-server", "Add Custom Server")
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.full_width()
|
||||
.icon(IconName::Plus)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click(|_event, window, cx| {
|
||||
window.dispatch_action(AddContextServer.boxed_clone(), cx)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex().w_full().child(
|
||||
Button::new(
|
||||
"install-context-server-extensions",
|
||||
"Install MCP Extensions",
|
||||
)
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.full_width()
|
||||
.icon(IconName::ToolHammer)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click(|_event, window, cx| {
|
||||
.anchor(gpui::Corner::TopRight)
|
||||
.menu({
|
||||
move |window, cx| {
|
||||
Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
|
||||
menu.entry("Add Custom Server", None, {
|
||||
|window, cx| window.dispatch_action(AddContextServer.boxed_clone(), cx)
|
||||
})
|
||||
.entry("Install from Extensions", None, {
|
||||
|window, cx| {
|
||||
window.dispatch_action(
|
||||
zed_actions::Extensions {
|
||||
category_filter: Some(
|
||||
@@ -609,10 +594,68 @@ impl AgentConfiguration {
|
||||
.boxed_clone(),
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
),
|
||||
),
|
||||
}
|
||||
})
|
||||
}))
|
||||
}
|
||||
});
|
||||
|
||||
v_flex()
|
||||
.p(DynamicSpacing::Base16.rems(cx))
|
||||
.pr(DynamicSpacing::Base20.rems(cx))
|
||||
.gap_2()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.items_start()
|
||||
.justify_between()
|
||||
.gap_1()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(Headline::new("Model Context Protocol (MCP) Servers"))
|
||||
.child(
|
||||
Label::new(
|
||||
"All MCP servers connected directly or via a Zed extension.",
|
||||
)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(add_server_popover),
|
||||
)
|
||||
.child(v_flex().w_full().gap_1().map(|mut parent| {
|
||||
if context_server_ids.is_empty() {
|
||||
parent.child(
|
||||
h_flex()
|
||||
.p_4()
|
||||
.justify_center()
|
||||
.border_1()
|
||||
.border_dashed()
|
||||
.border_color(cx.theme().colors().border.opacity(0.6))
|
||||
.rounded_sm()
|
||||
.child(
|
||||
Label::new("No MCP servers added yet.")
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
for (index, context_server_id) in context_server_ids.into_iter().enumerate() {
|
||||
if index > 0 {
|
||||
parent = parent.child(
|
||||
Divider::horizontal()
|
||||
.color(DividerColor::BorderFaded)
|
||||
.into_any_element(),
|
||||
);
|
||||
}
|
||||
parent =
|
||||
parent.child(self.render_context_server(context_server_id, window, cx));
|
||||
}
|
||||
parent
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
fn render_context_server(
|
||||
@@ -705,7 +748,7 @@ impl AgentConfiguration {
|
||||
IconButton::new("context-server-config-menu", IconName::Settings)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_size(IconSize::Small),
|
||||
Tooltip::text("Open MCP server options"),
|
||||
Tooltip::text("Configure MCP Server"),
|
||||
)
|
||||
.anchor(Corner::TopRight)
|
||||
.menu({
|
||||
@@ -714,6 +757,8 @@ impl AgentConfiguration {
|
||||
let language_registry = self.language_registry.clone();
|
||||
let context_server_store = self.context_server_store.clone();
|
||||
let workspace = self.workspace.clone();
|
||||
let tools = self.tools.clone();
|
||||
|
||||
move |window, cx| {
|
||||
Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
|
||||
menu.entry("Configure Server", None, {
|
||||
@@ -730,7 +775,28 @@ impl AgentConfiguration {
|
||||
)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
})
|
||||
}).when(tool_count >= 1, |this| this.entry("View Tools", None, {
|
||||
let context_server_id = context_server_id.clone();
|
||||
let tools = tools.clone();
|
||||
let workspace = workspace.clone();
|
||||
|
||||
move |window, cx| {
|
||||
let context_server_id = context_server_id.clone();
|
||||
let tools = tools.clone();
|
||||
let workspace = workspace.clone();
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
ConfigureContextServerToolsModal::toggle(
|
||||
context_server_id,
|
||||
tools,
|
||||
workspace,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}))
|
||||
.separator()
|
||||
.entry("Uninstall", None, {
|
||||
let fs = fs.clone();
|
||||
@@ -776,14 +842,14 @@ impl AgentConfiguration {
|
||||
async move |cx| {
|
||||
uninstall_extension_task.await?;
|
||||
cx.update(|cx| {
|
||||
update_settings_file::<ProjectSettings>(
|
||||
update_settings_file(
|
||||
fs.clone(),
|
||||
cx,
|
||||
{
|
||||
let context_server_id =
|
||||
context_server_id.clone();
|
||||
move |settings, _| {
|
||||
settings
|
||||
settings.project
|
||||
.context_servers
|
||||
.remove(&context_server_id.0);
|
||||
}
|
||||
@@ -801,17 +867,11 @@ impl AgentConfiguration {
|
||||
|
||||
v_flex()
|
||||
.id(item_id.clone())
|
||||
.border_1()
|
||||
.rounded_md()
|
||||
.border_color(self.card_item_border_color(cx))
|
||||
.bg(self.card_item_bg_color(cx))
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
h_flex()
|
||||
.p_1()
|
||||
.justify_between()
|
||||
.when(
|
||||
error.is_some() || are_tools_expanded && tool_count >= 1,
|
||||
error.is_none() && are_tools_expanded && tool_count >= 1,
|
||||
|element| {
|
||||
element
|
||||
.border_b_1()
|
||||
@@ -820,40 +880,25 @@ impl AgentConfiguration {
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.child(
|
||||
Disclosure::new(
|
||||
"tool-list-disclosure",
|
||||
are_tools_expanded || error.is_some(),
|
||||
)
|
||||
.disabled(tool_count == 0)
|
||||
.on_click(cx.listener({
|
||||
let context_server_id = context_server_id.clone();
|
||||
move |this, _event, _window, _cx| {
|
||||
let is_open = this
|
||||
.expanded_context_server_tools
|
||||
.entry(context_server_id.clone())
|
||||
.or_insert(false);
|
||||
|
||||
*is_open = !*is_open;
|
||||
}
|
||||
})),
|
||||
)
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.child(
|
||||
h_flex()
|
||||
.id(SharedString::from(format!("tooltip-{}", item_id)))
|
||||
.h_full()
|
||||
.w_3()
|
||||
.mx_1()
|
||||
.mr_2()
|
||||
.justify_center()
|
||||
.tooltip(Tooltip::text(tooltip_text))
|
||||
.child(status_indicator),
|
||||
)
|
||||
.child(Label::new(item_id).ml_0p5())
|
||||
.child(Label::new(item_id).truncate())
|
||||
.child(
|
||||
div()
|
||||
.id("extension-source")
|
||||
.mt_0p5()
|
||||
.mx_1()
|
||||
.flex_none()
|
||||
.tooltip(Tooltip::text(source_tooltip))
|
||||
.child(
|
||||
Icon::new(source_icon)
|
||||
@@ -875,78 +920,65 @@ impl AgentConfiguration {
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.gap_0p5()
|
||||
.flex_none()
|
||||
.child(context_server_configuration_menu)
|
||||
.child(
|
||||
Switch::new("context-server-switch", is_running.into())
|
||||
.color(SwitchColor::Accent)
|
||||
.on_click({
|
||||
let context_server_manager =
|
||||
self.context_server_store.clone();
|
||||
let fs = self.fs.clone();
|
||||
Switch::new("context-server-switch", is_running.into())
|
||||
.color(SwitchColor::Accent)
|
||||
.on_click({
|
||||
let context_server_manager = self.context_server_store.clone();
|
||||
let fs = self.fs.clone();
|
||||
|
||||
move |state, _window, cx| {
|
||||
let is_enabled = match state {
|
||||
ToggleState::Unselected
|
||||
| ToggleState::Indeterminate => {
|
||||
context_server_manager.update(
|
||||
cx,
|
||||
|this, cx| {
|
||||
this.stop_server(
|
||||
&context_server_id,
|
||||
cx,
|
||||
)
|
||||
.log_err();
|
||||
},
|
||||
);
|
||||
false
|
||||
}
|
||||
ToggleState::Selected => {
|
||||
context_server_manager.update(
|
||||
cx,
|
||||
|this, cx| {
|
||||
if let Some(server) =
|
||||
this.get_server(&context_server_id)
|
||||
{
|
||||
this.start_server(server, cx);
|
||||
}
|
||||
},
|
||||
);
|
||||
true
|
||||
}
|
||||
};
|
||||
update_settings_file::<ProjectSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
{
|
||||
let context_server_id =
|
||||
context_server_id.clone();
|
||||
|
||||
move |settings, _| {
|
||||
settings
|
||||
.context_servers
|
||||
.entry(context_server_id.0)
|
||||
.or_insert_with(|| {
|
||||
ContextServerSettings::Extension {
|
||||
enabled: is_enabled,
|
||||
settings: serde_json::json!({}),
|
||||
}
|
||||
})
|
||||
.set_enabled(is_enabled);
|
||||
move |state, _window, cx| {
|
||||
let is_enabled = match state {
|
||||
ToggleState::Unselected
|
||||
| ToggleState::Indeterminate => {
|
||||
context_server_manager.update(cx, |this, cx| {
|
||||
this.stop_server(&context_server_id, cx)
|
||||
.log_err();
|
||||
});
|
||||
false
|
||||
}
|
||||
ToggleState::Selected => {
|
||||
context_server_manager.update(cx, |this, cx| {
|
||||
if let Some(server) =
|
||||
this.get_server(&context_server_id)
|
||||
{
|
||||
this.start_server(server, cx);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}),
|
||||
),
|
||||
});
|
||||
true
|
||||
}
|
||||
};
|
||||
update_settings_file(fs.clone(), cx, {
|
||||
let context_server_id = context_server_id.clone();
|
||||
|
||||
move |settings, _| {
|
||||
settings
|
||||
.project
|
||||
.context_servers
|
||||
.entry(context_server_id.0)
|
||||
.or_insert_with(|| {
|
||||
settings::ContextServerSettingsContent::Extension {
|
||||
enabled: is_enabled,
|
||||
settings: serde_json::json!({}),
|
||||
}
|
||||
})
|
||||
.set_enabled(is_enabled);
|
||||
}
|
||||
});
|
||||
}
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
.map(|parent| {
|
||||
if let Some(error) = error {
|
||||
return parent.child(
|
||||
h_flex()
|
||||
.p_2()
|
||||
.gap_2()
|
||||
.pr_4()
|
||||
.items_start()
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -974,60 +1006,26 @@ impl AgentConfiguration {
|
||||
return parent;
|
||||
}
|
||||
|
||||
parent.child(v_flex().py_1p5().px_1().gap_1().children(
|
||||
tools.iter().enumerate().map(|(ix, tool)| {
|
||||
h_flex()
|
||||
.id(("tool-item", ix))
|
||||
.px_1()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.hover(|style| style.bg(cx.theme().colors().element_hover))
|
||||
.rounded_sm()
|
||||
.child(
|
||||
Label::new(tool.name())
|
||||
.buffer_font(cx)
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::Info)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Ignored),
|
||||
)
|
||||
.tooltip(Tooltip::text(tool.description()))
|
||||
}),
|
||||
))
|
||||
parent
|
||||
})
|
||||
}
|
||||
|
||||
fn render_agent_servers_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let custom_settings = cx
|
||||
.global::<SettingsStore>()
|
||||
.get::<AllAgentServersSettings>(None)
|
||||
.custom
|
||||
.clone();
|
||||
let user_defined_agents = self
|
||||
.agent_server_store
|
||||
.read(cx)
|
||||
.external_agents()
|
||||
.filter(|name| name.0 != GEMINI_NAME && name.0 != CLAUDE_CODE_NAME)
|
||||
.filter(|name| {
|
||||
name.0 != GEMINI_NAME && name.0 != CLAUDE_CODE_NAME && name.0 != CODEX_NAME
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let user_defined_agents = user_defined_agents
|
||||
.into_iter()
|
||||
.map(|name| {
|
||||
self.render_agent_server(
|
||||
IconName::Ai,
|
||||
name.clone(),
|
||||
ExternalAgent::Custom {
|
||||
name: name.clone().into(),
|
||||
command: custom_settings
|
||||
.get(&name.0)
|
||||
.map(|settings| settings.command.clone())
|
||||
.unwrap_or(placeholder_command()),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.into_any_element()
|
||||
self.render_agent_server(IconName::Ai, name)
|
||||
.into_any_element()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -1051,6 +1049,8 @@ impl AgentConfiguration {
|
||||
.child(Headline::new("External Agents"))
|
||||
.child(
|
||||
Button::new("add-agent", "Add Agent")
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon(IconName::Plus)
|
||||
.icon_size(IconSize::Small)
|
||||
@@ -1080,19 +1080,30 @@ impl AgentConfiguration {
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(self.render_agent_server(
|
||||
IconName::AiGemini,
|
||||
"Gemini CLI",
|
||||
ExternalAgent::Gemini,
|
||||
cx,
|
||||
))
|
||||
.child(self.render_agent_server(
|
||||
IconName::AiClaude,
|
||||
"Claude Code",
|
||||
ExternalAgent::ClaudeCode,
|
||||
cx,
|
||||
))
|
||||
.children(user_defined_agents),
|
||||
.child(Divider::horizontal().color(DividerColor::BorderFaded))
|
||||
.when(cx.has_flag::<CodexAcpFeatureFlag>(), |this| {
|
||||
this
|
||||
.child(self.render_agent_server(
|
||||
IconName::AiOpenAi,
|
||||
"Codex",
|
||||
))
|
||||
.child(Divider::horizontal().color(DividerColor::BorderFaded))
|
||||
})
|
||||
.child(self.render_agent_server(
|
||||
IconName::AiGemini,
|
||||
"Gemini CLI",
|
||||
))
|
||||
.map(|mut parent| {
|
||||
for agent in user_defined_agents {
|
||||
parent = parent.child(Divider::horizontal().color(DividerColor::BorderFaded))
|
||||
.child(agent);
|
||||
}
|
||||
parent
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1100,46 +1111,18 @@ impl AgentConfiguration {
|
||||
&self,
|
||||
icon: IconName,
|
||||
name: impl Into<SharedString>,
|
||||
agent: ExternalAgent,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let name = name.into();
|
||||
h_flex()
|
||||
.p_1()
|
||||
.pl_2()
|
||||
.gap_1p5()
|
||||
.justify_between()
|
||||
.border_1()
|
||||
.rounded_md()
|
||||
.border_color(self.card_item_border_color(cx))
|
||||
.bg(self.card_item_bg_color(cx))
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
|
||||
.child(Label::new(name.clone())),
|
||||
)
|
||||
.child(
|
||||
Button::new(
|
||||
SharedString::from(format!("start_acp_thread-{name}")),
|
||||
"Start New Thread",
|
||||
)
|
||||
.label_size(LabelSize::Small)
|
||||
.icon(IconName::Thread)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.on_click(move |_, window, cx| {
|
||||
window.dispatch_action(
|
||||
NewExternalAgentThread {
|
||||
agent: Some(agent.clone()),
|
||||
}
|
||||
.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
)
|
||||
h_flex().gap_1p5().justify_between().child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
|
||||
.child(Label::new(name.into()))
|
||||
.child(
|
||||
Icon::new(IconName::Check)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Success),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1153,42 +1136,21 @@ impl Render for AgentConfiguration {
|
||||
.size_full()
|
||||
.pb_8()
|
||||
.bg(cx.theme().colors().panel_background)
|
||||
.child(
|
||||
v_flex()
|
||||
.id("assistant-configuration-content")
|
||||
.track_scroll(&self.scroll_handle)
|
||||
.size_full()
|
||||
.overflow_y_scroll()
|
||||
.child(self.render_general_settings_section(cx))
|
||||
.child(self.render_agent_servers_section(cx))
|
||||
.child(self.render_context_servers_section(window, cx))
|
||||
.child(self.render_provider_configuration_section(cx)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("assistant-configuration-scrollbar")
|
||||
.occlude()
|
||||
.absolute()
|
||||
.right(px(3.))
|
||||
.top_0()
|
||||
.bottom_0()
|
||||
.pb_6()
|
||||
.w(px(12.))
|
||||
.cursor_default()
|
||||
.on_mouse_move(cx.listener(|_, _, _window, cx| {
|
||||
cx.notify();
|
||||
cx.stop_propagation()
|
||||
}))
|
||||
.on_hover(|_, _window, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_any_mouse_down(|_, _window, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_scroll_wheel(cx.listener(|_, _, _window, cx| {
|
||||
cx.notify();
|
||||
}))
|
||||
.children(Scrollbar::vertical(self.scrollbar_state.clone())),
|
||||
.size_full()
|
||||
.child(
|
||||
v_flex()
|
||||
.id("assistant-configuration-content")
|
||||
.track_scroll(&self.scroll_handle)
|
||||
.size_full()
|
||||
.overflow_y_scroll()
|
||||
.child(self.render_general_settings_section(cx))
|
||||
.child(self.render_agent_servers_section(cx))
|
||||
.child(self.render_context_servers_section(window, cx))
|
||||
.child(self.render_provider_configuration_section(cx)),
|
||||
)
|
||||
.vertical_scrollbar_for(self.scroll_handle.clone(), window, cx),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1256,15 +1218,12 @@ fn show_unable_to_uninstall_extension_with_context_server(
|
||||
let context_server_id = context_server_id.clone();
|
||||
async move |_workspace_handle, cx| {
|
||||
cx.update(|cx| {
|
||||
update_settings_file::<ProjectSettings>(
|
||||
fs,
|
||||
cx,
|
||||
move |settings, _| {
|
||||
settings
|
||||
.context_servers
|
||||
.remove(&context_server_id.0);
|
||||
},
|
||||
);
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
settings
|
||||
.project
|
||||
.context_servers
|
||||
.remove(&context_server_id.0);
|
||||
});
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
}
|
||||
@@ -1302,7 +1261,7 @@ async fn open_new_agent_servers_entry_in_settings_editor(
|
||||
let settings = cx.global::<SettingsStore>();
|
||||
|
||||
let mut unique_server_name = None;
|
||||
let edits = settings.edits_for_update::<AllAgentServersSettings>(&text, |file| {
|
||||
let edits = settings.edits_for_update(&text, |settings| {
|
||||
let server_name: Option<SharedString> = (0..u8::MAX)
|
||||
.map(|i| {
|
||||
if i == 0 {
|
||||
@@ -1311,20 +1270,27 @@ async fn open_new_agent_servers_entry_in_settings_editor(
|
||||
format!("your_agent_{}", i).into()
|
||||
}
|
||||
})
|
||||
.find(|name| !file.custom.contains_key(name));
|
||||
.find(|name| {
|
||||
!settings
|
||||
.agent_servers
|
||||
.as_ref()
|
||||
.is_some_and(|agent_servers| agent_servers.custom.contains_key(name))
|
||||
});
|
||||
if let Some(server_name) = server_name {
|
||||
unique_server_name = Some(server_name.clone());
|
||||
file.custom.insert(
|
||||
server_name,
|
||||
CustomAgentServerSettings {
|
||||
command: AgentServerCommand {
|
||||
settings
|
||||
.agent_servers
|
||||
.get_or_insert_default()
|
||||
.custom
|
||||
.insert(
|
||||
server_name,
|
||||
settings::CustomAgentServerSettings {
|
||||
path: "path_to_executable".into(),
|
||||
args: vec![],
|
||||
env: Some(HashMap::default()),
|
||||
default_mode: None,
|
||||
},
|
||||
default_mode: None,
|
||||
},
|
||||
);
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -5,11 +5,8 @@ use collections::HashSet;
|
||||
use fs::Fs;
|
||||
use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, Task};
|
||||
use language_model::LanguageModelRegistry;
|
||||
use language_models::{
|
||||
AllLanguageModelSettings, OpenAiCompatibleSettingsContent,
|
||||
provider::open_ai_compatible::{AvailableModel, ModelCapabilities},
|
||||
};
|
||||
use settings::update_settings_file;
|
||||
use language_models::provider::open_ai_compatible::{AvailableModel, ModelCapabilities};
|
||||
use settings::{OpenAiCompatibleSettingsContent, update_settings_file};
|
||||
use ui::{
|
||||
Banner, Checkbox, KeyBinding, Modal, ModalFooter, ModalHeader, Section, ToggleState, prelude::*,
|
||||
};
|
||||
@@ -238,14 +235,19 @@ fn save_provider_to_settings(
|
||||
task.await
|
||||
.map_err(|_| "Failed to write API key to keychain")?;
|
||||
cx.update(|cx| {
|
||||
update_settings_file::<AllLanguageModelSettings>(fs, cx, |settings, _cx| {
|
||||
settings.openai_compatible.get_or_insert_default().insert(
|
||||
provider_name,
|
||||
OpenAiCompatibleSettingsContent {
|
||||
api_url,
|
||||
available_models: models,
|
||||
},
|
||||
);
|
||||
update_settings_file(fs, cx, |settings, _cx| {
|
||||
settings
|
||||
.language_models
|
||||
.get_or_insert_default()
|
||||
.openai_compatible
|
||||
.get_or_insert_default()
|
||||
.insert(
|
||||
provider_name,
|
||||
OpenAiCompatibleSettingsContent {
|
||||
api_url,
|
||||
available_models: models,
|
||||
},
|
||||
);
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
|
||||
@@ -422,18 +422,17 @@ impl ConfigureContextServerModal {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
let fs = workspace.app_state().fs.clone();
|
||||
let original_server_id = self.original_server_id.clone();
|
||||
update_settings_file::<ProjectSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |project_settings, _| {
|
||||
if let Some(original_id) = original_server_id {
|
||||
if original_id != id {
|
||||
project_settings.context_servers.remove(&original_id.0);
|
||||
}
|
||||
update_settings_file(fs.clone(), cx, move |current, _| {
|
||||
if let Some(original_id) = original_server_id {
|
||||
if original_id != id {
|
||||
current.project.context_servers.remove(&original_id.0);
|
||||
}
|
||||
project_settings.context_servers.insert(id.0, settings);
|
||||
},
|
||||
);
|
||||
}
|
||||
current
|
||||
.project
|
||||
.context_servers
|
||||
.insert(id.0, settings.into());
|
||||
});
|
||||
});
|
||||
} else if let Some(existing_server) = existing_server {
|
||||
self.context_server_store
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
use assistant_tool::{ToolSource, ToolWorkingSet};
|
||||
use context_server::ContextServerId;
|
||||
use gpui::{
|
||||
DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, ScrollHandle, Window, prelude::*,
|
||||
};
|
||||
use ui::{Divider, DividerColor, Modal, ModalHeader, WithScrollbar, prelude::*};
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
pub struct ConfigureContextServerToolsModal {
|
||||
context_server_id: ContextServerId,
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
focus_handle: FocusHandle,
|
||||
expanded_tools: std::collections::HashMap<String, bool>,
|
||||
scroll_handle: ScrollHandle,
|
||||
}
|
||||
|
||||
impl ConfigureContextServerToolsModal {
|
||||
fn new(
|
||||
context_server_id: ContextServerId,
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
Self {
|
||||
context_server_id,
|
||||
tools,
|
||||
focus_handle: cx.focus_handle(),
|
||||
expanded_tools: std::collections::HashMap::new(),
|
||||
scroll_handle: ScrollHandle::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle(
|
||||
context_server_id: ContextServerId,
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
workspace: &mut Workspace,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
workspace.toggle_modal(window, cx, |window, cx| {
|
||||
Self::new(context_server_id, tools, window, cx)
|
||||
});
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.emit(DismissEvent)
|
||||
}
|
||||
|
||||
fn render_modal_content(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let tools_by_source = self.tools.read(cx).tools_by_source(cx);
|
||||
let server_tools = tools_by_source
|
||||
.get(&ToolSource::ContextServer {
|
||||
id: self.context_server_id.0.clone().into(),
|
||||
})
|
||||
.map(|tools| tools.as_slice())
|
||||
.unwrap_or(&[]);
|
||||
|
||||
div()
|
||||
.size_full()
|
||||
.pb_2()
|
||||
.child(
|
||||
v_flex()
|
||||
.id("modal_content")
|
||||
.px_2()
|
||||
.gap_1()
|
||||
.max_h_128()
|
||||
.overflow_y_scroll()
|
||||
.track_scroll(&self.scroll_handle)
|
||||
.children(server_tools.iter().enumerate().flat_map(|(index, tool)| {
|
||||
let tool_name = tool.name();
|
||||
let is_expanded = self
|
||||
.expanded_tools
|
||||
.get(&tool_name)
|
||||
.copied()
|
||||
.unwrap_or(false);
|
||||
|
||||
let icon = if is_expanded {
|
||||
IconName::ChevronUp
|
||||
} else {
|
||||
IconName::ChevronDown
|
||||
};
|
||||
|
||||
let mut items = vec![
|
||||
v_flex()
|
||||
.child(
|
||||
h_flex()
|
||||
.id(SharedString::from(format!("tool-header-{}", index)))
|
||||
.py_1()
|
||||
.pl_1()
|
||||
.pr_2()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.rounded_sm()
|
||||
.hover(|s| s.bg(cx.theme().colors().element_hover))
|
||||
.child(
|
||||
Label::new(tool_name.clone())
|
||||
.buffer_font(cx)
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.child(
|
||||
Icon::new(icon)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.on_click(cx.listener({
|
||||
move |this, _event, _window, _cx| {
|
||||
let current = this
|
||||
.expanded_tools
|
||||
.get(&tool_name)
|
||||
.copied()
|
||||
.unwrap_or(false);
|
||||
this.expanded_tools
|
||||
.insert(tool_name.clone(), !current);
|
||||
_cx.notify();
|
||||
}
|
||||
})),
|
||||
)
|
||||
.when(is_expanded, |this| {
|
||||
this.child(
|
||||
Label::new(tool.description()).color(Color::Muted).mx_1(),
|
||||
)
|
||||
})
|
||||
.into_any_element(),
|
||||
];
|
||||
|
||||
if index < server_tools.len() - 1 {
|
||||
items.push(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.child(Divider::horizontal().color(DividerColor::BorderVariant))
|
||||
.into_any_element(),
|
||||
);
|
||||
}
|
||||
|
||||
items
|
||||
})),
|
||||
)
|
||||
.vertical_scrollbar_for(self.scroll_handle.clone(), window, cx)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl ModalView for ConfigureContextServerToolsModal {}
|
||||
|
||||
impl Focusable for ConfigureContextServerToolsModal {
|
||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for ConfigureContextServerToolsModal {}
|
||||
|
||||
impl Render for ConfigureContextServerToolsModal {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.key_context("ContextServerToolsModal")
|
||||
.occlude()
|
||||
.elevation_3(cx)
|
||||
.w(rems(34.))
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.track_focus(&self.focus_handle)
|
||||
.child(
|
||||
Modal::new("configure-context-server-tools", None::<ScrollHandle>)
|
||||
.header(
|
||||
ModalHeader::new()
|
||||
.headline(format!("Tools from {}", self.context_server_id.0))
|
||||
.show_dismiss_button(true),
|
||||
)
|
||||
.child(self.render_modal_content(window, cx)),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ mod profile_modal_header;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use agent_settings::{AgentProfileId, AgentSettings, builtin_profiles};
|
||||
use agent_settings::{AgentProfile, AgentProfileId, AgentSettings, builtin_profiles};
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use editor::Editor;
|
||||
use fs::Fs;
|
||||
@@ -16,7 +16,6 @@ use workspace::{ModalView, Workspace};
|
||||
use crate::agent_configuration::manage_profiles_modal::profile_modal_header::ProfileModalHeader;
|
||||
use crate::agent_configuration::tool_picker::{ToolPicker, ToolPickerDelegate};
|
||||
use crate::{AgentPanel, ManageProfiles};
|
||||
use agent::agent_profile::AgentProfile;
|
||||
|
||||
use super::tool_picker::ToolPickerMode;
|
||||
|
||||
@@ -156,7 +155,7 @@ impl ManageProfilesModal {
|
||||
) {
|
||||
let name_editor = cx.new(|cx| Editor::single_line(window, cx));
|
||||
name_editor.update(cx, |editor, cx| {
|
||||
editor.set_placeholder_text("Profile name", cx);
|
||||
editor.set_placeholder_text("Profile name", window, cx);
|
||||
});
|
||||
|
||||
self.mode = Mode::NewProfile(NewProfileMode {
|
||||
@@ -318,6 +317,8 @@ impl ManageProfilesModal {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement + use<> {
|
||||
let is_focused = profile.navigation.focus_handle.contains_focused(window, cx);
|
||||
|
||||
div()
|
||||
.id(SharedString::from(format!("profile-{}", profile.id)))
|
||||
.track_focus(&profile.navigation.focus_handle)
|
||||
@@ -329,25 +330,27 @@ impl ManageProfilesModal {
|
||||
})
|
||||
.child(
|
||||
ListItem::new(SharedString::from(format!("profile-{}", profile.id)))
|
||||
.toggle_state(profile.navigation.focus_handle.contains_focused(window, cx))
|
||||
.toggle_state(is_focused)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.child(Label::new(profile.name.clone()))
|
||||
.end_slot(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new("Customize")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.children(KeyBinding::for_action_in(
|
||||
&menu::Confirm,
|
||||
&self.focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.when(is_focused, |this| {
|
||||
this.end_slot(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new("Customize")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.children(KeyBinding::for_action_in(
|
||||
&menu::Confirm,
|
||||
&self.focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
})
|
||||
.on_click({
|
||||
let profile_id = profile.id.clone();
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
use std::{collections::BTreeMap, sync::Arc};
|
||||
|
||||
use agent_settings::{
|
||||
AgentProfileContent, AgentProfileId, AgentProfileSettings, AgentSettings, AgentSettingsContent,
|
||||
ContextServerPresetContent,
|
||||
};
|
||||
use agent_settings::{AgentProfileId, AgentProfileSettings};
|
||||
use assistant_tool::{ToolSource, ToolWorkingSet};
|
||||
use fs::Fs;
|
||||
use gpui::{App, Context, DismissEvent, Entity, EventEmitter, Focusable, Task, WeakEntity, Window};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use settings::update_settings_file;
|
||||
use settings::{AgentProfileContent, ContextServerPresetContent, update_settings_file};
|
||||
use ui::{ListItem, ListItemSpacing, prelude::*};
|
||||
use util::ResultExt as _;
|
||||
|
||||
@@ -266,15 +263,19 @@ impl PickerDelegate for ToolPickerDelegate {
|
||||
is_enabled
|
||||
};
|
||||
|
||||
update_settings_file::<AgentSettings>(self.fs.clone(), cx, {
|
||||
update_settings_file(self.fs.clone(), cx, {
|
||||
let profile_id = self.profile_id.clone();
|
||||
let default_profile = self.profile_settings.clone();
|
||||
let server_id = server_id.clone();
|
||||
let tool_name = tool_name.clone();
|
||||
move |settings: &mut AgentSettingsContent, _cx| {
|
||||
let profiles = settings.profiles.get_or_insert_default();
|
||||
move |settings, _cx| {
|
||||
let profiles = settings
|
||||
.agent
|
||||
.get_or_insert_default()
|
||||
.profiles
|
||||
.get_or_insert_default();
|
||||
let profile = profiles
|
||||
.entry(profile_id)
|
||||
.entry(profile_id.0)
|
||||
.or_insert_with(|| AgentProfileContent {
|
||||
name: default_profile.name.into(),
|
||||
tools: default_profile.tools,
|
||||
@@ -318,7 +319,7 @@ impl PickerDelegate for ToolPickerDelegate {
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let item = &self.filtered_items[ix];
|
||||
let item = &self.filtered_items.get(ix)?;
|
||||
match item {
|
||||
PickerItem::ContextServer { server_id, .. } => Some(
|
||||
div()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use crate::{Keep, KeepAll, OpenAgentDiff, Reject, RejectAll};
|
||||
use acp_thread::{AcpThread, AcpThreadEvent};
|
||||
use action_log::ActionLog;
|
||||
use agent::{Thread, ThreadEvent, ThreadSummary};
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::Result;
|
||||
use buffer_diff::DiffHunkStatus;
|
||||
@@ -19,7 +18,6 @@ use gpui::{
|
||||
};
|
||||
|
||||
use language::{Buffer, Capability, DiskState, OffsetRangeExt, Point};
|
||||
use language_model::StopReason;
|
||||
use multi_buffer::PathKey;
|
||||
use project::{Project, ProjectItem, ProjectPath};
|
||||
use settings::{Settings, SettingsStore};
|
||||
@@ -51,34 +49,29 @@ pub struct AgentDiffPane {
|
||||
|
||||
#[derive(PartialEq, Eq, Clone)]
|
||||
pub enum AgentDiffThread {
|
||||
Native(Entity<Thread>),
|
||||
AcpThread(Entity<AcpThread>),
|
||||
}
|
||||
|
||||
impl AgentDiffThread {
|
||||
fn project(&self, cx: &App) -> Entity<Project> {
|
||||
match self {
|
||||
AgentDiffThread::Native(thread) => thread.read(cx).project().clone(),
|
||||
AgentDiffThread::AcpThread(thread) => thread.read(cx).project().clone(),
|
||||
}
|
||||
}
|
||||
fn action_log(&self, cx: &App) -> Entity<ActionLog> {
|
||||
match self {
|
||||
AgentDiffThread::Native(thread) => thread.read(cx).action_log().clone(),
|
||||
AgentDiffThread::AcpThread(thread) => thread.read(cx).action_log().clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn summary(&self, cx: &App) -> ThreadSummary {
|
||||
fn title(&self, cx: &App) -> SharedString {
|
||||
match self {
|
||||
AgentDiffThread::Native(thread) => thread.read(cx).summary().clone(),
|
||||
AgentDiffThread::AcpThread(thread) => ThreadSummary::Ready(thread.read(cx).title()),
|
||||
AgentDiffThread::AcpThread(thread) => thread.read(cx).title(),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_generating(&self, cx: &App) -> bool {
|
||||
match self {
|
||||
AgentDiffThread::Native(thread) => thread.read(cx).is_generating(),
|
||||
AgentDiffThread::AcpThread(thread) => {
|
||||
thread.read(cx).status() == acp_thread::ThreadStatus::Generating
|
||||
}
|
||||
@@ -87,14 +80,12 @@ impl AgentDiffThread {
|
||||
|
||||
fn has_pending_edit_tool_uses(&self, cx: &App) -> bool {
|
||||
match self {
|
||||
AgentDiffThread::Native(thread) => thread.read(cx).has_pending_edit_tool_uses(),
|
||||
AgentDiffThread::AcpThread(thread) => thread.read(cx).has_pending_edit_tool_calls(),
|
||||
}
|
||||
}
|
||||
|
||||
fn downgrade(&self) -> WeakAgentDiffThread {
|
||||
match self {
|
||||
AgentDiffThread::Native(thread) => WeakAgentDiffThread::Native(thread.downgrade()),
|
||||
AgentDiffThread::AcpThread(thread) => {
|
||||
WeakAgentDiffThread::AcpThread(thread.downgrade())
|
||||
}
|
||||
@@ -102,12 +93,6 @@ impl AgentDiffThread {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Entity<Thread>> for AgentDiffThread {
|
||||
fn from(entity: Entity<Thread>) -> Self {
|
||||
AgentDiffThread::Native(entity)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Entity<AcpThread>> for AgentDiffThread {
|
||||
fn from(entity: Entity<AcpThread>) -> Self {
|
||||
AgentDiffThread::AcpThread(entity)
|
||||
@@ -116,25 +101,17 @@ impl From<Entity<AcpThread>> for AgentDiffThread {
|
||||
|
||||
#[derive(PartialEq, Eq, Clone)]
|
||||
pub enum WeakAgentDiffThread {
|
||||
Native(WeakEntity<Thread>),
|
||||
AcpThread(WeakEntity<AcpThread>),
|
||||
}
|
||||
|
||||
impl WeakAgentDiffThread {
|
||||
pub fn upgrade(&self) -> Option<AgentDiffThread> {
|
||||
match self {
|
||||
WeakAgentDiffThread::Native(weak) => weak.upgrade().map(AgentDiffThread::Native),
|
||||
WeakAgentDiffThread::AcpThread(weak) => weak.upgrade().map(AgentDiffThread::AcpThread),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WeakEntity<Thread>> for WeakAgentDiffThread {
|
||||
fn from(entity: WeakEntity<Thread>) -> Self {
|
||||
WeakAgentDiffThread::Native(entity)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WeakEntity<AcpThread>> for WeakAgentDiffThread {
|
||||
fn from(entity: WeakEntity<AcpThread>) -> Self {
|
||||
WeakAgentDiffThread::AcpThread(entity)
|
||||
@@ -203,10 +180,6 @@ impl AgentDiffPane {
|
||||
this.update_excerpts(window, cx)
|
||||
}),
|
||||
match &thread {
|
||||
AgentDiffThread::Native(thread) => cx
|
||||
.subscribe(thread, |this, _thread, event, cx| {
|
||||
this.handle_native_thread_event(event, cx)
|
||||
}),
|
||||
AgentDiffThread::AcpThread(thread) => cx
|
||||
.subscribe(thread, |this, _thread, event, cx| {
|
||||
this.handle_acp_thread_event(event, cx)
|
||||
@@ -313,19 +286,13 @@ impl AgentDiffPane {
|
||||
}
|
||||
|
||||
fn update_title(&mut self, cx: &mut Context<Self>) {
|
||||
let new_title = self.thread.summary(cx).unwrap_or("Agent Changes");
|
||||
let new_title = self.thread.title(cx);
|
||||
if new_title != self.title {
|
||||
self.title = new_title;
|
||||
cx.emit(EditorEvent::TitleChanged);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_native_thread_event(&mut self, event: &ThreadEvent, cx: &mut Context<Self>) {
|
||||
if let ThreadEvent::SummaryGenerated = event {
|
||||
self.update_title(cx)
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_acp_thread_event(&mut self, event: &AcpThreadEvent, cx: &mut Context<Self>) {
|
||||
if let AcpThreadEvent::TitleUpdated = event {
|
||||
self.update_title(cx)
|
||||
@@ -569,8 +536,8 @@ impl Item for AgentDiffPane {
|
||||
}
|
||||
|
||||
fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
|
||||
let summary = self.thread.summary(cx).unwrap_or("Agent Changes");
|
||||
Label::new(format!("Review: {}", summary))
|
||||
let title = self.thread.title(cx);
|
||||
Label::new(format!("Review: {}", title))
|
||||
.color(if params.selected {
|
||||
Color::Default
|
||||
} else {
|
||||
@@ -1339,12 +1306,6 @@ impl AgentDiff {
|
||||
});
|
||||
|
||||
let thread_subscription = match &thread {
|
||||
AgentDiffThread::Native(thread) => cx.subscribe_in(thread, window, {
|
||||
let workspace = workspace.clone();
|
||||
move |this, _thread, event, window, cx| {
|
||||
this.handle_native_thread_event(&workspace, event, window, cx)
|
||||
}
|
||||
}),
|
||||
AgentDiffThread::AcpThread(thread) => cx.subscribe_in(thread, window, {
|
||||
let workspace = workspace.clone();
|
||||
move |this, thread, event, window, cx| {
|
||||
@@ -1447,47 +1408,6 @@ impl AgentDiff {
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_native_thread_event(
|
||||
&mut self,
|
||||
workspace: &WeakEntity<Workspace>,
|
||||
event: &ThreadEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
ThreadEvent::NewRequest
|
||||
| ThreadEvent::Stopped(Ok(StopReason::EndTurn))
|
||||
| ThreadEvent::Stopped(Ok(StopReason::MaxTokens))
|
||||
| ThreadEvent::Stopped(Ok(StopReason::Refusal))
|
||||
| ThreadEvent::Stopped(Err(_))
|
||||
| ThreadEvent::ShowError(_)
|
||||
| ThreadEvent::CompletionCanceled => {
|
||||
self.update_reviewing_editors(workspace, window, cx);
|
||||
}
|
||||
// intentionally being exhaustive in case we add a variant we should handle
|
||||
ThreadEvent::Stopped(Ok(StopReason::ToolUse))
|
||||
| ThreadEvent::StreamedCompletion
|
||||
| ThreadEvent::ReceivedTextChunk
|
||||
| ThreadEvent::StreamedAssistantText(_, _)
|
||||
| ThreadEvent::StreamedAssistantThinking(_, _)
|
||||
| ThreadEvent::StreamedToolUse { .. }
|
||||
| ThreadEvent::InvalidToolInput { .. }
|
||||
| ThreadEvent::MissingToolUse { .. }
|
||||
| ThreadEvent::MessageAdded(_)
|
||||
| ThreadEvent::MessageEdited(_)
|
||||
| ThreadEvent::MessageDeleted(_)
|
||||
| ThreadEvent::SummaryGenerated
|
||||
| ThreadEvent::SummaryChanged
|
||||
| ThreadEvent::UsePendingTools { .. }
|
||||
| ThreadEvent::ToolFinished { .. }
|
||||
| ThreadEvent::CheckpointChanged
|
||||
| ThreadEvent::ToolConfirmationNeeded
|
||||
| ThreadEvent::ToolUseLimitReached
|
||||
| ThreadEvent::CancelEditing
|
||||
| ThreadEvent::ProfileChanged => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_acp_thread_event(
|
||||
&mut self,
|
||||
workspace: &WeakEntity<Workspace>,
|
||||
@@ -1890,16 +1810,14 @@ impl editor::Addon for EditorAgentDiffAddon {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::Keep;
|
||||
use agent::thread_store::{self, ThreadStore};
|
||||
use acp_thread::AgentConnection as _;
|
||||
use agent_settings::AgentSettings;
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use editor::EditorSettings;
|
||||
use gpui::{TestAppContext, UpdateGlobal, VisualTestContext};
|
||||
use project::{FakeFs, Project};
|
||||
use prompt_store::PromptBuilder;
|
||||
use serde_json::json;
|
||||
use settings::{Settings, SettingsStore};
|
||||
use std::sync::Arc;
|
||||
use std::{path::Path, rc::Rc};
|
||||
use theme::ThemeSettings;
|
||||
use util::path;
|
||||
|
||||
@@ -1912,7 +1830,6 @@ mod tests {
|
||||
Project::init_settings(cx);
|
||||
AgentSettings::register(cx);
|
||||
prompt_store::init(cx);
|
||||
thread_store::init(cx);
|
||||
workspace::init_settings(cx);
|
||||
ThemeSettings::register(cx);
|
||||
EditorSettings::register(cx);
|
||||
@@ -1932,21 +1849,17 @@ mod tests {
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let prompt_store = None;
|
||||
let thread_store = cx
|
||||
let connection = Rc::new(acp_thread::StubAgentConnection::new());
|
||||
let thread = cx
|
||||
.update(|cx| {
|
||||
ThreadStore::load(
|
||||
project.clone(),
|
||||
cx.new(|_| ToolWorkingSet::default()),
|
||||
prompt_store,
|
||||
Arc::new(PromptBuilder::new(None).unwrap()),
|
||||
cx,
|
||||
)
|
||||
connection
|
||||
.clone()
|
||||
.new_thread(project.clone(), Path::new(path!("/test")), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let thread =
|
||||
AgentDiffThread::Native(thread_store.update(cx, |store, cx| store.create_thread(cx)));
|
||||
|
||||
let thread = AgentDiffThread::AcpThread(thread);
|
||||
let action_log = cx.read(|cx| thread.action_log(cx));
|
||||
|
||||
let (workspace, cx) =
|
||||
@@ -2069,7 +1982,6 @@ mod tests {
|
||||
Project::init_settings(cx);
|
||||
AgentSettings::register(cx);
|
||||
prompt_store::init(cx);
|
||||
thread_store::init(cx);
|
||||
workspace::init_settings(cx);
|
||||
ThemeSettings::register(cx);
|
||||
EditorSettings::register(cx);
|
||||
@@ -2098,22 +2010,6 @@ mod tests {
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let prompt_store = None;
|
||||
let thread_store = cx
|
||||
.update(|cx| {
|
||||
ThreadStore::load(
|
||||
project.clone(),
|
||||
cx.new(|_| ToolWorkingSet::default()),
|
||||
prompt_store,
|
||||
Arc::new(PromptBuilder::new(None).unwrap()),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let thread = thread_store.update(cx, |store, cx| store.create_thread(cx));
|
||||
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
|
||||
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
|
||||
@@ -2132,8 +2028,19 @@ mod tests {
|
||||
}
|
||||
});
|
||||
|
||||
let connection = Rc::new(acp_thread::StubAgentConnection::new());
|
||||
let thread = cx
|
||||
.update(|_, cx| {
|
||||
connection
|
||||
.clone()
|
||||
.new_thread(project.clone(), Path::new(path!("/test")), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
|
||||
|
||||
// Set the active thread
|
||||
let thread = AgentDiffThread::Native(thread);
|
||||
let thread = AgentDiffThread::AcpThread(thread);
|
||||
cx.update(|window, cx| {
|
||||
AgentDiff::set_active_thread(&workspace.downgrade(), thread.clone(), window, cx)
|
||||
});
|
||||
|
||||
@@ -2,10 +2,8 @@ use crate::{
|
||||
ModelUsageContext,
|
||||
language_model_selector::{LanguageModelSelector, language_model_selector},
|
||||
};
|
||||
use agent_settings::AgentSettings;
|
||||
use fs::Fs;
|
||||
use gpui::{Entity, FocusHandle, SharedString};
|
||||
use language_model::{ConfiguredModel, LanguageModelRegistry};
|
||||
use picker::popover_menu::PickerPopoverMenu;
|
||||
use settings::update_settings_file;
|
||||
use std::sync::Arc;
|
||||
@@ -39,37 +37,13 @@ impl AgentModelSelector {
|
||||
let provider = model.provider_id().0.to_string();
|
||||
let model_id = model.id().0.to_string();
|
||||
match &model_usage_context {
|
||||
ModelUsageContext::Thread(thread) => {
|
||||
thread.update(cx, |thread, cx| {
|
||||
let registry = LanguageModelRegistry::read_global(cx);
|
||||
if let Some(provider) = registry.provider(&model.provider_id())
|
||||
{
|
||||
thread.set_configured_model(
|
||||
Some(ConfiguredModel {
|
||||
provider,
|
||||
model: model.clone(),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
});
|
||||
update_settings_file::<AgentSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _cx| {
|
||||
settings.set_model(model.clone());
|
||||
},
|
||||
);
|
||||
}
|
||||
ModelUsageContext::InlineAssistant => {
|
||||
update_settings_file::<AgentSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _cx| {
|
||||
settings
|
||||
.set_inline_assistant_model(provider.clone(), model_id);
|
||||
},
|
||||
);
|
||||
update_settings_file(fs.clone(), cx, move |settings, _cx| {
|
||||
settings
|
||||
.agent
|
||||
.get_or_insert_default()
|
||||
.set_inline_assistant_model(provider.clone(), model_id);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,4 @@
|
||||
mod acp;
|
||||
mod active_thread;
|
||||
mod agent_configuration;
|
||||
mod agent_diff;
|
||||
mod agent_model_selector;
|
||||
@@ -8,7 +7,6 @@ mod buffer_codegen;
|
||||
mod context_picker;
|
||||
mod context_server_configuration;
|
||||
mod context_strip;
|
||||
mod debug;
|
||||
mod inline_assistant;
|
||||
mod inline_prompt_editor;
|
||||
mod language_model_selector;
|
||||
@@ -16,19 +14,16 @@ mod message_editor;
|
||||
mod profile_selector;
|
||||
mod slash_command;
|
||||
mod slash_command_picker;
|
||||
mod slash_command_settings;
|
||||
mod terminal_codegen;
|
||||
mod terminal_inline_assistant;
|
||||
mod text_thread_editor;
|
||||
mod thread_history;
|
||||
mod tool_compatibility;
|
||||
mod ui;
|
||||
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use agent::{Thread, ThreadId};
|
||||
use agent_settings::{AgentProfileId, AgentSettings, LanguageModelSelection};
|
||||
use agent::ThreadId;
|
||||
use agent_settings::{AgentProfileId, AgentSettings};
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use client::Client;
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
@@ -44,17 +39,14 @@ use project::agent_server_store::AgentServerCommand;
|
||||
use prompt_store::PromptBuilder;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings as _, SettingsStore};
|
||||
use settings::{LanguageModelSelection, Settings as _, SettingsStore};
|
||||
use std::any::TypeId;
|
||||
|
||||
pub use crate::active_thread::ActiveThread;
|
||||
use crate::agent_configuration::{ConfigureContextServerModal, ManageProfilesModal};
|
||||
pub use crate::agent_panel::{AgentPanel, ConcreteAssistantPanelDelegate};
|
||||
pub use crate::inline_assistant::InlineAssistant;
|
||||
use crate::slash_command_settings::SlashCommandSettings;
|
||||
pub use agent_diff::{AgentDiffPane, AgentDiffToolbar};
|
||||
pub use text_thread_editor::{AgentPanelDelegate, TextThreadEditor};
|
||||
pub use ui::preview::{all_agent_previews, get_agent_preview};
|
||||
use zed_actions;
|
||||
|
||||
actions!(
|
||||
@@ -175,6 +167,7 @@ enum ExternalAgent {
|
||||
#[default]
|
||||
Gemini,
|
||||
ClaudeCode,
|
||||
Codex,
|
||||
NativeAgent,
|
||||
Custom {
|
||||
name: SharedString,
|
||||
@@ -196,6 +189,7 @@ impl ExternalAgent {
|
||||
Self::NativeAgent => "zed",
|
||||
Self::Gemini => "gemini-cli",
|
||||
Self::ClaudeCode => "claude-code",
|
||||
Self::Codex => "codex",
|
||||
Self::Custom { .. } => "custom",
|
||||
}
|
||||
}
|
||||
@@ -208,6 +202,7 @@ impl ExternalAgent {
|
||||
match self {
|
||||
Self::Gemini => Rc::new(agent_servers::Gemini),
|
||||
Self::ClaudeCode => Rc::new(agent_servers::ClaudeCode),
|
||||
Self::Codex => Rc::new(agent_servers::Codex),
|
||||
Self::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs, history)),
|
||||
Self::Custom { name, command: _ } => {
|
||||
Rc::new(agent_servers::CustomAgentServer::new(name.clone()))
|
||||
@@ -235,14 +230,12 @@ impl ManageProfiles {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum ModelUsageContext {
|
||||
Thread(Entity<Thread>),
|
||||
InlineAssistant,
|
||||
}
|
||||
|
||||
impl ModelUsageContext {
|
||||
pub fn configured_model(&self, cx: &App) -> Option<ConfiguredModel> {
|
||||
match self {
|
||||
Self::Thread(thread) => thread.read(cx).configured_model(),
|
||||
Self::InlineAssistant => {
|
||||
LanguageModelRegistry::read_global(cx).inline_assistant_model()
|
||||
}
|
||||
@@ -265,7 +258,6 @@ pub fn init(
|
||||
cx: &mut App,
|
||||
) {
|
||||
AgentSettings::register(cx);
|
||||
SlashCommandSettings::register(cx);
|
||||
|
||||
assistant_context::init(client.clone(), cx);
|
||||
rules_library::init(cx);
|
||||
@@ -275,7 +267,7 @@ pub fn init(
|
||||
init_language_model_settings(cx);
|
||||
}
|
||||
assistant_slash_command::init(cx);
|
||||
agent::init(cx);
|
||||
agent::init(fs.clone(), cx);
|
||||
agent_panel::init(cx);
|
||||
context_server_configuration::init(language_registry.clone(), fs.clone(), cx);
|
||||
TextThreadEditor::init(cx);
|
||||
@@ -421,8 +413,6 @@ fn register_slash_commands(cx: &mut App) {
|
||||
slash_command_registry.register_command(assistant_slash_commands::DeltaSlashCommand, true);
|
||||
slash_command_registry.register_command(assistant_slash_commands::OutlineSlashCommand, true);
|
||||
slash_command_registry.register_command(assistant_slash_commands::TabSlashCommand, true);
|
||||
slash_command_registry
|
||||
.register_command(assistant_slash_commands::CargoWorkspaceSlashCommand, true);
|
||||
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);
|
||||
@@ -442,21 +432,4 @@ fn register_slash_commands(cx: &mut App) {
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
update_slash_commands_from_settings(cx);
|
||||
cx.observe_global::<SettingsStore>(update_slash_commands_from_settings)
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn update_slash_commands_from_settings(cx: &mut App) {
|
||||
let slash_command_registry = SlashCommandRegistry::global(cx);
|
||||
let settings = SlashCommandSettings::get_global(cx);
|
||||
|
||||
if settings.cargo_workspace.enabled {
|
||||
slash_command_registry
|
||||
.register_command(assistant_slash_commands::CargoWorkspaceSlashCommand, true);
|
||||
} else {
|
||||
slash_command_registry
|
||||
.unregister_command(assistant_slash_commands::CargoWorkspaceSlashCommand);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ pub(crate) mod symbol_context_picker;
|
||||
pub(crate) mod thread_context_picker;
|
||||
|
||||
use std::ops::Range;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
@@ -23,9 +23,8 @@ use gpui::{
|
||||
};
|
||||
use language::Buffer;
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use paths::contexts_dir;
|
||||
use project::{Entry, ProjectPath};
|
||||
use prompt_store::{PromptStore, UserPromptId};
|
||||
use project::ProjectPath;
|
||||
use prompt_store::PromptStore;
|
||||
use rules_context_picker::{RulesContextEntry, RulesContextPicker};
|
||||
use symbol_context_picker::SymbolContextPicker;
|
||||
use thread_context_picker::{
|
||||
@@ -34,10 +33,10 @@ use thread_context_picker::{
|
||||
use ui::{
|
||||
ButtonLike, ContextMenu, ContextMenuEntry, ContextMenuItem, Disclosure, TintColor, prelude::*,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
use util::paths::PathStyle;
|
||||
use util::rel_path::RelPath;
|
||||
use workspace::{Workspace, notifications::NotifyResultExt};
|
||||
|
||||
use crate::AgentPanel;
|
||||
use agent::{
|
||||
ThreadId,
|
||||
context::RULES_ICON,
|
||||
@@ -231,12 +230,19 @@ impl ContextPicker {
|
||||
let context_picker = cx.entity();
|
||||
|
||||
let menu = ContextMenu::build(window, cx, move |menu, _window, cx| {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return menu;
|
||||
};
|
||||
let path_style = workspace.read(cx).path_style(cx);
|
||||
let recent = self.recent_entries(cx);
|
||||
let has_recent = !recent.is_empty();
|
||||
let recent_entries = recent
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(ix, entry)| self.recent_menu_item(context_picker.clone(), ix, entry));
|
||||
.map(|(ix, entry)| {
|
||||
self.recent_menu_item(context_picker.clone(), ix, entry, path_style)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let entries = self
|
||||
.workspace
|
||||
@@ -398,6 +404,7 @@ impl ContextPicker {
|
||||
context_picker: Entity<ContextPicker>,
|
||||
ix: usize,
|
||||
entry: RecentEntry,
|
||||
path_style: PathStyle,
|
||||
) -> ContextMenuItem {
|
||||
match entry {
|
||||
RecentEntry::File {
|
||||
@@ -416,6 +423,7 @@ impl ContextPicker {
|
||||
&path,
|
||||
&path_prefix,
|
||||
false,
|
||||
path_style,
|
||||
context_store.clone(),
|
||||
cx,
|
||||
)
|
||||
@@ -589,7 +597,7 @@ impl Render for ContextPicker {
|
||||
pub(crate) enum RecentEntry {
|
||||
File {
|
||||
project_path: ProjectPath,
|
||||
path_prefix: Arc<str>,
|
||||
path_prefix: Arc<RelPath>,
|
||||
},
|
||||
Thread(ThreadContextEntry),
|
||||
}
|
||||
@@ -664,7 +672,7 @@ pub(crate) fn recent_context_picker_entries(
|
||||
text_thread_store: Option<WeakEntity<TextThreadStore>>,
|
||||
workspace: Entity<Workspace>,
|
||||
exclude_paths: &HashSet<PathBuf>,
|
||||
exclude_threads: &HashSet<ThreadId>,
|
||||
_exclude_threads: &HashSet<ThreadId>,
|
||||
cx: &App,
|
||||
) -> Vec<RecentEntry> {
|
||||
let mut recent = Vec::with_capacity(6);
|
||||
@@ -690,19 +698,13 @@ pub(crate) fn recent_context_picker_entries(
|
||||
}),
|
||||
);
|
||||
|
||||
let active_thread_id = workspace
|
||||
.panel::<AgentPanel>(cx)
|
||||
.and_then(|panel| Some(panel.read(cx).active_thread(cx)?.read(cx).id()));
|
||||
|
||||
if let Some((thread_store, text_thread_store)) = thread_store
|
||||
.and_then(|store| store.upgrade())
|
||||
.zip(text_thread_store.and_then(|store| store.upgrade()))
|
||||
{
|
||||
let mut threads = unordered_thread_entries(thread_store, text_thread_store, cx)
|
||||
.filter(|(_, thread)| match thread {
|
||||
ThreadContextEntry::Thread { id, .. } => {
|
||||
Some(id) != active_thread_id && !exclude_threads.contains(id)
|
||||
}
|
||||
ThreadContextEntry::Thread { .. } => false,
|
||||
ThreadContextEntry::Context { .. } => true,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
@@ -874,15 +876,7 @@ fn fold_toggle(
|
||||
}
|
||||
}
|
||||
|
||||
pub enum MentionLink {
|
||||
File(ProjectPath, Entry),
|
||||
Symbol(ProjectPath, String),
|
||||
Selection(ProjectPath, Range<usize>),
|
||||
Fetch(String),
|
||||
Thread(ThreadId),
|
||||
TextThread(Arc<Path>),
|
||||
Rule(UserPromptId),
|
||||
}
|
||||
pub struct MentionLink;
|
||||
|
||||
impl MentionLink {
|
||||
const FILE: &str = "@file";
|
||||
@@ -894,17 +888,6 @@ impl MentionLink {
|
||||
|
||||
const TEXT_THREAD_URL_PREFIX: &str = "text-thread://";
|
||||
|
||||
const SEPARATOR: &str = ":";
|
||||
|
||||
pub fn is_valid(url: &str) -> bool {
|
||||
url.starts_with(Self::FILE)
|
||||
|| url.starts_with(Self::SYMBOL)
|
||||
|| url.starts_with(Self::FETCH)
|
||||
|| url.starts_with(Self::SELECTION)
|
||||
|| url.starts_with(Self::THREAD)
|
||||
|| url.starts_with(Self::RULE)
|
||||
}
|
||||
|
||||
pub fn for_file(file_name: &str, full_path: &str) -> String {
|
||||
format!("[@{}]({}:{})", file_name, Self::FILE, full_path)
|
||||
}
|
||||
@@ -958,75 +941,4 @@ impl MentionLink {
|
||||
pub fn for_rule(rule: &RulesContextEntry) -> String {
|
||||
format!("[@{}]({}:{})", rule.title, Self::RULE, rule.prompt_id.0)
|
||||
}
|
||||
|
||||
pub fn try_parse(link: &str, workspace: &Entity<Workspace>, cx: &App) -> Option<Self> {
|
||||
fn extract_project_path_from_link(
|
||||
path: &str,
|
||||
workspace: &Entity<Workspace>,
|
||||
cx: &App,
|
||||
) -> Option<ProjectPath> {
|
||||
let path = PathBuf::from(path);
|
||||
let worktree_name = path.iter().next()?;
|
||||
let path: PathBuf = path.iter().skip(1).collect();
|
||||
let worktree_id = workspace
|
||||
.read(cx)
|
||||
.visible_worktrees(cx)
|
||||
.find(|worktree| worktree.read(cx).root_name() == worktree_name)
|
||||
.map(|worktree| worktree.read(cx).id())?;
|
||||
Some(ProjectPath {
|
||||
worktree_id,
|
||||
path: path.into(),
|
||||
})
|
||||
}
|
||||
|
||||
let (prefix, argument) = link.split_once(Self::SEPARATOR)?;
|
||||
match prefix {
|
||||
Self::FILE => {
|
||||
let project_path = extract_project_path_from_link(argument, workspace, cx)?;
|
||||
let entry = workspace
|
||||
.read(cx)
|
||||
.project()
|
||||
.read(cx)
|
||||
.entry_for_path(&project_path, cx)?
|
||||
.clone();
|
||||
Some(MentionLink::File(project_path, entry))
|
||||
}
|
||||
Self::SYMBOL => {
|
||||
let (path, symbol) = argument.split_once(Self::SEPARATOR)?;
|
||||
let project_path = extract_project_path_from_link(path, workspace, cx)?;
|
||||
Some(MentionLink::Symbol(project_path, symbol.to_string()))
|
||||
}
|
||||
Self::SELECTION => {
|
||||
let (path, line_args) = argument.split_once(Self::SEPARATOR)?;
|
||||
let project_path = extract_project_path_from_link(path, workspace, cx)?;
|
||||
|
||||
let line_range = {
|
||||
let (start, end) = line_args
|
||||
.trim_start_matches('(')
|
||||
.trim_end_matches(')')
|
||||
.split_once('-')?;
|
||||
start.parse::<usize>().ok()?..end.parse::<usize>().ok()?
|
||||
};
|
||||
|
||||
Some(MentionLink::Selection(project_path, line_range))
|
||||
}
|
||||
Self::THREAD => {
|
||||
if let Some(encoded_filename) = argument.strip_prefix(Self::TEXT_THREAD_URL_PREFIX)
|
||||
{
|
||||
let filename = urlencoding::decode(encoded_filename).ok()?;
|
||||
let path = contexts_dir().join(filename.as_ref()).into();
|
||||
Some(MentionLink::TextThread(path))
|
||||
} else {
|
||||
let thread_id = ThreadId::from(argument);
|
||||
Some(MentionLink::Thread(thread_id))
|
||||
}
|
||||
}
|
||||
Self::FETCH => Some(MentionLink::Fetch(argument.to_string())),
|
||||
Self::RULE => {
|
||||
let prompt_id = UserPromptId(Uuid::try_parse(argument).ok()?);
|
||||
Some(MentionLink::Rule(prompt_id))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ use http_client::HttpClientWithUrl;
|
||||
use itertools::Itertools;
|
||||
use language::{Buffer, CodeLabel, HighlightId};
|
||||
use lsp::CompletionContext;
|
||||
use project::lsp_store::SymbolLocation;
|
||||
use project::{
|
||||
Completion, CompletionDisplayOptions, CompletionIntent, CompletionResponse, ProjectPath,
|
||||
Symbol, WorktreeId,
|
||||
@@ -22,6 +23,8 @@ use rope::Point;
|
||||
use text::{Anchor, OffsetRangeExt, ToPoint};
|
||||
use ui::prelude::*;
|
||||
use util::ResultExt as _;
|
||||
use util::paths::PathStyle;
|
||||
use util::rel_path::RelPath;
|
||||
use workspace::Workspace;
|
||||
|
||||
use agent::{
|
||||
@@ -574,11 +577,12 @@ impl ContextPickerCompletionProvider {
|
||||
|
||||
fn completion_for_path(
|
||||
project_path: ProjectPath,
|
||||
path_prefix: &str,
|
||||
path_prefix: &RelPath,
|
||||
is_recent: bool,
|
||||
is_directory: bool,
|
||||
excerpt_id: ExcerptId,
|
||||
source_range: Range<Anchor>,
|
||||
path_style: PathStyle,
|
||||
editor: Entity<Editor>,
|
||||
context_store: Entity<ContextStore>,
|
||||
cx: &App,
|
||||
@@ -586,6 +590,7 @@ impl ContextPickerCompletionProvider {
|
||||
let (file_name, directory) = super::file_context_picker::extract_file_name_and_directory(
|
||||
&project_path.path,
|
||||
path_prefix,
|
||||
path_style,
|
||||
);
|
||||
|
||||
let label =
|
||||
@@ -596,11 +601,12 @@ impl ContextPickerCompletionProvider {
|
||||
file_name.to_string()
|
||||
};
|
||||
|
||||
let path = Path::new(&full_path);
|
||||
let crease_icon_path = if is_directory {
|
||||
FileIcons::get_folder_icon(false, cx).unwrap_or_else(|| IconName::Folder.path().into())
|
||||
FileIcons::get_folder_icon(false, path, cx)
|
||||
.unwrap_or_else(|| IconName::Folder.path().into())
|
||||
} else {
|
||||
FileIcons::get_icon(Path::new(&full_path), cx)
|
||||
.unwrap_or_else(|| IconName::File.path().into())
|
||||
FileIcons::get_icon(path, cx).unwrap_or_else(|| IconName::File.path().into())
|
||||
};
|
||||
let completion_icon_path = if is_recent {
|
||||
IconName::HistoryRerun.path().into()
|
||||
@@ -656,17 +662,22 @@ impl ContextPickerCompletionProvider {
|
||||
workspace: Entity<Workspace>,
|
||||
cx: &mut App,
|
||||
) -> Option<Completion> {
|
||||
let path_style = workspace.read(cx).path_style(cx);
|
||||
let SymbolLocation::InProject(symbol_path) = &symbol.path else {
|
||||
return None;
|
||||
};
|
||||
let path_prefix = workspace
|
||||
.read(cx)
|
||||
.project()
|
||||
.read(cx)
|
||||
.worktree_for_id(symbol.path.worktree_id, cx)?
|
||||
.worktree_for_id(symbol_path.worktree_id, cx)?
|
||||
.read(cx)
|
||||
.root_name();
|
||||
|
||||
let (file_name, directory) = super::file_context_picker::extract_file_name_and_directory(
|
||||
&symbol.path.path,
|
||||
&symbol_path.path,
|
||||
path_prefix,
|
||||
path_style,
|
||||
);
|
||||
let full_path = if let Some(directory) = directory {
|
||||
format!("{}{}", directory, file_name)
|
||||
@@ -742,15 +753,15 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> Task<Result<Vec<CompletionResponse>>> {
|
||||
let state = buffer.update(cx, |buffer, _cx| {
|
||||
let position = buffer_position.to_point(buffer);
|
||||
let line_start = Point::new(position.row, 0);
|
||||
let offset_to_line = buffer.point_to_offset(line_start);
|
||||
let mut lines = buffer.text_for_range(line_start..position).lines();
|
||||
let line = lines.next()?;
|
||||
MentionCompletion::try_parse(line, offset_to_line)
|
||||
});
|
||||
let Some(state) = state else {
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let position = buffer_position.to_point(&snapshot);
|
||||
let line_start = Point::new(position.row, 0);
|
||||
let offset_to_line = snapshot.point_to_offset(line_start);
|
||||
let mut lines = snapshot.text_for_range(line_start..position).lines();
|
||||
let Some(line) = lines.next() else {
|
||||
return Task::ready(Ok(Vec::new()));
|
||||
};
|
||||
let Some(state) = MentionCompletion::try_parse(line, offset_to_line) else {
|
||||
return Task::ready(Ok(Vec::new()));
|
||||
};
|
||||
|
||||
@@ -760,7 +771,6 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
return Task::ready(Ok(Vec::new()));
|
||||
};
|
||||
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let source_range = snapshot.anchor_before(state.source_range.start)
|
||||
..snapshot.anchor_after(state.source_range.end);
|
||||
|
||||
@@ -768,6 +778,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
let text_thread_store = self.text_thread_store.clone();
|
||||
let editor = self.editor.clone();
|
||||
let http_client = workspace.read(cx).client().http_client();
|
||||
let path_style = workspace.read(cx).path_style(cx);
|
||||
|
||||
let MentionCompletion { mode, argument, .. } = state;
|
||||
let query = argument.unwrap_or_else(|| "".to_string());
|
||||
@@ -834,6 +845,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
mat.is_dir,
|
||||
excerpt_id,
|
||||
source_range.clone(),
|
||||
path_style,
|
||||
editor.clone(),
|
||||
context_store.clone(),
|
||||
cx,
|
||||
@@ -1064,7 +1076,7 @@ mod tests {
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use std::{ops::Deref, rc::Rc};
|
||||
use util::path;
|
||||
use util::{path, rel_path::rel_path};
|
||||
use workspace::{AppState, Item};
|
||||
|
||||
#[test]
|
||||
@@ -1215,16 +1227,18 @@ mod tests {
|
||||
let mut cx = VisualTestContext::from_window(*window.deref(), cx);
|
||||
|
||||
let paths = vec![
|
||||
path!("a/one.txt"),
|
||||
path!("a/two.txt"),
|
||||
path!("a/three.txt"),
|
||||
path!("a/four.txt"),
|
||||
path!("b/five.txt"),
|
||||
path!("b/six.txt"),
|
||||
path!("b/seven.txt"),
|
||||
path!("b/eight.txt"),
|
||||
rel_path("a/one.txt"),
|
||||
rel_path("a/two.txt"),
|
||||
rel_path("a/three.txt"),
|
||||
rel_path("a/four.txt"),
|
||||
rel_path("b/five.txt"),
|
||||
rel_path("b/six.txt"),
|
||||
rel_path("b/seven.txt"),
|
||||
rel_path("b/eight.txt"),
|
||||
];
|
||||
|
||||
let slash = PathStyle::local().separator();
|
||||
|
||||
let mut opened_editors = Vec::new();
|
||||
for path in paths {
|
||||
let buffer = workspace
|
||||
@@ -1232,7 +1246,7 @@ mod tests {
|
||||
workspace.open_path(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: Path::new(path).into(),
|
||||
path: path.into(),
|
||||
},
|
||||
None,
|
||||
false,
|
||||
@@ -1308,13 +1322,13 @@ mod tests {
|
||||
assert_eq!(
|
||||
current_completion_labels(editor),
|
||||
&[
|
||||
"seven.txt dir/b/",
|
||||
"six.txt dir/b/",
|
||||
"five.txt dir/b/",
|
||||
"four.txt dir/a/",
|
||||
"Files & Directories",
|
||||
"Symbols",
|
||||
"Fetch"
|
||||
format!("seven.txt dir{slash}b{slash}"),
|
||||
format!("six.txt dir{slash}b{slash}"),
|
||||
format!("five.txt dir{slash}b{slash}"),
|
||||
format!("four.txt dir{slash}a{slash}"),
|
||||
"Files & Directories".into(),
|
||||
"Symbols".into(),
|
||||
"Fetch".into()
|
||||
]
|
||||
);
|
||||
});
|
||||
@@ -1341,7 +1355,10 @@ mod tests {
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(editor.text(cx), "Lorem @file one");
|
||||
assert!(editor.has_visible_completions_menu());
|
||||
assert_eq!(current_completion_labels(editor), vec!["one.txt dir/a/"]);
|
||||
assert_eq!(
|
||||
current_completion_labels(editor),
|
||||
vec![format!("one.txt dir{slash}a{slash}")]
|
||||
);
|
||||
});
|
||||
|
||||
editor.update_in(&mut cx, |editor, window, cx| {
|
||||
@@ -1350,7 +1367,10 @@ 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),
|
||||
format!("Lorem [@one.txt](@file:dir{slash}a{slash}one.txt) ")
|
||||
);
|
||||
assert!(!editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
fold_ranges(editor, cx),
|
||||
@@ -1361,7 +1381,10 @@ 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),
|
||||
format!("Lorem [@one.txt](@file:dir{slash}a{slash}one.txt) ")
|
||||
);
|
||||
assert!(!editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
fold_ranges(editor, cx),
|
||||
@@ -1374,7 +1397,7 @@ mod tests {
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum ",
|
||||
format!("Lorem [@one.txt](@file:dir{slash}a{slash}one.txt) Ipsum "),
|
||||
);
|
||||
assert!(!editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
@@ -1388,7 +1411,7 @@ mod tests {
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum @file ",
|
||||
format!("Lorem [@one.txt](@file:dir{slash}a{slash}one.txt) Ipsum @file "),
|
||||
);
|
||||
assert!(editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
@@ -1406,7 +1429,7 @@ 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) "
|
||||
format!("Lorem [@one.txt](@file:dir{slash}a{slash}one.txt) Ipsum [@seven.txt](@file:dir{slash}b{slash}seven.txt) ")
|
||||
);
|
||||
assert!(!editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
@@ -1423,7 +1446,7 @@ 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@"
|
||||
format!("Lorem [@one.txt](@file:dir{slash}a{slash}one.txt) Ipsum [@seven.txt](@file:dir{slash}b{slash}seven.txt) \n@")
|
||||
);
|
||||
assert!(editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
@@ -1444,7 +1467,7 @@ 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) "
|
||||
format!("Lorem [@one.txt](@file:dir{slash}a{slash}one.txt) Ipsum [@seven.txt](@file:dir{slash}b{slash}seven.txt) \n[@six.txt](@file:dir{slash}b{slash}six.txt) ")
|
||||
);
|
||||
assert!(!editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user