Compare commits
679 Commits
bash-timeo
...
code-block
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ff95fec62 | ||
|
|
845cf799c6 | ||
|
|
22c1090163 | ||
|
|
122af4fd53 | ||
|
|
e07ffe7cf1 | ||
|
|
5e4be013af | ||
|
|
f055dca592 | ||
|
|
5872276511 | ||
|
|
1bf9e15f26 | ||
|
|
f046d70625 | ||
|
|
afeb3d4fd9 | ||
|
|
92dd6b67c7 | ||
|
|
38ede4bae3 | ||
|
|
fc920bf63d | ||
|
|
04c68dc0cf | ||
|
|
399eced884 | ||
|
|
50f705e779 | ||
|
|
8173534ad5 | ||
|
|
8c03934b26 | ||
|
|
84e4891d54 | ||
|
|
d03d8ccec1 | ||
|
|
4d934f2884 | ||
|
|
e697cf9747 | ||
|
|
e4e455ae6b | ||
|
|
ff4900c942 | ||
|
|
632f08d2a3 | ||
|
|
5589e78a69 | ||
|
|
128b7d2245 | ||
|
|
fbb0fe40ec | ||
|
|
7587340fb1 | ||
|
|
5073cba08d | ||
|
|
0a02869513 | ||
|
|
854c554580 | ||
|
|
59708ef56c | ||
|
|
152ea04a21 | ||
|
|
074b6965e2 | ||
|
|
edda386827 | ||
|
|
9767033985 | ||
|
|
edf78e770d | ||
|
|
8d77efa781 | ||
|
|
747a029487 | ||
|
|
c8685dc90f | ||
|
|
d566864891 | ||
|
|
75a9ed164b | ||
|
|
e364e48266 | ||
|
|
b4732235e3 | ||
|
|
b1395c5fdf | ||
|
|
1a4d7249f6 | ||
|
|
d7004030b3 | ||
|
|
2508e491d5 | ||
|
|
a09e5d255b | ||
|
|
33abf1ee7c | ||
|
|
f7a3e00074 | ||
|
|
24e47de02a | ||
|
|
15a83b5a10 | ||
|
|
fde1cc78a1 | ||
|
|
f2813f60ed | ||
|
|
4758173c33 | ||
|
|
d732a7d361 | ||
|
|
cf6b051b99 | ||
|
|
a376038803 | ||
|
|
2973bf188b | ||
|
|
386970c29a | ||
|
|
f7f44bfbed | ||
|
|
fd17f2d8ae | ||
|
|
5507958327 | ||
|
|
fa40353fc5 | ||
|
|
83b8530e1f | ||
|
|
5afb89ca93 | ||
|
|
6386336eee | ||
|
|
c168fc335c | ||
|
|
b2df395918 | ||
|
|
2b431d3e9d | ||
|
|
4812c9094b | ||
|
|
fcef101227 | ||
|
|
7e25460708 | ||
|
|
9b37206147 | ||
|
|
756fcd0733 | ||
|
|
3fd37799b4 | ||
|
|
ab180855de | ||
|
|
2beefc8158 | ||
|
|
5092f0f18b | ||
|
|
3a212e72a4 | ||
|
|
4dc8ce8cf7 | ||
|
|
2cc5a0de26 | ||
|
|
bc665b2a76 | ||
|
|
17903a0999 | ||
|
|
5102c4c002 | ||
|
|
2139219832 | ||
|
|
9abeedf0c6 | ||
|
|
1d7c86bf0d | ||
|
|
17703310ae | ||
|
|
bbe8d6a654 | ||
|
|
bbc66748dd | ||
|
|
99df1190a9 | ||
|
|
0e477e7db9 | ||
|
|
0afb980f7b | ||
|
|
d360f77796 | ||
|
|
92b9bc599d | ||
|
|
ed367e1636 | ||
|
|
b41ffae161 | ||
|
|
ef33666701 | ||
|
|
cd86905ebe | ||
|
|
abb48b7711 | ||
|
|
8afac388bb | ||
|
|
53b36b328e | ||
|
|
ce93961fe0 | ||
|
|
e3c987e2fb | ||
|
|
4dc0551105 | ||
|
|
bf9e5b4f76 | ||
|
|
cfb8cae29c | ||
|
|
68e0105627 | ||
|
|
e98e6c7426 | ||
|
|
3a1bd38503 | ||
|
|
8a69d252f5 | ||
|
|
bf30beacc2 | ||
|
|
2a0be48875 | ||
|
|
1c4ba07b20 | ||
|
|
8a717abe0d | ||
|
|
f735c90c3f | ||
|
|
ddfeb202a3 | ||
|
|
9bd0828303 | ||
|
|
4dff47ae20 | ||
|
|
52eef3c35d | ||
|
|
f060918b57 | ||
|
|
609c528ceb | ||
|
|
6db974dd32 | ||
|
|
60ec55b179 | ||
|
|
bb7a5b13df | ||
|
|
1e47dfce79 | ||
|
|
3fdbc3090d | ||
|
|
f2b4004c00 | ||
|
|
ec5821f76d | ||
|
|
e22cae6459 | ||
|
|
21bafd7856 | ||
|
|
ee74edbbb1 | ||
|
|
d832b8e687 | ||
|
|
539f4f1576 | ||
|
|
9a325a23e5 | ||
|
|
ce31312268 | ||
|
|
d46890978a | ||
|
|
67615b968b | ||
|
|
053fafa90e | ||
|
|
d23024609f | ||
|
|
3961d87ae0 | ||
|
|
8b910e1cd9 | ||
|
|
12c645e154 | ||
|
|
cfb7a30724 | ||
|
|
7623fce4b4 | ||
|
|
7f5c874a38 | ||
|
|
8cc2ade21c | ||
|
|
c3177e6f5b | ||
|
|
c3570fbcf3 | ||
|
|
3aa313010f | ||
|
|
5f9c91d05a | ||
|
|
6692bd9f2b | ||
|
|
cc57bc7c96 | ||
|
|
c157b1c455 | ||
|
|
136e83e0b1 | ||
|
|
b28756ae3f | ||
|
|
65401d6d7b | ||
|
|
a5405fcbd7 | ||
|
|
4f9cadabf7 | ||
|
|
7443f89a2e | ||
|
|
9bee765d7f | ||
|
|
8c553ee9f0 | ||
|
|
3389327df5 | ||
|
|
f106dfca42 | ||
|
|
37fa437990 | ||
|
|
9be7bf72a4 | ||
|
|
357e38b471 | ||
|
|
ae37f3ca2e | ||
|
|
49003d8038 | ||
|
|
93862838bd | ||
|
|
c39adc5242 | ||
|
|
ebb39d9231 | ||
|
|
187f851613 | ||
|
|
a77db45865 | ||
|
|
6bb6be826d | ||
|
|
7d9a55d101 | ||
|
|
57d8397f53 | ||
|
|
17ecf94f6f | ||
|
|
d492939bed | ||
|
|
720dfee803 | ||
|
|
a98c648201 | ||
|
|
c147daae4a | ||
|
|
d3911e34de | ||
|
|
87f85f1863 | ||
|
|
1a4dab97db | ||
|
|
cd365b0cf5 | ||
|
|
58604fba86 | ||
|
|
b0609272c0 | ||
|
|
a17807d8b1 | ||
|
|
f81e65ae7c | ||
|
|
952fe34aaa | ||
|
|
f527df6fa1 | ||
|
|
b54bbebc03 | ||
|
|
8bb7a1f9e7 | ||
|
|
e70d8d4dfd | ||
|
|
ea5ce2a1a4 | ||
|
|
fd8eeb537d | ||
|
|
92f21ee39d | ||
|
|
fcfeea4825 | ||
|
|
c0f8e0f605 | ||
|
|
9d10489607 | ||
|
|
8836c6fb42 | ||
|
|
f125353b6f | ||
|
|
fef2681cfa | ||
|
|
8b5835de17 | ||
|
|
2124b7ea99 | ||
|
|
74442b68ea | ||
|
|
ba3d82629e | ||
|
|
ecc600a68f | ||
|
|
218496744c | ||
|
|
d095bab8ad | ||
|
|
f8c3fe7871 | ||
|
|
aa161078fb | ||
|
|
f11c749353 | ||
|
|
40b5a1b028 | ||
|
|
2d43818c04 | ||
|
|
636c6e7f2d | ||
|
|
45d3f5168a | ||
|
|
8366cd0b52 | ||
|
|
f6774ae60d | ||
|
|
92e810bfec | ||
|
|
724c935196 | ||
|
|
ef54b58346 | ||
|
|
01bdd170ec | ||
|
|
4b9f4feff1 | ||
|
|
19fb1e1b0d | ||
|
|
f2cb6d69d5 | ||
|
|
822b6f837d | ||
|
|
09db31288a | ||
|
|
a320d324f1 | ||
|
|
266c41ed9a | ||
|
|
4f4bbf264f | ||
|
|
990ca48744 | ||
|
|
f69aeb6311 | ||
|
|
d5f3fbdc88 | ||
|
|
76a78b550b | ||
|
|
e515b2c714 | ||
|
|
55ea481707 | ||
|
|
5e31d86f1f | ||
|
|
4a8f114528 | ||
|
|
ce1a674eba | ||
|
|
0d3fe474db | ||
|
|
6a009b447a | ||
|
|
75ab8ff9a1 | ||
|
|
3705986fac | ||
|
|
aefb3aa2fa | ||
|
|
8e7c145f20 | ||
|
|
a2a502f026 | ||
|
|
c231c95521 | ||
|
|
fcc6a86c90 | ||
|
|
338a6a3b7e | ||
|
|
a0eaede13d | ||
|
|
abf2b9d7d3 | ||
|
|
a50fbc9b5c | ||
|
|
9bbc2e0fb2 | ||
|
|
6caf34ab7e | ||
|
|
8607c7d3ee | ||
|
|
e26bb05567 | ||
|
|
b3b89c8443 | ||
|
|
962b024248 | ||
|
|
833653a3ea | ||
|
|
886f0b7214 | ||
|
|
207fb04969 | ||
|
|
36d02de784 | ||
|
|
36da97935a | ||
|
|
19b547565d | ||
|
|
109f1d43fc | ||
|
|
a5852d4537 | ||
|
|
10ded0ab75 | ||
|
|
b0b620af56 | ||
|
|
eca6d5a04e | ||
|
|
3357736aea | ||
|
|
458ffaa134 | ||
|
|
b14356d1d3 | ||
|
|
19ef56ba7c | ||
|
|
dfbd132d9f | ||
|
|
2e8ee9b64f | ||
|
|
c15382c4d8 | ||
|
|
70c51b513b | ||
|
|
38afae86a9 | ||
|
|
9249919b7a | ||
|
|
9fe4a14f73 | ||
|
|
7cc3c03b08 | ||
|
|
4f2f9ff762 | ||
|
|
7aa0fa1543 | ||
|
|
3b31860d52 | ||
|
|
733cd6b68c | ||
|
|
e8fe0eb2e6 | ||
|
|
0f3ac38332 | ||
|
|
32e9757a85 | ||
|
|
be76942a69 | ||
|
|
942d4eb126 | ||
|
|
9d35f0389d | ||
|
|
d13cd007a2 | ||
|
|
f8ac6eef75 | ||
|
|
6d2bdc3bac | ||
|
|
9a3434efb4 | ||
|
|
333de5d673 | ||
|
|
97ab0980d1 | ||
|
|
3a27e8c311 | ||
|
|
bfb2ed3824 | ||
|
|
9db0c4f19a | ||
|
|
a4f5c4fef2 | ||
|
|
4dcfe0cff9 | ||
|
|
4473b45c3d | ||
|
|
ceeae790b7 | ||
|
|
107d8ca483 | ||
|
|
4278d894d2 | ||
|
|
a91948aeb4 | ||
|
|
2178b36cbc | ||
|
|
0fb0059b5f | ||
|
|
fbf7caf93e | ||
|
|
d48152d958 | ||
|
|
bafc086d27 | ||
|
|
f737c4d01e | ||
|
|
8f308d835a | ||
|
|
703a68eedf | ||
|
|
cc2fcb2f42 | ||
|
|
f0ef3110d3 | ||
|
|
0454e7a22e | ||
|
|
d88b06a5dc | ||
|
|
98ceffe026 | ||
|
|
bab28560ef | ||
|
|
8102a16747 | ||
|
|
9875521d4e | ||
|
|
8c55063417 | ||
|
|
7abe2c9c31 | ||
|
|
73a767fc45 | ||
|
|
327fee4d22 | ||
|
|
b1d5918fdc | ||
|
|
6f685b9f8e | ||
|
|
65a7076ba8 | ||
|
|
3fe15ee915 | ||
|
|
bae3ef01c6 | ||
|
|
810b39ce34 | ||
|
|
a93aa598d6 | ||
|
|
28aba94369 | ||
|
|
e147ef006c | ||
|
|
e18d787dbc | ||
|
|
d02c8bcc02 | ||
|
|
e27f6a984f | ||
|
|
cce661b64b | ||
|
|
3932a6c51e | ||
|
|
1e0ae35f69 | ||
|
|
4405ed04d0 | ||
|
|
c585dbd8ff | ||
|
|
c7fc95e732 | ||
|
|
f97546b6ef | ||
|
|
7badd6053d | ||
|
|
502a0f6535 | ||
|
|
eea6cfb383 | ||
|
|
0dc0701967 | ||
|
|
62b8ef980b | ||
|
|
269f6403dd | ||
|
|
3538acec7c | ||
|
|
87512d0814 | ||
|
|
6254efe39d | ||
|
|
72218f4a61 | ||
|
|
5f7189e5af | ||
|
|
f6d13645ce | ||
|
|
6ffd3f034f | ||
|
|
6e0732a9d7 | ||
|
|
f8d097acd6 | ||
|
|
7cf4926130 | ||
|
|
676cc109a3 | ||
|
|
ba7f886c62 | ||
|
|
c2cd4fd7a1 | ||
|
|
4095011af5 | ||
|
|
80a2f71d8e | ||
|
|
d93141bded | ||
|
|
b402007de6 | ||
|
|
be63d51eb7 | ||
|
|
8660101b83 | ||
|
|
1aa1b2bede | ||
|
|
58d8b91131 | ||
|
|
ba588161d9 | ||
|
|
7e928dd615 | ||
|
|
fade49a11a | ||
|
|
e4f692ac75 | ||
|
|
c21bca07e2 | ||
|
|
acc4a5ccb3 | ||
|
|
7a95c14625 | ||
|
|
6dd622d6c3 | ||
|
|
e7afbbd725 | ||
|
|
133932ed74 | ||
|
|
3ca63584b9 | ||
|
|
2a878ee6d0 | ||
|
|
8117940aca | ||
|
|
002235d0da | ||
|
|
f07695c4cd | ||
|
|
bdd0cbb717 | ||
|
|
022a110f8e | ||
|
|
b0200c4368 | ||
|
|
ae47829fa8 | ||
|
|
5ebb18c47e | ||
|
|
ded1c7012c | ||
|
|
ad25cd09b6 | ||
|
|
a7a7335da4 | ||
|
|
cbb6c221b3 | ||
|
|
63b4b60b79 | ||
|
|
9ea8a9a1d3 | ||
|
|
19f542b8d6 | ||
|
|
70b3cb04bb | ||
|
|
0170f522bf | ||
|
|
602ae84542 | ||
|
|
3fef3cc392 | ||
|
|
78c856cb75 | ||
|
|
21946691a4 | ||
|
|
fcb1efdf21 | ||
|
|
54b46fdfaa | ||
|
|
94cf1b0353 | ||
|
|
56856fb992 | ||
|
|
64a67a1071 | ||
|
|
040046ed2a | ||
|
|
0286b8ab3e | ||
|
|
8de53bd89f | ||
|
|
10507f9a4c | ||
|
|
7bdde8f14f | ||
|
|
7c7f69f4c5 | ||
|
|
12c9526f6a | ||
|
|
97b044acf5 | ||
|
|
1e25e6b3cc | ||
|
|
f565994da9 | ||
|
|
db94d6d767 | ||
|
|
456e54b87c | ||
|
|
2b277123be | ||
|
|
bb0b2a5b7b | ||
|
|
5c2c6d7e5e | ||
|
|
4f58bdee28 | ||
|
|
486a9e4d61 | ||
|
|
0d8f77b5de | ||
|
|
cb79420773 | ||
|
|
c641209341 | ||
|
|
48a716fcb5 | ||
|
|
25956c49c1 | ||
|
|
4efabe17dd | ||
|
|
320abe9b22 | ||
|
|
9a9f2e71ca | ||
|
|
609895d95f | ||
|
|
da2d8bd845 | ||
|
|
6267a147ba | ||
|
|
aceecec6bf | ||
|
|
f3f2c6d811 | ||
|
|
41cffa64b0 | ||
|
|
b486e32f05 | ||
|
|
222d4a2546 | ||
|
|
1eb948654a | ||
|
|
35da1502e1 | ||
|
|
1d98b33ae0 | ||
|
|
4e8ecfc0c4 | ||
|
|
134a0563c2 | ||
|
|
3f4d4af080 | ||
|
|
68ec1d724c | ||
|
|
102ea6ac79 | ||
|
|
5d3718df2d | ||
|
|
f1f5d602fc | ||
|
|
60624d81ba | ||
|
|
91755b2db1 | ||
|
|
e34fee55a0 | ||
|
|
dad6067e18 | ||
|
|
5619a3e618 | ||
|
|
06ad45ce08 | ||
|
|
7e6387052f | ||
|
|
0182e09e33 | ||
|
|
6f6e207eb5 | ||
|
|
149cdeca29 | ||
|
|
92dc812aea | ||
|
|
c7e80c80c6 | ||
|
|
c381a500f8 | ||
|
|
ff4334efc7 | ||
|
|
b1e4e6048a | ||
|
|
d0f806456c | ||
|
|
b6cce1ed91 | ||
|
|
05fc9ee396 | ||
|
|
8f52bb92b6 | ||
|
|
144fd0b00d | ||
|
|
cd4a3fd679 | ||
|
|
42c3f4e7cf | ||
|
|
90dec1d451 | ||
|
|
afabcd1547 | ||
|
|
ccf9aef767 | ||
|
|
aef78dcffd | ||
|
|
6f0951ff77 | ||
|
|
5e094553fa | ||
|
|
32829d9f12 | ||
|
|
e4cf7fe8f5 | ||
|
|
2b89b97cd1 | ||
|
|
e26f0a331f | ||
|
|
7e1b419243 | ||
|
|
98d001bad5 | ||
|
|
d4a985a6e3 | ||
|
|
616d17f517 | ||
|
|
e1c42315dc | ||
|
|
cfc848d24b | ||
|
|
d4761cea47 | ||
|
|
b794919842 | ||
|
|
fc1252b0cd | ||
|
|
12b012eab3 | ||
|
|
77f32582e2 | ||
|
|
0d6e455bf6 | ||
|
|
5f897b0e00 | ||
|
|
d74f0735c2 | ||
|
|
a8b1ef3531 | ||
|
|
c8ccc472b5 | ||
|
|
26b9c32e96 | ||
|
|
db56254517 | ||
|
|
9d91908256 | ||
|
|
6b80eb556c | ||
|
|
2603f36737 | ||
|
|
6c93d107c2 | ||
|
|
5b6efa4c02 | ||
|
|
84aa480344 | ||
|
|
6db29eb90a | ||
|
|
ff41be30dc | ||
|
|
47b663a8df | ||
|
|
1d9915f88a | ||
|
|
584fa3db53 | ||
|
|
a051194195 | ||
|
|
78ecc3cef0 | ||
|
|
ac8a4ba5d4 | ||
|
|
9863b48dd7 | ||
|
|
fddaa31655 | ||
|
|
b45230784d | ||
|
|
4a57664c7f | ||
|
|
0eb0a3c7dc | ||
|
|
6278761460 | ||
|
|
f2ce183286 | ||
|
|
98891e4c70 | ||
|
|
5e57f148ac | ||
|
|
128779f615 | ||
|
|
b25c3334cc | ||
|
|
77544f42b1 | ||
|
|
b864a9b0ae | ||
|
|
e4844b281d | ||
|
|
d1ffda9bfe | ||
|
|
8ffa58414d | ||
|
|
fb78cbbd45 | ||
|
|
17719f9f87 | ||
|
|
055df30757 | ||
|
|
429d4580cf | ||
|
|
62ebae96e3 | ||
|
|
0036a33263 | ||
|
|
b22faf96e0 | ||
|
|
dafe994eef | ||
|
|
5994ac5cec | ||
|
|
97a9a5de10 | ||
|
|
730f2e7083 | ||
|
|
141ad72d97 | ||
|
|
a5fe6d1e61 | ||
|
|
5734ffbb18 | ||
|
|
932a7c6440 | ||
|
|
6a60bb189b | ||
|
|
5909d1258b | ||
|
|
78662f8fea | ||
|
|
c2e3134963 | ||
|
|
66b3e03baa | ||
|
|
7caa2c2ea0 | ||
|
|
08ce230bae | ||
|
|
1df01eabfe | ||
|
|
2f5c662c42 | ||
|
|
a03fb3791e | ||
|
|
dd7bc5f199 | ||
|
|
c7d3fbcac1 | ||
|
|
1164829cad | ||
|
|
e09eeb7446 | ||
|
|
cdcad708f6 | ||
|
|
bd4c9b45b6 | ||
|
|
d4736a5427 | ||
|
|
353ae2335b | ||
|
|
ad39d3226f | ||
|
|
c35238bd72 | ||
|
|
5757e352b0 | ||
|
|
c124838a73 | ||
|
|
5ebac7e30c | ||
|
|
c143846e42 | ||
|
|
71c2a11bd9 | ||
|
|
2440faf4b2 | ||
|
|
c0262cf62f | ||
|
|
fd256d159d | ||
|
|
a2a3d1a4bd | ||
|
|
294a1b63c0 | ||
|
|
ffdf725f32 | ||
|
|
8ee6a2b454 | ||
|
|
cf65d9437a | ||
|
|
66dd6726df | ||
|
|
44cb8e582b | ||
|
|
73305ce45e | ||
|
|
94b75f3ad9 | ||
|
|
384868e597 | ||
|
|
d88694f8da | ||
|
|
90f30b5c20 | ||
|
|
24d4f8ca18 | ||
|
|
804066a047 | ||
|
|
4a356466b1 | ||
|
|
0921762b59 | ||
|
|
46b1df2e2d | ||
|
|
986da332db | ||
|
|
dad33f7cc2 | ||
|
|
64241f7d2f | ||
|
|
fbbc23bec3 | ||
|
|
26f4705198 | ||
|
|
3abf95216c | ||
|
|
b0b52f299c | ||
|
|
53cde329da | ||
|
|
b55b310ad0 | ||
|
|
8ab25e2bac | ||
|
|
c10b1f7c61 | ||
|
|
cb1ee01a66 | ||
|
|
90bcde116f | ||
|
|
8ac378b86e | ||
|
|
55760295d9 | ||
|
|
9dfb907f97 | ||
|
|
e20daa7639 | ||
|
|
b46ab367ef | ||
|
|
12212dc329 | ||
|
|
324e4658ba | ||
|
|
ed500dacb6 | ||
|
|
2f4b48129b | ||
|
|
ed7c55a04e | ||
|
|
6db4ab381c | ||
|
|
0e72a7e6ce | ||
|
|
3dc3ab062d | ||
|
|
ed63f216e3 | ||
|
|
ba767a1998 | ||
|
|
23c3f5f410 | ||
|
|
b3be294c90 | ||
|
|
af5318df98 | ||
|
|
60c420a2da | ||
|
|
ee6c33ffb3 | ||
|
|
9ae4f4b158 | ||
|
|
915a1cb116 | ||
|
|
aead0e11ff | ||
|
|
2752c08810 | ||
|
|
780143298a | ||
|
|
088d7c1342 | ||
|
|
64de6bd2a8 | ||
|
|
6aa0248ab3 | ||
|
|
342134fbab | ||
|
|
b47aa33459 | ||
|
|
9f6c5e2877 | ||
|
|
7bf6cd4ccf | ||
|
|
c7963c8a93 | ||
|
|
dd4629433b | ||
|
|
2e56935997 | ||
|
|
e43a397f1d | ||
|
|
9d0fe164a7 | ||
|
|
6d7fef6fd3 | ||
|
|
b67d3fd21b | ||
|
|
1cb4f8288d | ||
|
|
3a8fe4d973 | ||
|
|
9d6d152918 | ||
|
|
31034f8296 | ||
|
|
c441b651fa | ||
|
|
61ddcd516f | ||
|
|
f12a554f86 | ||
|
|
9dae4d8c59 | ||
|
|
f0b7f355a2 | ||
|
|
b687a5e56d | ||
|
|
e66a24edcf | ||
|
|
301fc7cd7b | ||
|
|
020a1071d5 | ||
|
|
38d2487630 | ||
|
|
79c9f2bbd9 | ||
|
|
c8caae03df | ||
|
|
dabc4d8ff5 | ||
|
|
c0ad3e8183 | ||
|
|
afde25a5cb | ||
|
|
9f708ee789 | ||
|
|
58731e2fd1 | ||
|
|
d0632a5332 | ||
|
|
64cea2f1f1 | ||
|
|
ac958d4a2d | ||
|
|
2df06cd2e4 | ||
|
|
0d4ca71e68 | ||
|
|
e2d6505d12 |
1
.clinerules
Symbolic link
@@ -0,0 +1 @@
|
||||
.rules
|
||||
1
.cursorrules
Symbolic link
@@ -0,0 +1 @@
|
||||
.rules
|
||||
19
.github/ISSUE_TEMPLATE/99_other.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
name: Other [Staff Only]
|
||||
description: Zed Staff Only
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Summary
|
||||
value: |
|
||||
<!-- Please insert a one line summary of the issue below -->
|
||||
SUMMARY_SENTENCE_HERE
|
||||
|
||||
### Description
|
||||
|
||||
IF YOU DO NOT WORK FOR ZED INDUSTRIES DO NOT CREATE ISSUES WITH THIS TEMPLATE.
|
||||
THEY WILL BE AUTO-CLOSED AND MAY RESULT IN YOU BEING BANNED FROM THE ZED ISSUE TRACKER.
|
||||
|
||||
FEATURE REQUESTS / SUPPORT REQUESTS SHOULD BE OPENED AS DISCUSSIONS:
|
||||
https://github.com/zed-industries/zed/discussions/new/choose
|
||||
validations:
|
||||
required: true
|
||||
2
.github/actions/run_tests/action.yml
vendored
@@ -10,7 +10,7 @@ runs:
|
||||
cargo install cargo-nextest --locked
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
|
||||
2
.github/actions/run_tests_windows/action.yml
vendored
@@ -16,7 +16,7 @@ runs:
|
||||
run: cargo install cargo-nextest --locked
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
|
||||
44
.github/workflows/ci.yml
vendored
@@ -225,7 +225,7 @@ jobs:
|
||||
|
||||
- name: Check for new vulnerable dependencies
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4
|
||||
uses: actions/dependency-review-action@67d4f4bd7a9b17a0db54d2a7519187c65e339de8 # v4
|
||||
with:
|
||||
license-check: false
|
||||
|
||||
@@ -465,6 +465,7 @@ jobs:
|
||||
- job_spec
|
||||
- style
|
||||
- migration_checks
|
||||
# run_tests: If adding required tests, add them here and to script below.
|
||||
- workspace_hack
|
||||
- linux_tests
|
||||
- build_remote_server
|
||||
@@ -482,11 +483,14 @@ jobs:
|
||||
|
||||
# Only check test jobs if they were supposed to run
|
||||
if [[ "${{ needs.job_spec.outputs.run_tests }}" == "true" ]]; then
|
||||
[[ "${{ needs.workspace_hack.result }}" != 'success' ]] && { RET_CODE=1; echo "Workspace Hack failed"; }
|
||||
[[ "${{ needs.macos_tests.result }}" != 'success' ]] && { RET_CODE=1; echo "macOS tests failed"; }
|
||||
[[ "${{ needs.linux_tests.result }}" != 'success' ]] && { RET_CODE=1; echo "Linux tests failed"; }
|
||||
[[ "${{ needs.windows_tests.result }}" != 'success' ]] && { RET_CODE=1; echo "Windows tests failed"; }
|
||||
[[ "${{ needs.windows_clippy.result }}" != 'success' ]] && { RET_CODE=1; echo "Windows clippy failed"; }
|
||||
[[ "${{ needs.build_remote_server.result }}" != 'success' ]] && { RET_CODE=1; echo "Remote server build failed"; }
|
||||
# This check is intentionally disabled. See: https://github.com/zed-industries/zed/pull/28431
|
||||
# [[ "${{ needs.migration_checks.result }}" != 'success' ]] && { RET_CODE=1; echo "Migration Checks failed"; }
|
||||
fi
|
||||
if [[ "$RET_CODE" -eq 0 ]]; then
|
||||
echo "All tests passed successfully!"
|
||||
@@ -515,7 +519,7 @@ jobs:
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
steps:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
@@ -590,7 +594,7 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
name: Linux x86_x64 release bundle
|
||||
runs-on:
|
||||
- buildjet-16vcpu-ubuntu-2004
|
||||
- buildjet-16vcpu-ubuntu-2004 # ubuntu 20.04 for minimal glibc
|
||||
if: |
|
||||
startsWith(github.ref, 'refs/tags/v')
|
||||
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
|
||||
@@ -618,26 +622,23 @@ jobs:
|
||||
- name: Create Linux .tar.gz bundle
|
||||
run: script/bundle-linux
|
||||
|
||||
- name: Upload Linux bundle to workflow run if main branch or specific label
|
||||
- name: Upload Artifact to Workflow - zed (run-bundling)
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
if: |
|
||||
github.ref == 'refs/heads/main'
|
||||
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
|
||||
if: contains(github.event.pull_request.labels.*.name, 'run-bundling')
|
||||
with:
|
||||
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-x86_64-unknown-linux-gnu.tar.gz
|
||||
path: target/release/zed-*.tar.gz
|
||||
|
||||
- name: Upload Linux remote server to workflow run if main branch or specific label
|
||||
- name: Upload Artifact to Workflow - zed-remote-server (run-bundling)
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
if: |
|
||||
github.ref == 'refs/heads/main'
|
||||
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
|
||||
if: contains(github.event.pull_request.labels.*.name, 'run-bundling')
|
||||
with:
|
||||
name: zed-remote-server-${{ github.event.pull_request.head.sha || github.sha }}-x86_64-unknown-linux-gnu.gz
|
||||
path: target/zed-remote-server-linux-x86_64.gz
|
||||
|
||||
- name: Upload app bundle to release
|
||||
- name: Upload Artifacts to release
|
||||
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
|
||||
if: ${{ !(contains(github.event.pull_request.labels.*.name, 'run-bundling')) }}
|
||||
with:
|
||||
draft: true
|
||||
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
||||
@@ -676,29 +677,26 @@ jobs:
|
||||
# This exports RELEASE_CHANNEL into env (GITHUB_ENV)
|
||||
script/determine-release-channel
|
||||
|
||||
- name: Create and upload Linux .tar.gz bundle
|
||||
- name: Create and upload Linux .tar.gz bundles
|
||||
run: script/bundle-linux
|
||||
|
||||
- name: Upload Linux bundle to workflow run if main branch or specific label
|
||||
- name: Upload Artifact to Workflow - zed (run-bundling)
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
if: |
|
||||
github.ref == 'refs/heads/main'
|
||||
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
|
||||
if: contains(github.event.pull_request.labels.*.name, 'run-bundling')
|
||||
with:
|
||||
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-aarch64-unknown-linux-gnu.tar.gz
|
||||
path: target/release/zed-*.tar.gz
|
||||
|
||||
- name: Upload Linux remote server to workflow run if main branch or specific label
|
||||
- name: Upload Artifact to Workflow - zed-remote-server (run-bundling)
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
if: |
|
||||
github.ref == 'refs/heads/main'
|
||||
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
|
||||
if: contains(github.event.pull_request.labels.*.name, 'run-bundling')
|
||||
with:
|
||||
name: zed-remote-server-${{ github.event.pull_request.head.sha || github.sha }}-aarch64-unknown-linux-gnu.gz
|
||||
path: target/zed-remote-server-linux-aarch64.gz
|
||||
|
||||
- name: Upload app bundle to release
|
||||
- name: Upload Artifacts to release
|
||||
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
|
||||
if: ${{ !(contains(github.event.pull_request.labels.*.name, 'run-bundling')) }}
|
||||
with:
|
||||
draft: true
|
||||
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
||||
@@ -739,7 +737,7 @@ jobs:
|
||||
echo "/nix/var/nix/profiles/default/bin" >> $GITHUB_PATH
|
||||
echo "/Users/administrator/.nix-profile/bin" >> $GITHUB_PATH
|
||||
|
||||
- uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31
|
||||
- uses: cachix/install-nix-action@d1ca217b388ee87b2507a9a93bf01368bde7cec2 # v31
|
||||
if: ${{ matrix.system.install_nix }}
|
||||
with:
|
||||
github_access_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
2
.github/workflows/danger.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
version: 9
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: "20"
|
||||
cache: "pnpm"
|
||||
|
||||
8
.github/workflows/deploy_collab.yml
vendored
@@ -117,12 +117,10 @@ jobs:
|
||||
export ZED_KUBE_NAMESPACE=production
|
||||
export ZED_COLLAB_LOAD_BALANCER_SIZE_UNIT=10
|
||||
export ZED_API_LOAD_BALANCER_SIZE_UNIT=2
|
||||
export ZED_LLM_LOAD_BALANCER_SIZE_UNIT=2
|
||||
elif [[ $GITHUB_REF_NAME = "collab-staging" ]]; then
|
||||
export ZED_KUBE_NAMESPACE=staging
|
||||
export ZED_COLLAB_LOAD_BALANCER_SIZE_UNIT=1
|
||||
export ZED_API_LOAD_BALANCER_SIZE_UNIT=1
|
||||
export ZED_LLM_LOAD_BALANCER_SIZE_UNIT=1
|
||||
else
|
||||
echo "cowardly refusing to deploy from an unknown branch"
|
||||
exit 1
|
||||
@@ -147,9 +145,3 @@ jobs:
|
||||
envsubst < crates/collab/k8s/collab.template.yml | kubectl apply -f -
|
||||
kubectl -n "$ZED_KUBE_NAMESPACE" rollout status deployment/$ZED_SERVICE_NAME --watch
|
||||
echo "deployed ${ZED_SERVICE_NAME} to ${ZED_KUBE_NAMESPACE}"
|
||||
|
||||
export ZED_SERVICE_NAME=llm
|
||||
export ZED_LOAD_BALANCER_SIZE_UNIT=$ZED_LLM_LOAD_BALANCER_SIZE_UNIT
|
||||
envsubst < crates/collab/k8s/collab.template.yml | kubectl apply -f -
|
||||
kubectl -n "$ZED_KUBE_NAMESPACE" rollout status deployment/$ZED_SERVICE_NAME --watch
|
||||
echo "deployed ${ZED_SERVICE_NAME} to ${ZED_KUBE_NAMESPACE}"
|
||||
|
||||
80
.github/workflows/eval.yml
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
name: Run Agent Eval
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
pull_request:
|
||||
branches:
|
||||
- "**"
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
# Allow only one workflow per any non-`main` branch.
|
||||
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: 1
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
ZED_EVAL_TELEMETRY: 1
|
||||
|
||||
jobs:
|
||||
# This is a no-op job that we run to prevent GitHub from marking the workflow
|
||||
# as failed for PRs that don't have the `run-eval` label.
|
||||
noop:
|
||||
name: No-op
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: No-op
|
||||
run: echo "Nothing to do"
|
||||
|
||||
run_eval:
|
||||
timeout-minutes: 60
|
||||
name: Run Agent Eval
|
||||
if: >
|
||||
github.repository_owner == 'zed-industries' &&
|
||||
(github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-eval'))
|
||||
runs-on:
|
||||
- buildjet-16vcpu-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: Compile eval
|
||||
run: cargo build --package=eval
|
||||
|
||||
- name: Run eval
|
||||
run: cargo run --package=eval -- --repetitions=8 --concurrency=1
|
||||
|
||||
# Even the Linux runner is not stateful, in theory there is no need to do this cleanup.
|
||||
# But, to avoid potential issues in the future if we choose to use a stateful Linux runner and forget to add code
|
||||
# to clean up the config file, I’ve included the cleanup code here as a precaution.
|
||||
# While it’s not strictly necessary at this moment, I believe it’s better to err on the side of caution.
|
||||
- name: Clean CI config file
|
||||
if: always()
|
||||
run: rm -rf ./../.cargo
|
||||
2
.github/workflows/issue_response.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
version: 9
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: "20"
|
||||
cache: "pnpm"
|
||||
|
||||
2
.github/workflows/randomized_tests.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
- buildjet-16vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
|
||||
51
.github/workflows/release_nightly.yml
vendored
@@ -71,7 +71,7 @@ jobs:
|
||||
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
||||
steps:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
@@ -170,55 +170,6 @@ jobs:
|
||||
- name: Upload Zed Nightly
|
||||
run: script/upload-nightly linux-targz
|
||||
|
||||
bundle-nix:
|
||||
timeout-minutes: 60
|
||||
name: (${{ matrix.system.os }}) Nix Build
|
||||
continue-on-error: true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
system:
|
||||
- os: x86 Linux
|
||||
runner: buildjet-16vcpu-ubuntu-2204
|
||||
install_nix: true
|
||||
- os: arm Mac
|
||||
runner: [macOS, ARM64, test]
|
||||
install_nix: false
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: ${{ matrix.system.runner }}
|
||||
needs: tests
|
||||
env:
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
||||
GIT_LFS_SKIP_SMUDGE: 1 # breaks the livekit rust sdk examples which we don't actually depend on
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
# on our macs we manually install nix. for some reason the cachix action is running
|
||||
# under a non-login /bin/bash shell which doesn't source the proper script to add the
|
||||
# nix profile to PATH, so we manually add them here
|
||||
- name: Set path
|
||||
if: ${{ ! matrix.system.install_nix }}
|
||||
run: |
|
||||
echo "/nix/var/nix/profiles/default/bin" >> $GITHUB_PATH
|
||||
echo "/Users/administrator/.nix-profile/bin" >> $GITHUB_PATH
|
||||
|
||||
- uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31
|
||||
if: ${{ matrix.system.install_nix }}
|
||||
with:
|
||||
github_access_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
with:
|
||||
name: zed-industries
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
- run: nix build
|
||||
- name: Limit /nix/store to 50GB
|
||||
run: '[ $(du -sm /nix/store | cut -f1) -gt 50000 ] && nix-collect-garbage -d'
|
||||
|
||||
update-nightly-tag:
|
||||
name: Update nightly tag
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
|
||||
3
.gitignore
vendored
@@ -18,12 +18,12 @@
|
||||
.venv
|
||||
.vscode
|
||||
.wrangler
|
||||
/.direnv
|
||||
/assets/*licenses.*
|
||||
/crates/collab/seed.json
|
||||
/crates/theme/schemas/theme.json
|
||||
/crates/zed/resources/flatpak/flatpak-cargo-sources.json
|
||||
/dev.zed.Zed*.json
|
||||
/node_modules/
|
||||
/plugins/bin
|
||||
/script/node_modules
|
||||
/snap
|
||||
@@ -33,4 +33,5 @@ Packages
|
||||
xcuserdata/
|
||||
|
||||
# Don't commit any secrets to the repo.
|
||||
.env
|
||||
.env.secret.toml
|
||||
|
||||
126
.rules
Normal file
@@ -0,0 +1,126 @@
|
||||
# Rust coding guidelines
|
||||
|
||||
* Prioritize code correctness and clarity. Speed and efficiency are secondary priorities unless otherwise specified.
|
||||
* Do not write organizational or comments that summarize the code. Comments should only be written in order to explain "why" the code is written in some way in the case there is a reason that is tricky / non-obvious.
|
||||
* Prefer implementing functionality in existing files unless it is a new logical component. Avoid creating many small files.
|
||||
* Avoid using functions that panic like `unwrap()`, instead use mechanisms like `?` to propagate errors.
|
||||
* Be careful with operations like indexing which may panic if the indexes are out of bounds.
|
||||
* Never create files with `mod.rs` paths - prefer `src/some_module.rs` instead of `src/some_module/mod.rs`.
|
||||
|
||||
# GPUI
|
||||
|
||||
GPUI is a UI framework which also provides primitives for state and concurrency management.
|
||||
|
||||
## Context
|
||||
|
||||
Context types allow interaction with global state, windows, entities, and system services. They are typically passed to functions as the argument named `cx`. When a function takes callbacks they come after the `cx` parameter.
|
||||
|
||||
* `App` is the root context type, providing access to global state and read and update of entities.
|
||||
* `Context<T>` is provided when updating an `Entity<T>`. This context dereferences into `App`, so functions which take `&App` can also take `&Context<T>`.
|
||||
* `AsyncApp` and `AsyncWindowContext` are provided by `cx.spawn` and `cx.spawn_in`. These can be held across await points.
|
||||
|
||||
## `Window`
|
||||
|
||||
`Window` provides access to the state of an application window. It is passed to functions as an argument named `window` and comes before `cx` when present. It is used for managing focus, dispatching actions, directly drawing, getting user input state, etc.
|
||||
|
||||
## Entities
|
||||
|
||||
An `Entity<T>` is a handle to state of type `T`. With `thing: Entity<T>`:
|
||||
|
||||
* `thing.entity_id()` returns `EntityId`
|
||||
* `thing.downgrade()` returns `WeakEntity<T>`
|
||||
* `thing.read(cx: &App)` returns `&T`.
|
||||
* `thing.read_with(cx, |thing: &T, cx: &App| ...)` returns the closure's return value.
|
||||
* `thing.update(cx, |thing: &mut T, cx: &mut Context<T>| ...)` allows the closure to mutate the state, and provides a `Context<T>` for interacting with the entity. It returns the closure's return value.
|
||||
* `thing.update_in(cx, |thing: &mut T, window: &mut Window, cx: &mut Context<T>| ...)` takes a `AsyncWindowContext` or `VisualTestContext`. It's the same as `update` while also providing the `Window`.
|
||||
|
||||
Within the closures, the inner `cx` provided to the closure must be used instead of the outer `cx` to avoid issues with multiple borrows.
|
||||
|
||||
Trying to update an entity while it's already being updated must be avoided as this will cause a panic.
|
||||
|
||||
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.
|
||||
|
||||
## Concurrency
|
||||
|
||||
All use of entities and UI rendering occurs on a single foreground thread.
|
||||
|
||||
`cx.spawn(async move |cx| ...)` runs an async closure on the foreground thread. Within the closure, `cx` is an async context like `AsyncApp` or `AsyncWindowContext`.
|
||||
|
||||
When the outer cx is a `Context<T>`, the use of `spawn` instead looks like `cx.spawn(async move |handle, cx| ...)`, where `handle: WeakEntity<T>`.
|
||||
|
||||
To do work on other threads, `cx.background_spawn(async move { ... })` is used. Often this background task is awaited on by a foreground task which uses the results to update state.
|
||||
|
||||
Both `cx.spawn` and `cx.background_spawn` return a `Task<R>`, which is a future that can be awaited upon. If this task is dropped, then its work is cancelled. To prevent this one of the following must be done:
|
||||
|
||||
* Awaiting the task in some other async context.
|
||||
* Detaching the task via `task.detach()` or `task.detach_and_log_err(cx)`, allowing it to run indefinitely.
|
||||
* Storing the task in a field, if the work should be halted when the struct is dropped.
|
||||
|
||||
A task which doesn't do anything but provide a value can be created with `Task::ready(value)`.
|
||||
|
||||
## Elements
|
||||
|
||||
The `Render` trait is used to render some state into an element tree that is laid out using flexbox layout. An `Entity<T>` where `T` implements `Render` is sometimes called a "view".
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
struct TextWithBorder(SharedString);
|
||||
|
||||
impl Render for TextWithBorder {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div().border_1().child(self.0.clone())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Since `impl IntoElement for SharedString` exists, it can be used as an argument to `child`. `SharedString` is used to avoid copying strings, and is either an `&'static str` or `Arc<str>`.
|
||||
|
||||
UI components that are constructed just to be turned into elements can instead implement the `RenderOnce` trait, which is similar to `Render`, but its `render` method takes ownership of `self`. Types that implement this trait can use `#[derive(IntoElement)]` to use them directly as children.
|
||||
|
||||
The style methods on elements are similar to those used by Tailwind CSS.
|
||||
|
||||
If some attributes or children of an element tree are conditional, `.when(condition, |this| ...)` can be used to run the closure only when `condition` is true. Similarly, `.when_some(option, |this, value| ...)` runs the closure when the `Option` has a value.
|
||||
|
||||
## Input events
|
||||
|
||||
Input event handlers can be registered on an element via methods like `.on_click(|event, window, cx: &mut App| ...)`.
|
||||
|
||||
Often event handlers will want to update the entity that's in the current `Context<T>`. The `cx.listener` method provides this - its use looks like `.on_click(cx.listener(|this: &mut T, event, window, cx: &mut Context<T>| ...)`.
|
||||
|
||||
## Actions
|
||||
|
||||
Actions are dispatched via user keyboard interaction or in code via `window.dispatch_action(SomeAction.boxed_clone(), cx)` or `focus_handle.dispatch_action(&SomeAction, window, cx)`.
|
||||
|
||||
Actions which have no data inside are created and registered with the `actions!(some_namespace, [SomeAction, AnotherAction])` macro call.
|
||||
|
||||
Actions that do have data must implement `Clone, Default, PartialEq, Deserialize, JsonSchema` and can be registered with an `impl_actions!(some_namespace, [SomeActionWithData])` macro call.
|
||||
|
||||
Action handlers can be registered on an element via the event handler `.on_action(|action, window, cx| ...)`. Like other event handlers, this is often used with `cx.listener`.
|
||||
|
||||
## Notify
|
||||
|
||||
When a view's state has changed in a way that may affect its rendering, it should call `cx.notify()`. This will cause the view to be rerendered. It will also cause any observe callbacks registered for the entity with `cx.observe` to be called.
|
||||
|
||||
## Entity events
|
||||
|
||||
While updating an entity (`cx: Context<T>`), it can emit an event using `cx.emit(event)`. Entities register which events they can emit by declaring `impl EventEmittor<EventType> for EntityType {}`.
|
||||
|
||||
Other entities can then register a callback to handle these events by doing `cx.subscribe(other_entity, |this, other_entity, event, cx| ...)`. This will return a `Subscription` which deregisters the callback when dropped. Typically `cx.subscribe` happens when creating a new entity and the subscriptions are stored in a `_subscriptions: Vec<Subscription>` field.
|
||||
|
||||
## Recent API changes
|
||||
|
||||
GPUI has had some changes to its APIs. Always write code using the new APIs:
|
||||
|
||||
* `spawn` methods now take async closures (`AsyncFn`), and so should be called like `cx.spawn(async move |cx| ...)`.
|
||||
* Use `Entity<T>`. This replaces `Model<T>` and `View<T>` which longer exists and should NEVER be used.
|
||||
* Use `App` references. This replaces `AppContext` which no longer exists and should NEVER be used.
|
||||
* Use `Context<T>` references. This replaces `ModelContext<T>` which no longer exists and should NEVER be used.
|
||||
* `Window` is now passed around explicitly. The new interface adds a `Window` reference parameter to some methods, and adds some new "*_in" methods for plumbing `Window`. The old types `WindowContext` and `ViewContext<T>` should NEVER be used.
|
||||
|
||||
|
||||
## General guidelines
|
||||
|
||||
- Use `./script/clippy` instead of `cargo clippy`
|
||||
1
.windsurfrules
Symbolic link
@@ -0,0 +1 @@
|
||||
.rules
|
||||
@@ -1,13 +1,13 @@
|
||||
[
|
||||
{
|
||||
"label": "Debug Zed with LLDB",
|
||||
"adapter": "LLDB",
|
||||
"label": "Debug Zed (CodeLLDB)",
|
||||
"adapter": "CodeLLDB",
|
||||
"program": "$ZED_WORKTREE_ROOT/target/debug/zed",
|
||||
"request": "launch",
|
||||
"cwd": "$ZED_WORKTREE_ROOT"
|
||||
},
|
||||
{
|
||||
"label": "Debug Zed with GDB",
|
||||
"label": "Debug Zed (GDB)",
|
||||
"adapter": "GDB",
|
||||
"program": "$ZED_WORKTREE_ROOT/target/debug/zed",
|
||||
"request": "launch",
|
||||
|
||||
@@ -45,5 +45,18 @@
|
||||
"hard_tabs": false,
|
||||
"formatter": "auto",
|
||||
"remove_trailing_whitespace_on_save": true,
|
||||
"ensure_final_newline_on_save": true
|
||||
"ensure_final_newline_on_save": true,
|
||||
"file_scan_exclusions": [
|
||||
"crates/eval/worktrees/",
|
||||
"crates/eval/repos/",
|
||||
"**/.git",
|
||||
"**/.svn",
|
||||
"**/.hg",
|
||||
"**/.jj",
|
||||
"**/CVS",
|
||||
"**/.DS_Store",
|
||||
"**/Thumbs.db",
|
||||
"**/.classpath",
|
||||
"**/.settings"
|
||||
]
|
||||
}
|
||||
|
||||
1033
Cargo.lock
generated
88
Cargo.toml
@@ -8,7 +8,6 @@ members = [
|
||||
"crates/assets",
|
||||
"crates/assistant",
|
||||
"crates/assistant_context_editor",
|
||||
"crates/agent_eval",
|
||||
"crates/assistant_settings",
|
||||
"crates/assistant_slash_command",
|
||||
"crates/assistant_slash_commands",
|
||||
@@ -16,6 +15,7 @@ members = [
|
||||
"crates/assistant_tools",
|
||||
"crates/audio",
|
||||
"crates/auto_update",
|
||||
"crates/auto_update_helper",
|
||||
"crates/auto_update_ui",
|
||||
"crates/aws_http_client",
|
||||
"crates/bedrock",
|
||||
@@ -39,14 +39,14 @@ members = [
|
||||
"crates/credentials_provider",
|
||||
"crates/dap",
|
||||
"crates/dap_adapters",
|
||||
"crates/db",
|
||||
"crates/debugger_tools",
|
||||
"crates/debugger_ui",
|
||||
"crates/db",
|
||||
"crates/deepseek",
|
||||
"crates/diagnostics",
|
||||
"crates/docs_preprocessor",
|
||||
"crates/editor",
|
||||
"crates/evals",
|
||||
"crates/eval",
|
||||
"crates/extension",
|
||||
"crates/extension_api",
|
||||
"crates/extension_cli",
|
||||
@@ -109,7 +109,6 @@ members = [
|
||||
"crates/project",
|
||||
"crates/project_panel",
|
||||
"crates/project_symbols",
|
||||
"crates/prompt_library",
|
||||
"crates/prompt_store",
|
||||
"crates/proto",
|
||||
"crates/recent_projects",
|
||||
@@ -123,6 +122,7 @@ members = [
|
||||
"crates/rich_text",
|
||||
"crates/rope",
|
||||
"crates/rpc",
|
||||
"crates/rules_library",
|
||||
"crates/schema_generator",
|
||||
"crates/search",
|
||||
"crates/semantic_index",
|
||||
@@ -164,6 +164,8 @@ members = [
|
||||
"crates/util_macros",
|
||||
"crates/vim",
|
||||
"crates/vim_mode_setting",
|
||||
"crates/web_search",
|
||||
"crates/web_search_providers",
|
||||
"crates/welcome",
|
||||
"crates/workspace",
|
||||
"crates/worktree",
|
||||
@@ -215,7 +217,6 @@ askpass = { path = "crates/askpass" }
|
||||
assets = { path = "crates/assets" }
|
||||
assistant = { path = "crates/assistant" }
|
||||
assistant_context_editor = { path = "crates/assistant_context_editor" }
|
||||
assistant_eval = { path = "crates/agent_eval" }
|
||||
assistant_settings = { path = "crates/assistant_settings" }
|
||||
assistant_slash_command = { path = "crates/assistant_slash_command" }
|
||||
assistant_slash_commands = { path = "crates/assistant_slash_commands" }
|
||||
@@ -223,10 +224,12 @@ assistant_tool = { path = "crates/assistant_tool" }
|
||||
assistant_tools = { path = "crates/assistant_tools" }
|
||||
audio = { path = "crates/audio" }
|
||||
auto_update = { path = "crates/auto_update" }
|
||||
auto_update_helper = { path = "crates/auto_update_helper" }
|
||||
auto_update_ui = { path = "crates/auto_update_ui" }
|
||||
aws_http_client = { path = "crates/aws_http_client" }
|
||||
bedrock = { path = "crates/bedrock" }
|
||||
breadcrumbs = { path = "crates/breadcrumbs" }
|
||||
buffer_diff = { path = "crates/buffer_diff" }
|
||||
call = { path = "crates/call" }
|
||||
channel = { path = "crates/channel" }
|
||||
cli = { path = "crates/cli" }
|
||||
@@ -246,11 +249,10 @@ credentials_provider = { path = "crates/credentials_provider" }
|
||||
dap = { path = "crates/dap" }
|
||||
dap_adapters = { path = "crates/dap_adapters" }
|
||||
db = { path = "crates/db" }
|
||||
debugger_ui = { path = "crates/debugger_ui" }
|
||||
debugger_tools = { path = "crates/debugger_tools" }
|
||||
debugger_ui = { path = "crates/debugger_ui" }
|
||||
deepseek = { path = "crates/deepseek" }
|
||||
diagnostics = { path = "crates/diagnostics" }
|
||||
buffer_diff = { path = "crates/buffer_diff" }
|
||||
editor = { path = "crates/editor" }
|
||||
extension = { path = "crates/extension" }
|
||||
extension_host = { path = "crates/extension_host" }
|
||||
@@ -307,8 +309,8 @@ ollama = { path = "crates/ollama" }
|
||||
open_ai = { path = "crates/open_ai" }
|
||||
outline = { path = "crates/outline" }
|
||||
outline_panel = { path = "crates/outline_panel" }
|
||||
paths = { path = "crates/paths" }
|
||||
panel = { path = "crates/panel" }
|
||||
paths = { path = "crates/paths" }
|
||||
picker = { path = "crates/picker" }
|
||||
plugin = { path = "crates/plugin" }
|
||||
plugin_macros = { path = "crates/plugin_macros" }
|
||||
@@ -316,7 +318,6 @@ prettier = { path = "crates/prettier" }
|
||||
project = { path = "crates/project" }
|
||||
project_panel = { path = "crates/project_panel" }
|
||||
project_symbols = { path = "crates/project_symbols" }
|
||||
prompt_library = { path = "crates/prompt_library" }
|
||||
prompt_store = { path = "crates/prompt_store" }
|
||||
proto = { path = "crates/proto" }
|
||||
recent_projects = { path = "crates/recent_projects" }
|
||||
@@ -329,6 +330,7 @@ reqwest_client = { path = "crates/reqwest_client" }
|
||||
rich_text = { path = "crates/rich_text" }
|
||||
rope = { path = "crates/rope" }
|
||||
rpc = { path = "crates/rpc" }
|
||||
rules_library = { path = "crates/rules_library" }
|
||||
search = { path = "crates/search" }
|
||||
semantic_index = { path = "crates/semantic_index" }
|
||||
semantic_version = { path = "crates/semantic_version" }
|
||||
@@ -369,6 +371,8 @@ util = { path = "crates/util" }
|
||||
util_macros = { path = "crates/util_macros" }
|
||||
vim = { path = "crates/vim" }
|
||||
vim_mode_setting = { path = "crates/vim_mode_setting" }
|
||||
web_search = { path = "crates/web_search" }
|
||||
web_search_providers = { path = "crates/web_search_providers" }
|
||||
welcome = { path = "crates/welcome" }
|
||||
workspace = { path = "crates/workspace" }
|
||||
worktree = { path = "crates/worktree" }
|
||||
@@ -400,8 +404,12 @@ async-tungstenite = "0.29.1"
|
||||
async-watch = "0.3.1"
|
||||
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
|
||||
aws-config = { version = "1.6.1", features = ["behavior-version-latest"] }
|
||||
aws-credential-types = { version = "1.2.2", features = ["hardcoded-credentials"] }
|
||||
aws-sdk-bedrockruntime = { version = "1.80.0", features = ["behavior-version-latest"] }
|
||||
aws-credential-types = { version = "1.2.2", features = [
|
||||
"hardcoded-credentials",
|
||||
] }
|
||||
aws-sdk-bedrockruntime = { version = "1.80.0", features = [
|
||||
"behavior-version-latest",
|
||||
] }
|
||||
aws-smithy-runtime-api = { version = "1.7.4", features = ["http-1x", "client"] }
|
||||
aws-smithy-types = { version = "1.3.0", features = ["http-body-1-x"] }
|
||||
base64 = "0.22"
|
||||
@@ -409,7 +417,6 @@ bitflags = "2.6.0"
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "b16f5c7bd873c7126f48c82c39e7ae64602ae74f" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "b16f5c7bd873c7126f48c82c39e7ae64602ae74f" }
|
||||
blade-util = { git = "https://github.com/kvark/blade", rev = "b16f5c7bd873c7126f48c82c39e7ae64602ae74f" }
|
||||
naga = { version = "23.1.0", features = ["wgsl-in"] }
|
||||
blake3 = "1.5.3"
|
||||
bytes = "1.0"
|
||||
cargo_metadata = "0.19"
|
||||
@@ -419,15 +426,17 @@ circular-buffer = "1.0"
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
cocoa = "0.26"
|
||||
cocoa-foundation = "0.2.0"
|
||||
core-video = { version = "0.4.3", features = ["metal"] }
|
||||
convert_case = "0.8.0"
|
||||
core-foundation = "0.10.0"
|
||||
core-foundation-sys = "0.8.6"
|
||||
core-video = { version = "0.4.3", features = ["metal"] }
|
||||
ctor = "0.4.0"
|
||||
dashmap = "6.0"
|
||||
dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "be69a016ba710191b9fdded28c8b042af4b617f7" }
|
||||
dashmap = "6.0"
|
||||
derive_more = "0.99.17"
|
||||
dirs = "4.0"
|
||||
documented = "0.9.1"
|
||||
dotenv = "0.15.0"
|
||||
ec4rs = "1.1"
|
||||
emojis = "0.6.1"
|
||||
env_logger = "0.11"
|
||||
@@ -440,11 +449,12 @@ futures-lite = "1.13"
|
||||
git2 = { version = "0.20.1", default-features = false }
|
||||
globset = "0.4"
|
||||
handlebars = "4.3"
|
||||
heck = "0.5"
|
||||
heed = { version = "0.21.0", features = ["read-txn-no-tls"] }
|
||||
hex = "0.4.3"
|
||||
html5ever = "0.27.0"
|
||||
hyper = "0.14"
|
||||
http = "1.1"
|
||||
hyper = "0.14"
|
||||
ignore = "0.4.22"
|
||||
image = "0.25.1"
|
||||
imara-diff = "0.1.8"
|
||||
@@ -460,23 +470,27 @@ libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
|
||||
linkify = "0.10.0"
|
||||
linkme = "0.3.31"
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
|
||||
lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "c9c189f1c5dd53c624a419ce35bc77ad6a908d18" }
|
||||
markup5ever_rcdom = "0.3.0"
|
||||
metal = "0.29"
|
||||
mlua = { version = "0.10", features = ["lua54", "vendored", "async", "send"] }
|
||||
naga = { version = "23.1.0", features = ["wgsl-in"] }
|
||||
nanoid = "0.4"
|
||||
nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
|
||||
nix = "0.29"
|
||||
num-format = "0.4.4"
|
||||
objc = "0.2"
|
||||
open = "5.0.0"
|
||||
num-format = "0.4.4"
|
||||
ordered-float = "2.1.1"
|
||||
palette = { version = "0.7.5", default-features = false, features = ["std"] }
|
||||
parking_lot = "0.12.1"
|
||||
partial-json-fixer = "0.5.3"
|
||||
pathdiff = "0.2"
|
||||
pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
|
||||
pet-fs = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
|
||||
pet-pixi = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
|
||||
pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
|
||||
pet-core = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
|
||||
pet-fs = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
|
||||
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" }
|
||||
postage = { version = "0.5", features = ["futures-traits"] }
|
||||
@@ -490,9 +504,10 @@ pulldown-cmark = { version = "0.12.0", default-features = false }
|
||||
quote = "1.0.9"
|
||||
rand = "0.8.5"
|
||||
rayon = "1.8"
|
||||
ref-cast = "1.0.24"
|
||||
regex = "1.5"
|
||||
repair_json = "0.1.0"
|
||||
reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "fd110f6998da16bbca97b6dddda9be7827c50e29", default-features = false, features = [
|
||||
reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "951c770a32f1998d6e999cef3e59e0013e6c4415", default-features = false, features = [
|
||||
"charset",
|
||||
"http2",
|
||||
"macos-system-configuration",
|
||||
@@ -504,10 +519,10 @@ rsa = "0.9.6"
|
||||
runtimelib = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734", default-features = false, features = [
|
||||
"async-dispatcher-runtime",
|
||||
] }
|
||||
rustc-demangle = "0.1.23"
|
||||
rust-embed = { version = "8.4", features = ["include-exclude"] }
|
||||
rustc-demangle = "0.1.23"
|
||||
rustc-hash = "2.1.0"
|
||||
rustls = { version = "0.23.22" }
|
||||
rustls = { version = "0.23.26" }
|
||||
rustls-platform-verifier = "0.5.0"
|
||||
scap = { git = "https://github.com/zed-industries/scap", rev = "08f0a01417505cc0990b9931a37e5120db92e0d0", default-features = false }
|
||||
schemars = { version = "0.8", features = ["impl_json_schema", "indexmap2"] }
|
||||
@@ -530,7 +545,7 @@ smol = "2.0"
|
||||
sqlformat = "0.2"
|
||||
streaming-iterator = "0.1"
|
||||
strsim = "0.11"
|
||||
strum = { version = "0.26.0", features = ["derive"] }
|
||||
strum = { version = "0.27.0", features = ["derive"] }
|
||||
subtle = "2.5.0"
|
||||
syn = { version = "1.0.72", features = ["full", "extra-traits"] }
|
||||
sys-locale = "0.3.1"
|
||||
@@ -547,15 +562,16 @@ time = { version = "0.3", features = [
|
||||
"formatting",
|
||||
] }
|
||||
tiny_http = "0.8"
|
||||
toml = "0.8"
|
||||
tokio = { version = "1" }
|
||||
tokio-tungstenite = { version = "0.26", features = ["__rustls-tls"] }
|
||||
toml = "0.8"
|
||||
tower-http = "0.4.4"
|
||||
tree-sitter = { version = "0.25.3", features = ["wasm"] }
|
||||
tree-sitter-bash = "0.23"
|
||||
tree-sitter-c = "0.23"
|
||||
tree-sitter-cpp = "0.23"
|
||||
tree-sitter-css = "0.23"
|
||||
tree-sitter-diff = "0.1.0"
|
||||
tree-sitter-elixir = "0.3"
|
||||
tree-sitter-embedded-template = "0.23.0"
|
||||
tree-sitter-gitcommit = { git = "https://github.com/zed-industries/tree-sitter-git-commit", rev = "88309716a69dd13ab83443721ba6e0b491d37ee9" }
|
||||
@@ -563,7 +579,6 @@ tree-sitter-go = "0.23"
|
||||
tree-sitter-go-mod = { git = "https://github.com/camdencheek/tree-sitter-go-mod", rev = "6efb59652d30e0e9cd5f3b3a669afd6f1a926d3c", package = "tree-sitter-gomod" }
|
||||
tree-sitter-gowork = { git = "https://github.com/zed-industries/tree-sitter-go-work", rev = "acb0617bf7f4fda02c6217676cc64acb89536dc7" }
|
||||
tree-sitter-heex = { git = "https://github.com/zed-industries/tree-sitter-heex", rev = "1dd45142fbb05562e35b2040c6129c9bca346592" }
|
||||
tree-sitter-diff = "0.1.0"
|
||||
tree-sitter-html = "0.23"
|
||||
tree-sitter-jsdoc = "0.23"
|
||||
tree-sitter-json = "0.24"
|
||||
@@ -575,15 +590,15 @@ tree-sitter-rust = "0.24"
|
||||
tree-sitter-typescript = "0.23"
|
||||
tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "baff0b51c64ef6a1fb1f8390f3ad6015b83ec13a" }
|
||||
unicase = "2.6"
|
||||
unindent = "0.2.0"
|
||||
unicode-segmentation = "1.10"
|
||||
unicode-script = "0.5.7"
|
||||
unicode-segmentation = "1.10"
|
||||
unindent = "0.2.0"
|
||||
url = "2.2"
|
||||
urlencoding = "2.1.2"
|
||||
uuid = { version = "1.1.2", features = ["v4", "v5", "v7", "serde"] }
|
||||
walkdir = "2.3"
|
||||
wasmparser = "0.221"
|
||||
wasm-encoder = "0.221"
|
||||
wasmparser = "0.221"
|
||||
wasmtime = { version = "29", default-features = false, features = [
|
||||
"async",
|
||||
"demangle",
|
||||
@@ -595,9 +610,8 @@ wasmtime-wasi = "29"
|
||||
which = "6.0.0"
|
||||
wit-component = "0.221"
|
||||
workspace-hack = "0.1.0"
|
||||
zed_llm_client = "0.4"
|
||||
zed_llm_client = "0.7.1"
|
||||
zstd = "0.11"
|
||||
metal = "0.29"
|
||||
|
||||
[workspace.dependencies.async-stripe]
|
||||
git = "https://github.com/zed-industries/async-stripe"
|
||||
@@ -616,12 +630,10 @@ features = [
|
||||
[workspace.dependencies.windows]
|
||||
version = "0.61"
|
||||
features = [
|
||||
"Foundation_Collections",
|
||||
"Foundation_Numerics",
|
||||
"Storage_Search",
|
||||
"Storage_Streams",
|
||||
"System_Threading",
|
||||
"UI_StartScreen",
|
||||
"UI_ViewManagement",
|
||||
"Wdk_System_SystemServices",
|
||||
"Win32_Globalization",
|
||||
@@ -648,6 +660,7 @@ features = [
|
||||
"Win32_System_SystemInformation",
|
||||
"Win32_System_SystemServices",
|
||||
"Win32_System_Threading",
|
||||
"Win32_System_Variant",
|
||||
"Win32_System_WinRT",
|
||||
"Win32_UI_Controls",
|
||||
"Win32_UI_HiDpi",
|
||||
@@ -655,6 +668,7 @@ features = [
|
||||
"Win32_UI_Input_KeyboardAndMouse",
|
||||
"Win32_UI_Shell",
|
||||
"Win32_UI_Shell_Common",
|
||||
"Win32_UI_Shell_PropertiesSystem",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
]
|
||||
|
||||
@@ -686,7 +700,6 @@ breadcrumbs = { codegen-units = 1 }
|
||||
collections = { codegen-units = 1 }
|
||||
command_palette = { codegen-units = 1 }
|
||||
command_palette_hooks = { codegen-units = 1 }
|
||||
evals = { codegen-units = 1 }
|
||||
extension_cli = { codegen-units = 1 }
|
||||
feature_flags = { codegen-units = 1 }
|
||||
file_icons = { codegen-units = 1 }
|
||||
@@ -778,4 +791,13 @@ let_underscore_future = "allow"
|
||||
too_many_arguments = "allow"
|
||||
|
||||
[workspace.metadata.cargo-machete]
|
||||
ignored = ["bindgen", "cbindgen", "prost_build", "serde", "component", "linkme", "workspace-hack"]
|
||||
ignored = [
|
||||
"bindgen",
|
||||
"cbindgen",
|
||||
"prost_build",
|
||||
"serde",
|
||||
"component",
|
||||
"linkme",
|
||||
"documented",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="16" height="16" rx="2" fill="black" fill-opacity="0.2"/>
|
||||
<g clip-path="url(#clip0_1916_18)">
|
||||
<path d="M10.652 3.79999H8.816L12.164 12.2H14L10.652 3.79999Z" fill="#1F1F1E"/>
|
||||
<path d="M5.348 3.79999L2 12.2H3.872L4.55672 10.436H8.05927L8.744 12.2H10.616L7.268 3.79999H5.348ZM5.16224 8.87599L6.308 5.92399L7.45374 8.87599H5.16224Z" fill="#1F1F1E"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1916_18">
|
||||
<rect width="12" height="8.4" fill="white" transform="translate(2 3.79999)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 601 B |
1
assets/icons/binary.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-binary-icon lucide-binary"><rect x="14" y="14" width="4" height="6" rx="2"/><rect x="6" y="4" width="4" height="6" rx="2"/><path d="M6 20h4"/><path d="M14 10h4"/><path d="M6 14h2v6"/><path d="M14 4h2v6"/></svg>
|
||||
|
After Width: | Height: | Size: 413 B |
1
assets/icons/file_icons/vyper.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" fill="none" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g style="fill:#000;fill-opacity:1" fill="#180c25"><path d="m-116.1-101.4-28.9-28.9a6.7 6.7 0 0 1-1.8-4.7v-41.2c0-2.4-2.4-4.8-4.8-4.8h-9.6a5.2 5.2 0 0 0-4.8 4.8v48c0 2.5 1 5 2.7 6.8l33.6 33.6a9.6 9.6 0 0 0 6.8 2.8h4.8c2.7 0 4.8-2.2 4.8-4.8v-4.8c0-2.5-1-5-2.8-6.8zM-79.6-176.2c0-2.4-2.4-4.8-4.8-4.8h-9.7a5.2 5.2 0 0 0-4.7 4.8v41.2c0 1.8-.8 3.5-2 4.7l-9.6 9.7a9.5 9.5 0 0 0-2.8 6.8v4.8c0 2.6 2.1 4.7 4.8 4.7h4.8c2.4 0 4.9-.9 6.7-2.8l14.4-14.3a9.6 9.6 0 0 0 2.8-6.8v-48z" style="fill:#000;fill-opacity:1;stroke-width:.255894" transform="translate(21.6 22.7) scale(.11067)"/></g></svg>
|
||||
|
After Width: | Height: | Size: 677 B |
1
assets/icons/flame.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-flame-icon lucide-flame"><path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"/></svg>
|
||||
|
After Width: | Height: | Size: 415 B |
1
assets/icons/function.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-function-icon lucide-square-function"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><path d="M9 17c2 0 2.8-1 2.8-2.8V10c0-2 1-3.3 3.2-3"/><path d="M9 11.2h5.7"/></svg>
|
||||
|
After Width: | Height: | Size: 387 B |
1
assets/icons/image.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-image-icon lucide-image"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg>
|
||||
|
After Width: | Height: | Size: 372 B |
5
assets/icons/layout.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20 14H4C3.44772 14 3 14.4477 3 15V20C3 20.5523 3.44772 21 4 21H20C20.5523 21 21 20.5523 21 20V15C21 14.4477 20.5523 14 20 14Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M11 3H4C3.44772 3 3 3.44772 3 4V9C3 9.55228 3.44772 10 4 10H11C11.5523 10 12 9.55228 12 9V4C12 3.44772 11.5523 3 11 3Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M20 3H17C16.4477 3 16 3.44772 16 4V9C16 9.55228 16.4477 10 17 10H20C20.5523 10 21 9.55228 21 9V4C21 3.44772 20.5523 3 20 3Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 746 B |
3
assets/icons/light_bulb.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.1331 11.3776C10.2754 10.6665 10.1331 9.78593 11.1998 8.53327C11.82 7.80489 12.2664 6.96894 12.2664 6.04456C12.2664 4.91305 11.8169 3.82788 11.0168 3.02778C10.2167 2.22769 9.13152 1.7782 8.00001 1.7782C6.8685 1.7782 5.78334 2.22769 4.98324 3.02778C4.18314 3.82788 3.73364 4.91305 3.73364 6.04456C3.73364 6.75562 3.87586 7.6089 4.80024 8.53327C5.86683 9.80679 5.72462 10.6665 5.86683 11.3776M10.1331 11.3776V12.8821C10.1331 13.622 9.53341 14.2218 8.79353 14.2218H7.2065C6.46662 14.2218 5.86683 13.622 5.86683 12.8821V11.3776M10.1331 11.3776H5.86683" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 751 B |
5
assets/icons/menu_alt.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 12H16" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4 6H20" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4 18H12" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 402 B |
14
assets/icons/zed_max_mode.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2489_484)">
|
||||
<path d="M11 8.9V11C8.51716 11 7.48284 11 5 11V10.4L11 5.6V5H5V7.1" stroke="black" stroke-width="1.5"/>
|
||||
<path d="M1.5 5.5V1.5H5" stroke="black" stroke-opacity="0.5" stroke-width="1.5"/>
|
||||
<path d="M14.5 5.5V1.5H11" stroke="black" stroke-opacity="0.5" stroke-width="1.5"/>
|
||||
<path d="M1.5 10.5V14.5H5" stroke="black" stroke-opacity="0.5" stroke-width="1.5"/>
|
||||
<path d="M14.5 10.5V14.5H11" stroke="black" stroke-opacity="0.5" stroke-width="1.5"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2489_484">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 687 B |
@@ -49,15 +49,6 @@
|
||||
"down": "menu::SelectNext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Prompt",
|
||||
"bindings": {
|
||||
"left": "menu::SelectPrevious",
|
||||
"right": "menu::SelectNext",
|
||||
"h": "menu::SelectPrevious",
|
||||
"l": "menu::SelectNext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
@@ -134,23 +125,9 @@
|
||||
"shift-f10": "editor::OpenContextMenu",
|
||||
"ctrl-shift-e": "editor::ToggleEditPrediction",
|
||||
"f9": "editor::ToggleBreakpoint",
|
||||
"shift-f9": "editor::EditLogBreakpoint"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && !agent_diff",
|
||||
"bindings": {
|
||||
"ctrl-k ctrl-r": "git::Restore",
|
||||
"ctrl-alt-y": "git::ToggleStaged",
|
||||
"alt-y": "git::StageAndNext",
|
||||
"alt-shift-y": "git::UnstageAndNext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentDiff",
|
||||
"bindings": {
|
||||
"ctrl-y": "agent::Keep",
|
||||
"ctrl-k ctrl-r": "agent::Reject"
|
||||
"shift-f9": "editor::EditLogBreakpoint",
|
||||
"ctrl-shift-backspace": "editor::GoToPreviousChange",
|
||||
"ctrl-shift-alt-backspace": "editor::GoToNextChange"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -201,6 +178,31 @@
|
||||
"ctrl-c": "markdown::Copy"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && jupyter && !ContextEditor",
|
||||
"bindings": {
|
||||
"ctrl-shift-enter": "repl::Run",
|
||||
"ctrl-alt-enter": "repl::RunInPlace"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && !agent_diff",
|
||||
"bindings": {
|
||||
"ctrl-k ctrl-r": "git::Restore",
|
||||
"ctrl-alt-y": "git::ToggleStaged",
|
||||
"alt-y": "git::StageAndNext",
|
||||
"alt-shift-y": "git::UnstageAndNext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentDiff",
|
||||
"bindings": {
|
||||
"ctrl-y": "agent::Keep",
|
||||
"ctrl-n": "agent::Reject",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AssistantPanel",
|
||||
"bindings": {
|
||||
@@ -210,18 +212,113 @@
|
||||
"ctrl-shift-g": "search::SelectPreviousMatch",
|
||||
"ctrl-alt-/": "assistant::ToggleModelSelector",
|
||||
"ctrl-k h": "assistant::DeployHistory",
|
||||
"ctrl-k l": "assistant::OpenPromptLibrary",
|
||||
"ctrl-k l": "assistant::OpenRulesLibrary",
|
||||
"new": "assistant::NewChat",
|
||||
"ctrl-t": "assistant::NewChat",
|
||||
"ctrl-n": "assistant::NewChat"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ContextEditor > Editor",
|
||||
"bindings": {
|
||||
"ctrl-enter": "assistant::Assist",
|
||||
"ctrl-shift-enter": "assistant::Edit",
|
||||
"ctrl-s": "workspace::Save",
|
||||
"save": "workspace::Save",
|
||||
"ctrl->": "assistant::QuoteSelection",
|
||||
"ctrl-<": "assistant::InsertIntoEditor",
|
||||
"ctrl-alt-/": "assistant::ToggleModelSelector",
|
||||
"shift-enter": "assistant::Split",
|
||||
"ctrl-r": "assistant::CycleMessageRole",
|
||||
"enter": "assistant::ConfirmCommand",
|
||||
"alt-enter": "editor::Newline"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel",
|
||||
"bindings": {
|
||||
"ctrl-n": "agent::NewThread",
|
||||
"ctrl-alt-n": "agent::NewTextThread",
|
||||
"ctrl-shift-h": "agent::OpenHistory",
|
||||
"ctrl-alt-c": "agent::OpenConfiguration",
|
||||
"ctrl-alt-p": "assistant::OpenRulesLibrary",
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"ctrl-alt-/": "assistant::ToggleModelSelector",
|
||||
"ctrl-shift-a": "agent::ToggleContextPicker",
|
||||
"ctrl-shift-o": "agent::ToggleNavigationMenu",
|
||||
"ctrl-shift-i": "agent::ToggleOptionsMenu",
|
||||
"shift-escape": "agent::ExpandMessageEditor",
|
||||
"ctrl-e": "agent::ChatMode",
|
||||
"ctrl-alt-e": "agent::RemoveAllContext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel > NavigationMenu",
|
||||
"bindings": {
|
||||
"shift-backspace": "agent::DeleteRecentlyOpenThread"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel > Markdown",
|
||||
"bindings": {
|
||||
"copy": "markdown::CopyAsMarkdown",
|
||||
"ctrl-c": "markdown::CopyAsMarkdown"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel && prompt_editor",
|
||||
"bindings": {
|
||||
"cmd-n": "agent::NewTextThread",
|
||||
"cmd-alt-t": "agent::NewThread"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "MessageEditor > Editor",
|
||||
"bindings": {
|
||||
"enter": "agent::Chat",
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "EditMessageEditor > Editor",
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel",
|
||||
"enter": "menu::Confirm",
|
||||
"alt-enter": "editor::Newline"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentFeedbackMessageEditor > Editor",
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel",
|
||||
"enter": "menu::Confirm",
|
||||
"alt-enter": "editor::Newline"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ContextStrip",
|
||||
"bindings": {
|
||||
"up": "agent::FocusUp",
|
||||
"right": "agent::FocusRight",
|
||||
"left": "agent::FocusLeft",
|
||||
"down": "agent::FocusDown",
|
||||
"backspace": "agent::RemoveFocusedContext",
|
||||
"enter": "agent::AcceptSuggestedContext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ThreadHistory",
|
||||
"bindings": {
|
||||
"backspace": "agent::RemoveSelectedThread"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "PromptLibrary",
|
||||
"bindings": {
|
||||
"new": "prompt_library::NewPrompt",
|
||||
"ctrl-n": "prompt_library::NewPrompt",
|
||||
"ctrl-shift-s": "prompt_library::ToggleDefaultPrompt"
|
||||
"new": "rules_library::NewRule",
|
||||
"ctrl-n": "rules_library::NewRule",
|
||||
"ctrl-shift-s": "rules_library::ToggleDefaultRule"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -352,11 +449,11 @@
|
||||
"alt-shift-left": "editor::SelectSmallerSyntaxNode", // Shrink Selection
|
||||
"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 }],
|
||||
"ctrl-shift-down": ["editor::SelectNext", { "replace_newest": false }], // Add selection to Next Find Match
|
||||
"ctrl-shift-up": ["editor::SelectPrevious", { "replace_newest": false }],
|
||||
"ctrl-k ctrl-d": ["editor::SelectNext", { "replace_newest": true }],
|
||||
"ctrl-k ctrl-shift-d": ["editor::SelectPrevious", { "replace_newest": true }],
|
||||
"ctrl-d": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch / find_under_expand
|
||||
"ctrl-shift-down": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch
|
||||
"ctrl-shift-up": ["editor::SelectPrevious", { "replace_newest": false }], // editor.action.addSelectionToPreviousFindMatch
|
||||
"ctrl-k ctrl-d": ["editor::SelectNext", { "replace_newest": true }], // editor.action.moveSelectionToNextFindMatch / find_under_expand_skip
|
||||
"ctrl-k ctrl-shift-d": ["editor::SelectPrevious", { "replace_newest": true }], // editor.action.moveSelectionToPreviousFindMatch
|
||||
"ctrl-k ctrl-i": "editor::Hover",
|
||||
"ctrl-/": ["editor::ToggleComments", { "advance_downwards": false }],
|
||||
"ctrl-u": "editor::UndoSelection",
|
||||
@@ -431,6 +528,7 @@
|
||||
"shift-new": "workspace::NewWindow",
|
||||
"ctrl-shift-n": "workspace::NewWindow",
|
||||
"ctrl-`": "terminal_panel::ToggleFocus",
|
||||
"f10": ["app_menu::OpenApplicationMenu", "Zed"],
|
||||
"alt-1": ["workspace::ActivatePane", 0],
|
||||
"alt-2": ["workspace::ActivatePane", 1],
|
||||
"alt-3": ["workspace::ActivatePane", 2],
|
||||
@@ -489,6 +587,7 @@
|
||||
{
|
||||
"context": "ApplicationMenu",
|
||||
"bindings": {
|
||||
"f10": "menu::Cancel",
|
||||
"left": "app_menu::ActivateMenuLeft",
|
||||
"right": "app_menu::ActivateMenuRight"
|
||||
}
|
||||
@@ -586,7 +685,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"context": "!ContextEditor > Editor && mode == full",
|
||||
"bindings": {
|
||||
"alt-enter": "editor::OpenExcerpts",
|
||||
"shift-enter": "editor::ExpandExcerpts",
|
||||
@@ -598,101 +697,6 @@
|
||||
"ctrl-:": "editor::ToggleInlayHints"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && jupyter && !ContextEditor",
|
||||
"bindings": {
|
||||
"ctrl-shift-enter": "repl::Run",
|
||||
"ctrl-alt-enter": "repl::RunInPlace"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ContextEditor > Editor",
|
||||
"bindings": {
|
||||
"ctrl-enter": "assistant::Assist",
|
||||
"ctrl-shift-enter": "assistant::Edit",
|
||||
"ctrl-s": "workspace::Save",
|
||||
"save": "workspace::Save",
|
||||
"ctrl->": "assistant::QuoteSelection",
|
||||
"ctrl-<": "assistant::InsertIntoEditor",
|
||||
"ctrl-alt-/": "assistant::ToggleModelSelector",
|
||||
"shift-enter": "assistant::Split",
|
||||
"ctrl-r": "assistant::CycleMessageRole",
|
||||
"enter": "assistant::ConfirmCommand",
|
||||
"alt-enter": "editor::Newline"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel",
|
||||
"bindings": {
|
||||
"ctrl-n": "agent::NewThread",
|
||||
"ctrl-alt-n": "agent::NewPromptEditor",
|
||||
"ctrl-shift-h": "agent::OpenHistory",
|
||||
"ctrl-alt-c": "agent::OpenConfiguration",
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"ctrl-alt-/": "assistant::ToggleModelSelector",
|
||||
"ctrl-shift-a": "agent::ToggleContextPicker",
|
||||
"ctrl-e": "agent::ChatMode",
|
||||
"ctrl-alt-e": "agent::RemoveAllContext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel > Markdown",
|
||||
"bindings": {
|
||||
"copy": "markdown::CopyAsMarkdown",
|
||||
"ctrl-c": "markdown::CopyAsMarkdown"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel && prompt_editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-n": "agent::NewPromptEditor",
|
||||
"cmd-alt-t": "agent::NewThread"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "MessageEditor > Editor",
|
||||
"bindings": {
|
||||
"enter": "agent::Chat",
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "EditMessageEditor > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel",
|
||||
"enter": "menu::Confirm",
|
||||
"alt-enter": "editor::Newline"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentFeedbackMessageEditor > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel",
|
||||
"enter": "menu::Confirm",
|
||||
"alt-enter": "editor::Newline"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ContextStrip",
|
||||
"bindings": {
|
||||
"up": "agent::FocusUp",
|
||||
"right": "agent::FocusRight",
|
||||
"left": "agent::FocusLeft",
|
||||
"down": "agent::FocusDown",
|
||||
"backspace": "agent::RemoveFocusedContext",
|
||||
"enter": "agent::AcceptSuggestedContext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ThreadHistory",
|
||||
"bindings": {
|
||||
"backspace": "agent::RemoveSelectedThread"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "PromptEditor",
|
||||
"bindings": {
|
||||
@@ -701,6 +705,15 @@
|
||||
"ctrl-alt-e": "agent::RemoveAllContext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Prompt",
|
||||
"bindings": {
|
||||
"left": "menu::SelectPrevious",
|
||||
"right": "menu::SelectNext",
|
||||
"h": "menu::SelectPrevious",
|
||||
"l": "menu::SelectNext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectSearchBar && !in_replace",
|
||||
"bindings": {
|
||||
@@ -718,7 +731,7 @@
|
||||
"alt-shift-copy": "workspace::CopyRelativePath",
|
||||
"alt-ctrl-shift-c": "workspace::CopyRelativePath",
|
||||
"alt-ctrl-r": "outline_panel::RevealInFileManager",
|
||||
"space": "outline_panel::Open",
|
||||
"space": "outline_panel::OpenSelectedEntry",
|
||||
"shift-down": "menu::SelectNext",
|
||||
"shift-up": "menu::SelectPrevious",
|
||||
"alt-enter": "editor::OpenExcerpts",
|
||||
@@ -782,6 +795,7 @@
|
||||
"shift-tab": "git_panel::FocusEditor",
|
||||
"escape": "git_panel::ToggleFocus",
|
||||
"ctrl-enter": "git::Commit",
|
||||
"ctrl-shift-enter": "git::Amend",
|
||||
"alt-enter": "menu::SecondaryConfirm",
|
||||
"delete": ["git::RestoreFile", { "skip_prompt": false }],
|
||||
"backspace": ["git::RestoreFile", { "skip_prompt": false }],
|
||||
@@ -790,18 +804,25 @@
|
||||
"ctrl-delete": ["git::RestoreFile", { "skip_prompt": false }]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitPanel && CommitEditor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"escape": "git::Cancel"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitCommit > Editor",
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel",
|
||||
"enter": "editor::Newline",
|
||||
"ctrl-enter": "git::Commit",
|
||||
"ctrl-shift-enter": "git::Amend",
|
||||
"alt-l": "git::GenerateCommitMessage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitPanel",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-g ctrl-g": "git::Fetch",
|
||||
"ctrl-g up": "git::Push",
|
||||
@@ -818,6 +839,7 @@
|
||||
"context": "GitDiff > Editor",
|
||||
"bindings": {
|
||||
"ctrl-enter": "git::Commit",
|
||||
"ctrl-shift-enter": "git::Amend",
|
||||
"ctrl-space": "git::StageAll",
|
||||
"ctrl-shift-space": "git::UnstageAll"
|
||||
}
|
||||
@@ -836,6 +858,7 @@
|
||||
"shift-tab": "git_panel::FocusChanges",
|
||||
"enter": "editor::Newline",
|
||||
"ctrl-enter": "git::Commit",
|
||||
"ctrl-shift-enter": "git::Amend",
|
||||
"alt-up": "git_panel::FocusChanges",
|
||||
"alt-l": "git::GenerateCommitMessage"
|
||||
}
|
||||
@@ -907,6 +930,7 @@
|
||||
"ctrl-enter": "assistant::InlineAssist",
|
||||
"alt-b": ["terminal::SendText", "\u001bb"],
|
||||
"alt-f": ["terminal::SendText", "\u001bf"],
|
||||
"alt-.": ["terminal::SendText", "\u001b."],
|
||||
// Overrides for conflicting keybindings
|
||||
"ctrl-b": ["terminal::SendKeystroke", "ctrl-b"],
|
||||
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"],
|
||||
@@ -938,5 +962,12 @@
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Diagnostics",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-r": "diagnostics::ToggleDiagnosticsRefresh"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
"context": "PromptLibrary",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-n": "prompt_library::NewPrompt",
|
||||
"cmd-shift-s": "prompt_library::ToggleDefaultPrompt",
|
||||
"cmd-n": "rules_library::NewRule",
|
||||
"cmd-shift-s": "rules_library::ToggleDefaultRule",
|
||||
"cmd-w": "workspace::CloseWindow"
|
||||
}
|
||||
},
|
||||
@@ -242,7 +242,9 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-y": "agent::Keep",
|
||||
"cmd-alt-z": "agent::Reject"
|
||||
"cmd-n": "agent::Reject",
|
||||
"cmd-shift-y": "agent::KeepAll",
|
||||
"cmd-shift-n": "agent::RejectAll"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -255,7 +257,7 @@
|
||||
"cmd-shift-g": "search::SelectPreviousMatch",
|
||||
"cmd-alt-/": "assistant::ToggleModelSelector",
|
||||
"cmd-k h": "assistant::DeployHistory",
|
||||
"cmd-k l": "assistant::OpenPromptLibrary",
|
||||
"cmd-k l": "assistant::OpenRulesLibrary",
|
||||
"cmd-t": "assistant::NewChat",
|
||||
"cmd-n": "assistant::NewChat"
|
||||
}
|
||||
@@ -281,16 +283,26 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-n": "agent::NewThread",
|
||||
"cmd-alt-n": "agent::NewPromptEditor",
|
||||
"cmd-alt-n": "agent::NewTextThread",
|
||||
"cmd-shift-h": "agent::OpenHistory",
|
||||
"cmd-alt-c": "agent::OpenConfiguration",
|
||||
"cmd-alt-p": "assistant::OpenRulesLibrary",
|
||||
"cmd-i": "agent::ToggleProfileSelector",
|
||||
"cmd-alt-/": "assistant::ToggleModelSelector",
|
||||
"cmd-shift-a": "agent::ToggleContextPicker",
|
||||
"cmd-shift-o": "agent::ToggleNavigationMenu",
|
||||
"cmd-shift-i": "agent::ToggleOptionsMenu",
|
||||
"shift-escape": "agent::ExpandMessageEditor",
|
||||
"cmd-e": "agent::ChatMode",
|
||||
"cmd-alt-e": "agent::RemoveAllContext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel > NavigationMenu",
|
||||
"bindings": {
|
||||
"shift-backspace": "agent::DeleteRecentlyOpenThread"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel > Markdown",
|
||||
"use_key_equivalents": true,
|
||||
@@ -302,7 +314,7 @@
|
||||
"context": "AgentPanel && prompt_editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-n": "agent::NewPromptEditor",
|
||||
"cmd-n": "agent::NewTextThread",
|
||||
"cmd-alt-t": "agent::NewThread"
|
||||
}
|
||||
},
|
||||
@@ -488,12 +500,15 @@
|
||||
"alt-shift-down": "editor::DuplicateLineDown",
|
||||
"ctrl-shift-right": "editor::SelectLargerSyntaxNode", // Expand Selection
|
||||
"ctrl-shift-left": "editor::SelectSmallerSyntaxNode", // Shrink Selection
|
||||
"cmd-d": ["editor::SelectNext", { "replace_newest": false }], // Add selection to Next Find Match
|
||||
"cmd-d": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch / find_under_expand
|
||||
"cmd-shift-l": "editor::SelectAllMatches", // Select all occurrences of current selection
|
||||
"cmd-f2": "editor::SelectAllMatches", // Select all occurrences of current word
|
||||
"ctrl-cmd-d": ["editor::SelectPrevious", { "replace_newest": false }],
|
||||
"cmd-k cmd-d": ["editor::SelectNext", { "replace_newest": true }],
|
||||
"cmd-k ctrl-cmd-d": ["editor::SelectPrevious", { "replace_newest": true }],
|
||||
"cmd-k cmd-d": ["editor::SelectNext", { "replace_newest": true }], // editor.action.moveSelectionToNextFindMatch / find_under_expand_skip
|
||||
// macOS binds `ctrl-cmd-d` to Show Dictionary which breaks these two binds
|
||||
// To use `ctrl-cmd-d` or `ctrl-k ctrl-cmd-d` in Zed you must execute this command and then restart:
|
||||
// defaults write com.apple.symbolichotkeys AppleSymbolicHotKeys -dict-add 70 '<dict><key>enabled</key><false/></dict>'
|
||||
"ctrl-cmd-d": ["editor::SelectPrevious", { "replace_newest": false }], // editor.action.addSelectionToPreviousFindMatch
|
||||
"cmd-k ctrl-cmd-d": ["editor::SelectPrevious", { "replace_newest": true }], // editor.action.moveSelectionToPreviousFindMatch
|
||||
"cmd-k cmd-i": "editor::Hover",
|
||||
"cmd-/": ["editor::ToggleComments", { "advance_downwards": false }],
|
||||
"cmd-u": "editor::UndoSelection",
|
||||
@@ -535,7 +550,9 @@
|
||||
"cmd-\\": "pane::SplitRight",
|
||||
"cmd-k v": "markdown::OpenPreviewToTheSide",
|
||||
"cmd-shift-v": "markdown::OpenPreview",
|
||||
"ctrl-cmd-c": "editor::DisplayCursorNames"
|
||||
"ctrl-cmd-c": "editor::DisplayCursorNames",
|
||||
"cmd-shift-backspace": "editor::GoToPreviousChange",
|
||||
"cmd-shift-alt-backspace": "editor::GoToNextChange"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -729,7 +746,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"context": "!ContextEditor > Editor && mode == full",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"alt-enter": "editor::OpenExcerpts",
|
||||
@@ -780,7 +797,7 @@
|
||||
"cmd-alt-c": "workspace::CopyPath",
|
||||
"alt-cmd-shift-c": "workspace::CopyRelativePath",
|
||||
"alt-cmd-r": "outline_panel::RevealInFileManager",
|
||||
"space": "outline_panel::Open",
|
||||
"space": "outline_panel::OpenSelectedEntry",
|
||||
"shift-down": "menu::SelectNext",
|
||||
"shift-up": "menu::SelectPrevious",
|
||||
"alt-enter": "editor::OpenExcerpts",
|
||||
@@ -849,17 +866,26 @@
|
||||
"shift-tab": "git_panel::FocusEditor",
|
||||
"escape": "git_panel::ToggleFocus",
|
||||
"cmd-enter": "git::Commit",
|
||||
"cmd-shift-enter": "git::Amend",
|
||||
"backspace": ["git::RestoreFile", { "skip_prompt": false }],
|
||||
"delete": ["git::RestoreFile", { "skip_prompt": false }],
|
||||
"cmd-backspace": ["git::RestoreFile", { "skip_prompt": true }],
|
||||
"cmd-delete": ["git::RestoreFile", { "skip_prompt": true }]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitPanel && CommitEditor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"escape": "git::Cancel"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitDiff > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-enter": "git::Commit",
|
||||
"cmd-shift-enter": "git::Amend",
|
||||
"cmd-ctrl-y": "git::StageAll",
|
||||
"cmd-ctrl-shift-y": "git::UnstageAll"
|
||||
}
|
||||
@@ -870,6 +896,7 @@
|
||||
"bindings": {
|
||||
"enter": "editor::Newline",
|
||||
"cmd-enter": "git::Commit",
|
||||
"cmd-shift-enter": "git::Amend",
|
||||
"tab": "git_panel::FocusChanges",
|
||||
"shift-tab": "git_panel::FocusChanges",
|
||||
"alt-up": "git_panel::FocusChanges",
|
||||
@@ -899,6 +926,7 @@
|
||||
"enter": "editor::Newline",
|
||||
"escape": "menu::Cancel",
|
||||
"cmd-enter": "git::Commit",
|
||||
"cmd-shift-enter": "git::Amend",
|
||||
"alt-tab": "git::GenerateCommitMessage"
|
||||
}
|
||||
},
|
||||
@@ -985,6 +1013,7 @@
|
||||
"alt-right": ["terminal::SendText", "\u001bf"],
|
||||
"alt-b": ["terminal::SendText", "\u001bb"],
|
||||
"alt-f": ["terminal::SendText", "\u001bf"],
|
||||
"alt-.": ["terminal::SendText", "\u001b."],
|
||||
// There are conflicting bindings for these keys in the global context.
|
||||
// these bindings override them, remove at your own risk:
|
||||
"up": ["terminal::SendKeystroke", "up"],
|
||||
@@ -1007,10 +1036,10 @@
|
||||
// Using `ctrl-shift-space` in Zed requires disabling the macOS global shortcut.
|
||||
// System Preferences->Keyboard->Keyboard Shortcuts->Input Sources->Select the previous input source (uncheck)
|
||||
"ctrl-shift-space": "terminal::ToggleViMode",
|
||||
"ctrl-k up": "pane::SplitUp",
|
||||
"ctrl-k down": "pane::SplitDown",
|
||||
"ctrl-k left": "pane::SplitLeft",
|
||||
"ctrl-k right": "pane::SplitRight"
|
||||
"ctrl-alt-up": "pane::SplitUp",
|
||||
"ctrl-alt-down": "pane::SplitDown",
|
||||
"ctrl-alt-left": "pane::SplitLeft",
|
||||
"ctrl-alt-right": "pane::SplitRight"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1039,5 +1068,12 @@
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Diagnostics",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-r": "diagnostics::ToggleDiagnosticsRefresh"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -58,7 +58,8 @@
|
||||
"ctrl-shift-home": "editor::SelectToBeginning",
|
||||
"ctrl-shift-end": "editor::SelectToEnd",
|
||||
"ctrl-f8": "editor::ToggleBreakpoint",
|
||||
"ctrl-shift-f8": "editor::EditLogBreakpoint"
|
||||
"ctrl-shift-f8": "editor::EditLogBreakpoint",
|
||||
"ctrl-shift-u": "editor::ToggleCase"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -37,6 +37,8 @@
|
||||
"ctrl-shift-a": "editor::SelectLargerSyntaxNode",
|
||||
"ctrl-shift-d": "editor::DuplicateSelection",
|
||||
"alt-f3": "editor::SelectAllMatches", // find_all_under
|
||||
// "ctrl-f3": "", // find_under (cancels any selections)
|
||||
// "cmd-alt-shift-g": "" // find_under_prev (cancels any selections)
|
||||
"f9": "editor::SortLinesCaseSensitive",
|
||||
"ctrl-f9": "editor::SortLinesCaseInsensitive",
|
||||
"f12": "editor::GoToDefinition",
|
||||
@@ -49,7 +51,9 @@
|
||||
"ctrl-k ctrl-l": "editor::ConvertToLowerCase",
|
||||
"shift-alt-m": "markdown::OpenPreviewToTheSide",
|
||||
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-delete": "editor::DeleteToNextWordEnd"
|
||||
"ctrl-delete": "editor::DeleteToNextWordEnd",
|
||||
"f3": "editor::FindNextMatch",
|
||||
"shift-f3": "editor::FindPreviousMatch"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -58,6 +62,12 @@
|
||||
"ctrl-r": "outline::Toggle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && !agent_diff",
|
||||
"bindings": {
|
||||
"ctrl-k ctrl-z": "git::Restore"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
|
||||
@@ -55,7 +55,8 @@
|
||||
"cmd-shift-home": "editor::SelectToBeginning",
|
||||
"cmd-shift-end": "editor::SelectToEnd",
|
||||
"ctrl-f8": "editor::ToggleBreakpoint",
|
||||
"ctrl-shift-f8": "editor::EditLogBreakpoint"
|
||||
"ctrl-shift-f8": "editor::EditLogBreakpoint",
|
||||
"cmd-shift-u": "editor::ToggleCase"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -38,6 +38,8 @@
|
||||
"cmd-shift-a": "editor::SelectLargerSyntaxNode",
|
||||
"cmd-shift-d": "editor::DuplicateSelection",
|
||||
"ctrl-cmd-g": "editor::SelectAllMatches", // find_all_under
|
||||
// "cmd-alt-g": "", // find_under (cancels any selections)
|
||||
// "cmd-alt-shift-g": "" // find_under_prev (cancels any selections)
|
||||
"f5": "editor::SortLinesCaseSensitive",
|
||||
"ctrl-f5": "editor::SortLinesCaseInsensitive",
|
||||
"shift-f12": "editor::FindAllReferences",
|
||||
@@ -51,7 +53,9 @@
|
||||
"cmd-shift-j": "editor::JoinLines",
|
||||
"shift-alt-m": "markdown::OpenPreviewToTheSide",
|
||||
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-delete": "editor::DeleteToNextWordEnd"
|
||||
"ctrl-delete": "editor::DeleteToNextWordEnd",
|
||||
"cmd-g": "editor::FindNextMatch",
|
||||
"cmd-shift-g": "editor::FindPreviousMatch"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -60,6 +64,12 @@
|
||||
"cmd-r": "outline::Toggle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && !agent_diff",
|
||||
"bindings": {
|
||||
"cmd-k cmd-z": "git::Restore"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
|
||||
@@ -50,6 +50,12 @@
|
||||
"] -": "vim::NextLesserIndent",
|
||||
"] +": "vim::NextGreaterIndent",
|
||||
"] =": "vim::NextSameIndent",
|
||||
"] b": "pane::ActivateNextItem",
|
||||
"[ b": "pane::ActivatePreviousItem",
|
||||
"] shift-b": "pane::ActivateLastItem",
|
||||
"[ shift-b": ["pane::ActivateItem", 0],
|
||||
"] space": "vim::InsertEmptyLineBelow",
|
||||
"[ space": "vim::InsertEmptyLineAbove",
|
||||
// Word motions
|
||||
"w": "vim::NextWordStart",
|
||||
"e": "vim::NextWordEnd",
|
||||
@@ -108,7 +114,11 @@
|
||||
"ctrl-e": "vim::LineDown",
|
||||
"ctrl-y": "vim::LineUp",
|
||||
// "g" commands
|
||||
"g r": "vim::PushReplaceWithRegister",
|
||||
"g shift-r": "vim::PushReplaceWithRegister",
|
||||
"g r n": "editor::Rename",
|
||||
"g r r": "editor::FindAllReferences",
|
||||
"g r i": "editor::GoToImplementation",
|
||||
"g r a": "editor::ToggleCodeActions",
|
||||
"g g": "vim::StartOfDocument",
|
||||
"g h": "editor::Hover",
|
||||
"g t": "pane::ActivateNextItem",
|
||||
@@ -127,6 +137,7 @@
|
||||
"g <": ["editor::SelectPrevious", { "replace_newest": true }],
|
||||
"g a": "editor::SelectAllMatches",
|
||||
"g s": "outline::Toggle",
|
||||
"g shift-o": "outline::Toggle",
|
||||
"g shift-s": "project_symbols::Toggle",
|
||||
"g .": "editor::ToggleCodeActions", // zed specific
|
||||
"g shift-a": "editor::FindAllReferences", // zed specific
|
||||
@@ -190,7 +201,8 @@
|
||||
"ctrl-w g shift-d": "editor::GoToTypeDefinitionSplit",
|
||||
"ctrl-w space": "editor::OpenExcerptsSplit",
|
||||
"ctrl-w g space": "editor::OpenExcerptsSplit",
|
||||
"ctrl-6": "pane::AlternateFile"
|
||||
"ctrl-6": "pane::AlternateFile",
|
||||
"ctrl-^": "pane::AlternateFile"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -203,6 +215,7 @@
|
||||
"c": "vim::PushChange",
|
||||
"shift-c": "vim::ChangeToEndOfLine",
|
||||
"d": "vim::PushDelete",
|
||||
"delete": "vim::DeleteRight",
|
||||
"shift-d": "vim::DeleteToEndOfLine",
|
||||
"shift-j": "vim::JoinLines",
|
||||
"g shift-j": "vim::JoinLinesNoWhitespace",
|
||||
@@ -303,7 +316,7 @@
|
||||
"!": "vim::ShellCommand",
|
||||
"i": ["vim::PushObject", { "around": false }],
|
||||
"a": ["vim::PushObject", { "around": true }],
|
||||
"g r": ["vim::Paste", { "preserve_clipboard": true }],
|
||||
"g shift-r": ["vim::Paste", { "preserve_clipboard": true }],
|
||||
"g c": "vim::ToggleComments",
|
||||
"g q": "vim::Rewrap",
|
||||
"g ?": "vim::ConvertToRot13",
|
||||
@@ -337,7 +350,8 @@
|
||||
"ctrl-shift-q": ["vim::PushLiteral", {}],
|
||||
"ctrl-r": "vim::PushRegister",
|
||||
"insert": "vim::ToggleReplace",
|
||||
"ctrl-o": "vim::TemporaryNormal"
|
||||
"ctrl-o": "vim::TemporaryNormal",
|
||||
"ctrl-s": "editor::ShowSignatureHelp"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -502,12 +516,14 @@
|
||||
"'": "vim::Quotes",
|
||||
"`": "vim::BackQuotes",
|
||||
"\"": "vim::DoubleQuotes",
|
||||
"q": "vim::AnyQuotes",
|
||||
// "q": "vim::AnyQuotes",
|
||||
"q": "vim::MiniQuotes",
|
||||
"|": "vim::VerticalBars",
|
||||
"(": "vim::Parentheses",
|
||||
")": "vim::Parentheses",
|
||||
"b": "vim::Parentheses",
|
||||
// "b": "vim::AnyBrackets",
|
||||
// "b": "vim::MiniBrackets",
|
||||
"[": "vim::SquareBrackets",
|
||||
"]": "vim::SquareBrackets",
|
||||
"r": "vim::SquareBrackets",
|
||||
@@ -538,6 +554,7 @@
|
||||
"bindings": {
|
||||
"d": "vim::CurrentLine",
|
||||
"s": "vim::PushDeleteSurrounds",
|
||||
"v": "vim::PushForcedMotion", // "d v"
|
||||
"o": "editor::ToggleSelectedDiffHunks", // "d o"
|
||||
"shift-o": "git::ToggleStaged",
|
||||
"p": "git::Restore", // "d p"
|
||||
@@ -586,6 +603,7 @@
|
||||
"context": "vim_operator == y",
|
||||
"bindings": {
|
||||
"y": "vim::CurrentLine",
|
||||
"v": "vim::PushForcedMotion",
|
||||
"s": ["vim::PushAddSurrounds", {}]
|
||||
}
|
||||
},
|
||||
@@ -626,9 +644,10 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == gr",
|
||||
"context": "vim_operator == gR",
|
||||
"bindings": {
|
||||
"r": "vim::CurrentLine"
|
||||
"r": "vim::CurrentLine",
|
||||
"shift-r": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -689,7 +708,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitPanel || ProjectPanel || CollabPanel || OutlinePanel || ChatPanel || VimControl || EmptyPane || SharedScreen || MarkdownPreview || KeyContextView",
|
||||
"context": "GitPanel || ProjectPanel || CollabPanel || OutlinePanel || ChatPanel || VimControl || EmptyPane || SharedScreen || MarkdownPreview || KeyContextView || DebugPanel",
|
||||
"bindings": {
|
||||
// window related commands (ctrl-w X)
|
||||
"ctrl-w": null,
|
||||
@@ -783,8 +802,7 @@
|
||||
"{": "project_panel::SelectPrevDirectory",
|
||||
"shift-g": "menu::SelectLast",
|
||||
"g g": "menu::SelectFirst",
|
||||
"-": "project_panel::SelectParent",
|
||||
"ctrl-6": "pane::AlternateFile"
|
||||
"-": "project_panel::SelectParent"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -827,5 +845,13 @@
|
||||
// and Windows.
|
||||
"alt-l": "editor::AcceptEditPrediction"
|
||||
}
|
||||
},
|
||||
{
|
||||
// Fixes https://github.com/zed-industries/zed/issues/29095 by ensuring that
|
||||
// the last binding for editor::ToggleComments is not ctrl-c.
|
||||
"context": "hack_to_fix_ctrl-c",
|
||||
"bindings": {
|
||||
"g c": "editor::ToggleComments"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,138 +1,108 @@
|
||||
You are an AI assistant integrated into a code editor. You have the programming ability of an expert programmer who takes pride in writing high-quality code and is driven to the point of obsession about solving problems effectively. Your goal is to do one of the following two things:
|
||||
You are a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.
|
||||
|
||||
1. Help users answer questions and perform tasks related to their codebase.
|
||||
2. Answer general-purpose questions unrelated to their particular codebase.
|
||||
## Communication
|
||||
|
||||
It will be up to you to decide which of these you are doing based on what the user has told you. When unclear, ask clarifying questions to understand the user's intent before proceeding.
|
||||
1. Be conversational but professional.
|
||||
2. Refer to the USER in the second person and yourself in the first person.
|
||||
3. Format your responses in markdown. Use backticks to format file, directory, function, and class names.
|
||||
4. NEVER lie or make things up.
|
||||
5. Refrain from apologizing all the time when results are unexpected. Instead, just try your best to proceed or explain the circumstances to the user without apologizing.
|
||||
|
||||
You should only perform actions that modify the user's system if explicitly requested by the user:
|
||||
- If the user asks a question about how to accomplish a task, provide guidance or information, and use read-only tools (e.g., search) to assist. You may suggest potential actions, but do not directly modify the user's system without explicit instruction.
|
||||
- If the user clearly requests that you perform an action, carry out the action directly without explaining why you are doing so.
|
||||
## Tool Use
|
||||
|
||||
When answering questions, it's okay to give incomplete examples containing comments about what would go there in a real version. When being asked to directly perform tasks on the code base, you must ALWAYS make fully working code. You may never "simplify" the code by omitting or deleting functionality you know the user has requested, and you must NEVER write comments like "in a full version, this would..." - instead, you must actually implement the real version. Don't be lazy!
|
||||
1. Make sure to adhere to the tools schema.
|
||||
2. Provide every required argument.
|
||||
3. DO NOT use tools to access items that are already available in the context section.
|
||||
4. Use only the tools that are currently available.
|
||||
5. DO NOT use a tool that is not available just because it appears in the conversation. This means the user turned it off.
|
||||
6. NEVER run commands that don't terminate on their own such as web servers (like `npm run start`, `npm run dev`, `python -m http.server`, etc) or file watchers.
|
||||
|
||||
Note that project files are automatically backed up. The user can always get them back later if anything goes wrong, so there's
|
||||
no need to create backup files (e.g. `.bak` files) because these files will just take up unnecessary space on the user's disk.
|
||||
## Searching and Reading
|
||||
|
||||
When attempting to resolve issues around failing tests, never simply remove the failing tests. Unless the user explicitly asks you to remove tests, ALWAYS attempt to fix the code causing the tests to fail.
|
||||
If you are unsure how to fulfill the user's request, gather more information with tool calls and/or clarifying questions.
|
||||
|
||||
Ignore "TODO"-type comments unless they're relevant to the user's explicit request or the user specifically asks you to address them. It is, however, okay to include them in codebase summaries.
|
||||
{{! TODO: If there are files, we should mention it but otherwise omit that fact }}
|
||||
If appropriate, use tool calls to explore the current project, which contains the following root directories:
|
||||
|
||||
<style>
|
||||
Editing code:
|
||||
- Make sure to take previous edits into account.
|
||||
- The edits you perform might lead to errors or warnings. At the end of your changes, check whether you introduced any problems, and fix them before providing a summary of the changes you made.
|
||||
- You may only attempt to fix these up to 3 times. If you have tried 3 times to fix them, and there are still problems remaining, you must not continue trying to fix them, and must instead tell the user that there are problems remaining - and ask if the user would like you to attempt to solve them further.
|
||||
- Do not fix errors unrelated to your changes unless the user explicitly asks you to do so.
|
||||
- Prefer to move files over recreating them. The move can be followed by minor edits if required.
|
||||
- If you seem to be stuck, never go back and "simplify the implementation" by deleting the parts of the implementation you're stuck on and replacing them with comments. If you ever feel the urge to do this, instead immediately stop whatever you're doing (even if the code is in a broken state), report that you are stuck, explain what you're stuck on, and ask the user how to proceed.
|
||||
{{#each worktrees}}
|
||||
- `{{root_name}}`
|
||||
{{/each}}
|
||||
|
||||
Tool use:
|
||||
- Make sure to adhere to the tools schema.
|
||||
- Provide every required argument.
|
||||
- DO NOT use tools to access items that are already available in the context section.
|
||||
- Use only the tools that are currently available.
|
||||
- DO NOT use a tool that is not available just because it appears in the conversation. This means the user turned it off.
|
||||
- Bias towards not asking the user for help if you can find the answer yourself.
|
||||
- When providing paths to tools, the path should always begin with a path that starts with a project root directory listed above.
|
||||
- Before you read or edit a file, you must first find the full path. DO NOT ever guess a file path!
|
||||
{{# if (has_tool 'grep') }}
|
||||
- When looking for symbols in the project, prefer the `grep` tool.
|
||||
- As you learn about the structure of the project, use that information to scope `grep` searches to targeted subtrees of the project.
|
||||
- The user might specify a partial file path. If you don't know the full path, use `find_path` (not `grep`) before you read the file.
|
||||
{{/if}}
|
||||
|
||||
Responding:
|
||||
- Be concise and direct in your responses.
|
||||
- Never apologize or thank the user.
|
||||
- Don't comment that you have just realized or understood something.
|
||||
- When you are going to make a tool call, tersely explain your reasoning for choosing to use that tool, with no flourishes or commentary beyond that information.
|
||||
For example, rather than saying "You're absolutely right! Thank you for providing that context. Now I understand that we're missing a dependency, and I need to add it:" say "I'll add that missing dependency:" instead.
|
||||
- Also, don't restate what a tool call is about to do (or just did).
|
||||
For example, don't say "Now I'm going to check diagnostics to see if there are any warnings or errors," followed by running a tool which checks diagnostics and reports warnings or errors; instead, just request the tool call without saying anything.
|
||||
- All tool results are provided to you automatically, so DO NOT thank the user when this happens.
|
||||
## Code Block Formatting
|
||||
|
||||
Whenever you mention a code block, you MUST use ONLY the following format:
|
||||
|
||||
```language path/to/Something.blah#L123-456
|
||||
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.
|
||||
|
||||
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.
|
||||
2. The system finds the first two headers, including everything in between. In this case, that would be:
|
||||
|
||||
```path/to/README.md#L8-12
|
||||
# First Header
|
||||
|
||||
This is the info under the first header.
|
||||
|
||||
## Sub-header
|
||||
```
|
||||
|
||||
3. Then the system finds the last header in the README:
|
||||
|
||||
```path/to/README.md#L27-29
|
||||
## Last Header
|
||||
|
||||
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
|
||||
# Level 1 heading
|
||||
## Level 2 heading
|
||||
### 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:
|
||||
|
||||
```
|
||||
# Level 1 heading
|
||||
## Level 2 heading
|
||||
### 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
|
||||
# Level 1 heading
|
||||
## Level 2 heading
|
||||
### Level 3 heading
|
||||
```
|
||||
</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.
|
||||
|
||||
<bad_example_do_not_do_this>
|
||||
In Markdown, hash marks signify headings. For example:
|
||||
|
||||
```markdown
|
||||
/dev/null/example.md#L1-3
|
||||
# Level 1 heading
|
||||
@@ -140,26 +110,58 @@ In Markdown, hash marks signify headings. For example:
|
||||
### Level 3 heading
|
||||
```
|
||||
</bad_example_do_not_do_this>
|
||||
|
||||
This example is unacceptable because the path is in the wrong place. The path must be directly after the opening backticks.
|
||||
</style>
|
||||
## Fixing Diagnostics
|
||||
|
||||
The user has opened a project that contains the following root directories/files. Whenever you specify a path in the project, it must be a relative path which begins with one of these root directories/files:
|
||||
1. Make 1-2 attempts at fixing diagnostics, then defer to the user.
|
||||
2. Never simplify code you've written just to solve diagnostics. Complete, mostly correct code is more valuable than perfect code that doesn't solve the problem.
|
||||
|
||||
## Debugging
|
||||
|
||||
When debugging, only make code changes if you are certain that you can solve the problem.
|
||||
Otherwise, follow debugging best practices:
|
||||
1. Address the root cause instead of the symptoms.
|
||||
2. Add descriptive logging statements and error messages to track variable and code state.
|
||||
3. Add test functions and statements to isolate the problem.
|
||||
|
||||
## Calling External APIs
|
||||
|
||||
1. Unless explicitly requested by the user, use the best suited external APIs and packages to solve the task. There is no need to ask the user for permission.
|
||||
2. When selecting which version of an API or package to use, choose one that is compatible with the user's dependency management file. If no such file exists or if the package is not present, use the latest version that is in your training data.
|
||||
3. If an external API requires an API Key, be sure to point this out to the user. Adhere to best security practices (e.g. DO NOT hardcode an API key in a place where it can be exposed)
|
||||
|
||||
## System Information
|
||||
|
||||
Operating System: {{os}}
|
||||
Default Shell: {{shell}}
|
||||
|
||||
{{#if (or has_rules has_default_user_rules)}}
|
||||
## User's Custom Instructions
|
||||
|
||||
The following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the tool use guidelines.
|
||||
|
||||
{{#each worktrees}}
|
||||
- `{{root_name}}` (absolute path: `{{abs_path}}`)
|
||||
{{/each}}
|
||||
{{#if has_rules}}
|
||||
|
||||
There are rules that apply to these root directories:
|
||||
There are project rules that apply to these root directories:
|
||||
{{#each worktrees}}
|
||||
{{#if rules_file}}
|
||||
|
||||
`{{root_name}}/{{rules_file.rel_path}}`:
|
||||
|
||||
`{{root_name}}/{{rules_file.path_in_worktree}}`:
|
||||
``````
|
||||
{{{rules_file.text}}}
|
||||
``````
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
||||
{{#if has_user_rules}}
|
||||
The user has specified the following rules that should be applied:
|
||||
{{#each user_rules}}
|
||||
|
||||
{{#if title}}
|
||||
Rules title: {{title}}
|
||||
{{/if}}
|
||||
``````
|
||||
{{contents}}}
|
||||
``````
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
@@ -80,6 +80,8 @@
|
||||
// Values are clamped to the [0.0, 1.0] range.
|
||||
"inactive_opacity": 1.0
|
||||
},
|
||||
// Layout mode of the bottom dock. Defaults to "contained"
|
||||
"bottom_dock_layout": "contained",
|
||||
// The direction that you want to split panes horizontally. Defaults to "up"
|
||||
"pane_split_direction_horizontal": "up",
|
||||
// The direction that you want to split panes horizontally. Defaults to "left"
|
||||
@@ -165,7 +167,23 @@
|
||||
// Default: not set, defaults to "bar"
|
||||
"cursor_shape": null,
|
||||
// Determines when the mouse cursor should be hidden in an editor or input box.
|
||||
//
|
||||
// 1. Never hide the mouse cursor:
|
||||
// "never"
|
||||
// 2. Hide only when typing:
|
||||
// "on_typing"
|
||||
// 3. Hide on both typing and cursor movement:
|
||||
// "on_typing_and_movement"
|
||||
"hide_mouse": "on_typing_and_movement",
|
||||
// Determines how snippets are sorted relative to other completion items.
|
||||
//
|
||||
// 1. Place snippets at the top of the completion list:
|
||||
// "top"
|
||||
// 2. Place snippets normally without any preference:
|
||||
// "inline"
|
||||
// 3. Place snippets at the bottom of the completion list:
|
||||
// "bottom"
|
||||
"snippet_sort_order": "inline",
|
||||
// How to highlight the current line in the editor.
|
||||
//
|
||||
// 1. Don't highlight the current line:
|
||||
@@ -179,8 +197,6 @@
|
||||
"current_line_highlight": "all",
|
||||
// Whether to highlight all occurrences of the selected text in an editor.
|
||||
"selection_highlight": true,
|
||||
// The debounce delay before querying highlights based on the selected text.
|
||||
"selection_highlight_debounce": 50,
|
||||
// The debounce delay before querying highlights from the language
|
||||
// server based on the current cursor location.
|
||||
"lsp_highlight_debounce": 75,
|
||||
@@ -210,7 +226,7 @@
|
||||
// Hide the values of in variables from visual display in private files
|
||||
"redact_private_values": false,
|
||||
// The default number of lines to expand excerpts in the multibuffer by.
|
||||
"expand_excerpt_lines": 3,
|
||||
"expand_excerpt_lines": 5,
|
||||
// Globs to match against file paths to determine if a file is private.
|
||||
"private_files": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/secrets.yml"],
|
||||
// Whether to use additional LSP queries to format (and amend) the code after
|
||||
@@ -586,6 +602,12 @@
|
||||
// Default: main
|
||||
"fallback_branch_name": "main",
|
||||
|
||||
// Whether to sort entries in the panel by path
|
||||
// or by status (the default).
|
||||
//
|
||||
// Default: false
|
||||
"sort_by_path": false,
|
||||
|
||||
"scrollbar": {
|
||||
// When to show the scrollbar in the git panel.
|
||||
//
|
||||
@@ -624,14 +646,14 @@
|
||||
// The provider to use.
|
||||
"provider": "zed.dev",
|
||||
// The model to use.
|
||||
"model": "claude-3-5-sonnet-latest"
|
||||
"model": "claude-3-7-sonnet-latest"
|
||||
},
|
||||
// The model to use when applying edits from the assistant.
|
||||
"editor_model": {
|
||||
// The provider to use.
|
||||
"provider": "zed.dev",
|
||||
// The model to use.
|
||||
"model": "claude-3-5-sonnet-latest"
|
||||
"model": "claude-3-7-sonnet-latest"
|
||||
},
|
||||
// When enabled, the agent can run potentially destructive actions without asking for your confirmation.
|
||||
"always_allow_tool_actions": false,
|
||||
@@ -642,38 +664,50 @@
|
||||
// We don't know which of the context server tools are safe for the "Ask" profile, so we don't enable them by default.
|
||||
// "enable_all_context_servers": true,
|
||||
"tools": {
|
||||
"contents": true,
|
||||
"diagnostics": true,
|
||||
"fetch": true,
|
||||
"list_directory": false,
|
||||
"now": true,
|
||||
"path_search": true,
|
||||
"find_path": true,
|
||||
"read_file": true,
|
||||
"regex_search": true,
|
||||
"thinking": true
|
||||
"open": true,
|
||||
"grep": true,
|
||||
"thinking": true,
|
||||
"web_search": true
|
||||
}
|
||||
},
|
||||
"write": {
|
||||
"name": "Write",
|
||||
"enable_all_context_servers": true,
|
||||
"tools": {
|
||||
"bash": true,
|
||||
"batch_tool": true,
|
||||
"code_symbols": true,
|
||||
"batch_tool": false,
|
||||
"code_actions": false,
|
||||
"code_symbols": false,
|
||||
"contents": false,
|
||||
"copy_path": false,
|
||||
"create_file": true,
|
||||
"delete_path": false,
|
||||
"diagnostics": true,
|
||||
"find_replace_file": true,
|
||||
"edit_file": true,
|
||||
"fetch": true,
|
||||
"list_directory": false,
|
||||
"list_directory": true,
|
||||
"move_path": false,
|
||||
"now": true,
|
||||
"path_search": true,
|
||||
"now": false,
|
||||
"find_path": true,
|
||||
"read_file": true,
|
||||
"regex_search": true,
|
||||
"symbol_info": true,
|
||||
"thinking": true
|
||||
"grep": true,
|
||||
"rename": false,
|
||||
"symbol_info": false,
|
||||
"terminal": true,
|
||||
"thinking": true,
|
||||
"web_search": true
|
||||
}
|
||||
},
|
||||
"manual": {
|
||||
"name": "Manual",
|
||||
"enable_all_context_servers": false,
|
||||
"tools": {}
|
||||
}
|
||||
},
|
||||
// Where to show notifications when an agent has either completed
|
||||
@@ -801,7 +835,20 @@
|
||||
// "modal_max_width": "full"
|
||||
//
|
||||
// Default: small
|
||||
"modal_max_width": "small"
|
||||
"modal_max_width": "small",
|
||||
// Determines whether the file finder should skip focus for the active file in search results.
|
||||
// There are 2 possible values:
|
||||
//
|
||||
// 1. true: When searching for files, if the currently active file appears as the first result,
|
||||
// auto-focus will skip it and focus the second result instead.
|
||||
// "skip_focus_for_active_in_search": true
|
||||
//
|
||||
// 2. false: When searching for files, the first result will always receive focus,
|
||||
// even if it's the currently active file.
|
||||
// "skip_focus_for_active_in_search": false
|
||||
//
|
||||
// Default: true
|
||||
"skip_focus_for_active_in_search": true
|
||||
},
|
||||
// Whether or not to remove any trailing whitespace from lines of a buffer
|
||||
// before saving it.
|
||||
@@ -884,6 +931,24 @@
|
||||
// The minimum severity of the diagnostics to show inline.
|
||||
// Shows all diagnostics when not specified.
|
||||
"max_severity": null
|
||||
},
|
||||
"rust": {
|
||||
// When enabled, Zed runs `cargo check --message-format=json`-based commands and
|
||||
// collect cargo diagnostics instead of rust-analyzer.
|
||||
"fetch_cargo_diagnostics": false,
|
||||
// A command override for fetching the cargo diagnostics.
|
||||
// First argument is the command, followed by the arguments.
|
||||
"diagnostics_fetch_command": [
|
||||
"cargo",
|
||||
"check",
|
||||
"--quiet",
|
||||
"--workspace",
|
||||
"--message-format=json",
|
||||
"--all-targets",
|
||||
"--keep-going"
|
||||
],
|
||||
// Extra environment variables to pass to the diagnostics fetch command.
|
||||
"env": {}
|
||||
}
|
||||
},
|
||||
// Files or globs of files that will be excluded by Zed entirely. They will be skipped during file
|
||||
@@ -1484,7 +1549,12 @@
|
||||
"use_multiline_find": false,
|
||||
"use_smartcase_find": false,
|
||||
"highlight_on_yank_duration": 200,
|
||||
"custom_digraphs": {}
|
||||
"custom_digraphs": {},
|
||||
// Cursor shape for the each mode.
|
||||
// Specify the mode as the key and the shape as the value.
|
||||
// The mode can be one of the following: "normal", "replace", "insert", "visual".
|
||||
// The shape can be one of the following: "block", "bar", "underline", "hollow".
|
||||
"cursor_shape": {}
|
||||
},
|
||||
// The server to connect to. If the environment variable
|
||||
// ZED_SERVER_URL is set, it will override this setting.
|
||||
@@ -1554,7 +1624,6 @@
|
||||
// }
|
||||
// ]
|
||||
"ssh_connections": [],
|
||||
|
||||
// Configures context servers for use in the Assistant.
|
||||
"context_servers": {},
|
||||
"debugger": {
|
||||
|
||||
@@ -1,2 +1,7 @@
|
||||
allow-private-module-inception = true
|
||||
avoid-breaking-exported-api = false
|
||||
ignore-interior-mutability = [
|
||||
# Suppresses clippy::mutable_key_type, which is a false positive as the Eq
|
||||
# and Hash impls do not use fields with interior mutability.
|
||||
"agent::context::AgentContextKey"
|
||||
]
|
||||
|
||||
@@ -28,9 +28,9 @@ async-watch.workspace = true
|
||||
buffer_diff.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
component.workspace = true
|
||||
context_server.workspace = true
|
||||
convert_case.workspace = true
|
||||
db.workspace = true
|
||||
@@ -50,6 +50,7 @@ itertools.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
language_model_selector.workspace = true
|
||||
linkme.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
markdown.workspace = true
|
||||
@@ -59,10 +60,12 @@ ordered-float.workspace = true
|
||||
parking_lot.workspace = true
|
||||
paths.workspace = true
|
||||
picker.workspace = true
|
||||
postage.workspace = true
|
||||
project.workspace = true
|
||||
prompt_library.workspace = true
|
||||
rules_library.workspace = true
|
||||
prompt_store.workspace = true
|
||||
proto.workspace = true
|
||||
ref-cast.workspace = true
|
||||
release_channel.workspace = true
|
||||
rope.workspace = true
|
||||
schemars.workspace = true
|
||||
@@ -78,15 +81,17 @@ terminal.workspace = true
|
||||
terminal_view.workspace = true
|
||||
text.workspace = true
|
||||
theme.workspace = true
|
||||
thiserror.workspace = true
|
||||
time.workspace = true
|
||||
time_format.workspace = true
|
||||
ui.workspace = true
|
||||
ui_input.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
buffer_diff = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
use crate::{Keep, Reject, Thread, ThreadEvent};
|
||||
use crate::{Keep, KeepAll, Reject, RejectAll, Thread, ThreadEvent, ui::AnimatedLabel};
|
||||
use anyhow::Result;
|
||||
use buffer_diff::DiffHunkStatus;
|
||||
use collections::HashSet;
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::{
|
||||
Direction, Editor, EditorEvent, MultiBuffer, ToPoint,
|
||||
actions::{GoToHunk, GoToPreviousHunk},
|
||||
scroll::Autoscroll,
|
||||
};
|
||||
use gpui::{
|
||||
Action, AnyElement, AnyView, App, Entity, EventEmitter, FocusHandle, Focusable, SharedString,
|
||||
Subscription, Task, WeakEntity, Window, prelude::*,
|
||||
Action, AnyElement, AnyView, App, Empty, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
SharedString, Subscription, Task, WeakEntity, Window, prelude::*,
|
||||
};
|
||||
use language::{Capability, DiskState, OffsetRangeExt, Point};
|
||||
use multi_buffer::PathKey;
|
||||
@@ -307,6 +307,10 @@ impl AgentDiff {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.thread.read(cx).is_generating() {
|
||||
return;
|
||||
}
|
||||
|
||||
let snapshot = self.multibuffer.read(cx).snapshot(cx);
|
||||
let diff_hunks_in_ranges = self
|
||||
.editor
|
||||
@@ -339,6 +343,10 @@ impl AgentDiff {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.thread.read(cx).is_generating() {
|
||||
return;
|
||||
}
|
||||
|
||||
let snapshot = self.multibuffer.read(cx).snapshot(cx);
|
||||
let diff_hunks_in_ranges = self
|
||||
.editor
|
||||
@@ -355,16 +363,24 @@ impl AgentDiff {
|
||||
self.update_selection(&diff_hunks_in_ranges, window, cx);
|
||||
}
|
||||
|
||||
let mut ranges_by_buffer = HashMap::default();
|
||||
for hunk in &diff_hunks_in_ranges {
|
||||
let buffer = self.multibuffer.read(cx).buffer(hunk.buffer_id);
|
||||
if let Some(buffer) = buffer {
|
||||
self.thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.reject_edits_in_range(buffer, hunk.buffer_range.clone(), cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
ranges_by_buffer
|
||||
.entry(buffer.clone())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(hunk.buffer_range.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for (buffer, ranges) in ranges_by_buffer {
|
||||
self.thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.reject_edits_in_ranges(buffer, ranges, cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_selection(
|
||||
@@ -581,6 +597,10 @@ impl Item for AgentDiff {
|
||||
editor.added_to_workspace(workspace, window, cx)
|
||||
});
|
||||
}
|
||||
|
||||
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
|
||||
"Agent Diff".into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AgentDiff {
|
||||
@@ -642,6 +662,11 @@ fn render_diff_hunk_controls(
|
||||
cx: &mut App,
|
||||
) -> AnyElement {
|
||||
let editor = editor.clone();
|
||||
|
||||
if agent_diff.read(cx).thread.read(cx).is_generating() {
|
||||
return Empty.into_any();
|
||||
}
|
||||
|
||||
h_flex()
|
||||
.h(line_height)
|
||||
.mr_0p5()
|
||||
@@ -843,18 +868,26 @@ impl ToolbarItemView for AgentDiffToolbar {
|
||||
}
|
||||
|
||||
impl Render for AgentDiffToolbar {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let agent_diff = match self.agent_diff(cx) {
|
||||
Some(ad) => ad,
|
||||
None => return div(),
|
||||
};
|
||||
|
||||
let is_empty = agent_diff.read(cx).multibuffer.read(cx).is_empty();
|
||||
let is_generating = agent_diff.read(cx).thread.read(cx).is_generating();
|
||||
if is_generating {
|
||||
return div()
|
||||
.w(rems(6.5625)) // Arbitrary 105px size—so the label doesn't dance around
|
||||
.child(AnimatedLabel::new("Generating"));
|
||||
}
|
||||
|
||||
let is_empty = agent_diff.read(cx).multibuffer.read(cx).is_empty();
|
||||
if is_empty {
|
||||
return div();
|
||||
}
|
||||
|
||||
let focus_handle = agent_diff.focus_handle(cx);
|
||||
|
||||
h_group_xl()
|
||||
.my_neg_1()
|
||||
.items_center()
|
||||
@@ -864,15 +897,25 @@ impl Render for AgentDiffToolbar {
|
||||
.child(
|
||||
h_group_sm()
|
||||
.child(
|
||||
Button::new("reject-all", "Reject All").on_click(cx.listener(
|
||||
|this, _, window, cx| {
|
||||
this.dispatch_action(&crate::RejectAll, window, cx)
|
||||
},
|
||||
)),
|
||||
Button::new("reject-all", "Reject All")
|
||||
.key_binding({
|
||||
KeyBinding::for_action_in(&RejectAll, &focus_handle, window, cx)
|
||||
.map(|kb| kb.size(rems_from_px(12.)))
|
||||
})
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.dispatch_action(&RejectAll, window, cx)
|
||||
})),
|
||||
)
|
||||
.child(Button::new("keep-all", "Keep All").on_click(cx.listener(
|
||||
|this, _, window, cx| this.dispatch_action(&crate::KeepAll, window, cx),
|
||||
))),
|
||||
.child(
|
||||
Button::new("keep-all", "Keep All")
|
||||
.key_binding({
|
||||
KeyBinding::for_action_in(&KeepAll, &focus_handle, window, cx)
|
||||
.map(|kb| kb.size(rems_from_px(12.)))
|
||||
})
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.dispatch_action(&KeepAll, window, cx)
|
||||
})),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -882,6 +925,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::{ThreadStore, thread_store};
|
||||
use assistant_settings::AssistantSettings;
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use context_server::ContextServerSettings;
|
||||
use editor::EditorSettings;
|
||||
use gpui::TestAppContext;
|
||||
@@ -901,11 +945,13 @@ mod tests {
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
AssistantSettings::register(cx);
|
||||
prompt_store::init(cx);
|
||||
thread_store::init(cx);
|
||||
workspace::init_settings(cx);
|
||||
ThemeSettings::register(cx);
|
||||
ContextServerSettings::register(cx);
|
||||
EditorSettings::register(cx);
|
||||
language_model::init_settings(cx);
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
@@ -921,15 +967,19 @@ mod tests {
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let thread_store = cx.update(|cx| {
|
||||
ThreadStore::new(
|
||||
project.clone(),
|
||||
Arc::default(),
|
||||
Arc::new(PromptBuilder::new(None).unwrap()),
|
||||
cx,
|
||||
)
|
||||
.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());
|
||||
|
||||
@@ -945,7 +995,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
cx.update(|_, cx| {
|
||||
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
|
||||
action_log.update(cx, |log, cx| log.track_buffer(buffer.clone(), cx));
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer
|
||||
.edit(
|
||||
|
||||
@@ -18,6 +18,7 @@ mod terminal_inline_assistant;
|
||||
mod thread;
|
||||
mod thread_history;
|
||||
mod thread_store;
|
||||
mod tool_compatibility;
|
||||
mod tool_use;
|
||||
mod ui;
|
||||
|
||||
@@ -38,18 +39,23 @@ use thread::ThreadId;
|
||||
pub use crate::active_thread::ActiveThread;
|
||||
use crate::assistant_configuration::{AddContextServerModal, ManageProfilesModal};
|
||||
pub use crate::assistant_panel::{AssistantPanel, ConcreteAssistantPanelDelegate};
|
||||
pub use crate::context::{ContextLoadResult, LoadedContext};
|
||||
pub use crate::inline_assistant::InlineAssistant;
|
||||
pub use crate::thread::{Message, RequestKind, Thread, ThreadEvent};
|
||||
pub use crate::thread::{Message, MessageSegment, Thread, ThreadEvent};
|
||||
pub use crate::thread_store::ThreadStore;
|
||||
pub use agent_diff::{AgentDiff, AgentDiffToolbar};
|
||||
|
||||
actions!(
|
||||
agent,
|
||||
[
|
||||
NewPromptEditor,
|
||||
NewTextThread,
|
||||
ToggleContextPicker,
|
||||
ToggleNavigationMenu,
|
||||
ToggleOptionsMenu,
|
||||
DeleteRecentlyOpenThread,
|
||||
ToggleProfileSelector,
|
||||
RemoveAllContext,
|
||||
ExpandMessageEditor,
|
||||
OpenHistory,
|
||||
AddContextServer,
|
||||
RemoveSelectedThread,
|
||||
|
||||
@@ -9,10 +9,15 @@ use assistant_tool::{ToolSource, ToolWorkingSet};
|
||||
use collections::HashMap;
|
||||
use context_server::manager::ContextServerManager;
|
||||
use fs::Fs;
|
||||
use gpui::{Action, AnyView, App, Entity, EventEmitter, FocusHandle, Focusable, Subscription};
|
||||
use gpui::{
|
||||
Action, AnyView, App, Entity, EventEmitter, FocusHandle, Focusable, ScrollHandle, Subscription,
|
||||
};
|
||||
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
|
||||
use settings::{Settings, update_settings_file};
|
||||
use ui::{Disclosure, Divider, DividerColor, ElevationIndex, Indicator, Switch, prelude::*};
|
||||
use ui::{
|
||||
Disclosure, Divider, DividerColor, ElevationIndex, Indicator, Scrollbar, ScrollbarState,
|
||||
Switch, SwitchColor, Tooltip, prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use zed_actions::ExtensionCategoryFilter;
|
||||
|
||||
@@ -27,15 +32,17 @@ pub struct AssistantConfiguration {
|
||||
configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>,
|
||||
context_server_manager: Entity<ContextServerManager>,
|
||||
expanded_context_server_tools: HashMap<Arc<str>, bool>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
_registry_subscription: Subscription,
|
||||
scroll_handle: ScrollHandle,
|
||||
scrollbar_state: ScrollbarState,
|
||||
}
|
||||
|
||||
impl AssistantConfiguration {
|
||||
pub fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
context_server_manager: Entity<ContextServerManager>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
@@ -58,6 +65,9 @@ impl AssistantConfiguration {
|
||||
},
|
||||
);
|
||||
|
||||
let scroll_handle = ScrollHandle::new();
|
||||
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
|
||||
|
||||
let mut this = Self {
|
||||
fs,
|
||||
focus_handle,
|
||||
@@ -66,6 +76,8 @@ impl AssistantConfiguration {
|
||||
expanded_context_server_tools: HashMap::default(),
|
||||
tools,
|
||||
_registry_subscription: registry_subscription,
|
||||
scroll_handle,
|
||||
scrollbar_state,
|
||||
};
|
||||
this.build_provider_configuration_views(window, cx);
|
||||
this
|
||||
@@ -107,7 +119,7 @@ pub enum AssistantConfigurationEvent {
|
||||
impl EventEmitter<AssistantConfigurationEvent> for AssistantConfiguration {}
|
||||
|
||||
impl AssistantConfiguration {
|
||||
fn render_provider_configuration(
|
||||
fn render_provider_configuration_block(
|
||||
&mut self,
|
||||
provider: &Arc<dyn LanguageModelProvider>,
|
||||
cx: &mut Context<Self>,
|
||||
@@ -120,7 +132,11 @@ impl AssistantConfiguration {
|
||||
.cloned();
|
||||
|
||||
v_flex()
|
||||
.pt_3()
|
||||
.pb_1()
|
||||
.gap_1p5()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border.opacity(0.6))
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
@@ -132,7 +148,7 @@ impl AssistantConfiguration {
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Label::new(provider_name.clone())),
|
||||
.child(Label::new(provider_name.clone()).size(LabelSize::Large)),
|
||||
)
|
||||
.when(provider.is_authenticated(cx), |parent| {
|
||||
parent.child(
|
||||
@@ -157,39 +173,54 @@ impl AssistantConfiguration {
|
||||
)
|
||||
}),
|
||||
)
|
||||
.map(|parent| match configuration_view {
|
||||
Some(configuration_view) => parent.child(configuration_view),
|
||||
None => parent.child(div().child(Label::new(format!(
|
||||
"No configuration view for {provider_name}",
|
||||
)))),
|
||||
})
|
||||
}
|
||||
|
||||
fn render_provider_configuration_section(
|
||||
&mut self,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let providers = LanguageModelRegistry::read_global(cx).providers();
|
||||
|
||||
v_flex()
|
||||
.p(DynamicSpacing::Base16.rems(cx))
|
||||
.pr(DynamicSpacing::Base20.rems(cx))
|
||||
.gap_4()
|
||||
.flex_1()
|
||||
.child(
|
||||
div()
|
||||
.p(DynamicSpacing::Base08.rems(cx))
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.rounded_sm()
|
||||
.map(|parent| match configuration_view {
|
||||
Some(configuration_view) => parent.child(configuration_view),
|
||||
None => parent.child(div().child(Label::new(format!(
|
||||
"No configuration view for {provider_name}",
|
||||
)))),
|
||||
}),
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(Headline::new("LLM Providers"))
|
||||
.child(
|
||||
Label::new("Add at least one provider to use AI-powered features.")
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.children(
|
||||
providers
|
||||
.into_iter()
|
||||
.map(|provider| self.render_provider_configuration_block(&provider, cx)),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_command_permission(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let always_allow_tool_actions = AssistantSettings::get_global(cx).always_allow_tool_actions;
|
||||
|
||||
const HEADING: &str = "Allow running tools without asking for confirmation";
|
||||
const HEADING: &str = "Allow running editing tools without asking for confirmation";
|
||||
|
||||
v_flex()
|
||||
.p(DynamicSpacing::Base16.rems(cx))
|
||||
.pr(DynamicSpacing::Base20.rems(cx))
|
||||
.gap_2()
|
||||
.flex_1()
|
||||
.child(Headline::new("General Settings").size(HeadlineSize::Small))
|
||||
.child(Headline::new("General Settings"))
|
||||
.child(
|
||||
h_flex()
|
||||
.p_2p5()
|
||||
.rounded_sm()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.gap_4()
|
||||
.justify_between()
|
||||
.flex_wrap()
|
||||
@@ -205,6 +236,7 @@ impl AssistantConfiguration {
|
||||
"always-allow-tool-actions-switch",
|
||||
always_allow_tool_actions.into(),
|
||||
)
|
||||
.color(SwitchColor::Accent)
|
||||
.on_click({
|
||||
let fs = self.fs.clone();
|
||||
move |state, _window, cx| {
|
||||
@@ -224,19 +256,20 @@ impl AssistantConfiguration {
|
||||
|
||||
fn render_context_servers_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let context_servers = self.context_server_manager.read(cx).all_servers().clone();
|
||||
let tools_by_source = self.tools.tools_by_source(cx);
|
||||
let tools_by_source = self.tools.read(cx).tools_by_source(cx);
|
||||
let empty = Vec::new();
|
||||
|
||||
const SUBHEADING: &str = "Connect to context servers via the Model Context Protocol either via Zed extensions or directly.";
|
||||
|
||||
v_flex()
|
||||
.p(DynamicSpacing::Base16.rems(cx))
|
||||
.pr(DynamicSpacing::Base20.rems(cx))
|
||||
.gap_2()
|
||||
.flex_1()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(Headline::new("Context Servers (MCP)").size(HeadlineSize::Small))
|
||||
.child(Headline::new("Model Context Protocol (MCP) Servers"))
|
||||
.child(Label::new(SUBHEADING).color(Color::Muted)),
|
||||
)
|
||||
.children(context_servers.into_iter().map(|context_server| {
|
||||
@@ -257,15 +290,14 @@ impl AssistantConfiguration {
|
||||
v_flex()
|
||||
.id(SharedString::from(context_server.id()))
|
||||
.border_1()
|
||||
.rounded_sm()
|
||||
.rounded_md()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.bg(cx.theme().colors().background.opacity(0.25))
|
||||
.child(
|
||||
h_flex()
|
||||
.p_1()
|
||||
.justify_between()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.when(are_tools_expanded, |element| {
|
||||
.when(are_tools_expanded && tool_count > 1, |element| {
|
||||
element
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
@@ -275,6 +307,7 @@ impl AssistantConfiguration {
|
||||
.gap_2()
|
||||
.child(
|
||||
Disclosure::new("tool-list-disclosure", are_tools_expanded)
|
||||
.disabled(tool_count == 0)
|
||||
.on_click(cx.listener({
|
||||
let context_server_id = context_server.id();
|
||||
move |this, _event, _window, _cx| {
|
||||
@@ -295,65 +328,78 @@ impl AssistantConfiguration {
|
||||
.child(Label::new(context_server.id()))
|
||||
.child(
|
||||
Label::new(format!("{tool_count} tools"))
|
||||
.color(Color::Muted),
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small),
|
||||
),
|
||||
)
|
||||
.child(h_flex().child(
|
||||
Switch::new("context-server-switch", is_running.into()).on_click({
|
||||
let context_server_manager =
|
||||
self.context_server_manager.clone();
|
||||
let context_server = context_server.clone();
|
||||
move |state, _window, cx| match state {
|
||||
ToggleState::Unselected | ToggleState::Indeterminate => {
|
||||
context_server_manager.update(cx, |this, cx| {
|
||||
this.stop_server(context_server.clone(), cx)
|
||||
.log_err();
|
||||
});
|
||||
}
|
||||
ToggleState::Selected => {
|
||||
cx.spawn({
|
||||
let context_server_manager =
|
||||
context_server_manager.clone();
|
||||
let context_server = context_server.clone();
|
||||
async move |cx| {
|
||||
if let Some(start_server_task) =
|
||||
context_server_manager
|
||||
.update(cx, |this, cx| {
|
||||
this.start_server(
|
||||
context_server,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.log_err()
|
||||
{
|
||||
start_server_task.await.log_err();
|
||||
.child(
|
||||
Switch::new("context-server-switch", is_running.into())
|
||||
.color(SwitchColor::Accent)
|
||||
.on_click({
|
||||
let context_server_manager =
|
||||
self.context_server_manager.clone();
|
||||
let context_server = context_server.clone();
|
||||
move |state, _window, cx| match state {
|
||||
ToggleState::Unselected
|
||||
| ToggleState::Indeterminate => {
|
||||
context_server_manager.update(cx, |this, cx| {
|
||||
this.stop_server(context_server.clone(), cx)
|
||||
.log_err();
|
||||
});
|
||||
}
|
||||
ToggleState::Selected => {
|
||||
cx.spawn({
|
||||
let context_server_manager =
|
||||
context_server_manager.clone();
|
||||
let context_server = context_server.clone();
|
||||
async move |cx| {
|
||||
if let Some(start_server_task) =
|
||||
context_server_manager
|
||||
.update(cx, |this, cx| {
|
||||
this.start_server(
|
||||
context_server,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.log_err()
|
||||
{
|
||||
start_server_task.await.log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
)),
|
||||
}),
|
||||
),
|
||||
)
|
||||
.map(|parent| {
|
||||
if !are_tools_expanded {
|
||||
return parent;
|
||||
}
|
||||
|
||||
parent.child(v_flex().children(tools.into_iter().enumerate().map(
|
||||
|(ix, tool)| {
|
||||
parent.child(v_flex().py_1p5().px_1().gap_1().children(
|
||||
tools.into_iter().enumerate().map(|(ix, tool)| {
|
||||
h_flex()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.when(ix < tool_count - 1, |element| {
|
||||
element
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
})
|
||||
.child(Label::new(tool.name()))
|
||||
},
|
||||
)))
|
||||
.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()))
|
||||
}),
|
||||
))
|
||||
})
|
||||
}))
|
||||
.child(
|
||||
@@ -362,7 +408,7 @@ impl AssistantConfiguration {
|
||||
.gap_2()
|
||||
.child(
|
||||
h_flex().w_full().child(
|
||||
Button::new("add-context-server", "Add Context Server")
|
||||
Button::new("add-context-server", "Add Custom Server")
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.full_width()
|
||||
@@ -378,7 +424,7 @@ impl AssistantConfiguration {
|
||||
h_flex().w_full().child(
|
||||
Button::new(
|
||||
"install-context-server-extensions",
|
||||
"Install Context Server Extensions",
|
||||
"Install MCP Extensions",
|
||||
)
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
@@ -405,39 +451,51 @@ impl AssistantConfiguration {
|
||||
|
||||
impl Render for AssistantConfiguration {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let providers = LanguageModelRegistry::read_global(cx).providers();
|
||||
|
||||
v_flex()
|
||||
.id("assistant-configuration")
|
||||
.key_context("AgentConfiguration")
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.bg(cx.theme().colors().panel_background)
|
||||
.relative()
|
||||
.size_full()
|
||||
.overflow_y_scroll()
|
||||
.child(self.render_command_permission(cx))
|
||||
.child(Divider::horizontal().color(DividerColor::Border))
|
||||
.child(self.render_context_servers_section(cx))
|
||||
.child(Divider::horizontal().color(DividerColor::Border))
|
||||
.pb_8()
|
||||
.bg(cx.theme().colors().panel_background)
|
||||
.child(
|
||||
v_flex()
|
||||
.p(DynamicSpacing::Base16.rems(cx))
|
||||
.mt_1()
|
||||
.gap_6()
|
||||
.flex_1()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(Headline::new("LLM Providers").size(HeadlineSize::Small))
|
||||
.child(
|
||||
Label::new("Add at least one provider to use AI-powered features.")
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.children(
|
||||
providers
|
||||
.into_iter()
|
||||
.map(|provider| self.render_provider_configuration(&provider, cx)),
|
||||
),
|
||||
.id("assistant-configuration-content")
|
||||
.track_scroll(&self.scroll_handle)
|
||||
.size_full()
|
||||
.overflow_y_scroll()
|
||||
.child(self.render_command_permission(cx))
|
||||
.child(Divider::horizontal().color(DividerColor::Border))
|
||||
.child(self.render_context_servers_section(cx))
|
||||
.child(Divider::horizontal().color(DividerColor::Border))
|
||||
.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())),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use context_server::{ContextServerSettings, ServerCommand, ServerConfig};
|
||||
use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity, prelude::*};
|
||||
use serde_json::json;
|
||||
use settings::update_settings_file;
|
||||
use ui::{Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*};
|
||||
use ui::{KeyBinding, Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*};
|
||||
use ui_input::SingleLineInput;
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
@@ -34,9 +34,9 @@ impl AddContextServerModal {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let name_editor =
|
||||
cx.new(|cx| SingleLineInput::new(window, cx, "Your server name").label("Name"));
|
||||
cx.new(|cx| SingleLineInput::new(window, cx, "my-custom-server").label("Name"));
|
||||
let command_editor = cx.new(|cx| {
|
||||
SingleLineInput::new(window, cx, "Command").label("Command to run the context server")
|
||||
SingleLineInput::new(window, cx, "Command").label("Command to run the MCP server")
|
||||
});
|
||||
|
||||
Self {
|
||||
@@ -46,7 +46,7 @@ impl AddContextServerModal {
|
||||
}
|
||||
}
|
||||
|
||||
fn confirm(&mut self, cx: &mut Context<Self>) {
|
||||
fn confirm(&mut self, _: &menu::Confirm, cx: &mut Context<Self>) {
|
||||
let name = self
|
||||
.name_editor
|
||||
.read(cx)
|
||||
@@ -96,7 +96,7 @@ impl AddContextServerModal {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
fn cancel(&mut self, cx: &mut Context<Self>) {
|
||||
fn cancel(&mut self, _: &menu::Cancel, cx: &mut Context<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
@@ -112,38 +112,68 @@ impl Focusable for AddContextServerModal {
|
||||
impl EventEmitter<DismissEvent> for AddContextServerModal {}
|
||||
|
||||
impl Render for AddContextServerModal {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let is_name_empty = self.name_editor.read(cx).is_empty(cx);
|
||||
let is_command_empty = self.command_editor.read(cx).is_empty(cx);
|
||||
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
|
||||
div()
|
||||
.elevation_3(cx)
|
||||
.w(rems(34.))
|
||||
.key_context("AddContextServerModal")
|
||||
.on_action(cx.listener(|this, _: &menu::Cancel, _window, cx| this.cancel(cx)))
|
||||
.on_action(cx.listener(|this, _: &menu::Confirm, _window, cx| this.confirm(cx)))
|
||||
.on_action(
|
||||
cx.listener(|this, _: &menu::Cancel, _window, cx| this.cancel(&menu::Cancel, cx)),
|
||||
)
|
||||
.on_action(
|
||||
cx.listener(|this, _: &menu::Confirm, _window, cx| {
|
||||
this.confirm(&menu::Confirm, cx)
|
||||
}),
|
||||
)
|
||||
.capture_any_mouse_down(cx.listener(|this, _, window, cx| {
|
||||
this.focus_handle(cx).focus(window);
|
||||
}))
|
||||
.on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent)))
|
||||
.child(
|
||||
Modal::new("add-context-server", None)
|
||||
.header(ModalHeader::new().headline("Add Context Server"))
|
||||
.header(ModalHeader::new().headline("Add MCP Server"))
|
||||
.section(
|
||||
Section::new()
|
||||
.child(self.name_editor.clone())
|
||||
.child(self.command_editor.clone()),
|
||||
Section::new().child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(self.name_editor.clone())
|
||||
.child(self.command_editor.clone()),
|
||||
),
|
||||
)
|
||||
.footer(
|
||||
ModalFooter::new()
|
||||
.start_slot(
|
||||
Button::new("cancel", "Cancel").on_click(
|
||||
cx.listener(|this, _event, _window, cx| this.cancel(cx)),
|
||||
),
|
||||
Button::new("cancel", "Cancel")
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(
|
||||
&menu::Cancel,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(12.))),
|
||||
)
|
||||
.on_click(cx.listener(|this, _event, _window, cx| {
|
||||
this.cancel(&menu::Cancel, cx)
|
||||
})),
|
||||
)
|
||||
.end_slot(
|
||||
Button::new("add-server", "Add Server")
|
||||
.disabled(is_name_empty || is_command_empty)
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(
|
||||
&menu::Confirm,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(12.))),
|
||||
)
|
||||
.map(|button| {
|
||||
if is_name_empty {
|
||||
button.tooltip(Tooltip::text("Name is required"))
|
||||
@@ -153,9 +183,9 @@ impl Render for AddContextServerModal {
|
||||
button
|
||||
}
|
||||
})
|
||||
.on_click(
|
||||
cx.listener(|this, _event, _window, cx| this.confirm(cx)),
|
||||
),
|
||||
.on_click(cx.listener(|this, _event, _window, cx| {
|
||||
this.confirm(&menu::Confirm, cx)
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -84,7 +84,7 @@ pub struct NewProfileMode {
|
||||
|
||||
pub struct ManageProfilesModal {
|
||||
fs: Arc<dyn Fs>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
thread_store: WeakEntity<ThreadStore>,
|
||||
focus_handle: FocusHandle,
|
||||
mode: Mode,
|
||||
@@ -117,7 +117,7 @@ impl ManageProfilesModal {
|
||||
|
||||
pub fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
thread_store: WeakEntity<ThreadStore>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::sync::Arc;
|
||||
|
||||
use assistant_settings::{
|
||||
AgentProfile, AgentProfileContent, AgentProfileId, AssistantSettings, AssistantSettingsContent,
|
||||
ContextServerPresetContent, VersionedAssistantSettingsContent,
|
||||
ContextServerPresetContent,
|
||||
};
|
||||
use assistant_tool::{ToolSource, ToolWorkingSet};
|
||||
use fs::Fs;
|
||||
@@ -60,7 +60,7 @@ pub struct ToolPickerDelegate {
|
||||
impl ToolPickerDelegate {
|
||||
pub fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
tool_set: Arc<ToolWorkingSet>,
|
||||
tool_set: Entity<ToolWorkingSet>,
|
||||
thread_store: WeakEntity<ThreadStore>,
|
||||
profile_id: AgentProfileId,
|
||||
profile: AgentProfile,
|
||||
@@ -68,7 +68,7 @@ impl ToolPickerDelegate {
|
||||
) -> Self {
|
||||
let mut tool_entries = Vec::new();
|
||||
|
||||
for (source, tools) in tool_set.tools_by_source(cx) {
|
||||
for (source, tools) in tool_set.read(cx).tools_by_source(cx) {
|
||||
tool_entries.extend(tools.into_iter().map(|tool| ToolEntry {
|
||||
name: tool.name().into(),
|
||||
source: source.clone(),
|
||||
@@ -192,7 +192,7 @@ impl PickerDelegate for ToolPickerDelegate {
|
||||
if active_profile_id == &self.profile_id {
|
||||
self.thread_store
|
||||
.update(cx, |this, cx| {
|
||||
this.load_profile(&self.profile, cx);
|
||||
this.load_profile(self.profile.clone(), cx);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
@@ -201,10 +201,10 @@ impl PickerDelegate for ToolPickerDelegate {
|
||||
let profile_id = self.profile_id.clone();
|
||||
let default_profile = self.profile.clone();
|
||||
let tool = tool.clone();
|
||||
move |settings, _cx| match settings {
|
||||
AssistantSettingsContent::Versioned(boxed) => {
|
||||
if let VersionedAssistantSettingsContent::V2(ref mut settings) = **boxed {
|
||||
let profiles = settings.profiles.get_or_insert_default();
|
||||
move |settings: &mut AssistantSettingsContent, _cx| {
|
||||
settings
|
||||
.v2_setting(|v2_settings| {
|
||||
let profiles = v2_settings.profiles.get_or_insert_default();
|
||||
let profile =
|
||||
profiles
|
||||
.entry(profile_id)
|
||||
@@ -240,9 +240,10 @@ impl PickerDelegate for ToolPickerDelegate {
|
||||
*preset.tools.entry(tool.name.clone()).or_default() = is_enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -271,7 +272,7 @@ impl PickerDelegate for ToolPickerDelegate {
|
||||
.get(id.as_ref())
|
||||
.and_then(|preset| preset.tools.get(&tool.name))
|
||||
.copied()
|
||||
.unwrap_or(false),
|
||||
.unwrap_or(self.profile.enable_all_context_servers),
|
||||
};
|
||||
|
||||
Some(
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use assistant_settings::AssistantSettings;
|
||||
use fs::Fs;
|
||||
use gpui::{Entity, FocusHandle, SharedString};
|
||||
use language_model::LanguageModelRegistry;
|
||||
|
||||
use crate::Thread;
|
||||
use language_model::{ConfiguredModel, LanguageModelRegistry};
|
||||
use language_model_selector::{
|
||||
LanguageModelSelector, LanguageModelSelectorPopoverMenu, ToggleModelSelector,
|
||||
};
|
||||
@@ -9,9 +11,9 @@ use settings::update_settings_file;
|
||||
use std::sync::Arc;
|
||||
use ui::{ButtonLike, PopoverMenuHandle, Tooltip, prelude::*};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(Clone)]
|
||||
pub enum ModelType {
|
||||
Default,
|
||||
Default(Entity<Thread>),
|
||||
InlineAssistant,
|
||||
}
|
||||
|
||||
@@ -19,7 +21,6 @@ pub struct AssistantModelSelector {
|
||||
selector: Entity<LanguageModelSelector>,
|
||||
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
|
||||
focus_handle: FocusHandle,
|
||||
model_type: ModelType,
|
||||
}
|
||||
|
||||
impl AssistantModelSelector {
|
||||
@@ -29,18 +30,39 @@ impl AssistantModelSelector {
|
||||
focus_handle: FocusHandle,
|
||||
model_type: ModelType,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
Self {
|
||||
selector: cx.new(|cx| {
|
||||
selector: cx.new(move |cx| {
|
||||
let fs = fs.clone();
|
||||
LanguageModelSelector::new(
|
||||
{
|
||||
let model_type = model_type.clone();
|
||||
move |cx| match &model_type {
|
||||
ModelType::Default(thread) => thread.read(cx).configured_model(),
|
||||
ModelType::InlineAssistant => {
|
||||
LanguageModelRegistry::read_global(cx).inline_assistant_model()
|
||||
}
|
||||
}
|
||||
},
|
||||
move |model, cx| {
|
||||
let provider = model.provider_id().0.to_string();
|
||||
let model_id = model.id().0.to_string();
|
||||
|
||||
match model_type {
|
||||
ModelType::Default => {
|
||||
match &model_type {
|
||||
ModelType::Default(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::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
@@ -69,7 +91,6 @@ impl AssistantModelSelector {
|
||||
}),
|
||||
menu_handle,
|
||||
focus_handle,
|
||||
model_type,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,17 +101,12 @@ impl AssistantModelSelector {
|
||||
|
||||
impl Render for AssistantModelSelector {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
|
||||
let model = match self.model_type {
|
||||
ModelType::Default => model_registry.default_model(),
|
||||
ModelType::InlineAssistant => model_registry.inline_assistant_model(),
|
||||
};
|
||||
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
let model_name = match model {
|
||||
Some(model) => model.model.name().0,
|
||||
_ => SharedString::from("No model selected"),
|
||||
|
||||
let model = self.selector.read(cx).active_model(cx);
|
||||
let (model_name, model_icon) = match model {
|
||||
Some(model) => (model.model.name().0, Some(model.provider.icon())),
|
||||
_ => (SharedString::from("No model selected"), None),
|
||||
};
|
||||
|
||||
LanguageModelSelectorPopoverMenu::new(
|
||||
@@ -100,10 +116,16 @@ impl Render for AssistantModelSelector {
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.children(
|
||||
model_icon.map(|icon| {
|
||||
Icon::new(icon).color(Color::Muted).size(IconSize::Small)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Label::new(model_name)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
.color(Color::Muted)
|
||||
.ml_1(),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
use crate::context::attach_context_to_message;
|
||||
use crate::context_store::ContextStore;
|
||||
use crate::context::ContextLoadResult;
|
||||
use crate::inline_prompt_editor::CodegenStatus;
|
||||
use anyhow::{Context as _, Result};
|
||||
use crate::{context::load_context, context_store::ContextStore};
|
||||
use anyhow::Result;
|
||||
use client::telemetry::Telemetry;
|
||||
use collections::HashSet;
|
||||
use editor::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint};
|
||||
use futures::{SinkExt, Stream, StreamExt, channel::mpsc, future::LocalBoxFuture, join};
|
||||
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Subscription, Task};
|
||||
use futures::{
|
||||
SinkExt, Stream, StreamExt, TryStreamExt as _, channel::mpsc, future::LocalBoxFuture, join,
|
||||
};
|
||||
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Subscription, Task, WeakEntity};
|
||||
use language::{Buffer, IndentKind, Point, TransactionId, line_diff};
|
||||
use language_model::{
|
||||
LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||
@@ -14,7 +16,9 @@ use language_model::{
|
||||
};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use parking_lot::Mutex;
|
||||
use project::Project;
|
||||
use prompt_store::PromptBuilder;
|
||||
use prompt_store::PromptStore;
|
||||
use rope::Rope;
|
||||
use smol::future::FutureExt;
|
||||
use std::{
|
||||
@@ -39,6 +43,8 @@ pub struct BufferCodegen {
|
||||
range: Range<Anchor>,
|
||||
initial_transaction_id: Option<TransactionId>,
|
||||
context_store: Entity<ContextStore>,
|
||||
project: WeakEntity<Project>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
telemetry: Arc<Telemetry>,
|
||||
builder: Arc<PromptBuilder>,
|
||||
pub is_insertion: bool,
|
||||
@@ -50,6 +56,8 @@ impl BufferCodegen {
|
||||
range: Range<Anchor>,
|
||||
initial_transaction_id: Option<TransactionId>,
|
||||
context_store: Entity<ContextStore>,
|
||||
project: WeakEntity<Project>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
telemetry: Arc<Telemetry>,
|
||||
builder: Arc<PromptBuilder>,
|
||||
cx: &mut Context<Self>,
|
||||
@@ -60,6 +68,8 @@ impl BufferCodegen {
|
||||
range.clone(),
|
||||
false,
|
||||
Some(context_store.clone()),
|
||||
project.clone(),
|
||||
prompt_store.clone(),
|
||||
Some(telemetry.clone()),
|
||||
builder.clone(),
|
||||
cx,
|
||||
@@ -75,6 +85,8 @@ impl BufferCodegen {
|
||||
range,
|
||||
initial_transaction_id,
|
||||
context_store,
|
||||
project,
|
||||
prompt_store,
|
||||
telemetry,
|
||||
builder,
|
||||
};
|
||||
@@ -131,7 +143,12 @@ impl BufferCodegen {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn start(&mut self, user_prompt: String, cx: &mut Context<Self>) -> Result<()> {
|
||||
pub fn start(
|
||||
&mut self,
|
||||
primary_model: Arc<dyn LanguageModel>,
|
||||
user_prompt: String,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Result<()> {
|
||||
let alternative_models = LanguageModelRegistry::read_global(cx)
|
||||
.inline_alternative_models()
|
||||
.to_vec();
|
||||
@@ -148,6 +165,8 @@ impl BufferCodegen {
|
||||
self.range.clone(),
|
||||
false,
|
||||
Some(self.context_store.clone()),
|
||||
self.project.clone(),
|
||||
self.prompt_store.clone(),
|
||||
Some(self.telemetry.clone()),
|
||||
self.builder.clone(),
|
||||
cx,
|
||||
@@ -155,11 +174,6 @@ impl BufferCodegen {
|
||||
}));
|
||||
}
|
||||
|
||||
let primary_model = LanguageModelRegistry::read_global(cx)
|
||||
.default_model()
|
||||
.context("no active model")?
|
||||
.model;
|
||||
|
||||
for (model, alternative) in iter::once(primary_model)
|
||||
.chain(alternative_models)
|
||||
.zip(&self.alternatives)
|
||||
@@ -229,13 +243,14 @@ pub struct CodegenAlternative {
|
||||
generation: Task<()>,
|
||||
diff: Diff,
|
||||
context_store: Option<Entity<ContextStore>>,
|
||||
project: WeakEntity<Project>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
_subscription: gpui::Subscription,
|
||||
builder: Arc<PromptBuilder>,
|
||||
active: bool,
|
||||
edits: Vec<(Range<Anchor>, String)>,
|
||||
line_operations: Vec<LineOperation>,
|
||||
request: Option<LanguageModelRequest>,
|
||||
elapsed_time: Option<f64>,
|
||||
completion: Option<String>,
|
||||
pub message_id: Option<String>,
|
||||
@@ -249,6 +264,8 @@ impl CodegenAlternative {
|
||||
range: Range<Anchor>,
|
||||
active: bool,
|
||||
context_store: Option<Entity<ContextStore>>,
|
||||
project: WeakEntity<Project>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
builder: Arc<PromptBuilder>,
|
||||
cx: &mut Context<Self>,
|
||||
@@ -290,6 +307,8 @@ impl CodegenAlternative {
|
||||
generation: Task::ready(()),
|
||||
diff: Diff::default(),
|
||||
context_store,
|
||||
project,
|
||||
prompt_store,
|
||||
telemetry,
|
||||
_subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
|
||||
builder,
|
||||
@@ -297,7 +316,6 @@ impl CodegenAlternative {
|
||||
edits: Vec::new(),
|
||||
line_operations: Vec::new(),
|
||||
range,
|
||||
request: None,
|
||||
elapsed_time: None,
|
||||
completion: None,
|
||||
}
|
||||
@@ -366,16 +384,18 @@ impl CodegenAlternative {
|
||||
async { Ok(LanguageModelTextStream::default()) }.boxed_local()
|
||||
} else {
|
||||
let request = self.build_request(user_prompt, cx)?;
|
||||
self.request = Some(request.clone());
|
||||
|
||||
cx.spawn(async move |_, cx| model.stream_completion_text(request, &cx).await)
|
||||
cx.spawn(async move |_, cx| model.stream_completion_text(request.await, &cx).await)
|
||||
.boxed_local()
|
||||
};
|
||||
self.handle_stream(telemetry_id, provider_id.to_string(), api_key, stream, cx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_request(&self, user_prompt: String, cx: &mut App) -> Result<LanguageModelRequest> {
|
||||
fn build_request(
|
||||
&self,
|
||||
user_prompt: String,
|
||||
cx: &mut App,
|
||||
) -> Result<Task<LanguageModelRequest>> {
|
||||
let buffer = self.buffer.read(cx).snapshot(cx);
|
||||
let language = buffer.language_at(self.range.start);
|
||||
let language_name = if let Some(language) = language.as_ref() {
|
||||
@@ -408,28 +428,45 @@ impl CodegenAlternative {
|
||||
.generate_inline_transformation_prompt(user_prompt, language_name, buffer, range)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to generate content prompt: {}", e))?;
|
||||
|
||||
let mut request_message = LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: Vec::new(),
|
||||
cache: false,
|
||||
};
|
||||
let context_task = self.context_store.as_ref().map(|context_store| {
|
||||
if let Some(project) = self.project.upgrade() {
|
||||
let context = context_store
|
||||
.read(cx)
|
||||
.context()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
load_context(context, &project, &self.prompt_store, cx)
|
||||
} else {
|
||||
Task::ready(ContextLoadResult::default())
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(context_store) = &self.context_store {
|
||||
attach_context_to_message(
|
||||
&mut request_message,
|
||||
context_store.read(cx).context().iter(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
Ok(cx.spawn(async move |_cx| {
|
||||
let mut request_message = LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: Vec::new(),
|
||||
cache: false,
|
||||
};
|
||||
|
||||
request_message.content.push(prompt.into());
|
||||
if let Some(context_task) = context_task {
|
||||
context_task
|
||||
.await
|
||||
.loaded_context
|
||||
.add_to_request_message(&mut request_message);
|
||||
}
|
||||
|
||||
Ok(LanguageModelRequest {
|
||||
tools: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
temperature: None,
|
||||
messages: vec![request_message],
|
||||
})
|
||||
request_message.content.push(prompt.into());
|
||||
|
||||
LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
mode: None,
|
||||
tools: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
temperature: None,
|
||||
messages: vec![request_message],
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn handle_stream(
|
||||
@@ -467,7 +504,7 @@ impl CodegenAlternative {
|
||||
}
|
||||
}
|
||||
|
||||
let http_client = cx.http_client().clone();
|
||||
let http_client = cx.http_client();
|
||||
let telemetry = self.telemetry.clone();
|
||||
let language_name = {
|
||||
let multibuffer = self.buffer.read(cx);
|
||||
@@ -506,7 +543,9 @@ impl CodegenAlternative {
|
||||
let mut response_latency = None;
|
||||
let request_start = Instant::now();
|
||||
let diff = async {
|
||||
let chunks = StripInvalidSpans::new(stream?.stream);
|
||||
let chunks = StripInvalidSpans::new(
|
||||
stream?.stream.map_err(|error| error.into()),
|
||||
);
|
||||
futures::pin_mut!(chunks);
|
||||
let mut diff = StreamingDiff::new(selected_text.to_string());
|
||||
let mut line_diff = LineDiff::default();
|
||||
@@ -1032,6 +1071,7 @@ impl Diff {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use fs::FakeFs;
|
||||
use futures::{
|
||||
Stream,
|
||||
stream::{self},
|
||||
@@ -1074,12 +1114,16 @@ mod tests {
|
||||
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5))
|
||||
});
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs, vec![], cx).await;
|
||||
let codegen = cx.new(|cx| {
|
||||
CodegenAlternative::new(
|
||||
buffer.clone(),
|
||||
range.clone(),
|
||||
true,
|
||||
None,
|
||||
project.downgrade(),
|
||||
None,
|
||||
None,
|
||||
prompt_builder,
|
||||
cx,
|
||||
@@ -1138,12 +1182,16 @@ mod tests {
|
||||
snapshot.anchor_before(Point::new(1, 6))..snapshot.anchor_after(Point::new(1, 6))
|
||||
});
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs, vec![], cx).await;
|
||||
let codegen = cx.new(|cx| {
|
||||
CodegenAlternative::new(
|
||||
buffer.clone(),
|
||||
range.clone(),
|
||||
true,
|
||||
None,
|
||||
project.downgrade(),
|
||||
None,
|
||||
None,
|
||||
prompt_builder,
|
||||
cx,
|
||||
@@ -1205,12 +1253,16 @@ mod tests {
|
||||
snapshot.anchor_before(Point::new(1, 2))..snapshot.anchor_after(Point::new(1, 2))
|
||||
});
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs, vec![], cx).await;
|
||||
let codegen = cx.new(|cx| {
|
||||
CodegenAlternative::new(
|
||||
buffer.clone(),
|
||||
range.clone(),
|
||||
true,
|
||||
None,
|
||||
project.downgrade(),
|
||||
None,
|
||||
None,
|
||||
prompt_builder,
|
||||
cx,
|
||||
@@ -1272,12 +1324,16 @@ mod tests {
|
||||
snapshot.anchor_before(Point::new(0, 0))..snapshot.anchor_after(Point::new(4, 2))
|
||||
});
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs, vec![], cx).await;
|
||||
let codegen = cx.new(|cx| {
|
||||
CodegenAlternative::new(
|
||||
buffer.clone(),
|
||||
range.clone(),
|
||||
true,
|
||||
None,
|
||||
project.downgrade(),
|
||||
None,
|
||||
None,
|
||||
prompt_builder,
|
||||
cx,
|
||||
@@ -1327,12 +1383,16 @@ mod tests {
|
||||
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(1, 14))
|
||||
});
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs, vec![], cx).await;
|
||||
let codegen = cx.new(|cx| {
|
||||
CodegenAlternative::new(
|
||||
buffer.clone(),
|
||||
range.clone(),
|
||||
false,
|
||||
None,
|
||||
project.downgrade(),
|
||||
None,
|
||||
None,
|
||||
prompt_builder,
|
||||
cx,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
mod completion_provider;
|
||||
mod fetch_context_picker;
|
||||
mod file_context_picker;
|
||||
mod rules_context_picker;
|
||||
mod symbol_context_picker;
|
||||
mod thread_context_picker;
|
||||
|
||||
@@ -9,35 +10,62 @@ use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
pub use completion_provider::ContextPickerCompletionProvider;
|
||||
use editor::display_map::{Crease, FoldId};
|
||||
use editor::{Anchor, AnchorRangeExt as _, Editor, ExcerptId, FoldPlaceholder, ToOffset};
|
||||
use fetch_context_picker::FetchContextPicker;
|
||||
use file_context_picker::FileContextPicker;
|
||||
use file_context_picker::render_file_context_entry;
|
||||
use gpui::{
|
||||
App, DismissEvent, Empty, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task,
|
||||
WeakEntity,
|
||||
};
|
||||
use language::Buffer;
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use project::{Entry, ProjectPath};
|
||||
use prompt_store::{PromptStore, UserPromptId};
|
||||
use rules_context_picker::{RulesContextEntry, RulesContextPicker};
|
||||
use symbol_context_picker::SymbolContextPicker;
|
||||
use thread_context_picker::{ThreadContextEntry, render_thread_context_entry};
|
||||
use thread_context_picker::{ThreadContextEntry, ThreadContextPicker, render_thread_context_entry};
|
||||
use ui::{
|
||||
ButtonLike, ContextMenu, ContextMenuEntry, ContextMenuItem, Disclosure, TintColor, prelude::*,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
use workspace::{Workspace, notifications::NotifyResultExt};
|
||||
|
||||
use crate::AssistantPanel;
|
||||
pub use crate::context_picker::completion_provider::ContextPickerCompletionProvider;
|
||||
use crate::context_picker::fetch_context_picker::FetchContextPicker;
|
||||
use crate::context_picker::file_context_picker::FileContextPicker;
|
||||
use crate::context_picker::thread_context_picker::ThreadContextPicker;
|
||||
use crate::context::RULES_ICON;
|
||||
use crate::context_store::ContextStore;
|
||||
use crate::thread::ThreadId;
|
||||
use crate::thread_store::ThreadStore;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ConfirmBehavior {
|
||||
KeepOpen,
|
||||
Close,
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum ContextPickerEntry {
|
||||
Mode(ContextPickerMode),
|
||||
Action(ContextPickerAction),
|
||||
}
|
||||
|
||||
impl ContextPickerEntry {
|
||||
pub fn keyword(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Mode(mode) => mode.keyword(),
|
||||
Self::Action(action) => action.keyword(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn label(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Mode(mode) => mode.label(),
|
||||
Self::Action(action) => action.label(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn icon(&self) -> IconName {
|
||||
match self {
|
||||
Self::Mode(mode) => mode.icon(),
|
||||
Self::Action(action) => action.icon(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -46,6 +74,32 @@ enum ContextPickerMode {
|
||||
Symbol,
|
||||
Fetch,
|
||||
Thread,
|
||||
Rules,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum ContextPickerAction {
|
||||
AddSelections,
|
||||
}
|
||||
|
||||
impl ContextPickerAction {
|
||||
pub fn keyword(&self) -> &'static str {
|
||||
match self {
|
||||
Self::AddSelections => "selection",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn label(&self) -> &'static str {
|
||||
match self {
|
||||
Self::AddSelections => "Selection",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn icon(&self) -> IconName {
|
||||
match self {
|
||||
Self::AddSelections => IconName::Context,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for ContextPickerMode {
|
||||
@@ -57,18 +111,20 @@ impl TryFrom<&str> for ContextPickerMode {
|
||||
"symbol" => Ok(Self::Symbol),
|
||||
"fetch" => Ok(Self::Fetch),
|
||||
"thread" => Ok(Self::Thread),
|
||||
"rules" => Ok(Self::Rules),
|
||||
_ => Err(format!("Invalid context picker mode: {}", value)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextPickerMode {
|
||||
pub fn mention_prefix(&self) -> &'static str {
|
||||
pub fn keyword(&self) -> &'static str {
|
||||
match self {
|
||||
Self::File => "file",
|
||||
Self::Symbol => "symbol",
|
||||
Self::Fetch => "fetch",
|
||||
Self::Thread => "thread",
|
||||
Self::Rules => "rules",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +134,7 @@ impl ContextPickerMode {
|
||||
Self::Symbol => "Symbols",
|
||||
Self::Fetch => "Fetch",
|
||||
Self::Thread => "Threads",
|
||||
Self::Rules => "Rules",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +144,7 @@ impl ContextPickerMode {
|
||||
Self::Symbol => IconName::Code,
|
||||
Self::Fetch => IconName::Globe,
|
||||
Self::Thread => IconName::MessageBubbles,
|
||||
Self::Rules => RULES_ICON,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,6 +156,7 @@ enum ContextPickerState {
|
||||
Symbol(Entity<SymbolContextPicker>),
|
||||
Fetch(Entity<FetchContextPicker>),
|
||||
Thread(Entity<ThreadContextPicker>),
|
||||
Rules(Entity<RulesContextPicker>),
|
||||
}
|
||||
|
||||
pub(super) struct ContextPicker {
|
||||
@@ -105,7 +164,7 @@ pub(super) struct ContextPicker {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
context_store: WeakEntity<ContextStore>,
|
||||
thread_store: Option<WeakEntity<ThreadStore>>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
@@ -114,7 +173,6 @@ impl ContextPicker {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
thread_store: Option<WeakEntity<ThreadStore>>,
|
||||
context_store: WeakEntity<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
@@ -134,6 +192,13 @@ impl ContextPicker {
|
||||
)
|
||||
.collect::<Vec<Subscription>>();
|
||||
|
||||
let prompt_store = thread_store.as_ref().and_then(|thread_store| {
|
||||
thread_store
|
||||
.read_with(cx, |thread_store, _cx| thread_store.prompt_store().clone())
|
||||
.ok()
|
||||
.flatten()
|
||||
});
|
||||
|
||||
ContextPicker {
|
||||
mode: ContextPickerState::Default(ContextMenu::build(
|
||||
window,
|
||||
@@ -143,7 +208,7 @@ impl ContextPicker {
|
||||
workspace,
|
||||
context_store,
|
||||
thread_store,
|
||||
confirm_behavior,
|
||||
prompt_store,
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
@@ -164,39 +229,45 @@ impl ContextPicker {
|
||||
.enumerate()
|
||||
.map(|(ix, entry)| self.recent_menu_item(context_picker.clone(), ix, entry));
|
||||
|
||||
let modes = supported_context_picker_modes(&self.thread_store);
|
||||
|
||||
let menu = menu
|
||||
.when(has_recent, |menu| {
|
||||
menu.custom_row(|_, _| {
|
||||
div()
|
||||
.mb_1()
|
||||
.child(
|
||||
Label::new("Recent")
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
let entries = self
|
||||
.workspace
|
||||
.upgrade()
|
||||
.map(|workspace| {
|
||||
available_context_picker_entries(
|
||||
&self.prompt_store,
|
||||
&self.thread_store,
|
||||
&workspace,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.extend(recent_entries)
|
||||
.when(has_recent, |menu| menu.separator())
|
||||
.extend(modes.into_iter().map(|mode| {
|
||||
let context_picker = context_picker.clone();
|
||||
.unwrap_or_default();
|
||||
|
||||
ContextMenuEntry::new(mode.label())
|
||||
.icon(mode.icon())
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.handler(move |window, cx| {
|
||||
context_picker.update(cx, |this, cx| this.select_mode(mode, window, cx))
|
||||
})
|
||||
}));
|
||||
menu.when(has_recent, |menu| {
|
||||
menu.custom_row(|_, _| {
|
||||
div()
|
||||
.mb_1()
|
||||
.child(
|
||||
Label::new("Recent")
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
})
|
||||
.extend(recent_entries)
|
||||
.when(has_recent, |menu| menu.separator())
|
||||
.extend(entries.into_iter().map(|entry| {
|
||||
let context_picker = context_picker.clone();
|
||||
|
||||
match self.confirm_behavior {
|
||||
ConfirmBehavior::KeepOpen => menu.keep_open_on_confirm(),
|
||||
ConfirmBehavior::Close => menu,
|
||||
}
|
||||
ContextMenuEntry::new(entry.label())
|
||||
.icon(entry.icon())
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.handler(move |window, cx| {
|
||||
context_picker.update(cx, |this, cx| this.select_entry(entry, window, cx))
|
||||
})
|
||||
}))
|
||||
.keep_open_on_confirm(true)
|
||||
});
|
||||
|
||||
cx.subscribe(&menu, move |_, _, _: &DismissEvent, cx| {
|
||||
@@ -212,65 +283,87 @@ impl ContextPicker {
|
||||
self.thread_store.is_some()
|
||||
}
|
||||
|
||||
fn select_mode(
|
||||
fn select_entry(
|
||||
&mut self,
|
||||
mode: ContextPickerMode,
|
||||
entry: ContextPickerEntry,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let context_picker = cx.entity().downgrade();
|
||||
|
||||
match mode {
|
||||
ContextPickerMode::File => {
|
||||
self.mode = ContextPickerState::File(cx.new(|cx| {
|
||||
FileContextPicker::new(
|
||||
context_picker.clone(),
|
||||
self.workspace.clone(),
|
||||
self.context_store.clone(),
|
||||
self.confirm_behavior,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
}
|
||||
ContextPickerMode::Symbol => {
|
||||
self.mode = ContextPickerState::Symbol(cx.new(|cx| {
|
||||
SymbolContextPicker::new(
|
||||
context_picker.clone(),
|
||||
self.workspace.clone(),
|
||||
self.context_store.clone(),
|
||||
self.confirm_behavior,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
}
|
||||
ContextPickerMode::Fetch => {
|
||||
self.mode = ContextPickerState::Fetch(cx.new(|cx| {
|
||||
FetchContextPicker::new(
|
||||
context_picker.clone(),
|
||||
self.workspace.clone(),
|
||||
self.context_store.clone(),
|
||||
self.confirm_behavior,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
}
|
||||
ContextPickerMode::Thread => {
|
||||
if let Some(thread_store) = self.thread_store.as_ref() {
|
||||
self.mode = ContextPickerState::Thread(cx.new(|cx| {
|
||||
ThreadContextPicker::new(
|
||||
thread_store.clone(),
|
||||
match entry {
|
||||
ContextPickerEntry::Mode(mode) => match mode {
|
||||
ContextPickerMode::File => {
|
||||
self.mode = ContextPickerState::File(cx.new(|cx| {
|
||||
FileContextPicker::new(
|
||||
context_picker.clone(),
|
||||
self.workspace.clone(),
|
||||
self.context_store.clone(),
|
||||
self.confirm_behavior,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
}
|
||||
}
|
||||
ContextPickerMode::Symbol => {
|
||||
self.mode = ContextPickerState::Symbol(cx.new(|cx| {
|
||||
SymbolContextPicker::new(
|
||||
context_picker.clone(),
|
||||
self.workspace.clone(),
|
||||
self.context_store.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
}
|
||||
ContextPickerMode::Rules => {
|
||||
if let Some(prompt_store) = self.prompt_store.as_ref() {
|
||||
self.mode = ContextPickerState::Rules(cx.new(|cx| {
|
||||
RulesContextPicker::new(
|
||||
prompt_store.clone(),
|
||||
context_picker.clone(),
|
||||
self.context_store.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
}
|
||||
}
|
||||
ContextPickerMode::Fetch => {
|
||||
self.mode = ContextPickerState::Fetch(cx.new(|cx| {
|
||||
FetchContextPicker::new(
|
||||
context_picker.clone(),
|
||||
self.workspace.clone(),
|
||||
self.context_store.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
}
|
||||
ContextPickerMode::Thread => {
|
||||
if let Some(thread_store) = self.thread_store.as_ref() {
|
||||
self.mode = ContextPickerState::Thread(cx.new(|cx| {
|
||||
ThreadContextPicker::new(
|
||||
thread_store.clone(),
|
||||
context_picker.clone(),
|
||||
self.context_store.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
}
|
||||
}
|
||||
},
|
||||
ContextPickerEntry::Action(action) => match action {
|
||||
ContextPickerAction::AddSelections => {
|
||||
if let Some((context_store, workspace)) =
|
||||
self.context_store.upgrade().zip(self.workspace.upgrade())
|
||||
{
|
||||
add_selections_as_context(&context_store, &workspace, cx);
|
||||
}
|
||||
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
@@ -289,12 +382,14 @@ impl ContextPicker {
|
||||
path_prefix,
|
||||
} => {
|
||||
let context_store = self.context_store.clone();
|
||||
let worktree_id = project_path.worktree_id;
|
||||
let path = project_path.path.clone();
|
||||
|
||||
ContextMenuItem::custom_entry(
|
||||
move |_window, cx| {
|
||||
render_file_context_entry(
|
||||
ElementId::NamedInteger("ctx-recent".into(), ix),
|
||||
ElementId::named_usize("ctx-recent", ix),
|
||||
worktree_id,
|
||||
&path,
|
||||
&path_prefix,
|
||||
false,
|
||||
@@ -397,6 +492,7 @@ impl ContextPicker {
|
||||
ContextPickerState::Symbol(entity) => entity.update(cx, |_, cx| cx.notify()),
|
||||
ContextPickerState::Fetch(entity) => entity.update(cx, |_, cx| cx.notify()),
|
||||
ContextPickerState::Thread(entity) => entity.update(cx, |_, cx| cx.notify()),
|
||||
ContextPickerState::Rules(entity) => entity.update(cx, |_, cx| cx.notify()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -411,6 +507,7 @@ impl Focusable for ContextPicker {
|
||||
ContextPickerState::Symbol(symbol_picker) => symbol_picker.focus_handle(cx),
|
||||
ContextPickerState::Fetch(fetch_picker) => fetch_picker.focus_handle(cx),
|
||||
ContextPickerState::Thread(thread_picker) => thread_picker.focus_handle(cx),
|
||||
ContextPickerState::Rules(user_rules_picker) => user_rules_picker.focus_handle(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -426,6 +523,9 @@ impl Render for ContextPicker {
|
||||
ContextPickerState::Symbol(symbol_picker) => parent.child(symbol_picker.clone()),
|
||||
ContextPickerState::Fetch(fetch_picker) => parent.child(fetch_picker.clone()),
|
||||
ContextPickerState::Thread(thread_picker) => parent.child(thread_picker.clone()),
|
||||
ContextPickerState::Rules(user_rules_picker) => {
|
||||
parent.child(user_rules_picker.clone())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -437,18 +537,41 @@ enum RecentEntry {
|
||||
Thread(ThreadContextEntry),
|
||||
}
|
||||
|
||||
fn supported_context_picker_modes(
|
||||
fn available_context_picker_entries(
|
||||
prompt_store: &Option<Entity<PromptStore>>,
|
||||
thread_store: &Option<WeakEntity<ThreadStore>>,
|
||||
) -> Vec<ContextPickerMode> {
|
||||
let mut modes = vec![
|
||||
ContextPickerMode::File,
|
||||
ContextPickerMode::Symbol,
|
||||
ContextPickerMode::Fetch,
|
||||
workspace: &Entity<Workspace>,
|
||||
cx: &mut App,
|
||||
) -> Vec<ContextPickerEntry> {
|
||||
let mut entries = vec![
|
||||
ContextPickerEntry::Mode(ContextPickerMode::File),
|
||||
ContextPickerEntry::Mode(ContextPickerMode::Symbol),
|
||||
];
|
||||
if thread_store.is_some() {
|
||||
modes.push(ContextPickerMode::Thread);
|
||||
|
||||
let has_selection = workspace
|
||||
.read(cx)
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.downcast::<Editor>())
|
||||
.map_or(false, |editor| {
|
||||
editor.update(cx, |editor, cx| editor.has_non_empty_selection(cx))
|
||||
});
|
||||
if has_selection {
|
||||
entries.push(ContextPickerEntry::Action(
|
||||
ContextPickerAction::AddSelections,
|
||||
));
|
||||
}
|
||||
modes
|
||||
|
||||
if thread_store.is_some() {
|
||||
entries.push(ContextPickerEntry::Mode(ContextPickerMode::Thread));
|
||||
}
|
||||
|
||||
if prompt_store.is_some() {
|
||||
entries.push(ContextPickerEntry::Mode(ContextPickerMode::Rules));
|
||||
}
|
||||
|
||||
entries.push(ContextPickerEntry::Mode(ContextPickerMode::Fetch));
|
||||
|
||||
entries
|
||||
}
|
||||
|
||||
fn recent_context_picker_entries(
|
||||
@@ -466,7 +589,7 @@ fn recent_context_picker_entries(
|
||||
recent.extend(
|
||||
workspace
|
||||
.recent_navigation_history_iter(cx)
|
||||
.filter(|(path, _)| !current_files.contains(&path.path.to_path_buf()))
|
||||
.filter(|(path, _)| !current_files.contains(path))
|
||||
.take(4)
|
||||
.filter_map(|(project_path, _)| {
|
||||
project
|
||||
@@ -478,22 +601,21 @@ fn recent_context_picker_entries(
|
||||
}),
|
||||
);
|
||||
|
||||
let mut current_threads = context_store.read(cx).thread_ids();
|
||||
let current_threads = context_store.read(cx).thread_ids();
|
||||
|
||||
if let Some(active_thread) = workspace
|
||||
let active_thread_id = workspace
|
||||
.panel::<AssistantPanel>(cx)
|
||||
.map(|panel| panel.read(cx).active_thread(cx))
|
||||
{
|
||||
current_threads.insert(active_thread.read(cx).id().clone());
|
||||
}
|
||||
.map(|panel| panel.read(cx).active_thread(cx).read(cx).id());
|
||||
|
||||
if let Some(thread_store) = thread_store.and_then(|thread_store| thread_store.upgrade()) {
|
||||
recent.extend(
|
||||
thread_store
|
||||
.read(cx)
|
||||
.threads()
|
||||
.reverse_chronological_threads()
|
||||
.into_iter()
|
||||
.filter(|thread| !current_threads.contains(&thread.id))
|
||||
.filter(|thread| {
|
||||
Some(&thread.id) != active_thread_id && !current_threads.contains(&thread.id)
|
||||
})
|
||||
.take(2)
|
||||
.map(|thread| {
|
||||
RecentEntry::Thread(ThreadContextEntry {
|
||||
@@ -507,14 +629,59 @@ fn recent_context_picker_entries(
|
||||
recent
|
||||
}
|
||||
|
||||
pub(crate) fn insert_crease_for_mention(
|
||||
fn add_selections_as_context(
|
||||
context_store: &Entity<ContextStore>,
|
||||
workspace: &Entity<Workspace>,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let selection_ranges = selection_ranges(workspace, cx);
|
||||
context_store.update(cx, |context_store, cx| {
|
||||
for (buffer, range) in selection_ranges {
|
||||
context_store.add_selection(buffer, range, cx);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn selection_ranges(
|
||||
workspace: &Entity<Workspace>,
|
||||
cx: &mut App,
|
||||
) -> Vec<(Entity<Buffer>, Range<text::Anchor>)> {
|
||||
let Some(editor) = workspace
|
||||
.read(cx)
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.act_as::<Editor>(cx))
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let selections = editor.selections.all_adjusted(cx);
|
||||
|
||||
let buffer = editor.buffer().clone().read(cx);
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
|
||||
selections
|
||||
.into_iter()
|
||||
.map(|s| snapshot.anchor_after(s.start)..snapshot.anchor_before(s.end))
|
||||
.flat_map(|range| {
|
||||
let (start_buffer, start) = buffer.text_anchor_for_position(range.start, cx)?;
|
||||
let (end_buffer, end) = buffer.text_anchor_for_position(range.end, cx)?;
|
||||
if start_buffer != end_buffer {
|
||||
return None;
|
||||
}
|
||||
Some((start_buffer, start..end))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn insert_fold_for_mention(
|
||||
excerpt_id: ExcerptId,
|
||||
crease_start: text::Anchor,
|
||||
content_len: usize,
|
||||
crease_label: SharedString,
|
||||
crease_icon_path: SharedString,
|
||||
editor_entity: Entity<Editor>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
editor_entity.update(cx, |editor, cx| {
|
||||
@@ -527,30 +694,42 @@ pub(crate) fn insert_crease_for_mention(
|
||||
let start = start.bias_right(&snapshot);
|
||||
let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
|
||||
|
||||
let placeholder = FoldPlaceholder {
|
||||
render: render_fold_icon_button(
|
||||
crease_icon_path,
|
||||
crease_label,
|
||||
editor_entity.downgrade(),
|
||||
),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let render_trailer =
|
||||
move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any();
|
||||
|
||||
let crease = Crease::inline(
|
||||
let crease = crease_for_mention(
|
||||
crease_label,
|
||||
crease_icon_path,
|
||||
start..end,
|
||||
placeholder.clone(),
|
||||
fold_toggle("mention"),
|
||||
render_trailer,
|
||||
editor_entity.downgrade(),
|
||||
);
|
||||
|
||||
editor.insert_creases(vec![crease.clone()], cx);
|
||||
editor.fold_creases(vec![crease], false, window, cx);
|
||||
editor.display_map.update(cx, |display_map, cx| {
|
||||
display_map.fold(vec![crease], cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn crease_for_mention(
|
||||
label: SharedString,
|
||||
icon_path: SharedString,
|
||||
range: Range<Anchor>,
|
||||
editor_entity: WeakEntity<Editor>,
|
||||
) -> Crease<Anchor> {
|
||||
let placeholder = FoldPlaceholder {
|
||||
render: render_fold_icon_button(icon_path, label, editor_entity),
|
||||
merge_adjacent: false,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let render_trailer = move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any();
|
||||
|
||||
let crease = Crease::inline(
|
||||
range,
|
||||
placeholder.clone(),
|
||||
fold_toggle("mention"),
|
||||
render_trailer,
|
||||
);
|
||||
crease
|
||||
}
|
||||
|
||||
fn render_fold_icon_button(
|
||||
icon_path: SharedString,
|
||||
label: SharedString,
|
||||
@@ -604,12 +783,13 @@ fn render_fold_icon_button(
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::from_path(icon_path.clone())
|
||||
.size(IconSize::Small)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Label::new(label.clone())
|
||||
.size(LabelSize::Small)
|
||||
.buffer_font(cx)
|
||||
.single_line(),
|
||||
),
|
||||
)
|
||||
@@ -638,15 +818,19 @@ fn fold_toggle(
|
||||
pub enum MentionLink {
|
||||
File(ProjectPath, Entry),
|
||||
Symbol(ProjectPath, String),
|
||||
Selection(ProjectPath, Range<usize>),
|
||||
Fetch(String),
|
||||
Thread(ThreadId),
|
||||
Rules(UserPromptId),
|
||||
}
|
||||
|
||||
impl MentionLink {
|
||||
const FILE: &str = "@file";
|
||||
const SYMBOL: &str = "@symbol";
|
||||
const SELECTION: &str = "@selection";
|
||||
const THREAD: &str = "@thread";
|
||||
const FETCH: &str = "@fetch";
|
||||
const RULES: &str = "@rules";
|
||||
|
||||
const SEPARATOR: &str = ":";
|
||||
|
||||
@@ -654,7 +838,9 @@ impl MentionLink {
|
||||
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::RULES)
|
||||
}
|
||||
|
||||
pub fn for_file(file_name: &str, full_path: &str) -> String {
|
||||
@@ -671,14 +857,31 @@ impl MentionLink {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn for_fetch(url: &str) -> String {
|
||||
format!("[@{}]({}:{})", url, Self::FETCH, url)
|
||||
pub fn for_selection(file_name: &str, full_path: &str, line_range: Range<usize>) -> String {
|
||||
format!(
|
||||
"[@{} ({}-{})]({}:{}:{}-{})",
|
||||
file_name,
|
||||
line_range.start,
|
||||
line_range.end,
|
||||
Self::SELECTION,
|
||||
full_path,
|
||||
line_range.start,
|
||||
line_range.end
|
||||
)
|
||||
}
|
||||
|
||||
pub fn for_thread(thread: &ThreadContextEntry) -> String {
|
||||
format!("[@{}]({}:{})", thread.summary, Self::THREAD, thread.id)
|
||||
}
|
||||
|
||||
pub fn for_fetch(url: &str) -> String {
|
||||
format!("[@{}]({}:{})", url, Self::FETCH, url)
|
||||
}
|
||||
|
||||
pub fn for_rules(rules: &RulesContextEntry) -> String {
|
||||
format!("[@{}]({}:{})", rules.title, Self::RULES, rules.prompt_id.0)
|
||||
}
|
||||
|
||||
pub fn try_parse(link: &str, workspace: &Entity<Workspace>, cx: &App) -> Option<Self> {
|
||||
fn extract_project_path_from_link(
|
||||
path: &str,
|
||||
@@ -715,11 +918,29 @@ impl MentionLink {
|
||||
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 => {
|
||||
let thread_id = ThreadId::from(argument);
|
||||
Some(MentionLink::Thread(thread_id))
|
||||
}
|
||||
Self::FETCH => Some(MentionLink::Fetch(argument.to_string())),
|
||||
Self::RULES => {
|
||||
let prompt_id = UserPromptId(Uuid::try_parse(argument).ok()?);
|
||||
Some(MentionLink::Rules(prompt_id))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ use picker::{Picker, PickerDelegate};
|
||||
use ui::{Context, ListItem, Window, prelude::*};
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
||||
use crate::context_picker::ContextPicker;
|
||||
use crate::context_store::ContextStore;
|
||||
|
||||
pub struct FetchContextPicker {
|
||||
@@ -23,16 +23,10 @@ impl FetchContextPicker {
|
||||
context_picker: WeakEntity<ContextPicker>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
context_store: WeakEntity<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let delegate = FetchContextPickerDelegate::new(
|
||||
context_picker,
|
||||
workspace,
|
||||
context_store,
|
||||
confirm_behavior,
|
||||
);
|
||||
let delegate = FetchContextPickerDelegate::new(context_picker, workspace, context_store);
|
||||
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
|
||||
|
||||
Self { picker }
|
||||
@@ -62,7 +56,6 @@ pub struct FetchContextPickerDelegate {
|
||||
context_picker: WeakEntity<ContextPicker>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
context_store: WeakEntity<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
url: String,
|
||||
}
|
||||
|
||||
@@ -71,13 +64,11 @@ impl FetchContextPickerDelegate {
|
||||
context_picker: WeakEntity<ContextPicker>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
context_store: WeakEntity<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
) -> Self {
|
||||
FetchContextPickerDelegate {
|
||||
context_picker,
|
||||
workspace,
|
||||
context_store,
|
||||
confirm_behavior,
|
||||
url: String::new(),
|
||||
}
|
||||
}
|
||||
@@ -202,27 +193,17 @@ impl PickerDelegate for FetchContextPickerDelegate {
|
||||
return;
|
||||
};
|
||||
|
||||
let http_client = workspace.read(cx).client().http_client().clone();
|
||||
let http_client = workspace.read(cx).client().http_client();
|
||||
let url = self.url.clone();
|
||||
let confirm_behavior = self.confirm_behavior;
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let text = cx
|
||||
.background_spawn(fetch_url_content(http_client, url.clone()))
|
||||
.await?;
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.delegate
|
||||
.context_store
|
||||
.update(cx, |context_store, cx| {
|
||||
context_store.add_fetched_url(url, text, cx)
|
||||
})?;
|
||||
|
||||
match confirm_behavior {
|
||||
ConfirmBehavior::KeepOpen => {}
|
||||
ConfirmBehavior::Close => this.delegate.dismissed(window, cx),
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
this.update(cx, |this, cx| {
|
||||
this.delegate.context_store.update(cx, |context_store, cx| {
|
||||
context_store.add_fetched_url(url, text, cx)
|
||||
})
|
||||
})??;
|
||||
|
||||
anyhow::Ok(())
|
||||
@@ -246,7 +227,7 @@ impl PickerDelegate for FetchContextPickerDelegate {
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let added = self.context_store.upgrade().map_or(false, |context_store| {
|
||||
context_store.read(cx).includes_url(&self.url).is_some()
|
||||
context_store.read(cx).includes_url(&self.url)
|
||||
});
|
||||
|
||||
Some(
|
||||
|
||||
@@ -11,9 +11,9 @@ use picker::{Picker, PickerDelegate};
|
||||
use project::{PathMatchCandidateSet, ProjectPath, WorktreeId};
|
||||
use ui::{ListItem, Tooltip, prelude::*};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{Workspace, notifications::NotifyResultExt};
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
||||
use crate::context_picker::ContextPicker;
|
||||
use crate::context_store::{ContextStore, FileInclusion};
|
||||
|
||||
pub struct FileContextPicker {
|
||||
@@ -25,16 +25,10 @@ impl FileContextPicker {
|
||||
context_picker: WeakEntity<ContextPicker>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
context_store: WeakEntity<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let delegate = FileContextPickerDelegate::new(
|
||||
context_picker,
|
||||
workspace,
|
||||
context_store,
|
||||
confirm_behavior,
|
||||
);
|
||||
let delegate = FileContextPickerDelegate::new(context_picker, workspace, context_store);
|
||||
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
|
||||
|
||||
Self { picker }
|
||||
@@ -57,8 +51,7 @@ pub struct FileContextPickerDelegate {
|
||||
context_picker: WeakEntity<ContextPicker>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
context_store: WeakEntity<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
matches: Vec<PathMatch>,
|
||||
matches: Vec<FileMatch>,
|
||||
selected_index: usize,
|
||||
}
|
||||
|
||||
@@ -67,13 +60,11 @@ impl FileContextPickerDelegate {
|
||||
context_picker: WeakEntity<ContextPicker>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
context_store: WeakEntity<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
) -> Self {
|
||||
Self {
|
||||
context_picker,
|
||||
workspace,
|
||||
context_store,
|
||||
confirm_behavior,
|
||||
matches: Vec::new(),
|
||||
selected_index: 0,
|
||||
}
|
||||
@@ -114,7 +105,7 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||
return Task::ready(());
|
||||
};
|
||||
|
||||
let search_task = search_paths(query, Arc::<AtomicBool>::default(), &workspace, cx);
|
||||
let search_task = search_files(query, Arc::<AtomicBool>::default(), &workspace, cx);
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
// TODO: This should be probably be run in the background.
|
||||
@@ -127,8 +118,8 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
let Some(mat) = self.matches.get(self.selected_index) else {
|
||||
fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
let Some(FileMatch { mat, .. }) = self.matches.get(self.selected_index) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -143,9 +134,9 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||
.context_store
|
||||
.update(cx, |context_store, cx| {
|
||||
if is_directory {
|
||||
context_store.add_directory(project_path, true, cx)
|
||||
Task::ready(context_store.add_directory(&project_path, true, cx))
|
||||
} else {
|
||||
context_store.add_file_from_path(project_path, true, cx)
|
||||
context_store.add_file_from_path(project_path.clone(), true, cx)
|
||||
}
|
||||
})
|
||||
.ok()
|
||||
@@ -153,17 +144,7 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||
return;
|
||||
};
|
||||
|
||||
let confirm_behavior = self.confirm_behavior;
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
match task.await.notify_async_err(cx) {
|
||||
None => anyhow::Ok(()),
|
||||
Some(()) => this.update_in(cx, |this, window, cx| match confirm_behavior {
|
||||
ConfirmBehavior::KeepOpen => {}
|
||||
ConfirmBehavior::Close => this.delegate.dismissed(window, cx),
|
||||
}),
|
||||
}
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
task.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
@@ -181,17 +162,18 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let path_match = &self.matches[ix];
|
||||
let FileMatch { mat, .. } = &self.matches[ix];
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.toggle_state(selected)
|
||||
.child(render_file_context_entry(
|
||||
ElementId::NamedInteger("file-ctx-picker".into(), ix),
|
||||
&path_match.path,
|
||||
&path_match.path_prefix,
|
||||
path_match.is_dir,
|
||||
ElementId::named_usize("file-ctx-picker", ix),
|
||||
WorktreeId::from_usize(mat.worktree_id),
|
||||
&mat.path,
|
||||
&mat.path_prefix,
|
||||
mat.is_dir,
|
||||
self.context_store.clone(),
|
||||
cx,
|
||||
)),
|
||||
@@ -199,12 +181,17 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn search_paths(
|
||||
pub struct FileMatch {
|
||||
pub mat: PathMatch,
|
||||
pub is_recent: bool,
|
||||
}
|
||||
|
||||
pub(crate) fn search_files(
|
||||
query: String,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
workspace: &Entity<Workspace>,
|
||||
cx: &App,
|
||||
) -> Task<Vec<PathMatch>> {
|
||||
) -> Task<Vec<FileMatch>> {
|
||||
if query.is_empty() {
|
||||
let workspace = workspace.read(cx);
|
||||
let project = workspace.project().read(cx);
|
||||
@@ -213,28 +200,34 @@ pub(crate) fn search_paths(
|
||||
.into_iter()
|
||||
.filter_map(|(project_path, _)| {
|
||||
let worktree = project.worktree_for_id(project_path.worktree_id, cx)?;
|
||||
Some(PathMatch {
|
||||
score: 0.,
|
||||
positions: Vec::new(),
|
||||
worktree_id: project_path.worktree_id.to_usize(),
|
||||
path: project_path.path,
|
||||
path_prefix: worktree.read(cx).root_name().into(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
is_dir: false,
|
||||
Some(FileMatch {
|
||||
mat: PathMatch {
|
||||
score: 0.,
|
||||
positions: Vec::new(),
|
||||
worktree_id: project_path.worktree_id.to_usize(),
|
||||
path: project_path.path,
|
||||
path_prefix: worktree.read(cx).root_name().into(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
is_dir: false,
|
||||
},
|
||||
is_recent: true,
|
||||
})
|
||||
});
|
||||
|
||||
let file_matches = project.worktrees(cx).flat_map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
let path_prefix: Arc<str> = worktree.root_name().into();
|
||||
worktree.entries(false, 0).map(move |entry| PathMatch {
|
||||
score: 0.,
|
||||
positions: Vec::new(),
|
||||
worktree_id: worktree.id().to_usize(),
|
||||
path: entry.path.clone(),
|
||||
path_prefix: path_prefix.clone(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
is_dir: entry.is_dir(),
|
||||
worktree.entries(false, 0).map(move |entry| FileMatch {
|
||||
mat: PathMatch {
|
||||
score: 0.,
|
||||
positions: Vec::new(),
|
||||
worktree_id: worktree.id().to_usize(),
|
||||
path: entry.path.clone(),
|
||||
path_prefix: path_prefix.clone(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
is_dir: entry.is_dir(),
|
||||
},
|
||||
is_recent: false,
|
||||
})
|
||||
});
|
||||
|
||||
@@ -269,6 +262,12 @@ pub(crate) fn search_paths(
|
||||
executor,
|
||||
)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|mat| FileMatch {
|
||||
mat,
|
||||
is_recent: false,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -311,19 +310,26 @@ pub fn extract_file_name_and_directory(
|
||||
|
||||
pub fn render_file_context_entry(
|
||||
id: ElementId,
|
||||
path: &Path,
|
||||
worktree_id: WorktreeId,
|
||||
path: &Arc<Path>,
|
||||
path_prefix: &Arc<str>,
|
||||
is_directory: bool,
|
||||
context_store: WeakEntity<ContextStore>,
|
||||
cx: &App,
|
||||
) -> Stateful<Div> {
|
||||
let (file_name, directory) = extract_file_name_and_directory(path, path_prefix);
|
||||
let (file_name, directory) = extract_file_name_and_directory(&path, path_prefix);
|
||||
|
||||
let added = context_store.upgrade().and_then(|context_store| {
|
||||
let project_path = ProjectPath {
|
||||
worktree_id,
|
||||
path: path.clone(),
|
||||
};
|
||||
if is_directory {
|
||||
context_store.read(cx).includes_directory(path)
|
||||
context_store
|
||||
.read(cx)
|
||||
.path_included_in_directory(&project_path, cx)
|
||||
} else {
|
||||
context_store.read(cx).will_include_file_path(path, cx)
|
||||
context_store.read(cx).file_path_included(&project_path, cx)
|
||||
}
|
||||
});
|
||||
|
||||
@@ -351,7 +357,7 @@ pub fn render_file_context_entry(
|
||||
})),
|
||||
)
|
||||
.when_some(added, |el, added| match added {
|
||||
FileInclusion::Direct(_) => el.child(
|
||||
FileInclusion::Direct => el.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_end()
|
||||
@@ -363,8 +369,8 @@ pub fn render_file_context_entry(
|
||||
)
|
||||
.child(Label::new("Added").size(LabelSize::Small)),
|
||||
),
|
||||
FileInclusion::InDirectory(dir_name) => {
|
||||
let dir_name = dir_name.to_string_lossy().into_owned();
|
||||
FileInclusion::InDirectory { full_path } => {
|
||||
let directory_full_path = full_path.to_string_lossy().into_owned();
|
||||
|
||||
el.child(
|
||||
h_flex()
|
||||
@@ -378,7 +384,7 @@ pub fn render_file_context_entry(
|
||||
)
|
||||
.child(Label::new("Included").size(LabelSize::Small)),
|
||||
)
|
||||
.tooltip(Tooltip::text(format!("in {dir_name}")))
|
||||
.tooltip(Tooltip::text(format!("in {directory_full_path}")))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
224
crates/agent/src/context_picker/rules_context_picker.rs
Normal file
@@ -0,0 +1,224 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use prompt_store::{PromptId, PromptStore, UserPromptId};
|
||||
use ui::{ListItem, prelude::*};
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::context::RULES_ICON;
|
||||
use crate::context_picker::ContextPicker;
|
||||
use crate::context_store::{self, ContextStore};
|
||||
|
||||
pub struct RulesContextPicker {
|
||||
picker: Entity<Picker<RulesContextPickerDelegate>>,
|
||||
}
|
||||
|
||||
impl RulesContextPicker {
|
||||
pub fn new(
|
||||
prompt_store: Entity<PromptStore>,
|
||||
context_picker: WeakEntity<ContextPicker>,
|
||||
context_store: WeakEntity<context_store::ContextStore>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let delegate = RulesContextPickerDelegate::new(prompt_store, context_picker, context_store);
|
||||
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
|
||||
|
||||
RulesContextPicker { picker }
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for RulesContextPicker {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for RulesContextPicker {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
self.picker.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RulesContextEntry {
|
||||
pub prompt_id: UserPromptId,
|
||||
pub title: SharedString,
|
||||
}
|
||||
|
||||
pub struct RulesContextPickerDelegate {
|
||||
prompt_store: Entity<PromptStore>,
|
||||
context_picker: WeakEntity<ContextPicker>,
|
||||
context_store: WeakEntity<context_store::ContextStore>,
|
||||
matches: Vec<RulesContextEntry>,
|
||||
selected_index: usize,
|
||||
}
|
||||
|
||||
impl RulesContextPickerDelegate {
|
||||
pub fn new(
|
||||
prompt_store: Entity<PromptStore>,
|
||||
context_picker: WeakEntity<ContextPicker>,
|
||||
context_store: WeakEntity<context_store::ContextStore>,
|
||||
) -> Self {
|
||||
RulesContextPickerDelegate {
|
||||
prompt_store,
|
||||
context_picker,
|
||||
context_store,
|
||||
matches: Vec::new(),
|
||||
selected_index: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for RulesContextPickerDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.matches.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(
|
||||
&mut self,
|
||||
ix: usize,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Picker<Self>>,
|
||||
) {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
|
||||
"Search available rules…".into()
|
||||
}
|
||||
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Task<()> {
|
||||
let search_task = search_rules(
|
||||
query,
|
||||
Arc::new(AtomicBool::default()),
|
||||
&self.prompt_store,
|
||||
cx,
|
||||
);
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let matches = search_task.await;
|
||||
this.update(cx, |this, cx| {
|
||||
this.delegate.matches = matches;
|
||||
this.delegate.selected_index = 0;
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
let Some(entry) = self.matches.get(self.selected_index) else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.context_store
|
||||
.update(cx, |context_store, cx| {
|
||||
context_store.add_rules(entry.prompt_id, true, cx)
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
self.context_picker
|
||||
.update(cx, |_, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let thread = &self.matches[ix];
|
||||
|
||||
Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
|
||||
render_thread_context_entry(thread, self.context_store.clone(), cx),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_thread_context_entry(
|
||||
user_rules: &RulesContextEntry,
|
||||
context_store: WeakEntity<ContextStore>,
|
||||
cx: &mut App,
|
||||
) -> Div {
|
||||
let added = context_store.upgrade().map_or(false, |context_store| {
|
||||
context_store
|
||||
.read(cx)
|
||||
.includes_user_rules(user_rules.prompt_id)
|
||||
});
|
||||
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.max_w_72()
|
||||
.child(
|
||||
Icon::new(RULES_ICON)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Label::new(user_rules.title.clone()).truncate()),
|
||||
)
|
||||
.when(added, |el| {
|
||||
el.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::new(IconName::Check)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Success),
|
||||
)
|
||||
.child(Label::new("Added").size(LabelSize::Small)),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn search_rules(
|
||||
query: String,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
prompt_store: &Entity<PromptStore>,
|
||||
cx: &mut App,
|
||||
) -> Task<Vec<RulesContextEntry>> {
|
||||
let search_task = prompt_store.read(cx).search(query, cancellation_flag, cx);
|
||||
cx.background_spawn(async move {
|
||||
search_task
|
||||
.await
|
||||
.into_iter()
|
||||
.flat_map(|metadata| {
|
||||
// Default prompts are filtered out as they are automatically included.
|
||||
if metadata.default {
|
||||
None
|
||||
} else {
|
||||
match metadata.id {
|
||||
PromptId::EditWorkflow => None,
|
||||
PromptId::User { uuid } => Some(RulesContextEntry {
|
||||
prompt_id: uuid,
|
||||
title: metadata.title?,
|
||||
}),
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
}
|
||||
@@ -2,7 +2,7 @@ use std::cmp::Reverse;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use anyhow::Result;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
App, AppContext, DismissEvent, Entity, FocusHandle, Focusable, Stateful, Task, WeakEntity,
|
||||
@@ -10,12 +10,11 @@ use gpui::{
|
||||
use ordered_float::OrderedFloat;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::{DocumentSymbol, Symbol};
|
||||
use text::OffsetRangeExt;
|
||||
use ui::{ListItem, prelude::*};
|
||||
use util::ResultExt as _;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
||||
use crate::context_picker::ContextPicker;
|
||||
use crate::context_store::ContextStore;
|
||||
|
||||
pub struct SymbolContextPicker {
|
||||
@@ -27,16 +26,10 @@ impl SymbolContextPicker {
|
||||
context_picker: WeakEntity<ContextPicker>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
context_store: WeakEntity<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let delegate = SymbolContextPickerDelegate::new(
|
||||
context_picker,
|
||||
workspace,
|
||||
context_store,
|
||||
confirm_behavior,
|
||||
);
|
||||
let delegate = SymbolContextPickerDelegate::new(context_picker, workspace, context_store);
|
||||
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
|
||||
|
||||
Self { picker }
|
||||
@@ -59,7 +52,6 @@ pub struct SymbolContextPickerDelegate {
|
||||
context_picker: WeakEntity<ContextPicker>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
context_store: WeakEntity<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
matches: Vec<SymbolEntry>,
|
||||
selected_index: usize,
|
||||
}
|
||||
@@ -69,13 +61,11 @@ impl SymbolContextPickerDelegate {
|
||||
context_picker: WeakEntity<ContextPicker>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
context_store: WeakEntity<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
) -> Self {
|
||||
Self {
|
||||
context_picker,
|
||||
workspace,
|
||||
context_store,
|
||||
confirm_behavior,
|
||||
matches: Vec::new(),
|
||||
selected_index: 0,
|
||||
}
|
||||
@@ -119,11 +109,7 @@ impl PickerDelegate for SymbolContextPickerDelegate {
|
||||
let search_task = search_symbols(query, Arc::<AtomicBool>::default(), &workspace, cx);
|
||||
let context_store = self.context_store.clone();
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let symbols = search_task
|
||||
.await
|
||||
.context("Failed to load symbols")
|
||||
.log_err()
|
||||
.unwrap_or_default();
|
||||
let symbols = search_task.await;
|
||||
|
||||
let symbol_entries = context_store
|
||||
.read_with(cx, |context_store, cx| {
|
||||
@@ -139,7 +125,7 @@ impl PickerDelegate for SymbolContextPickerDelegate {
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
let Some(mat) = self.matches.get(self.selected_index) else {
|
||||
return;
|
||||
};
|
||||
@@ -147,7 +133,6 @@ impl PickerDelegate for SymbolContextPickerDelegate {
|
||||
return;
|
||||
};
|
||||
|
||||
let confirm_behavior = self.confirm_behavior;
|
||||
let add_symbol_task = add_symbol(
|
||||
mat.symbol.clone(),
|
||||
true,
|
||||
@@ -157,16 +142,12 @@ impl PickerDelegate for SymbolContextPickerDelegate {
|
||||
);
|
||||
|
||||
let selected_index = self.selected_index;
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
cx.spawn(async move |this, cx| {
|
||||
let included = add_symbol_task.await?;
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.update(cx, |this, _| {
|
||||
if let Some(mat) = this.delegate.matches.get_mut(selected_index) {
|
||||
mat.is_included = included;
|
||||
}
|
||||
match confirm_behavior {
|
||||
ConfirmBehavior::KeepOpen => {}
|
||||
ConfirmBehavior::Close => this.delegate.dismissed(window, cx),
|
||||
}
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
@@ -190,10 +171,7 @@ impl PickerDelegate for SymbolContextPickerDelegate {
|
||||
let mat = &self.matches[ix];
|
||||
|
||||
Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
|
||||
render_symbol_context_entry(
|
||||
ElementId::NamedInteger("symbol-ctx-picker".into(), ix),
|
||||
mat,
|
||||
),
|
||||
render_symbol_context_entry(ElementId::named_usize("symbol-ctx-picker", ix), mat),
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -246,18 +224,16 @@ pub(crate) fn add_symbol(
|
||||
)
|
||||
})?;
|
||||
|
||||
context_store
|
||||
.update(cx, move |context_store, cx| {
|
||||
context_store.add_symbol(
|
||||
buffer,
|
||||
name.into(),
|
||||
range,
|
||||
enclosing_range,
|
||||
remove_if_exists,
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await
|
||||
context_store.update(cx, move |context_store, cx| {
|
||||
context_store.add_symbol(
|
||||
buffer,
|
||||
name.into(),
|
||||
range,
|
||||
enclosing_range,
|
||||
remove_if_exists,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -285,12 +261,16 @@ fn find_matching_symbol(symbol: &Symbol, candidates: &[DocumentSymbol]) -> Optio
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SymbolMatch {
|
||||
pub symbol: Symbol,
|
||||
}
|
||||
|
||||
pub(crate) fn search_symbols(
|
||||
query: String,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
workspace: &Entity<Workspace>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Vec<(StringMatch, Symbol)>>> {
|
||||
) -> Task<Vec<SymbolMatch>> {
|
||||
let symbols_task = workspace.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.project()
|
||||
@@ -298,19 +278,28 @@ pub(crate) fn search_symbols(
|
||||
});
|
||||
let project = workspace.read(cx).project().clone();
|
||||
cx.spawn(async move |cx| {
|
||||
let symbols = symbols_task.await?;
|
||||
let (visible_match_candidates, external_match_candidates): (Vec<_>, Vec<_>) = project
|
||||
.update(cx, |project, cx| {
|
||||
symbols
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, symbol)| StringMatchCandidate::new(id, &symbol.label.filter_text()))
|
||||
.partition(|candidate| {
|
||||
project
|
||||
.entry_for_path(&symbols[candidate.id].path, cx)
|
||||
.map_or(false, |e| !e.is_ignored)
|
||||
})
|
||||
})?;
|
||||
let Some(symbols) = symbols_task.await.log_err() else {
|
||||
return Vec::new();
|
||||
};
|
||||
let Some((visible_match_candidates, external_match_candidates)): Option<(Vec<_>, Vec<_>)> =
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
symbols
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, symbol)| {
|
||||
StringMatchCandidate::new(id, &symbol.label.filter_text())
|
||||
})
|
||||
.partition(|candidate| {
|
||||
project
|
||||
.entry_for_path(&symbols[candidate.id].path, cx)
|
||||
.map_or(false, |e| !e.is_ignored)
|
||||
})
|
||||
})
|
||||
.log_err()
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
const MAX_MATCHES: usize = 100;
|
||||
let mut visible_matches = cx.background_executor().block(fuzzy::match_strings(
|
||||
@@ -339,7 +328,7 @@ pub(crate) fn search_symbols(
|
||||
let mut matches = visible_matches;
|
||||
matches.append(&mut external_matches);
|
||||
|
||||
Ok(matches
|
||||
matches
|
||||
.into_iter()
|
||||
.map(|mut mat| {
|
||||
let symbol = symbols[mat.candidate_id].clone();
|
||||
@@ -347,49 +336,24 @@ pub(crate) fn search_symbols(
|
||||
for position in &mut mat.positions {
|
||||
*position += filter_start;
|
||||
}
|
||||
(mat, symbol)
|
||||
SymbolMatch { symbol }
|
||||
})
|
||||
.collect())
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
fn compute_symbol_entries(
|
||||
symbols: Vec<(StringMatch, Symbol)>,
|
||||
symbols: Vec<SymbolMatch>,
|
||||
context_store: &ContextStore,
|
||||
cx: &App,
|
||||
) -> Vec<SymbolEntry> {
|
||||
let mut symbol_entries = Vec::with_capacity(symbols.len());
|
||||
for (_, symbol) in symbols {
|
||||
let symbols_for_path = context_store.included_symbols_by_path().get(&symbol.path);
|
||||
let is_included = if let Some(symbols_for_path) = symbols_for_path {
|
||||
let mut is_included = false;
|
||||
for included_symbol_id in symbols_for_path {
|
||||
if included_symbol_id.name.as_ref() == symbol.name.as_str() {
|
||||
if let Some(buffer) = context_store.buffer_for_symbol(included_symbol_id) {
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let included_symbol_range =
|
||||
included_symbol_id.range.to_point_utf16(&snapshot);
|
||||
|
||||
if included_symbol_range.start == symbol.range.start.0
|
||||
&& included_symbol_range.end == symbol.range.end.0
|
||||
{
|
||||
is_included = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is_included
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
symbol_entries.push(SymbolEntry {
|
||||
symbols
|
||||
.into_iter()
|
||||
.map(|SymbolMatch { symbol, .. }| SymbolEntry {
|
||||
is_included: context_store.includes_symbol(&symbol, cx),
|
||||
symbol,
|
||||
is_included,
|
||||
})
|
||||
}
|
||||
symbol_entries
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
pub fn render_symbol_context_entry(id: ElementId, entry: &SymbolEntry) -> Stateful<Div> {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use ui::{ListItem, prelude::*};
|
||||
|
||||
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
||||
use crate::context_picker::ContextPicker;
|
||||
use crate::context_store::{self, ContextStore};
|
||||
use crate::thread::ThreadId;
|
||||
use crate::thread_store::ThreadStore;
|
||||
@@ -19,16 +20,11 @@ impl ThreadContextPicker {
|
||||
thread_store: WeakEntity<ThreadStore>,
|
||||
context_picker: WeakEntity<ContextPicker>,
|
||||
context_store: WeakEntity<context_store::ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let delegate = ThreadContextPickerDelegate::new(
|
||||
thread_store,
|
||||
context_picker,
|
||||
context_store,
|
||||
confirm_behavior,
|
||||
);
|
||||
let delegate =
|
||||
ThreadContextPickerDelegate::new(thread_store, context_picker, context_store);
|
||||
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
|
||||
|
||||
ThreadContextPicker { picker }
|
||||
@@ -57,7 +53,6 @@ pub struct ThreadContextPickerDelegate {
|
||||
thread_store: WeakEntity<ThreadStore>,
|
||||
context_picker: WeakEntity<ContextPicker>,
|
||||
context_store: WeakEntity<context_store::ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
matches: Vec<ThreadContextEntry>,
|
||||
selected_index: usize,
|
||||
}
|
||||
@@ -67,13 +62,11 @@ impl ThreadContextPickerDelegate {
|
||||
thread_store: WeakEntity<ThreadStore>,
|
||||
context_picker: WeakEntity<ContextPicker>,
|
||||
context_store: WeakEntity<context_store::ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
) -> Self {
|
||||
ThreadContextPickerDelegate {
|
||||
thread_store,
|
||||
context_picker,
|
||||
context_store,
|
||||
confirm_behavior,
|
||||
matches: Vec::new(),
|
||||
selected_index: 0,
|
||||
}
|
||||
@@ -110,15 +103,15 @@ impl PickerDelegate for ThreadContextPickerDelegate {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Task<()> {
|
||||
let Some(threads) = self.thread_store.upgrade() else {
|
||||
let Some(thread_store) = self.thread_store.upgrade() else {
|
||||
return Task::ready(());
|
||||
};
|
||||
|
||||
let search_task = search_threads(query, threads, cx);
|
||||
let search_task = search_threads(query, Arc::new(AtomicBool::default()), thread_store, cx);
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let matches = search_task.await;
|
||||
this.update(cx, |this, cx| {
|
||||
this.delegate.matches = matches;
|
||||
this.delegate.matches = matches.into_iter().map(|mat| mat.thread).collect();
|
||||
this.delegate.selected_index = 0;
|
||||
cx.notify();
|
||||
})
|
||||
@@ -126,7 +119,7 @@ impl PickerDelegate for ThreadContextPickerDelegate {
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
let Some(entry) = self.matches.get(self.selected_index) else {
|
||||
return;
|
||||
};
|
||||
@@ -137,20 +130,15 @@ impl PickerDelegate for ThreadContextPickerDelegate {
|
||||
|
||||
let open_thread_task = thread_store.update(cx, |this, cx| this.open_thread(&entry.id, cx));
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
cx.spawn(async move |this, cx| {
|
||||
let thread = open_thread_task.await?;
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.delegate
|
||||
.context_store
|
||||
.update(cx, |context_store, cx| {
|
||||
context_store.add_thread(thread, true, cx)
|
||||
})
|
||||
.ok();
|
||||
|
||||
match this.delegate.confirm_behavior {
|
||||
ConfirmBehavior::KeepOpen => {}
|
||||
ConfirmBehavior::Close => this.delegate.dismissed(window, cx),
|
||||
}
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
@@ -185,7 +173,7 @@ pub fn render_thread_context_entry(
|
||||
cx: &mut App,
|
||||
) -> Div {
|
||||
let added = context_store.upgrade().map_or(false, |ctx_store| {
|
||||
ctx_store.read(cx).includes_thread(&thread.id).is_some()
|
||||
ctx_store.read(cx).includes_thread(&thread.id)
|
||||
});
|
||||
|
||||
h_flex()
|
||||
@@ -217,25 +205,38 @@ pub fn render_thread_context_entry(
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ThreadMatch {
|
||||
pub thread: ThreadContextEntry,
|
||||
pub is_recent: bool,
|
||||
}
|
||||
|
||||
pub(crate) fn search_threads(
|
||||
query: String,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
thread_store: Entity<ThreadStore>,
|
||||
cx: &mut App,
|
||||
) -> Task<Vec<ThreadContextEntry>> {
|
||||
let threads = thread_store.update(cx, |this, _cx| {
|
||||
this.threads()
|
||||
.into_iter()
|
||||
.map(|thread| ThreadContextEntry {
|
||||
id: thread.id,
|
||||
summary: thread.summary,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
) -> Task<Vec<ThreadMatch>> {
|
||||
let threads = thread_store
|
||||
.read(cx)
|
||||
.reverse_chronological_threads()
|
||||
.into_iter()
|
||||
.map(|thread| ThreadContextEntry {
|
||||
id: thread.id,
|
||||
summary: thread.summary,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let executor = cx.background_executor().clone();
|
||||
cx.background_spawn(async move {
|
||||
if query.is_empty() {
|
||||
threads
|
||||
.into_iter()
|
||||
.map(|thread| ThreadMatch {
|
||||
thread,
|
||||
is_recent: false,
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
let candidates = threads
|
||||
.iter()
|
||||
@@ -247,14 +248,17 @@ pub(crate) fn search_threads(
|
||||
&query,
|
||||
false,
|
||||
100,
|
||||
&Default::default(),
|
||||
&cancellation_flag,
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
|
||||
matches
|
||||
.into_iter()
|
||||
.map(|mat| threads[mat.candidate_id].clone())
|
||||
.map(|mat| ThreadMatch {
|
||||
thread: threads[mat.candidate_id].clone(),
|
||||
is_recent: false,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
|
||||
use collections::HashSet;
|
||||
@@ -9,11 +10,12 @@ use gpui::{
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::Buffer;
|
||||
use project::ProjectItem;
|
||||
use ui::{KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
|
||||
use workspace::{Workspace, notifications::NotifyResultExt};
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::context::{ContextId, ContextKind};
|
||||
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
||||
use crate::context::{AgentContextHandle, ContextKind};
|
||||
use crate::context_picker::ContextPicker;
|
||||
use crate::context_store::ContextStore;
|
||||
use crate::thread::Thread;
|
||||
use crate::thread_store::ThreadStore;
|
||||
@@ -30,6 +32,7 @@ pub struct ContextStrip {
|
||||
focus_handle: FocusHandle,
|
||||
suggest_context_kind: SuggestContextKind,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
thread_store: Option<WeakEntity<ThreadStore>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
focused_index: Option<usize>,
|
||||
children_bounds: Option<Vec<Bounds<Pixels>>>,
|
||||
@@ -50,7 +53,6 @@ impl ContextStrip {
|
||||
workspace.clone(),
|
||||
thread_store.clone(),
|
||||
context_store.downgrade(),
|
||||
ConfirmBehavior::KeepOpen,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -72,12 +74,33 @@ impl ContextStrip {
|
||||
focus_handle,
|
||||
suggest_context_kind,
|
||||
workspace,
|
||||
thread_store,
|
||||
_subscriptions: subscriptions,
|
||||
focused_index: None,
|
||||
children_bounds: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn added_contexts(&self, cx: &App) -> Vec<AddedContext> {
|
||||
if let Some(workspace) = self.workspace.upgrade() {
|
||||
let project = workspace.read(cx).project().read(cx);
|
||||
let prompt_store = self
|
||||
.thread_store
|
||||
.as_ref()
|
||||
.and_then(|thread_store| thread_store.upgrade())
|
||||
.and_then(|thread_store| thread_store.read(cx).prompt_store().as_ref());
|
||||
self.context_store
|
||||
.read(cx)
|
||||
.context()
|
||||
.flat_map(|context| {
|
||||
AddedContext::new_pending(context.clone(), prompt_store, project, cx)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
fn suggested_context(&self, cx: &Context<Self>) -> Option<SuggestedContext> {
|
||||
match self.suggest_context_kind {
|
||||
SuggestContextKind::File => self.suggested_file(cx),
|
||||
@@ -92,27 +115,21 @@ impl ContextStrip {
|
||||
let editor = active_item.to_any().downcast::<Editor>().ok()?.read(cx);
|
||||
let active_buffer_entity = editor.buffer().read(cx).as_singleton()?;
|
||||
let active_buffer = active_buffer_entity.read(cx);
|
||||
|
||||
let path = active_buffer.file()?.full_path(cx);
|
||||
let project_path = active_buffer.project_path(cx)?;
|
||||
|
||||
if self
|
||||
.context_store
|
||||
.read(cx)
|
||||
.will_include_buffer(active_buffer.remote_id(), &path)
|
||||
.file_path_included(&project_path, cx)
|
||||
.is_some()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let name = match path.file_name() {
|
||||
Some(name) => name.to_string_lossy().into_owned().into(),
|
||||
None => path.to_string_lossy().into_owned().into(),
|
||||
};
|
||||
|
||||
let icon_path = FileIcons::get_icon(&path, cx);
|
||||
|
||||
let file_name = active_buffer.file()?.file_name(cx);
|
||||
let icon_path = FileIcons::get_icon(&Path::new(&file_name), cx);
|
||||
Some(SuggestedContext::File {
|
||||
name,
|
||||
name: file_name.to_string_lossy().into_owned().into(),
|
||||
buffer: active_buffer_entity.downgrade(),
|
||||
icon_path,
|
||||
})
|
||||
@@ -137,7 +154,6 @@ impl ContextStrip {
|
||||
.context_store
|
||||
.read(cx)
|
||||
.includes_thread(active_thread.id())
|
||||
.is_some()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
@@ -274,12 +290,12 @@ impl ContextStrip {
|
||||
best.map(|(index, _, _)| index)
|
||||
}
|
||||
|
||||
fn open_context(&mut self, id: ContextId, window: &mut Window, cx: &mut App) {
|
||||
fn open_context(&mut self, context: &AgentContextHandle, window: &mut Window, cx: &mut App) {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
crate::active_thread::open_context(id, self.context_store.clone(), workspace, window, cx);
|
||||
crate::active_thread::open_context(context, workspace, window, cx);
|
||||
}
|
||||
|
||||
fn remove_focused_context(
|
||||
@@ -289,17 +305,17 @@ impl ContextStrip {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(index) = self.focused_index {
|
||||
let mut is_empty = false;
|
||||
let added_contexts = self.added_contexts(cx);
|
||||
let Some(context) = added_contexts.get(index) else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.context_store.update(cx, |this, cx| {
|
||||
if let Some(item) = this.context().get(index) {
|
||||
this.remove_context(item.id(), cx);
|
||||
}
|
||||
|
||||
is_empty = this.context().is_empty();
|
||||
this.remove_context(&context.handle, cx);
|
||||
});
|
||||
|
||||
if is_empty {
|
||||
let is_now_empty = added_contexts.len() == 1;
|
||||
if is_now_empty {
|
||||
cx.emit(ContextStripEvent::BlurredEmpty);
|
||||
} else {
|
||||
self.focused_index = Some(index.saturating_sub(1));
|
||||
@@ -308,49 +324,28 @@ impl ContextStrip {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_suggested_focused<T>(&self, context: &Vec<T>) -> bool {
|
||||
fn is_suggested_focused(&self, added_contexts: &Vec<AddedContext>) -> bool {
|
||||
// We only suggest one item after the actual context
|
||||
self.focused_index == Some(context.len())
|
||||
self.focused_index == Some(added_contexts.len())
|
||||
}
|
||||
|
||||
fn accept_suggested_context(
|
||||
&mut self,
|
||||
_: &AcceptSuggestedContext,
|
||||
window: &mut Window,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(suggested) = self.suggested_context(cx) {
|
||||
let context_store = self.context_store.read(cx);
|
||||
|
||||
if self.is_suggested_focused(context_store.context()) {
|
||||
self.add_suggested_context(&suggested, window, cx);
|
||||
if self.is_suggested_focused(&self.added_contexts(cx)) {
|
||||
self.add_suggested_context(&suggested, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn add_suggested_context(
|
||||
&mut self,
|
||||
suggested: &SuggestedContext,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let task = self.context_store.update(cx, |context_store, cx| {
|
||||
context_store.accept_suggested_context(&suggested, cx)
|
||||
fn add_suggested_context(&mut self, suggested: &SuggestedContext, cx: &mut Context<Self>) {
|
||||
self.context_store.update(cx, |context_store, cx| {
|
||||
context_store.add_suggested_context(&suggested, cx)
|
||||
});
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
match task.await.notify_async_err(cx) {
|
||||
None => {}
|
||||
Some(()) => {
|
||||
if let Some(this) = this.upgrade() {
|
||||
this.update(cx, |_, cx| cx.notify())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
@@ -363,17 +358,10 @@ impl Focusable for ContextStrip {
|
||||
|
||||
impl Render for ContextStrip {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let context_store = self.context_store.read(cx);
|
||||
let context = context_store.context();
|
||||
let context_picker = self.context_picker.clone();
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
let suggested_context = self.suggested_context(cx);
|
||||
|
||||
let added_contexts = context
|
||||
.iter()
|
||||
.map(|c| AddedContext::new(c, cx))
|
||||
.collect::<Vec<_>>();
|
||||
let added_contexts = self.added_contexts(cx);
|
||||
let dupe_names = added_contexts
|
||||
.iter()
|
||||
.map(|c| c.name.clone())
|
||||
@@ -382,6 +370,14 @@ impl Render for ContextStrip {
|
||||
.filter(|(a, b)| a == b)
|
||||
.map(|(a, _)| a)
|
||||
.collect::<HashSet<SharedString>>();
|
||||
let no_added_context = added_contexts.is_empty();
|
||||
|
||||
let suggested_context = self.suggested_context(cx).map(|suggested_context| {
|
||||
(
|
||||
suggested_context,
|
||||
self.is_suggested_focused(&added_contexts),
|
||||
)
|
||||
});
|
||||
|
||||
h_flex()
|
||||
.flex_wrap()
|
||||
@@ -438,7 +434,7 @@ impl Render for ContextStrip {
|
||||
})
|
||||
.with_handle(self.context_picker_menu_handle.clone()),
|
||||
)
|
||||
.when(context.is_empty() && suggested_context.is_none(), {
|
||||
.when(no_added_context && suggested_context.is_none(), {
|
||||
|parent| {
|
||||
parent.child(
|
||||
h_flex()
|
||||
@@ -468,16 +464,17 @@ impl Render for ContextStrip {
|
||||
.enumerate()
|
||||
.map(|(i, added_context)| {
|
||||
let name = added_context.name.clone();
|
||||
let id = added_context.id;
|
||||
let context = added_context.handle.clone();
|
||||
ContextPill::added(
|
||||
added_context,
|
||||
dupe_names.contains(&name),
|
||||
self.focused_index == Some(i),
|
||||
Some({
|
||||
let context = context.clone();
|
||||
let context_store = self.context_store.clone();
|
||||
Rc::new(cx.listener(move |_this, _event, _window, cx| {
|
||||
context_store.update(cx, |this, cx| {
|
||||
this.remove_context(id, cx);
|
||||
this.remove_context(&context, cx);
|
||||
});
|
||||
cx.notify();
|
||||
}))
|
||||
@@ -486,7 +483,7 @@ impl Render for ContextStrip {
|
||||
.on_click({
|
||||
Rc::new(cx.listener(move |this, event: &ClickEvent, window, cx| {
|
||||
if event.down.click_count > 1 {
|
||||
this.open_context(id, window, cx);
|
||||
this.open_context(&context, window, cx);
|
||||
} else {
|
||||
this.focused_index = Some(i);
|
||||
}
|
||||
@@ -495,22 +492,22 @@ impl Render for ContextStrip {
|
||||
})
|
||||
}),
|
||||
)
|
||||
.when_some(suggested_context, |el, suggested| {
|
||||
.when_some(suggested_context, |el, (suggested, focused)| {
|
||||
el.child(
|
||||
ContextPill::suggested(
|
||||
suggested.name().clone(),
|
||||
suggested.icon_path(),
|
||||
suggested.kind(),
|
||||
self.is_suggested_focused(&context),
|
||||
focused,
|
||||
)
|
||||
.on_click(Rc::new(cx.listener(
|
||||
move |this, _event, window, cx| {
|
||||
this.add_suggested_context(&suggested, window, cx);
|
||||
move |this, _event, _window, cx| {
|
||||
this.add_suggested_context(&suggested, cx);
|
||||
},
|
||||
))),
|
||||
)
|
||||
})
|
||||
.when(!context.is_empty(), {
|
||||
.when(!no_added_context, {
|
||||
move |parent| {
|
||||
parent.child(
|
||||
IconButton::new("remove-all-context", IconName::Eraser)
|
||||
@@ -536,6 +533,7 @@ impl Render for ContextStrip {
|
||||
)
|
||||
}
|
||||
})
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,27 @@
|
||||
use assistant_context_editor::SavedContextMetadata;
|
||||
use std::{collections::VecDeque, path::Path};
|
||||
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use assistant_context_editor::{AssistantContext, SavedContextMetadata};
|
||||
use chrono::{DateTime, Utc};
|
||||
use gpui::{Entity, prelude::*};
|
||||
use futures::future::{TryFutureExt as _, join_all};
|
||||
use gpui::{Entity, Task, prelude::*};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smol::future::FutureExt;
|
||||
use std::time::Duration;
|
||||
use ui::{App, SharedString};
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::thread_store::{SerializedThreadMetadata, ThreadStore};
|
||||
use crate::{
|
||||
Thread,
|
||||
thread::ThreadId,
|
||||
thread_store::{SerializedThreadMetadata, ThreadStore},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
const MAX_RECENTLY_OPENED_ENTRIES: usize = 6;
|
||||
const NAVIGATION_HISTORY_PATH: &str = "agent-navigation-history.json";
|
||||
const SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE: Duration = Duration::from_millis(50);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum HistoryEntry {
|
||||
Thread(SerializedThreadMetadata),
|
||||
Context(SavedContextMetadata),
|
||||
@@ -19,16 +36,52 @@ impl HistoryEntry {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum RecentEntry {
|
||||
Thread(ThreadId, Entity<Thread>),
|
||||
Context(Entity<AssistantContext>),
|
||||
}
|
||||
|
||||
impl PartialEq for RecentEntry {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Self::Thread(l0, _), Self::Thread(r0, _)) => l0 == r0,
|
||||
(Self::Context(l0), Self::Context(r0)) => l0 == r0,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for RecentEntry {}
|
||||
|
||||
impl RecentEntry {
|
||||
pub(crate) fn summary(&self, cx: &App) -> SharedString {
|
||||
match self {
|
||||
RecentEntry::Thread(_, thread) => thread.read(cx).summary_or_default(),
|
||||
RecentEntry::Context(context) => context.read(cx).summary_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
enum SerializedRecentEntry {
|
||||
Thread(String),
|
||||
Context(String),
|
||||
}
|
||||
|
||||
pub struct HistoryStore {
|
||||
thread_store: Entity<ThreadStore>,
|
||||
context_store: Entity<assistant_context_editor::ContextStore>,
|
||||
recently_opened_entries: VecDeque<RecentEntry>,
|
||||
_subscriptions: Vec<gpui::Subscription>,
|
||||
_save_recently_opened_entries_task: Task<()>,
|
||||
}
|
||||
|
||||
impl HistoryStore {
|
||||
pub fn new(
|
||||
thread_store: Entity<ThreadStore>,
|
||||
context_store: Entity<assistant_context_editor::ContextStore>,
|
||||
initial_recent_entries: impl IntoIterator<Item = RecentEntry>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let subscriptions = vec![
|
||||
@@ -36,10 +89,62 @@ impl HistoryStore {
|
||||
cx.observe(&context_store, |_, _, cx| cx.notify()),
|
||||
];
|
||||
|
||||
cx.spawn({
|
||||
let thread_store = thread_store.downgrade();
|
||||
let context_store = context_store.downgrade();
|
||||
async move |this, cx| {
|
||||
let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH);
|
||||
let contents = cx
|
||||
.background_spawn(async move { std::fs::read_to_string(path) })
|
||||
.await
|
||||
.context("reading persisted agent panel navigation history")?;
|
||||
let entries = serde_json::from_str::<Vec<SerializedRecentEntry>>(&contents)
|
||||
.context("deserializing persisted agent panel navigation history")?
|
||||
.into_iter()
|
||||
.take(MAX_RECENTLY_OPENED_ENTRIES)
|
||||
.map(|serialized| match serialized {
|
||||
SerializedRecentEntry::Thread(id) => thread_store
|
||||
.update(cx, |thread_store, cx| {
|
||||
let thread_id = ThreadId::from(id.as_str());
|
||||
thread_store
|
||||
.open_thread(&thread_id, cx)
|
||||
.map_ok(|thread| RecentEntry::Thread(thread_id, thread))
|
||||
.boxed()
|
||||
})
|
||||
.unwrap_or_else(|_| async { Err(anyhow!("no thread store")) }.boxed()),
|
||||
SerializedRecentEntry::Context(id) => context_store
|
||||
.update(cx, |context_store, cx| {
|
||||
context_store
|
||||
.open_local_context(Path::new(&id).into(), cx)
|
||||
.map_ok(RecentEntry::Context)
|
||||
.boxed()
|
||||
})
|
||||
.unwrap_or_else(|_| async { Err(anyhow!("no context store")) }.boxed()),
|
||||
});
|
||||
let entries = join_all(entries)
|
||||
.await
|
||||
.into_iter()
|
||||
.filter_map(|result| result.log_err())
|
||||
.collect::<VecDeque<_>>();
|
||||
|
||||
this.update(cx, |this, _| {
|
||||
this.recently_opened_entries.extend(entries);
|
||||
this.recently_opened_entries
|
||||
.truncate(MAX_RECENTLY_OPENED_ENTRIES);
|
||||
})
|
||||
.ok();
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
Self {
|
||||
thread_store,
|
||||
context_store,
|
||||
recently_opened_entries: initial_recent_entries.into_iter().collect(),
|
||||
_subscriptions: subscriptions,
|
||||
_save_recently_opened_entries_task: Task::ready(()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +156,10 @@ impl HistoryStore {
|
||||
return history_entries;
|
||||
}
|
||||
|
||||
for thread in self.thread_store.update(cx, |this, _cx| this.threads()) {
|
||||
for thread in self
|
||||
.thread_store
|
||||
.update(cx, |this, _cx| this.reverse_chronological_threads())
|
||||
{
|
||||
history_entries.push(HistoryEntry::Thread(thread));
|
||||
}
|
||||
|
||||
@@ -66,4 +174,63 @@ impl HistoryStore {
|
||||
pub fn recent_entries(&self, limit: usize, cx: &mut Context<Self>) -> Vec<HistoryEntry> {
|
||||
self.entries(cx).into_iter().take(limit).collect()
|
||||
}
|
||||
|
||||
fn save_recently_opened_entries(&mut self, cx: &mut Context<Self>) {
|
||||
let serialized_entries = self
|
||||
.recently_opened_entries
|
||||
.iter()
|
||||
.filter_map(|entry| match entry {
|
||||
RecentEntry::Context(context) => Some(SerializedRecentEntry::Context(
|
||||
context.read(cx).path()?.to_str()?.to_owned(),
|
||||
)),
|
||||
RecentEntry::Thread(id, _) => Some(SerializedRecentEntry::Thread(id.to_string())),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
self._save_recently_opened_entries_task = cx.spawn(async move |_, cx| {
|
||||
cx.background_executor()
|
||||
.timer(SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE)
|
||||
.await;
|
||||
cx.background_spawn(async move {
|
||||
let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH);
|
||||
let content = serde_json::to_string(&serialized_entries)?;
|
||||
std::fs::write(path, content)?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.await
|
||||
.log_err();
|
||||
});
|
||||
}
|
||||
|
||||
pub fn push_recently_opened_entry(&mut self, entry: RecentEntry, cx: &mut Context<Self>) {
|
||||
self.recently_opened_entries
|
||||
.retain(|old_entry| old_entry != &entry);
|
||||
self.recently_opened_entries.push_front(entry);
|
||||
self.recently_opened_entries
|
||||
.truncate(MAX_RECENTLY_OPENED_ENTRIES);
|
||||
self.save_recently_opened_entries(cx);
|
||||
}
|
||||
|
||||
pub fn remove_recently_opened_thread(&mut self, id: ThreadId, cx: &mut Context<Self>) {
|
||||
self.recently_opened_entries.retain(|entry| match entry {
|
||||
RecentEntry::Thread(thread_id, _) if thread_id == &id => false,
|
||||
_ => true,
|
||||
});
|
||||
self.save_recently_opened_entries(cx);
|
||||
}
|
||||
|
||||
pub fn remove_recently_opened_entry(&mut self, entry: &RecentEntry, cx: &mut Context<Self>) {
|
||||
self.recently_opened_entries
|
||||
.retain(|old_entry| old_entry != entry);
|
||||
self.save_recently_opened_entries(cx);
|
||||
}
|
||||
|
||||
pub fn recently_opened_entries(&self, _cx: &mut Context<Self>) -> VecDeque<RecentEntry> {
|
||||
#[cfg(debug_assertions)]
|
||||
if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
|
||||
return VecDeque::new();
|
||||
}
|
||||
|
||||
self.recently_opened_entries.clone()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,12 +24,15 @@ use gpui::{
|
||||
WeakEntity, Window, point,
|
||||
};
|
||||
use language::{Buffer, Point, Selection, TransactionId};
|
||||
use language_model::ConfiguredModel;
|
||||
use language_model::{LanguageModelRegistry, report_assistant_event};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use parking_lot::Mutex;
|
||||
use project::LspAction;
|
||||
use project::Project;
|
||||
use project::{CodeAction, ProjectTransaction};
|
||||
use prompt_store::PromptBuilder;
|
||||
use prompt_store::PromptStore;
|
||||
use settings::{Settings, SettingsStore};
|
||||
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
|
||||
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
|
||||
@@ -243,9 +246,13 @@ impl InlineAssistant {
|
||||
.map_or(false, |model| model.provider.is_authenticated(cx))
|
||||
};
|
||||
|
||||
let thread_store = workspace
|
||||
let assistant_panel = workspace
|
||||
.panel::<AssistantPanel>(cx)
|
||||
.map(|assistant_panel| assistant_panel.read(cx).thread_store().downgrade());
|
||||
.map(|assistant_panel| assistant_panel.read(cx));
|
||||
let prompt_store = assistant_panel
|
||||
.and_then(|assistant_panel| assistant_panel.prompt_store().as_ref().cloned());
|
||||
let thread_store =
|
||||
assistant_panel.map(|assistant_panel| assistant_panel.thread_store().downgrade());
|
||||
|
||||
let handle_assist =
|
||||
|window: &mut Window, cx: &mut Context<Workspace>| match inline_assist_target {
|
||||
@@ -254,6 +261,8 @@ impl InlineAssistant {
|
||||
assistant.assist(
|
||||
&active_editor,
|
||||
cx.entity().downgrade(),
|
||||
workspace.project().downgrade(),
|
||||
prompt_store,
|
||||
thread_store,
|
||||
window,
|
||||
cx,
|
||||
@@ -265,6 +274,8 @@ impl InlineAssistant {
|
||||
assistant.assist(
|
||||
&active_terminal,
|
||||
cx.entity().downgrade(),
|
||||
workspace.project().downgrade(),
|
||||
prompt_store,
|
||||
thread_store,
|
||||
window,
|
||||
cx,
|
||||
@@ -318,6 +329,8 @@ impl InlineAssistant {
|
||||
&mut self,
|
||||
editor: &Entity<Editor>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: WeakEntity<Project>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
thread_store: Option<WeakEntity<ThreadStore>>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
@@ -425,13 +438,15 @@ impl InlineAssistant {
|
||||
for range in codegen_ranges {
|
||||
let assist_id = self.next_assist_id.post_inc();
|
||||
let context_store =
|
||||
cx.new(|_cx| ContextStore::new(workspace.clone(), thread_store.clone()));
|
||||
cx.new(|_cx| ContextStore::new(project.clone(), thread_store.clone()));
|
||||
let codegen = cx.new(|cx| {
|
||||
BufferCodegen::new(
|
||||
editor.read(cx).buffer().clone(),
|
||||
range.clone(),
|
||||
None,
|
||||
context_store.clone(),
|
||||
project.clone(),
|
||||
prompt_store.clone(),
|
||||
self.telemetry.clone(),
|
||||
self.prompt_builder.clone(),
|
||||
cx,
|
||||
@@ -519,7 +534,8 @@ impl InlineAssistant {
|
||||
initial_prompt: String,
|
||||
initial_transaction_id: Option<TransactionId>,
|
||||
focus: bool,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
workspace: Entity<Workspace>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
thread_store: Option<WeakEntity<ThreadStore>>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
@@ -537,8 +553,8 @@ impl InlineAssistant {
|
||||
range.end = range.end.bias_right(&snapshot);
|
||||
}
|
||||
|
||||
let context_store =
|
||||
cx.new(|_cx| ContextStore::new(workspace.clone(), thread_store.clone()));
|
||||
let project = workspace.read(cx).project().downgrade();
|
||||
let context_store = cx.new(|_cx| ContextStore::new(project.clone(), thread_store.clone()));
|
||||
|
||||
let codegen = cx.new(|cx| {
|
||||
BufferCodegen::new(
|
||||
@@ -546,6 +562,8 @@ impl InlineAssistant {
|
||||
range.clone(),
|
||||
initial_transaction_id,
|
||||
context_store.clone(),
|
||||
project,
|
||||
prompt_store,
|
||||
self.telemetry.clone(),
|
||||
self.prompt_builder.clone(),
|
||||
cx,
|
||||
@@ -562,7 +580,7 @@ impl InlineAssistant {
|
||||
codegen.clone(),
|
||||
self.fs.clone(),
|
||||
context_store,
|
||||
workspace.clone(),
|
||||
workspace.downgrade(),
|
||||
thread_store,
|
||||
window,
|
||||
cx,
|
||||
@@ -589,7 +607,7 @@ impl InlineAssistant {
|
||||
end_block_id,
|
||||
range,
|
||||
codegen.clone(),
|
||||
workspace.clone(),
|
||||
workspace.downgrade(),
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
@@ -1217,9 +1235,15 @@ impl InlineAssistant {
|
||||
self.prompt_history.pop_front();
|
||||
}
|
||||
|
||||
let Some(ConfiguredModel { model, .. }) =
|
||||
LanguageModelRegistry::read_global(cx).inline_assistant_model()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
assist
|
||||
.codegen
|
||||
.update(cx, |codegen, cx| codegen.start(user_prompt, cx))
|
||||
.update(cx, |codegen, cx| codegen.start(model, user_prompt, cx))
|
||||
.log_err();
|
||||
}
|
||||
|
||||
@@ -1317,7 +1341,7 @@ impl InlineAssistant {
|
||||
editor.highlight_rows::<InlineAssist>(
|
||||
row_range,
|
||||
cx.theme().status().info_background,
|
||||
false,
|
||||
Default::default(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
@@ -1382,7 +1406,7 @@ impl InlineAssistant {
|
||||
editor.highlight_rows::<DeletedLines>(
|
||||
Anchor::min()..Anchor::max(),
|
||||
cx.theme().status().deleted_background,
|
||||
false,
|
||||
Default::default(),
|
||||
cx,
|
||||
);
|
||||
editor
|
||||
@@ -1778,7 +1802,9 @@ impl CodeActionProvider for AssistantCodeActionProvider {
|
||||
let editor = self.editor.clone();
|
||||
let workspace = self.workspace.clone();
|
||||
let thread_store = self.thread_store.clone();
|
||||
let prompt_store = PromptStore::global(cx);
|
||||
window.spawn(cx, async move |cx| {
|
||||
let workspace = workspace.upgrade().context("workspace was released")?;
|
||||
let editor = editor.upgrade().context("editor was released")?;
|
||||
let range = editor
|
||||
.update(cx, |editor, cx| {
|
||||
@@ -1817,6 +1843,7 @@ impl CodeActionProvider for AssistantCodeActionProvider {
|
||||
})?
|
||||
.context("invalid range")?;
|
||||
|
||||
let prompt_store = prompt_store.await.ok();
|
||||
cx.update_global(|assistant: &mut InlineAssistant, window, cx| {
|
||||
let assist_id = assistant.suggest_assist(
|
||||
&editor,
|
||||
@@ -1825,6 +1852,7 @@ impl CodeActionProvider for AssistantCodeActionProvider {
|
||||
None,
|
||||
true,
|
||||
workspace,
|
||||
prompt_store,
|
||||
thread_store,
|
||||
window,
|
||||
cx,
|
||||
|
||||
@@ -13,7 +13,7 @@ use editor::{
|
||||
Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, GutterDimensions, MultiBuffer,
|
||||
actions::{MoveDown, MoveUp},
|
||||
};
|
||||
use feature_flags::{FeatureFlagAppExt as _, ZedPro};
|
||||
use feature_flags::{FeatureFlagAppExt as _, ZedProFeatureFlag};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
AnyElement, App, ClickEvent, Context, CursorStyle, Entity, EventEmitter, FocusHandle,
|
||||
@@ -132,7 +132,7 @@ impl<T: 'static> Render for PromptEditor<T> {
|
||||
|
||||
let error_message = SharedString::from(error.to_string());
|
||||
if error.error_code() == proto::ErrorCode::RateLimitExceeded
|
||||
&& cx.has_flag::<ZedPro>()
|
||||
&& cx.has_flag::<ZedProFeatureFlag>()
|
||||
{
|
||||
el.child(
|
||||
v_flex()
|
||||
@@ -931,7 +931,7 @@ impl PromptEditor<BufferCodegen> {
|
||||
.update(cx, |editor, _| editor.set_read_only(false));
|
||||
}
|
||||
CodegenStatus::Error(error) => {
|
||||
if cx.has_flag::<ZedPro>()
|
||||
if cx.has_flag::<ZedProFeatureFlag>()
|
||||
&& error.error_code() == proto::ErrorCode::RateLimitExceeded
|
||||
&& !dismissed_rate_limit_notice()
|
||||
{
|
||||
|
||||
@@ -68,30 +68,41 @@ impl ProfileSelector {
|
||||
|
||||
menu = menu.header("Profiles");
|
||||
for (profile_id, profile) in self.profiles.clone() {
|
||||
menu = menu.toggleable_entry(
|
||||
profile.name.clone(),
|
||||
profile_id == settings.default_profile,
|
||||
icon_position,
|
||||
None,
|
||||
{
|
||||
let fs = self.fs.clone();
|
||||
let thread_store = self.thread_store.clone();
|
||||
move |_window, cx| {
|
||||
update_settings_file::<AssistantSettings>(fs.clone(), cx, {
|
||||
let profile_id = profile_id.clone();
|
||||
move |settings, _cx| {
|
||||
settings.set_profile(profile_id.clone());
|
||||
}
|
||||
});
|
||||
let documentation = match profile.name.to_lowercase().as_str() {
|
||||
"write" => Some("Get help to write anything."),
|
||||
"ask" => Some("Chat about your codebase."),
|
||||
"manual" => Some("Chat about anything; no tools."),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
thread_store
|
||||
.update(cx, |this, cx| {
|
||||
this.load_profile_by_id(&profile_id, cx);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
},
|
||||
);
|
||||
let entry = ContextMenuEntry::new(profile.name.clone())
|
||||
.toggleable(icon_position, profile_id == settings.default_profile);
|
||||
|
||||
let entry = if let Some(doc_text) = documentation {
|
||||
entry.documentation_aside(move |_| Label::new(doc_text).into_any_element())
|
||||
} else {
|
||||
entry
|
||||
};
|
||||
|
||||
menu = menu.item(entry.handler({
|
||||
let fs = self.fs.clone();
|
||||
let thread_store = self.thread_store.clone();
|
||||
let profile_id = profile_id.clone();
|
||||
move |_window, cx| {
|
||||
update_settings_file::<AssistantSettings>(fs.clone(), cx, {
|
||||
let profile_id = profile_id.clone();
|
||||
move |settings, _cx| {
|
||||
settings.set_profile(profile_id.clone());
|
||||
}
|
||||
});
|
||||
|
||||
thread_store
|
||||
.update(cx, |this, cx| {
|
||||
this.load_profile_by_id(profile_id.clone(), cx);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
menu = menu.separator();
|
||||
@@ -141,6 +152,7 @@ impl Render for ProfileSelector {
|
||||
|
||||
let this = cx.entity().clone();
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
PopoverMenu::new("profile-selector")
|
||||
.menu(move |window, cx| {
|
||||
Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
|
||||
@@ -183,7 +195,7 @@ impl Render for ProfileSelector {
|
||||
)
|
||||
.tooltip(Tooltip::text("The current model does not support tools."))
|
||||
})
|
||||
.anchor(gpui::Corner::BottomLeft)
|
||||
.anchor(gpui::Corner::BottomRight)
|
||||
.with_handle(self.menu_handle.clone())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ impl TerminalCodegen {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(&mut self, prompt: LanguageModelRequest, cx: &mut Context<Self>) {
|
||||
pub fn start(&mut self, prompt_task: Task<LanguageModelRequest>, cx: &mut Context<Self>) {
|
||||
let Some(ConfiguredModel { model, .. }) =
|
||||
LanguageModelRegistry::read_global(cx).inline_assistant_model()
|
||||
else {
|
||||
@@ -45,6 +45,7 @@ impl TerminalCodegen {
|
||||
self.status = CodegenStatus::Pending;
|
||||
self.transaction = Some(TerminalTransaction::start(self.terminal.clone()));
|
||||
self.generation = cx.spawn(async move |this, cx| {
|
||||
let prompt = prompt_task.await;
|
||||
let model_telemetry_id = model.telemetry_id();
|
||||
let model_provider_id = model.provider_id();
|
||||
let response = model.stream_completion_text(prompt, &cx).await;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::context::attach_context_to_message;
|
||||
use crate::context::load_context;
|
||||
use crate::context_store::ContextStore;
|
||||
use crate::inline_prompt_editor::{
|
||||
CodegenStatus, PromptEditor, PromptEditorEvent, TerminalInlineAssistId,
|
||||
@@ -10,13 +10,14 @@ use client::telemetry::Telemetry;
|
||||
use collections::{HashMap, VecDeque};
|
||||
use editor::{MultiBuffer, actions::SelectAll};
|
||||
use fs::Fs;
|
||||
use gpui::{App, Entity, Focusable, Global, Subscription, UpdateGlobal, WeakEntity};
|
||||
use gpui::{App, Entity, Focusable, Global, Subscription, Task, UpdateGlobal, WeakEntity};
|
||||
use language::Buffer;
|
||||
use language_model::{
|
||||
ConfiguredModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||
Role, report_assistant_event,
|
||||
};
|
||||
use prompt_store::PromptBuilder;
|
||||
use project::Project;
|
||||
use prompt_store::{PromptBuilder, PromptStore};
|
||||
use std::sync::Arc;
|
||||
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
|
||||
use terminal_view::TerminalView;
|
||||
@@ -67,6 +68,8 @@ impl TerminalInlineAssistant {
|
||||
&mut self,
|
||||
terminal_view: &Entity<TerminalView>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: WeakEntity<Project>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
thread_store: Option<WeakEntity<ThreadStore>>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
@@ -75,8 +78,7 @@ impl TerminalInlineAssistant {
|
||||
let assist_id = self.next_assist_id.post_inc();
|
||||
let prompt_buffer =
|
||||
cx.new(|cx| MultiBuffer::singleton(cx.new(|cx| Buffer::local(String::new(), cx)), cx));
|
||||
let context_store =
|
||||
cx.new(|_cx| ContextStore::new(workspace.clone(), thread_store.clone()));
|
||||
let context_store = cx.new(|_cx| ContextStore::new(project, thread_store.clone()));
|
||||
let codegen = cx.new(|_| TerminalCodegen::new(terminal, self.telemetry.clone()));
|
||||
|
||||
let prompt_editor = cx.new(|cx| {
|
||||
@@ -108,6 +110,7 @@ impl TerminalInlineAssistant {
|
||||
prompt_editor,
|
||||
workspace.clone(),
|
||||
context_store,
|
||||
prompt_store,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -195,11 +198,11 @@ impl TerminalInlineAssistant {
|
||||
.log_err();
|
||||
|
||||
let codegen = assist.codegen.clone();
|
||||
let Some(request) = self.request_for_inline_assist(assist_id, cx).log_err() else {
|
||||
let Some(request_task) = self.request_for_inline_assist(assist_id, cx).log_err() else {
|
||||
return;
|
||||
};
|
||||
|
||||
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
|
||||
codegen.update(cx, |codegen, cx| codegen.start(request_task, cx));
|
||||
}
|
||||
|
||||
fn stop_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut App) {
|
||||
@@ -216,7 +219,7 @@ impl TerminalInlineAssistant {
|
||||
&self,
|
||||
assist_id: TerminalInlineAssistId,
|
||||
cx: &mut App,
|
||||
) -> Result<LanguageModelRequest> {
|
||||
) -> Result<Task<LanguageModelRequest>> {
|
||||
let assist = self.assists.get(&assist_id).context("invalid assist")?;
|
||||
|
||||
let shell = std::env::var("SHELL").ok();
|
||||
@@ -245,26 +248,41 @@ impl TerminalInlineAssistant {
|
||||
&latest_output,
|
||||
)?;
|
||||
|
||||
let mut request_message = LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![],
|
||||
cache: false,
|
||||
};
|
||||
let contexts = assist
|
||||
.context_store
|
||||
.read(cx)
|
||||
.context()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
let context_load_task = assist.workspace.update(cx, |workspace, cx| {
|
||||
let project = workspace.project();
|
||||
load_context(contexts, project, &assist.prompt_store, cx)
|
||||
})?;
|
||||
|
||||
attach_context_to_message(
|
||||
&mut request_message,
|
||||
assist.context_store.read(cx).context().iter(),
|
||||
cx,
|
||||
);
|
||||
Ok(cx.background_spawn(async move {
|
||||
let mut request_message = LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![],
|
||||
cache: false,
|
||||
};
|
||||
|
||||
request_message.content.push(prompt.into());
|
||||
context_load_task
|
||||
.await
|
||||
.loaded_context
|
||||
.add_to_request_message(&mut request_message);
|
||||
|
||||
Ok(LanguageModelRequest {
|
||||
messages: vec![request_message],
|
||||
tools: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
temperature: None,
|
||||
})
|
||||
request_message.content.push(prompt.into());
|
||||
|
||||
LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
mode: None,
|
||||
messages: vec![request_message],
|
||||
tools: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
temperature: None,
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
fn finish_assist(
|
||||
@@ -377,6 +395,7 @@ struct TerminalInlineAssist {
|
||||
codegen: Entity<TerminalCodegen>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
context_store: Entity<ContextStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
@@ -387,6 +406,7 @@ impl TerminalInlineAssist {
|
||||
prompt_editor: Entity<PromptEditor<TerminalCodegen>>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
context_store: Entity<ContextStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Self {
|
||||
@@ -397,6 +417,7 @@ impl TerminalInlineAssist {
|
||||
codegen: codegen.clone(),
|
||||
workspace: workspace.clone(),
|
||||
context_store,
|
||||
prompt_store,
|
||||
_subscriptions: vec![
|
||||
window.subscribe(&prompt_editor, cx, |prompt_editor, event, window, cx| {
|
||||
TerminalInlineAssistant::update_global(cx, |this, cx| {
|
||||
|
||||
@@ -4,11 +4,14 @@ use assistant_context_editor::SavedContextMetadata;
|
||||
use editor::{Editor, EditorEvent};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
App, Entity, FocusHandle, Focusable, ScrollStrategy, Task, UniformListScrollHandle, WeakEntity,
|
||||
Window, uniform_list,
|
||||
App, Entity, FocusHandle, Focusable, ScrollStrategy, Stateful, Task, UniformListScrollHandle,
|
||||
WeakEntity, Window, uniform_list,
|
||||
};
|
||||
use time::{OffsetDateTime, UtcOffset};
|
||||
use ui::{HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Tooltip, prelude::*};
|
||||
use ui::{
|
||||
HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Scrollbar, ScrollbarState,
|
||||
Tooltip, prelude::*,
|
||||
};
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::history_store::{HistoryEntry, HistoryStore};
|
||||
@@ -26,6 +29,8 @@ pub struct ThreadHistory {
|
||||
matches: Vec<StringMatch>,
|
||||
_subscriptions: Vec<gpui::Subscription>,
|
||||
_search_task: Option<Task<()>>,
|
||||
scrollbar_visibility: bool,
|
||||
scrollbar_state: ScrollbarState,
|
||||
}
|
||||
|
||||
impl ThreadHistory {
|
||||
@@ -58,10 +63,13 @@ impl ThreadHistory {
|
||||
this.update_all_entries(cx);
|
||||
});
|
||||
|
||||
let scroll_handle = UniformListScrollHandle::default();
|
||||
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
|
||||
|
||||
Self {
|
||||
assistant_panel,
|
||||
history_store,
|
||||
scroll_handle: UniformListScrollHandle::default(),
|
||||
scroll_handle,
|
||||
selected_index: 0,
|
||||
search_query: SharedString::new_static(""),
|
||||
all_entries: entries,
|
||||
@@ -69,6 +77,8 @@ impl ThreadHistory {
|
||||
search_editor,
|
||||
_subscriptions: vec![search_editor_subscription, history_store_subscription],
|
||||
_search_task: None,
|
||||
scrollbar_visibility: true,
|
||||
scrollbar_state,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,12 +230,49 @@ impl ThreadHistory {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
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 confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(entry) = self.get_match(self.selected_index) {
|
||||
let task_result = match entry {
|
||||
HistoryEntry::Thread(thread) => self
|
||||
.assistant_panel
|
||||
.update(cx, move |this, cx| this.open_thread(&thread.id, window, cx)),
|
||||
HistoryEntry::Thread(thread) => self.assistant_panel.update(cx, move |this, cx| {
|
||||
this.open_thread_by_id(&thread.id, window, cx)
|
||||
}),
|
||||
HistoryEntry::Context(context) => {
|
||||
self.assistant_panel.update(cx, move |this, cx| {
|
||||
this.open_saved_prompt_editor(context.path.clone(), window, cx)
|
||||
@@ -305,7 +352,11 @@ impl Render for ThreadHistory {
|
||||
)
|
||||
})
|
||||
.child({
|
||||
let view = v_flex().overflow_hidden().flex_grow();
|
||||
let view = v_flex()
|
||||
.id("list-container")
|
||||
.relative()
|
||||
.overflow_hidden()
|
||||
.flex_grow();
|
||||
|
||||
if self.all_entries.is_empty() {
|
||||
view.justify_center()
|
||||
@@ -322,59 +373,70 @@ impl Render for ThreadHistory {
|
||||
),
|
||||
)
|
||||
} else {
|
||||
view.p_1().child(
|
||||
uniform_list(
|
||||
cx.entity().clone(),
|
||||
"thread-history",
|
||||
self.matched_count(),
|
||||
move |history, range, _window, _cx| {
|
||||
let range_start = range.start;
|
||||
let assistant_panel = history.assistant_panel.clone();
|
||||
view.pr_5()
|
||||
.child(
|
||||
uniform_list(
|
||||
cx.entity().clone(),
|
||||
"thread-history",
|
||||
self.matched_count(),
|
||||
move |history, range, _window, _cx| {
|
||||
let range_start = range.start;
|
||||
let assistant_panel = history.assistant_panel.clone();
|
||||
|
||||
let render_item = |index: usize,
|
||||
entry: &HistoryEntry,
|
||||
highlight_positions: Vec<usize>|
|
||||
-> Div {
|
||||
h_flex().w_full().pb_1().child(match entry {
|
||||
HistoryEntry::Thread(thread) => PastThread::new(
|
||||
thread.clone(),
|
||||
assistant_panel.clone(),
|
||||
selected_index == index + range_start,
|
||||
highlight_positions,
|
||||
)
|
||||
.into_any_element(),
|
||||
HistoryEntry::Context(context) => PastContext::new(
|
||||
context.clone(),
|
||||
assistant_panel.clone(),
|
||||
selected_index == index + range_start,
|
||||
highlight_positions,
|
||||
)
|
||||
.into_any_element(),
|
||||
})
|
||||
};
|
||||
|
||||
if history.has_search_query() {
|
||||
history.matches[range]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, m)| {
|
||||
history.all_entries.get(m.candidate_id).map(|entry| {
|
||||
render_item(index, entry, m.positions.clone())
|
||||
})
|
||||
let render_item = |index: usize,
|
||||
entry: &HistoryEntry,
|
||||
highlight_positions: Vec<usize>|
|
||||
-> Div {
|
||||
h_flex().w_full().pb_1().child(match entry {
|
||||
HistoryEntry::Thread(thread) => PastThread::new(
|
||||
thread.clone(),
|
||||
assistant_panel.clone(),
|
||||
selected_index == index + range_start,
|
||||
highlight_positions,
|
||||
)
|
||||
.into_any_element(),
|
||||
HistoryEntry::Context(context) => PastContext::new(
|
||||
context.clone(),
|
||||
assistant_panel.clone(),
|
||||
selected_index == index + range_start,
|
||||
highlight_positions,
|
||||
)
|
||||
.into_any_element(),
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
history.all_entries[range]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, entry)| render_item(index, entry, vec![]))
|
||||
.collect()
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if history.has_search_query() {
|
||||
history.matches[range]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, m)| {
|
||||
history.all_entries.get(m.candidate_id).map(
|
||||
|entry| {
|
||||
render_item(
|
||||
index,
|
||||
entry,
|
||||
m.positions.clone(),
|
||||
)
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
history.all_entries[range]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, entry)| render_item(index, entry, vec![]))
|
||||
.collect()
|
||||
}
|
||||
},
|
||||
)
|
||||
.p_1()
|
||||
.track_scroll(self.scroll_handle.clone())
|
||||
.flex_grow(),
|
||||
)
|
||||
.track_scroll(self.scroll_handle.clone())
|
||||
.flex_grow(),
|
||||
)
|
||||
.when_some(self.render_scrollbar(cx), |div, scrollbar| {
|
||||
div.child(scrollbar)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -431,17 +493,6 @@ impl RenderOnce for PastThread {
|
||||
.end_slot(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
Label::new("Thread")
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::XSmall),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.size(px(3.))
|
||||
.rounded_full()
|
||||
.bg(cx.theme().colors().text_disabled),
|
||||
)
|
||||
.child(
|
||||
Label::new(thread_timestamp)
|
||||
.color(Color::Muted)
|
||||
@@ -451,13 +502,9 @@ impl RenderOnce for PastThread {
|
||||
IconButton::new("delete", IconName::TrashAlt)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::for_action(
|
||||
"Delete Thread",
|
||||
&RemoveSelectedThread,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx)
|
||||
})
|
||||
.on_click({
|
||||
let assistant_panel = self.assistant_panel.clone();
|
||||
@@ -478,7 +525,8 @@ impl RenderOnce for PastThread {
|
||||
move |_event, window, cx| {
|
||||
assistant_panel
|
||||
.update(cx, |this, cx| {
|
||||
this.open_thread(&id, window, cx).detach_and_log_err(cx);
|
||||
this.open_thread_by_id(&id, window, cx)
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
@@ -538,17 +586,6 @@ impl RenderOnce for PastContext {
|
||||
.end_slot(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
Label::new("Prompt Editor")
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::XSmall),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.size(px(3.))
|
||||
.rounded_full()
|
||||
.bg(cx.theme().colors().text_disabled),
|
||||
)
|
||||
.child(
|
||||
Label::new(context_timestamp)
|
||||
.color(Color::Muted)
|
||||
@@ -558,13 +595,9 @@ impl RenderOnce for PastContext {
|
||||
IconButton::new("delete", IconName::TrashAlt)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::for_action(
|
||||
"Delete Prompt Editor",
|
||||
&RemoveSelectedThread,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx)
|
||||
})
|
||||
.on_click({
|
||||
let assistant_panel = self.assistant_panel.clone();
|
||||
|
||||
@@ -1,88 +1,368 @@
|
||||
use std::borrow::Cow;
|
||||
use std::path::PathBuf;
|
||||
use std::cell::{Ref, RefCell};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_settings::{AgentProfile, AgentProfileId, AssistantSettings};
|
||||
use assistant_tool::{ToolId, ToolSource, ToolWorkingSet};
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::HashMap;
|
||||
use context_server::manager::ContextServerManager;
|
||||
use context_server::{ContextServerFactoryRegistry, ContextServerTool};
|
||||
use futures::FutureExt as _;
|
||||
use futures::channel::{mpsc, oneshot};
|
||||
use futures::future::{self, BoxFuture, Shared};
|
||||
use futures::{FutureExt as _, StreamExt as _};
|
||||
use gpui::{
|
||||
App, BackgroundExecutor, Context, Entity, Global, ReadGlobal, SharedString, Subscription, Task,
|
||||
prelude::*,
|
||||
App, BackgroundExecutor, Context, Entity, EventEmitter, Global, ReadGlobal, SharedString,
|
||||
Subscription, Task, prelude::*,
|
||||
};
|
||||
use heed::Database;
|
||||
use heed::types::SerdeBincode;
|
||||
use language_model::{LanguageModelToolUseId, Role, TokenUsage};
|
||||
use project::Project;
|
||||
use prompt_store::PromptBuilder;
|
||||
use project::{Project, ProjectItem, ProjectPath, Worktree};
|
||||
use prompt_store::{
|
||||
ProjectContext, PromptBuilder, PromptId, PromptStore, PromptsUpdatedEvent, RulesFileContext,
|
||||
UserRulesContext, WorktreeContext,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings as _, SettingsStore};
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::thread::{
|
||||
DetailedSummaryState, MessageId, ProjectSnapshot, Thread, ThreadEvent, ThreadId,
|
||||
DetailedSummaryState, ExceededWindowError, MessageId, ProjectSnapshot, Thread, ThreadId,
|
||||
};
|
||||
|
||||
const RULES_FILE_NAMES: [&'static str; 6] = [
|
||||
".rules",
|
||||
".cursorrules",
|
||||
".windsurfrules",
|
||||
".clinerules",
|
||||
".github/copilot-instructions.md",
|
||||
"CLAUDE.md",
|
||||
];
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
ThreadsDatabase::init(cx);
|
||||
}
|
||||
|
||||
/// A system prompt shared by all threads created by this ThreadStore
|
||||
#[derive(Clone, Default)]
|
||||
pub struct SharedProjectContext(Rc<RefCell<Option<ProjectContext>>>);
|
||||
|
||||
impl SharedProjectContext {
|
||||
pub fn borrow(&self) -> Ref<Option<ProjectContext>> {
|
||||
self.0.borrow()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ThreadStore {
|
||||
project: Entity<Project>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
context_server_manager: Entity<ContextServerManager>,
|
||||
context_server_tool_ids: HashMap<Arc<str>, Vec<ToolId>>,
|
||||
threads: Vec<SerializedThreadMetadata>,
|
||||
project_context: SharedProjectContext,
|
||||
reload_system_prompt_tx: mpsc::Sender<()>,
|
||||
_reload_system_prompt_task: Task<()>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
pub struct RulesLoadingError {
|
||||
pub message: SharedString,
|
||||
}
|
||||
|
||||
impl EventEmitter<RulesLoadingError> for ThreadStore {}
|
||||
|
||||
impl ThreadStore {
|
||||
pub fn new(
|
||||
pub fn load(
|
||||
project: Entity<Project>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
cx: &mut App,
|
||||
) -> Result<Entity<Self>> {
|
||||
let this = cx.new(|cx| {
|
||||
let context_server_factory_registry = ContextServerFactoryRegistry::default_global(cx);
|
||||
let context_server_manager = cx.new(|cx| {
|
||||
ContextServerManager::new(context_server_factory_registry, project.clone(), cx)
|
||||
});
|
||||
let settings_subscription =
|
||||
cx.observe_global::<SettingsStore>(move |this: &mut Self, cx| {
|
||||
this.load_default_profile(cx);
|
||||
) -> Task<Result<Entity<Self>>> {
|
||||
cx.spawn(async move |cx| {
|
||||
let (thread_store, ready_rx) = cx.update(|cx| {
|
||||
let mut option_ready_rx = None;
|
||||
let thread_store = cx.new(|cx| {
|
||||
let (thread_store, ready_rx) =
|
||||
Self::new(project, tools, prompt_builder, prompt_store, cx);
|
||||
option_ready_rx = Some(ready_rx);
|
||||
thread_store
|
||||
});
|
||||
(thread_store, option_ready_rx.take().unwrap())
|
||||
})?;
|
||||
ready_rx.await?;
|
||||
Ok(thread_store)
|
||||
})
|
||||
}
|
||||
|
||||
let this = Self {
|
||||
project,
|
||||
tools,
|
||||
prompt_builder,
|
||||
context_server_manager,
|
||||
context_server_tool_ids: HashMap::default(),
|
||||
threads: Vec::new(),
|
||||
_subscriptions: vec![settings_subscription],
|
||||
};
|
||||
this.load_default_profile(cx);
|
||||
this.register_context_server_handlers(cx);
|
||||
this.reload(cx).detach_and_log_err(cx);
|
||||
|
||||
this
|
||||
fn new(
|
||||
project: Entity<Project>,
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> (Self, oneshot::Receiver<()>) {
|
||||
let context_server_factory_registry = ContextServerFactoryRegistry::default_global(cx);
|
||||
let context_server_manager = cx.new(|cx| {
|
||||
ContextServerManager::new(context_server_factory_registry, project.clone(), cx)
|
||||
});
|
||||
|
||||
Ok(this)
|
||||
let mut subscriptions = vec![
|
||||
cx.observe_global::<SettingsStore>(move |this: &mut Self, cx| {
|
||||
this.load_default_profile(cx);
|
||||
}),
|
||||
cx.subscribe(&project, Self::handle_project_event),
|
||||
];
|
||||
|
||||
if let Some(prompt_store) = prompt_store.as_ref() {
|
||||
subscriptions.push(cx.subscribe(
|
||||
prompt_store,
|
||||
|this, _prompt_store, PromptsUpdatedEvent, _cx| {
|
||||
this.enqueue_system_prompt_reload();
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
// This channel and task prevent concurrent and redundant loading of the system prompt.
|
||||
let (reload_system_prompt_tx, mut reload_system_prompt_rx) = mpsc::channel(1);
|
||||
let (ready_tx, ready_rx) = oneshot::channel();
|
||||
let mut ready_tx = Some(ready_tx);
|
||||
let reload_system_prompt_task = cx.spawn({
|
||||
let prompt_store = prompt_store.clone();
|
||||
async move |thread_store, cx| {
|
||||
loop {
|
||||
let Some(reload_task) = thread_store
|
||||
.update(cx, |thread_store, cx| {
|
||||
thread_store.reload_system_prompt(prompt_store.clone(), cx)
|
||||
})
|
||||
.ok()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
reload_task.await;
|
||||
if let Some(ready_tx) = ready_tx.take() {
|
||||
ready_tx.send(()).ok();
|
||||
}
|
||||
reload_system_prompt_rx.next().await;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let this = Self {
|
||||
project,
|
||||
tools,
|
||||
prompt_builder,
|
||||
prompt_store,
|
||||
context_server_manager,
|
||||
context_server_tool_ids: HashMap::default(),
|
||||
threads: Vec::new(),
|
||||
project_context: SharedProjectContext::default(),
|
||||
reload_system_prompt_tx,
|
||||
_reload_system_prompt_task: reload_system_prompt_task,
|
||||
_subscriptions: subscriptions,
|
||||
};
|
||||
this.load_default_profile(cx);
|
||||
this.register_context_server_handlers(cx);
|
||||
this.reload(cx).detach_and_log_err(cx);
|
||||
(this, ready_rx)
|
||||
}
|
||||
|
||||
fn handle_project_event(
|
||||
&mut self,
|
||||
_project: Entity<Project>,
|
||||
event: &project::Event,
|
||||
_cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
project::Event::WorktreeAdded(_) | project::Event::WorktreeRemoved(_) => {
|
||||
self.enqueue_system_prompt_reload();
|
||||
}
|
||||
project::Event::WorktreeUpdatedEntries(_, items) => {
|
||||
if items.iter().any(|(path, _, _)| {
|
||||
RULES_FILE_NAMES
|
||||
.iter()
|
||||
.any(|name| path.as_ref() == Path::new(name))
|
||||
}) {
|
||||
self.enqueue_system_prompt_reload();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn enqueue_system_prompt_reload(&mut self) {
|
||||
self.reload_system_prompt_tx.try_send(()).ok();
|
||||
}
|
||||
|
||||
// Note that this should only be called from `reload_system_prompt_task`.
|
||||
fn reload_system_prompt(
|
||||
&self,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<()> {
|
||||
let worktrees = self
|
||||
.project
|
||||
.read(cx)
|
||||
.visible_worktrees(cx)
|
||||
.collect::<Vec<_>>();
|
||||
let worktree_tasks = worktrees
|
||||
.into_iter()
|
||||
.map(|worktree| {
|
||||
Self::load_worktree_info_for_system_prompt(worktree, self.project.clone(), cx)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let default_user_rules_task = match prompt_store {
|
||||
None => Task::ready(vec![]),
|
||||
Some(prompt_store) => prompt_store.read_with(cx, |prompt_store, cx| {
|
||||
let prompts = prompt_store.default_prompt_metadata();
|
||||
let load_tasks = prompts.into_iter().map(|prompt_metadata| {
|
||||
let contents = prompt_store.load(prompt_metadata.id, cx);
|
||||
async move { (contents.await, prompt_metadata) }
|
||||
});
|
||||
cx.background_spawn(future::join_all(load_tasks))
|
||||
}),
|
||||
};
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let (worktrees, default_user_rules) =
|
||||
future::join(future::join_all(worktree_tasks), default_user_rules_task).await;
|
||||
|
||||
let worktrees = worktrees
|
||||
.into_iter()
|
||||
.map(|(worktree, rules_error)| {
|
||||
if let Some(rules_error) = rules_error {
|
||||
this.update(cx, |_, cx| cx.emit(rules_error)).ok();
|
||||
}
|
||||
worktree
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let default_user_rules = default_user_rules
|
||||
.into_iter()
|
||||
.flat_map(|(contents, prompt_metadata)| match contents {
|
||||
Ok(contents) => Some(UserRulesContext {
|
||||
uuid: match prompt_metadata.id {
|
||||
PromptId::User { uuid } => uuid,
|
||||
PromptId::EditWorkflow => return None,
|
||||
},
|
||||
title: prompt_metadata.title.map(|title| title.to_string()),
|
||||
contents,
|
||||
}),
|
||||
Err(err) => {
|
||||
this.update(cx, |_, cx| {
|
||||
cx.emit(RulesLoadingError {
|
||||
message: format!("{err:?}").into(),
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
this.update(cx, |this, _cx| {
|
||||
*this.project_context.0.borrow_mut() =
|
||||
Some(ProjectContext::new(worktrees, default_user_rules));
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
|
||||
fn load_worktree_info_for_system_prompt(
|
||||
worktree: Entity<Worktree>,
|
||||
project: Entity<Project>,
|
||||
cx: &mut App,
|
||||
) -> Task<(WorktreeContext, Option<RulesLoadingError>)> {
|
||||
let root_name = worktree.read(cx).root_name().into();
|
||||
|
||||
let rules_task = Self::load_worktree_rules_file(worktree, project, cx);
|
||||
let Some(rules_task) = rules_task else {
|
||||
return Task::ready((
|
||||
WorktreeContext {
|
||||
root_name,
|
||||
rules_file: None,
|
||||
},
|
||||
None,
|
||||
));
|
||||
};
|
||||
|
||||
cx.spawn(async move |_| {
|
||||
let (rules_file, rules_file_error) = match rules_task.await {
|
||||
Ok(rules_file) => (Some(rules_file), None),
|
||||
Err(err) => (
|
||||
None,
|
||||
Some(RulesLoadingError {
|
||||
message: format!("{err}").into(),
|
||||
}),
|
||||
),
|
||||
};
|
||||
let worktree_info = WorktreeContext {
|
||||
root_name,
|
||||
rules_file,
|
||||
};
|
||||
(worktree_info, rules_file_error)
|
||||
})
|
||||
}
|
||||
|
||||
fn load_worktree_rules_file(
|
||||
worktree: Entity<Worktree>,
|
||||
project: Entity<Project>,
|
||||
cx: &mut App,
|
||||
) -> Option<Task<Result<RulesFileContext>>> {
|
||||
let worktree_ref = worktree.read(cx);
|
||||
let worktree_id = worktree_ref.id();
|
||||
let selected_rules_file = RULES_FILE_NAMES
|
||||
.into_iter()
|
||||
.filter_map(|name| {
|
||||
worktree_ref
|
||||
.entry_for_path(name)
|
||||
.filter(|entry| entry.is_file())
|
||||
.map(|entry| entry.path.clone())
|
||||
})
|
||||
.next();
|
||||
|
||||
// Note that Cline supports `.clinerules` being a directory, but that is not currently
|
||||
// supported. This doesn't seem to occur often in GitHub repositories.
|
||||
selected_rules_file.map(|path_in_worktree| {
|
||||
let project_path = ProjectPath {
|
||||
worktree_id,
|
||||
path: path_in_worktree.clone(),
|
||||
};
|
||||
let buffer_task =
|
||||
project.update(cx, |project, cx| project.open_buffer(project_path, cx));
|
||||
let rope_task = cx.spawn(async move |cx| {
|
||||
buffer_task.await?.read_with(cx, |buffer, cx| {
|
||||
let project_entry_id = buffer.entry_id(cx).context("buffer has no file")?;
|
||||
anyhow::Ok((project_entry_id, buffer.as_rope().clone()))
|
||||
})?
|
||||
});
|
||||
// Build a string from the rope on a background thread.
|
||||
cx.background_spawn(async move {
|
||||
let (project_entry_id, rope) = rope_task.await?;
|
||||
anyhow::Ok(RulesFileContext {
|
||||
path_in_worktree,
|
||||
text: rope.to_string().trim().to_string(),
|
||||
project_entry_id: project_entry_id.to_usize(),
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn context_server_manager(&self) -> Entity<ContextServerManager> {
|
||||
self.context_server_manager.clone()
|
||||
}
|
||||
|
||||
pub fn tools(&self) -> Arc<ToolWorkingSet> {
|
||||
pub fn prompt_store(&self) -> &Option<Entity<PromptStore>> {
|
||||
&self.prompt_store
|
||||
}
|
||||
|
||||
pub fn tools(&self) -> Entity<ToolWorkingSet> {
|
||||
self.tools.clone()
|
||||
}
|
||||
|
||||
@@ -91,22 +371,19 @@ impl ThreadStore {
|
||||
self.threads.len()
|
||||
}
|
||||
|
||||
pub fn threads(&self) -> Vec<SerializedThreadMetadata> {
|
||||
pub fn reverse_chronological_threads(&self) -> Vec<SerializedThreadMetadata> {
|
||||
let mut threads = self.threads.iter().cloned().collect::<Vec<_>>();
|
||||
threads.sort_unstable_by_key(|thread| std::cmp::Reverse(thread.updated_at));
|
||||
threads
|
||||
}
|
||||
|
||||
pub fn recent_threads(&self, limit: usize) -> Vec<SerializedThreadMetadata> {
|
||||
self.threads().into_iter().take(limit).collect()
|
||||
}
|
||||
|
||||
pub fn create_thread(&mut self, cx: &mut Context<Self>) -> Entity<Thread> {
|
||||
cx.new(|cx| {
|
||||
Thread::new(
|
||||
self.project.clone(),
|
||||
self.tools.clone(),
|
||||
self.prompt_builder.clone(),
|
||||
self.project_context.clone(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
@@ -134,21 +411,12 @@ impl ThreadStore {
|
||||
this.project.clone(),
|
||||
this.tools.clone(),
|
||||
this.prompt_builder.clone(),
|
||||
this.project_context.clone(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})?;
|
||||
|
||||
let (system_prompt_context, load_error) = thread
|
||||
.update(cx, |thread, cx| thread.load_system_prompt_context(cx))?
|
||||
.await;
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.set_system_prompt_context(system_prompt_context);
|
||||
if let Some(load_error) = load_error {
|
||||
cx.emit(ThreadEvent::ShowError(load_error));
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(thread)
|
||||
})
|
||||
}
|
||||
@@ -197,52 +465,76 @@ impl ThreadStore {
|
||||
})
|
||||
}
|
||||
|
||||
fn load_default_profile(&self, cx: &Context<Self>) {
|
||||
fn load_default_profile(&self, cx: &mut Context<Self>) {
|
||||
let assistant_settings = AssistantSettings::get_global(cx);
|
||||
|
||||
self.load_profile_by_id(&assistant_settings.default_profile, cx);
|
||||
self.load_profile_by_id(assistant_settings.default_profile.clone(), cx);
|
||||
}
|
||||
|
||||
pub fn load_profile_by_id(&self, profile_id: &AgentProfileId, cx: &Context<Self>) {
|
||||
pub fn load_profile_by_id(&self, profile_id: AgentProfileId, cx: &mut Context<Self>) {
|
||||
let assistant_settings = AssistantSettings::get_global(cx);
|
||||
|
||||
if let Some(profile) = assistant_settings.profiles.get(profile_id) {
|
||||
self.load_profile(profile, cx);
|
||||
if let Some(profile) = assistant_settings.profiles.get(&profile_id) {
|
||||
self.load_profile(profile.clone(), cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_profile(&self, profile: &AgentProfile, cx: &Context<Self>) {
|
||||
self.tools.disable_all_tools();
|
||||
self.tools.enable(
|
||||
ToolSource::Native,
|
||||
&profile
|
||||
.tools
|
||||
.iter()
|
||||
.filter_map(|(tool, enabled)| enabled.then(|| tool.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
pub fn load_profile(&self, profile: AgentProfile, cx: &mut Context<Self>) {
|
||||
self.tools.update(cx, |tools, cx| {
|
||||
tools.disable_all_tools(cx);
|
||||
tools.enable(
|
||||
ToolSource::Native,
|
||||
&profile
|
||||
.tools
|
||||
.iter()
|
||||
.filter_map(|(tool, enabled)| enabled.then(|| tool.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
if profile.enable_all_context_servers {
|
||||
for context_server in self.context_server_manager.read(cx).all_servers() {
|
||||
self.tools.enable_source(
|
||||
ToolSource::ContextServer {
|
||||
id: context_server.id().into(),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
self.tools.update(cx, |tools, cx| {
|
||||
tools.enable_source(
|
||||
ToolSource::ContextServer {
|
||||
id: context_server.id().into(),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
// Enable all the tools from all context servers, but disable the ones that are explicitly disabled
|
||||
for (context_server_id, preset) in &profile.context_servers {
|
||||
self.tools.update(cx, |tools, cx| {
|
||||
tools.disable(
|
||||
ToolSource::ContextServer {
|
||||
id: context_server_id.clone().into(),
|
||||
},
|
||||
&preset
|
||||
.tools
|
||||
.iter()
|
||||
.filter_map(|(tool, enabled)| (!enabled).then(|| tool.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
for (context_server_id, preset) in &profile.context_servers {
|
||||
self.tools.enable(
|
||||
ToolSource::ContextServer {
|
||||
id: context_server_id.clone().into(),
|
||||
},
|
||||
&preset
|
||||
.tools
|
||||
.iter()
|
||||
.filter_map(|(tool, enabled)| enabled.then(|| tool.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
self.tools.update(cx, |tools, cx| {
|
||||
tools.enable(
|
||||
ToolSource::ContextServer {
|
||||
id: context_server_id.clone().into(),
|
||||
},
|
||||
&preset
|
||||
.tools
|
||||
.iter()
|
||||
.filter_map(|(tool, enabled)| enabled.then(|| tool.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -276,29 +568,36 @@ impl ThreadStore {
|
||||
|
||||
if protocol.capable(context_server::protocol::ServerCapability::Tools) {
|
||||
if let Some(tools) = protocol.list_tools().await.log_err() {
|
||||
let tool_ids = tools
|
||||
.tools
|
||||
.into_iter()
|
||||
.map(|tool| {
|
||||
log::info!(
|
||||
"registering context server tool: {:?}",
|
||||
tool.name
|
||||
);
|
||||
tool_working_set.insert(Arc::new(
|
||||
ContextServerTool::new(
|
||||
context_server_manager.clone(),
|
||||
server.id(),
|
||||
tool,
|
||||
),
|
||||
))
|
||||
let tool_ids = tool_working_set
|
||||
.update(cx, |tool_working_set, _| {
|
||||
tools
|
||||
.tools
|
||||
.into_iter()
|
||||
.map(|tool| {
|
||||
log::info!(
|
||||
"registering context server tool: {:?}",
|
||||
tool.name
|
||||
);
|
||||
tool_working_set.insert(Arc::new(
|
||||
ContextServerTool::new(
|
||||
context_server_manager.clone(),
|
||||
server.id(),
|
||||
tool,
|
||||
),
|
||||
))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
.log_err();
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.context_server_tool_ids.insert(server_id, tool_ids);
|
||||
this.load_default_profile(cx);
|
||||
})
|
||||
.log_err();
|
||||
if let Some(tool_ids) = tool_ids {
|
||||
this.update(cx, |this, cx| {
|
||||
this.context_server_tool_ids
|
||||
.insert(server_id, tool_ids);
|
||||
this.load_default_profile(cx);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -308,7 +607,9 @@ impl ThreadStore {
|
||||
}
|
||||
context_server::manager::Event::ServerStopped { server_id } => {
|
||||
if let Some(tool_ids) = self.context_server_tool_ids.remove(server_id) {
|
||||
tool_working_set.remove(&tool_ids);
|
||||
tool_working_set.update(cx, |tool_working_set, _| {
|
||||
tool_working_set.remove(&tool_ids);
|
||||
});
|
||||
self.load_default_profile(cx);
|
||||
}
|
||||
}
|
||||
@@ -334,16 +635,33 @@ pub struct SerializedThread {
|
||||
#[serde(default)]
|
||||
pub cumulative_token_usage: TokenUsage,
|
||||
#[serde(default)]
|
||||
pub request_token_usage: Vec<TokenUsage>,
|
||||
#[serde(default)]
|
||||
pub detailed_summary_state: DetailedSummaryState,
|
||||
#[serde(default)]
|
||||
pub exceeded_window_error: Option<ExceededWindowError>,
|
||||
#[serde(default)]
|
||||
pub model: Option<SerializedLanguageModel>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct SerializedLanguageModel {
|
||||
pub provider: String,
|
||||
pub model: String,
|
||||
}
|
||||
|
||||
impl SerializedThread {
|
||||
pub const VERSION: &'static str = "0.1.0";
|
||||
pub const VERSION: &'static str = "0.2.0";
|
||||
|
||||
pub fn from_json(json: &[u8]) -> Result<Self> {
|
||||
let saved_thread_json = serde_json::from_slice::<serde_json::Value>(json)?;
|
||||
match saved_thread_json.get("version") {
|
||||
Some(serde_json::Value::String(version)) => match version.as_str() {
|
||||
SerializedThreadV0_1_0::VERSION => {
|
||||
let saved_thread =
|
||||
serde_json::from_value::<SerializedThreadV0_1_0>(saved_thread_json)?;
|
||||
Ok(saved_thread.upgrade())
|
||||
}
|
||||
SerializedThread::VERSION => Ok(serde_json::from_value::<SerializedThread>(
|
||||
saved_thread_json,
|
||||
)?),
|
||||
@@ -365,6 +683,38 @@ impl SerializedThread {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct SerializedThreadV0_1_0(
|
||||
// The structure did not change, so we are reusing the latest SerializedThread.
|
||||
// When making the next version, make sure this points to SerializedThreadV0_2_0
|
||||
SerializedThread,
|
||||
);
|
||||
|
||||
impl SerializedThreadV0_1_0 {
|
||||
pub const VERSION: &'static str = "0.1.0";
|
||||
|
||||
pub fn upgrade(self) -> SerializedThread {
|
||||
debug_assert_eq!(SerializedThread::VERSION, "0.2.0");
|
||||
|
||||
let mut messages: Vec<SerializedMessage> = Vec::with_capacity(self.0.messages.len());
|
||||
|
||||
for message in self.0.messages {
|
||||
if message.role == Role::User && !message.tool_results.is_empty() {
|
||||
if let Some(last_message) = messages.last_mut() {
|
||||
debug_assert!(last_message.role == Role::Assistant);
|
||||
|
||||
last_message.tool_results = message.tool_results;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
messages.push(message);
|
||||
}
|
||||
|
||||
SerializedThread { messages, ..self.0 }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SerializedMessage {
|
||||
pub id: MessageId,
|
||||
@@ -383,9 +733,18 @@ pub struct SerializedMessage {
|
||||
#[serde(tag = "type")]
|
||||
pub enum SerializedMessageSegment {
|
||||
#[serde(rename = "text")]
|
||||
Text { text: String },
|
||||
Text {
|
||||
text: String,
|
||||
},
|
||||
#[serde(rename = "thinking")]
|
||||
Thinking { text: String },
|
||||
Thinking {
|
||||
text: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
signature: Option<String>,
|
||||
},
|
||||
RedactedThinking {
|
||||
data: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -420,7 +779,10 @@ impl LegacySerializedThread {
|
||||
messages: self.messages.into_iter().map(|msg| msg.upgrade()).collect(),
|
||||
initial_project_snapshot: self.initial_project_snapshot,
|
||||
cumulative_token_usage: TokenUsage::default(),
|
||||
request_token_usage: Vec::new(),
|
||||
detailed_summary_state: DetailedSummaryState::default(),
|
||||
exceeded_window_error: None,
|
||||
model: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -491,7 +853,7 @@ impl ThreadsDatabase {
|
||||
let database_future = executor
|
||||
.spawn({
|
||||
let executor = executor.clone();
|
||||
let database_path = paths::support_dir().join("threads/threads-db.1.mdb");
|
||||
let database_path = paths::data_dir().join("threads/threads-db.1.mdb");
|
||||
async move { ThreadsDatabase::new(database_path, executor) }
|
||||
})
|
||||
.then(|result| future::ready(result.map(Arc::new).map_err(Arc::new)))
|
||||
|
||||
94
crates/agent/src/tool_compatibility.rs
Normal file
@@ -0,0 +1,94 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_tool::{Tool, ToolSource, ToolWorkingSet, ToolWorkingSetEvent};
|
||||
use collections::HashMap;
|
||||
use gpui::{App, Context, Entity, IntoElement, Render, Subscription, Window};
|
||||
use language_model::{LanguageModel, LanguageModelToolSchemaFormat};
|
||||
use ui::prelude::*;
|
||||
|
||||
pub struct IncompatibleToolsState {
|
||||
cache: HashMap<LanguageModelToolSchemaFormat, Vec<Arc<dyn Tool>>>,
|
||||
tool_working_set: Entity<ToolWorkingSet>,
|
||||
_tool_working_set_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl IncompatibleToolsState {
|
||||
pub fn new(tool_working_set: Entity<ToolWorkingSet>, cx: &mut Context<Self>) -> Self {
|
||||
let _tool_working_set_subscription =
|
||||
cx.subscribe(&tool_working_set, |this, _, event, _| match event {
|
||||
ToolWorkingSetEvent::EnabledToolsChanged => {
|
||||
this.cache.clear();
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
cache: HashMap::default(),
|
||||
tool_working_set,
|
||||
_tool_working_set_subscription,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn incompatible_tools(
|
||||
&mut self,
|
||||
model: &Arc<dyn LanguageModel>,
|
||||
cx: &App,
|
||||
) -> &[Arc<dyn Tool>] {
|
||||
self.cache
|
||||
.entry(model.tool_input_format())
|
||||
.or_insert_with(|| {
|
||||
self.tool_working_set
|
||||
.read(cx)
|
||||
.enabled_tools(cx)
|
||||
.iter()
|
||||
.filter(|tool| tool.input_schema(model.tool_input_format()).is_err())
|
||||
.cloned()
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct IncompatibleToolsTooltip {
|
||||
pub incompatible_tools: Vec<Arc<dyn Tool>>,
|
||||
}
|
||||
|
||||
impl Render for IncompatibleToolsTooltip {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
ui::tooltip_container(window, cx, |container, _, cx| {
|
||||
container
|
||||
.w_72()
|
||||
.child(Label::new("Incompatible Tools").size(LabelSize::Small))
|
||||
.child(
|
||||
Label::new(
|
||||
"This model is incompatible with the following tools from your MCPs:",
|
||||
)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.my_1p5()
|
||||
.py_0p5()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.children(
|
||||
self.incompatible_tools
|
||||
.iter()
|
||||
.map(|tool| h_flex().gap_4().child(Label::new(tool.name()).size(LabelSize::Small)).map(|parent|
|
||||
match tool.source() {
|
||||
ToolSource::Native => parent,
|
||||
ToolSource::ContextServer { id } => parent.child(Label::new(id).size(LabelSize::Small).color(Color::Muted)),
|
||||
}
|
||||
)),
|
||||
),
|
||||
)
|
||||
.child(Label::new("What To Do Instead").size(LabelSize::Small))
|
||||
.child(
|
||||
Label::new(
|
||||
"Every other tool continues to work with this model, but to specifically use those, switch to another model.",
|
||||
)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,19 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use assistant_tool::{Tool, ToolWorkingSet};
|
||||
use assistant_tool::{AnyToolCard, Tool, ToolUseStatus, ToolWorkingSet};
|
||||
use collections::HashMap;
|
||||
use futures::FutureExt as _;
|
||||
use futures::future::Shared;
|
||||
use gpui::{App, SharedString, Task};
|
||||
use gpui::{App, Entity, SharedString, Task};
|
||||
use language_model::{
|
||||
LanguageModelRegistry, LanguageModelRequestMessage, LanguageModelToolResult,
|
||||
ConfiguredModel, LanguageModel, LanguageModelRequestMessage, LanguageModelToolResult,
|
||||
LanguageModelToolUse, LanguageModelToolUseId, MessageContent, Role,
|
||||
};
|
||||
use ui::IconName;
|
||||
use util::truncate_lines_to_byte_limit;
|
||||
|
||||
use crate::thread::MessageId;
|
||||
use crate::thread::{MessageId, PromptId, ThreadId};
|
||||
use crate::thread_store::SerializedMessage;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -27,45 +27,24 @@ pub struct ToolUse {
|
||||
pub needs_confirmation: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ToolUseStatus {
|
||||
NeedsConfirmation,
|
||||
Pending,
|
||||
Running,
|
||||
Finished(SharedString),
|
||||
Error(SharedString),
|
||||
}
|
||||
|
||||
impl ToolUseStatus {
|
||||
pub fn text(&self) -> SharedString {
|
||||
match self {
|
||||
ToolUseStatus::NeedsConfirmation => "".into(),
|
||||
ToolUseStatus::Pending => "".into(),
|
||||
ToolUseStatus::Running => "".into(),
|
||||
ToolUseStatus::Finished(out) => out.clone(),
|
||||
ToolUseStatus::Error(out) => out.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ToolUseState {
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
tool_uses_by_assistant_message: HashMap<MessageId, Vec<LanguageModelToolUse>>,
|
||||
tool_uses_by_user_message: HashMap<MessageId, Vec<LanguageModelToolUseId>>,
|
||||
tool_results: HashMap<LanguageModelToolUseId, LanguageModelToolResult>,
|
||||
pending_tool_uses_by_id: HashMap<LanguageModelToolUseId, PendingToolUse>,
|
||||
tool_result_cards: HashMap<LanguageModelToolUseId, AnyToolCard>,
|
||||
tool_use_metadata_by_id: HashMap<LanguageModelToolUseId, ToolUseMetadata>,
|
||||
}
|
||||
|
||||
pub const USING_TOOL_MARKER: &str = "<using_tool>";
|
||||
|
||||
impl ToolUseState {
|
||||
pub fn new(tools: Arc<ToolWorkingSet>) -> Self {
|
||||
pub fn new(tools: Entity<ToolWorkingSet>) -> Self {
|
||||
Self {
|
||||
tools,
|
||||
tool_uses_by_assistant_message: HashMap::default(),
|
||||
tool_uses_by_user_message: HashMap::default(),
|
||||
tool_results: HashMap::default(),
|
||||
pending_tool_uses_by_id: HashMap::default(),
|
||||
tool_result_cards: HashMap::default(),
|
||||
tool_use_metadata_by_id: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,9 +52,8 @@ impl ToolUseState {
|
||||
///
|
||||
/// Accepts a function to filter the tools that should be used to populate the state.
|
||||
pub fn from_serialized_messages(
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
messages: &[SerializedMessage],
|
||||
mut filter_by_tool_name: impl FnMut(&str) -> bool,
|
||||
) -> Self {
|
||||
let mut this = Self::new(tools);
|
||||
let mut tool_names_by_id = HashMap::default();
|
||||
@@ -87,11 +65,12 @@ impl ToolUseState {
|
||||
let tool_uses = message
|
||||
.tool_uses
|
||||
.iter()
|
||||
.filter(|tool_use| (filter_by_tool_name)(tool_use.name.as_ref()))
|
||||
.map(|tool_use| LanguageModelToolUse {
|
||||
id: tool_use.id.clone(),
|
||||
name: tool_use.name.clone().into(),
|
||||
raw_input: tool_use.input.to_string(),
|
||||
input: tool_use.input.clone(),
|
||||
is_input_complete: true,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -103,14 +82,6 @@ impl ToolUseState {
|
||||
|
||||
this.tool_uses_by_assistant_message
|
||||
.insert(message.id, tool_uses);
|
||||
}
|
||||
}
|
||||
Role::User => {
|
||||
if !message.tool_results.is_empty() {
|
||||
let tool_uses_by_user_message = this
|
||||
.tool_uses_by_user_message
|
||||
.entry(message.id)
|
||||
.or_default();
|
||||
|
||||
for tool_result in &message.tool_results {
|
||||
let tool_use_id = tool_result.tool_use_id.clone();
|
||||
@@ -119,11 +90,6 @@ impl ToolUseState {
|
||||
continue;
|
||||
};
|
||||
|
||||
if !(filter_by_tool_name)(tool_use.as_ref()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
tool_uses_by_user_message.push(tool_use_id.clone());
|
||||
this.tool_results.insert(
|
||||
tool_use_id.clone(),
|
||||
LanguageModelToolResult {
|
||||
@@ -136,7 +102,7 @@ impl ToolUseState {
|
||||
}
|
||||
}
|
||||
}
|
||||
Role::System => {}
|
||||
Role::System | Role::User => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,23 +159,31 @@ impl ToolUseState {
|
||||
PendingToolUseStatus::Error(ref err) => {
|
||||
ToolUseStatus::Error(err.clone().into())
|
||||
}
|
||||
PendingToolUseStatus::InputStillStreaming => {
|
||||
ToolUseStatus::InputStillStreaming
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ToolUseStatus::Pending
|
||||
}
|
||||
})();
|
||||
|
||||
let (icon, needs_confirmation) = if let Some(tool) = self.tools.tool(&tool_use.name, cx)
|
||||
{
|
||||
(tool.icon(), tool.needs_confirmation(&tool_use.input, cx))
|
||||
} else {
|
||||
(IconName::Cog, false)
|
||||
};
|
||||
let (icon, needs_confirmation) =
|
||||
if let Some(tool) = self.tools.read(cx).tool(&tool_use.name, cx) {
|
||||
(tool.icon(), tool.needs_confirmation(&tool_use.input, cx))
|
||||
} else {
|
||||
(IconName::Cog, false)
|
||||
};
|
||||
|
||||
tool_uses.push(ToolUse {
|
||||
id: tool_use.id.clone(),
|
||||
name: tool_use.name.clone().into(),
|
||||
ui_text: self.tool_ui_label(&tool_use.name, &tool_use.input, cx),
|
||||
ui_text: self.tool_ui_label(
|
||||
&tool_use.name,
|
||||
&tool_use.input,
|
||||
tool_use.is_input_complete,
|
||||
cx,
|
||||
),
|
||||
input: tool_use.input.clone(),
|
||||
status,
|
||||
icon,
|
||||
@@ -224,29 +198,40 @@ impl ToolUseState {
|
||||
&self,
|
||||
tool_name: &str,
|
||||
input: &serde_json::Value,
|
||||
is_input_complete: bool,
|
||||
cx: &App,
|
||||
) -> SharedString {
|
||||
if let Some(tool) = self.tools.tool(tool_name, cx) {
|
||||
tool.ui_text(input).into()
|
||||
if let Some(tool) = self.tools.read(cx).tool(tool_name, cx) {
|
||||
if is_input_complete {
|
||||
tool.ui_text(input).into()
|
||||
} else {
|
||||
tool.still_streaming_ui_text(input).into()
|
||||
}
|
||||
} else {
|
||||
format!("Unknown tool {tool_name:?}").into()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tool_results_for_message(&self, message_id: MessageId) -> Vec<&LanguageModelToolResult> {
|
||||
let empty = Vec::new();
|
||||
pub fn tool_results_for_message(
|
||||
&self,
|
||||
assistant_message_id: MessageId,
|
||||
) -> Vec<&LanguageModelToolResult> {
|
||||
let Some(tool_uses) = self
|
||||
.tool_uses_by_assistant_message
|
||||
.get(&assistant_message_id)
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
self.tool_uses_by_user_message
|
||||
.get(&message_id)
|
||||
.unwrap_or(&empty)
|
||||
tool_uses
|
||||
.iter()
|
||||
.filter_map(|tool_use_id| self.tool_results.get(&tool_use_id))
|
||||
.filter_map(|tool_use| self.tool_results.get(&tool_use.id))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn message_has_tool_results(&self, message_id: MessageId) -> bool {
|
||||
self.tool_uses_by_user_message
|
||||
.get(&message_id)
|
||||
pub fn message_has_tool_results(&self, assistant_message_id: MessageId) -> bool {
|
||||
self.tool_uses_by_assistant_message
|
||||
.get(&assistant_message_id)
|
||||
.map_or(false, |results| !results.is_empty())
|
||||
}
|
||||
|
||||
@@ -257,24 +242,60 @@ impl ToolUseState {
|
||||
self.tool_results.get(tool_use_id)
|
||||
}
|
||||
|
||||
pub fn tool_result_card(&self, tool_use_id: &LanguageModelToolUseId) -> Option<&AnyToolCard> {
|
||||
self.tool_result_cards.get(tool_use_id)
|
||||
}
|
||||
|
||||
pub fn insert_tool_result_card(
|
||||
&mut self,
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
card: AnyToolCard,
|
||||
) {
|
||||
self.tool_result_cards.insert(tool_use_id, card);
|
||||
}
|
||||
|
||||
pub fn request_tool_use(
|
||||
&mut self,
|
||||
assistant_message_id: MessageId,
|
||||
tool_use: LanguageModelToolUse,
|
||||
metadata: ToolUseMetadata,
|
||||
cx: &App,
|
||||
) {
|
||||
self.tool_uses_by_assistant_message
|
||||
) -> Arc<str> {
|
||||
let tool_uses = self
|
||||
.tool_uses_by_assistant_message
|
||||
.entry(assistant_message_id)
|
||||
.or_default()
|
||||
.push(tool_use.clone());
|
||||
.or_default();
|
||||
|
||||
// The tool use is being requested by the Assistant, so we want to
|
||||
// attach the tool results to the next user message.
|
||||
let next_user_message_id = MessageId(assistant_message_id.0 + 1);
|
||||
self.tool_uses_by_user_message
|
||||
.entry(next_user_message_id)
|
||||
.or_default()
|
||||
.push(tool_use.id.clone());
|
||||
let mut existing_tool_use_found = false;
|
||||
|
||||
for existing_tool_use in tool_uses.iter_mut() {
|
||||
if existing_tool_use.id == tool_use.id {
|
||||
*existing_tool_use = tool_use.clone();
|
||||
existing_tool_use_found = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !existing_tool_use_found {
|
||||
tool_uses.push(tool_use.clone());
|
||||
}
|
||||
|
||||
let status = if tool_use.is_input_complete {
|
||||
self.tool_use_metadata_by_id
|
||||
.insert(tool_use.id.clone(), metadata);
|
||||
|
||||
PendingToolUseStatus::Idle
|
||||
} else {
|
||||
PendingToolUseStatus::InputStillStreaming
|
||||
};
|
||||
|
||||
let ui_text: Arc<str> = self
|
||||
.tool_ui_label(
|
||||
&tool_use.name,
|
||||
&tool_use.input,
|
||||
tool_use.is_input_complete,
|
||||
cx,
|
||||
)
|
||||
.into();
|
||||
|
||||
self.pending_tool_uses_by_id.insert(
|
||||
tool_use.id.clone(),
|
||||
@@ -282,13 +303,13 @@ impl ToolUseState {
|
||||
assistant_message_id,
|
||||
id: tool_use.id,
|
||||
name: tool_use.name.clone(),
|
||||
ui_text: self
|
||||
.tool_ui_label(&tool_use.name, &tool_use.input, cx)
|
||||
.into(),
|
||||
ui_text: ui_text.clone(),
|
||||
input: tool_use.input,
|
||||
status: PendingToolUseStatus::Idle,
|
||||
status,
|
||||
},
|
||||
);
|
||||
|
||||
ui_text
|
||||
}
|
||||
|
||||
pub fn run_pending_tool(
|
||||
@@ -332,17 +353,30 @@ impl ToolUseState {
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
tool_name: Arc<str>,
|
||||
output: Result<String>,
|
||||
cx: &App,
|
||||
configured_model: Option<&ConfiguredModel>,
|
||||
) -> Option<PendingToolUse> {
|
||||
let metadata = self.tool_use_metadata_by_id.remove(&tool_use_id);
|
||||
|
||||
telemetry::event!(
|
||||
"Agent Tool Finished",
|
||||
model = metadata
|
||||
.as_ref()
|
||||
.map(|metadata| metadata.model.telemetry_id()),
|
||||
model_provider = metadata
|
||||
.as_ref()
|
||||
.map(|metadata| metadata.model.provider_id().to_string()),
|
||||
thread_id = metadata.as_ref().map(|metadata| metadata.thread_id.clone()),
|
||||
prompt_id = metadata.as_ref().map(|metadata| metadata.prompt_id.clone()),
|
||||
tool_name,
|
||||
success = output.is_ok()
|
||||
);
|
||||
|
||||
match output {
|
||||
Ok(tool_result) => {
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
|
||||
const BYTES_PER_TOKEN_ESTIMATE: usize = 3;
|
||||
|
||||
// Protect from clearly large output
|
||||
let tool_output_limit = model_registry
|
||||
.default_model()
|
||||
let tool_output_limit = configured_model
|
||||
.map(|model| model.model.max_token_count() * BYTES_PER_TOKEN_ESTIMATE)
|
||||
.unwrap_or(usize::MAX);
|
||||
|
||||
@@ -395,28 +429,8 @@ impl ToolUseState {
|
||||
request_message: &mut LanguageModelRequestMessage,
|
||||
) {
|
||||
if let Some(tool_uses) = self.tool_uses_by_assistant_message.get(&message_id) {
|
||||
let mut found_tool_use = false;
|
||||
|
||||
for tool_use in tool_uses {
|
||||
if self.tool_results.contains_key(&tool_use.id) {
|
||||
if !found_tool_use {
|
||||
// The API fails if a message contains a tool use without any (non-whitespace) text around it
|
||||
match request_message.content.last_mut() {
|
||||
Some(MessageContent::Text(txt)) => {
|
||||
if txt.is_empty() {
|
||||
txt.push_str(USING_TOOL_MARKER);
|
||||
}
|
||||
}
|
||||
None | Some(_) => {
|
||||
request_message
|
||||
.content
|
||||
.push(MessageContent::Text(USING_TOOL_MARKER.into()));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
found_tool_use = true;
|
||||
|
||||
// Do not send tool uses until they are completed
|
||||
request_message
|
||||
.content
|
||||
@@ -431,31 +445,49 @@ impl ToolUseState {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn attach_tool_results(
|
||||
pub fn has_tool_results(&self, assistant_message_id: MessageId) -> bool {
|
||||
self.tool_uses_by_assistant_message
|
||||
.contains_key(&assistant_message_id)
|
||||
}
|
||||
|
||||
pub fn tool_results_message(
|
||||
&self,
|
||||
message_id: MessageId,
|
||||
request_message: &mut LanguageModelRequestMessage,
|
||||
) {
|
||||
if let Some(tool_uses) = self.tool_uses_by_user_message.get(&message_id) {
|
||||
for tool_use_id in tool_uses {
|
||||
if let Some(tool_result) = self.tool_results.get(tool_use_id) {
|
||||
request_message.content.push(MessageContent::ToolResult(
|
||||
LanguageModelToolResult {
|
||||
tool_use_id: tool_use_id.clone(),
|
||||
tool_name: tool_result.tool_name.clone(),
|
||||
is_error: tool_result.is_error,
|
||||
content: if tool_result.content.is_empty() {
|
||||
// Surprisingly, the API fails if we return an empty string here.
|
||||
// It thinks we are sending a tool use without a tool result.
|
||||
"<Tool returned an empty string>".into()
|
||||
} else {
|
||||
tool_result.content.clone()
|
||||
},
|
||||
assistant_message_id: MessageId,
|
||||
) -> Option<LanguageModelRequestMessage> {
|
||||
let tool_uses = self
|
||||
.tool_uses_by_assistant_message
|
||||
.get(&assistant_message_id)?;
|
||||
|
||||
if tool_uses.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut request_message = LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![],
|
||||
cache: false,
|
||||
};
|
||||
|
||||
for tool_use in tool_uses {
|
||||
if let Some(tool_result) = self.tool_results.get(&tool_use.id) {
|
||||
request_message
|
||||
.content
|
||||
.push(MessageContent::ToolResult(LanguageModelToolResult {
|
||||
tool_use_id: tool_use.id.clone(),
|
||||
tool_name: tool_result.tool_name.clone(),
|
||||
is_error: tool_result.is_error,
|
||||
content: if tool_result.content.is_empty() {
|
||||
// Surprisingly, the API fails if we return an empty string here.
|
||||
// It thinks we are sending a tool use without a tool result.
|
||||
"<Tool returned an empty string>".into()
|
||||
} else {
|
||||
tool_result.content.clone()
|
||||
},
|
||||
));
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
Some(request_message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -482,6 +514,7 @@ pub struct Confirmation {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum PendingToolUseStatus {
|
||||
InputStillStreaming,
|
||||
Idle,
|
||||
NeedsConfirmation(Arc<Confirmation>),
|
||||
Running { _task: Shared<Task<()>> },
|
||||
@@ -501,3 +534,10 @@ impl PendingToolUseStatus {
|
||||
matches!(self, PendingToolUseStatus::NeedsConfirmation { .. })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ToolUseMetadata {
|
||||
pub model: Arc<dyn LanguageModel>,
|
||||
pub thread_id: ThreadId,
|
||||
pub prompt_id: PromptId,
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
mod agent_notification;
|
||||
mod animated_label;
|
||||
mod context_pill;
|
||||
mod usage_banner;
|
||||
|
||||
pub use agent_notification::*;
|
||||
pub use animated_label::*;
|
||||
pub use context_pill::*;
|
||||
pub use usage_banner::*;
|
||||
|
||||
@@ -12,6 +12,7 @@ pub struct AgentNotification {
|
||||
title: SharedString,
|
||||
caption: SharedString,
|
||||
icon: IconName,
|
||||
project_name: Option<SharedString>,
|
||||
}
|
||||
|
||||
impl AgentNotification {
|
||||
@@ -19,11 +20,13 @@ impl AgentNotification {
|
||||
title: impl Into<SharedString>,
|
||||
caption: impl Into<SharedString>,
|
||||
icon: IconName,
|
||||
project_name: Option<impl Into<SharedString>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
title: title.into(),
|
||||
caption: caption.into(),
|
||||
icon,
|
||||
project_name: project_name.map(|name| name.into()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,11 +133,34 @@ impl Render for AgentNotification {
|
||||
.child(gradient_overflow()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
h_flex()
|
||||
.relative()
|
||||
.gap_1p5()
|
||||
.text_size(px(12.))
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.truncate()
|
||||
.when_some(
|
||||
self.project_name.clone(),
|
||||
|description, project_name| {
|
||||
description.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
div()
|
||||
.max_w_16()
|
||||
.truncate()
|
||||
.child(project_name),
|
||||
)
|
||||
.child(
|
||||
div().size(px(3.)).rounded_full().bg(cx
|
||||
.theme()
|
||||
.colors()
|
||||
.text
|
||||
.opacity(0.5)),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
.child(self.caption.clone())
|
||||
.child(gradient_overflow()),
|
||||
),
|
||||
|
||||
121
crates/agent/src/ui/animated_label.rs
Normal file
@@ -0,0 +1,121 @@
|
||||
use gpui::{Animation, AnimationExt, FontWeight, pulsating_between};
|
||||
use std::time::Duration;
|
||||
use ui::prelude::*;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct AnimatedLabel {
|
||||
base: Label,
|
||||
text: SharedString,
|
||||
}
|
||||
|
||||
impl AnimatedLabel {
|
||||
pub fn new(text: impl Into<SharedString>) -> Self {
|
||||
let text = text.into();
|
||||
AnimatedLabel {
|
||||
base: Label::new(text.clone()),
|
||||
text,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LabelCommon for AnimatedLabel {
|
||||
fn size(mut self, size: LabelSize) -> Self {
|
||||
self.base = self.base.size(size);
|
||||
self
|
||||
}
|
||||
|
||||
fn weight(mut self, weight: FontWeight) -> Self {
|
||||
self.base = self.base.weight(weight);
|
||||
self
|
||||
}
|
||||
|
||||
fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self {
|
||||
self.base = self.base.line_height_style(line_height_style);
|
||||
self
|
||||
}
|
||||
|
||||
fn color(mut self, color: Color) -> Self {
|
||||
self.base = self.base.color(color);
|
||||
self
|
||||
}
|
||||
|
||||
fn strikethrough(mut self) -> Self {
|
||||
self.base = self.base.strikethrough();
|
||||
self
|
||||
}
|
||||
|
||||
fn italic(mut self) -> Self {
|
||||
self.base = self.base.italic();
|
||||
self
|
||||
}
|
||||
|
||||
fn alpha(mut self, alpha: f32) -> Self {
|
||||
self.base = self.base.alpha(alpha);
|
||||
self
|
||||
}
|
||||
|
||||
fn underline(mut self) -> Self {
|
||||
self.base = self.base.underline();
|
||||
self
|
||||
}
|
||||
|
||||
fn truncate(mut self) -> Self {
|
||||
self.base = self.base.truncate();
|
||||
self
|
||||
}
|
||||
|
||||
fn single_line(mut self) -> Self {
|
||||
self.base = self.base.single_line();
|
||||
self
|
||||
}
|
||||
|
||||
fn buffer_font(mut self, cx: &App) -> Self {
|
||||
self.base = self.base.buffer_font(cx);
|
||||
self
|
||||
}
|
||||
|
||||
fn inline_code(mut self, cx: &App) -> Self {
|
||||
self.base = self.base.inline_code(cx);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for AnimatedLabel {
|
||||
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
let text = self.text.clone();
|
||||
|
||||
self.base
|
||||
.color(Color::Muted)
|
||||
.with_animations(
|
||||
"animated-label",
|
||||
vec![
|
||||
Animation::new(Duration::from_secs(1)),
|
||||
Animation::new(Duration::from_secs(1)).repeat(),
|
||||
],
|
||||
move |mut label, animation_ix, delta| {
|
||||
match animation_ix {
|
||||
0 => {
|
||||
let chars_to_show = (delta * text.len() as f32).ceil() as usize;
|
||||
let text = SharedString::from(text[0..chars_to_show].to_string());
|
||||
label.set_text(text);
|
||||
}
|
||||
1 => match delta {
|
||||
d if d < 0.25 => label.set_text(text.clone()),
|
||||
d if d < 0.5 => label.set_text(format!("{}.", text)),
|
||||
d if d < 0.75 => label.set_text(format!("{}..", text)),
|
||||
_ => label.set_text(format!("{}...", text)),
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
label
|
||||
},
|
||||
)
|
||||
.with_animation(
|
||||
"pulsating-label",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.6, 1.)),
|
||||
|label, delta| label.map_element(|label| label.alpha(delta)),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,23 @@
|
||||
use std::{rc::Rc, time::Duration};
|
||||
use std::{ops::Range, path::Path, rc::Rc, sync::Arc, time::Duration};
|
||||
|
||||
use file_icons::FileIcons;
|
||||
use gpui::ClickEvent;
|
||||
use gpui::{Animation, AnimationExt as _, pulsating_between};
|
||||
use ui::{IconButtonShape, Tooltip, prelude::*};
|
||||
use futures::FutureExt as _;
|
||||
use gpui::{
|
||||
Animation, AnimationExt as _, AnyView, ClickEvent, Entity, Image, MouseButton, Task,
|
||||
pulsating_between,
|
||||
};
|
||||
use language_model::LanguageModelImage;
|
||||
use project::Project;
|
||||
use prompt_store::PromptStore;
|
||||
use rope::Point;
|
||||
use ui::{IconButtonShape, Tooltip, prelude::*, tooltip_container};
|
||||
|
||||
use crate::context::{AssistantContext, ContextId, ContextKind};
|
||||
use crate::context::{
|
||||
AgentContext, AgentContextHandle, ContextId, ContextKind, DirectoryContext,
|
||||
DirectoryContextHandle, FetchedUrlContext, FileContext, FileContextHandle, ImageContext,
|
||||
ImageStatus, RulesContext, RulesContextHandle, SelectionContext, SelectionContextHandle,
|
||||
SymbolContext, SymbolContextHandle, ThreadContext, ThreadContextHandle,
|
||||
};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub enum ContextPill {
|
||||
@@ -70,9 +82,7 @@ impl ContextPill {
|
||||
|
||||
pub fn id(&self) -> ElementId {
|
||||
match self {
|
||||
Self::Added { context, .. } => {
|
||||
ElementId::NamedInteger("context-pill".into(), context.id.0)
|
||||
}
|
||||
Self::Added { context, .. } => context.handle.element_id("context-pill".into()),
|
||||
Self::Suggested { .. } => "suggested-context-pill".into(),
|
||||
}
|
||||
}
|
||||
@@ -120,43 +130,81 @@ impl RenderOnce for ContextPill {
|
||||
on_remove,
|
||||
focused,
|
||||
on_click,
|
||||
} => base_pill
|
||||
.bg(color.element_background)
|
||||
.border_color(if *focused {
|
||||
color.border_focused
|
||||
} else {
|
||||
color.border.opacity(0.5)
|
||||
})
|
||||
.pr(if on_remove.is_some() { px(2.) } else { px(4.) })
|
||||
.child(
|
||||
h_flex()
|
||||
.id("context-data")
|
||||
.gap_1()
|
||||
.child(
|
||||
div().max_w_64().child(
|
||||
Label::new(context.name.clone())
|
||||
.size(LabelSize::Small)
|
||||
.truncate(),
|
||||
),
|
||||
)
|
||||
.when_some(context.parent.as_ref(), |element, parent_name| {
|
||||
if *dupe_name {
|
||||
element.child(
|
||||
Label::new(parent_name.clone())
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
} else {
|
||||
element
|
||||
}
|
||||
})
|
||||
.when_some(context.tooltip.as_ref(), |element, tooltip| {
|
||||
element.tooltip(Tooltip::text(tooltip.clone()))
|
||||
}),
|
||||
)
|
||||
.when_some(on_remove.as_ref(), |element, on_remove| {
|
||||
element.child(
|
||||
IconButton::new(("remove", context.id.0), IconName::Close)
|
||||
} => {
|
||||
let status_is_error = matches!(context.status, ContextStatus::Error { .. });
|
||||
|
||||
base_pill
|
||||
.pr(if on_remove.is_some() { px(2.) } else { px(4.) })
|
||||
.map(|pill| {
|
||||
if status_is_error {
|
||||
pill.bg(cx.theme().status().error_background)
|
||||
.border_color(cx.theme().status().error_border)
|
||||
} else if *focused {
|
||||
pill.bg(color.element_background)
|
||||
.border_color(color.border_focused)
|
||||
} else {
|
||||
pill.bg(color.element_background)
|
||||
.border_color(color.border.opacity(0.5))
|
||||
}
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.id("context-data")
|
||||
.gap_1()
|
||||
.child(
|
||||
div().max_w_64().child(
|
||||
Label::new(context.name.clone())
|
||||
.size(LabelSize::Small)
|
||||
.truncate(),
|
||||
),
|
||||
)
|
||||
.when_some(context.parent.as_ref(), |element, parent_name| {
|
||||
if *dupe_name {
|
||||
element.child(
|
||||
Label::new(parent_name.clone())
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
} else {
|
||||
element
|
||||
}
|
||||
})
|
||||
.when_some(context.tooltip.as_ref(), |element, tooltip| {
|
||||
element.tooltip(Tooltip::text(tooltip.clone()))
|
||||
})
|
||||
.map(|element| match &context.status {
|
||||
ContextStatus::Ready => element
|
||||
.when_some(
|
||||
context.render_hover.as_ref(),
|
||||
|element, render_hover| {
|
||||
let render_hover = render_hover.clone();
|
||||
element.hoverable_tooltip(move |window, cx| {
|
||||
render_hover(window, cx)
|
||||
})
|
||||
},
|
||||
)
|
||||
.into_any(),
|
||||
ContextStatus::Loading { message } => element
|
||||
.tooltip(ui::Tooltip::text(message.clone()))
|
||||
.with_animation(
|
||||
"pulsating-ctx-pill",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.4, 0.8)),
|
||||
|label, delta| label.opacity(delta),
|
||||
)
|
||||
.into_any_element(),
|
||||
ContextStatus::Error { message } => element
|
||||
.tooltip(ui::Tooltip::text(message.clone()))
|
||||
.into_any_element(),
|
||||
}),
|
||||
)
|
||||
.when_some(on_remove.as_ref(), |element, on_remove| {
|
||||
element.child(
|
||||
IconButton::new(
|
||||
context.handle.element_id("remove".into()),
|
||||
IconName::Close,
|
||||
)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.tooltip(Tooltip::text("Remove Context"))
|
||||
@@ -164,73 +212,43 @@ impl RenderOnce for ContextPill {
|
||||
let on_remove = on_remove.clone();
|
||||
move |event, window, cx| on_remove(event, window, cx)
|
||||
}),
|
||||
)
|
||||
})
|
||||
.when_some(on_click.as_ref(), |element, on_click| {
|
||||
let on_click = on_click.clone();
|
||||
element
|
||||
.cursor_pointer()
|
||||
.on_click(move |event, window, cx| on_click(event, window, cx))
|
||||
})
|
||||
.map(|element| {
|
||||
if context.summarizing {
|
||||
)
|
||||
})
|
||||
.when_some(on_click.as_ref(), |element, on_click| {
|
||||
let on_click = on_click.clone();
|
||||
element
|
||||
.tooltip(ui::Tooltip::text("Summarizing..."))
|
||||
.with_animation(
|
||||
"pulsating-ctx-pill",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.4, 0.8)),
|
||||
|label, delta| label.opacity(delta),
|
||||
)
|
||||
.into_any_element()
|
||||
} else {
|
||||
element.into_any()
|
||||
}
|
||||
}),
|
||||
.cursor_pointer()
|
||||
.on_click(move |event, window, cx| on_click(event, window, cx))
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
ContextPill::Suggested {
|
||||
name,
|
||||
icon_path: _,
|
||||
kind,
|
||||
kind: _,
|
||||
focused,
|
||||
on_click,
|
||||
} => base_pill
|
||||
.cursor_pointer()
|
||||
.pr_1()
|
||||
.when(*focused, |this| {
|
||||
this.bg(color.element_background.opacity(0.5))
|
||||
})
|
||||
.border_dashed()
|
||||
.border_color(if *focused {
|
||||
color.border_focused
|
||||
} else {
|
||||
color.border
|
||||
.map(|pill| {
|
||||
if *focused {
|
||||
pill.border_color(color.border_focused)
|
||||
.bg(color.element_background.opacity(0.5))
|
||||
} else {
|
||||
pill.border_color(color.border)
|
||||
}
|
||||
})
|
||||
.hover(|style| style.bg(color.element_hover.opacity(0.5)))
|
||||
.child(
|
||||
div().px_0p5().max_w_64().child(
|
||||
div().max_w_64().child(
|
||||
Label::new(name.clone())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.truncate(),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Label::new(match kind {
|
||||
ContextKind::File => "Active Tab",
|
||||
ContextKind::Thread
|
||||
| ContextKind::Directory
|
||||
| ContextKind::FetchedUrl
|
||||
| ContextKind::Symbol => "Active",
|
||||
})
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::Plus)
|
||||
.size(IconSize::XSmall)
|
||||
.into_any_element(),
|
||||
)
|
||||
.tooltip(|window, cx| {
|
||||
Tooltip::with_meta("Suggested Context", None, "Click to add it", window, cx)
|
||||
})
|
||||
@@ -243,99 +261,513 @@ impl RenderOnce for ContextPill {
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ContextStatus {
|
||||
Ready,
|
||||
Loading { message: SharedString },
|
||||
Error { message: SharedString },
|
||||
}
|
||||
|
||||
#[derive(RegisterComponent)]
|
||||
pub struct AddedContext {
|
||||
pub id: ContextId,
|
||||
pub handle: AgentContextHandle,
|
||||
pub kind: ContextKind,
|
||||
pub name: SharedString,
|
||||
pub parent: Option<SharedString>,
|
||||
pub tooltip: Option<SharedString>,
|
||||
pub icon_path: Option<SharedString>,
|
||||
pub summarizing: bool,
|
||||
pub status: ContextStatus,
|
||||
pub render_hover: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyView + 'static>>,
|
||||
}
|
||||
|
||||
impl AddedContext {
|
||||
pub fn new(context: &AssistantContext, cx: &App) -> AddedContext {
|
||||
/// Creates an `AddedContext` by retrieving relevant details of `AgentContext`. This returns a
|
||||
/// `None` if `DirectoryContext` or `RulesContext` no longer exist.
|
||||
///
|
||||
/// TODO: `None` cases are unremovable from `ContextStore` and so are a very minor memory leak.
|
||||
pub fn new_pending(
|
||||
handle: AgentContextHandle,
|
||||
prompt_store: Option<&Entity<PromptStore>>,
|
||||
project: &Project,
|
||||
cx: &App,
|
||||
) -> Option<AddedContext> {
|
||||
match handle {
|
||||
AgentContextHandle::File(handle) => Self::pending_file(handle, cx),
|
||||
AgentContextHandle::Directory(handle) => Self::pending_directory(handle, project, cx),
|
||||
AgentContextHandle::Symbol(handle) => Self::pending_symbol(handle, cx),
|
||||
AgentContextHandle::Selection(handle) => Self::pending_selection(handle, cx),
|
||||
AgentContextHandle::FetchedUrl(handle) => Some(Self::fetched_url(handle)),
|
||||
AgentContextHandle::Thread(handle) => Some(Self::pending_thread(handle, cx)),
|
||||
AgentContextHandle::Rules(handle) => Self::pending_rules(handle, prompt_store, cx),
|
||||
AgentContextHandle::Image(handle) => Some(Self::image(handle)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_attached(context: &AgentContext, cx: &App) -> AddedContext {
|
||||
match context {
|
||||
AssistantContext::File(file_context) => {
|
||||
let full_path = file_context.context_buffer.file.full_path(cx);
|
||||
let full_path_string: SharedString =
|
||||
full_path.to_string_lossy().into_owned().into();
|
||||
let name = full_path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned().into())
|
||||
.unwrap_or_else(|| full_path_string.clone());
|
||||
let parent = full_path
|
||||
.parent()
|
||||
.and_then(|p| p.file_name())
|
||||
.map(|n| n.to_string_lossy().into_owned().into());
|
||||
AddedContext {
|
||||
id: file_context.id,
|
||||
kind: ContextKind::File,
|
||||
name,
|
||||
parent,
|
||||
tooltip: Some(full_path_string),
|
||||
icon_path: FileIcons::get_icon(&full_path, cx),
|
||||
summarizing: false,
|
||||
AgentContext::File(context) => Self::attached_file(context, cx),
|
||||
AgentContext::Directory(context) => Self::attached_directory(context),
|
||||
AgentContext::Symbol(context) => Self::attached_symbol(context, cx),
|
||||
AgentContext::Selection(context) => Self::attached_selection(context, cx),
|
||||
AgentContext::FetchedUrl(context) => Self::fetched_url(context.clone()),
|
||||
AgentContext::Thread(context) => Self::attached_thread(context),
|
||||
AgentContext::Rules(context) => Self::attached_rules(context),
|
||||
AgentContext::Image(context) => Self::image(context.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn pending_file(handle: FileContextHandle, cx: &App) -> Option<AddedContext> {
|
||||
let full_path = handle.buffer.read(cx).file()?.full_path(cx);
|
||||
Some(Self::file(handle, &full_path, cx))
|
||||
}
|
||||
|
||||
fn attached_file(context: &FileContext, cx: &App) -> AddedContext {
|
||||
Self::file(context.handle.clone(), &context.full_path, cx)
|
||||
}
|
||||
|
||||
fn file(handle: FileContextHandle, full_path: &Path, cx: &App) -> AddedContext {
|
||||
let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into();
|
||||
let name = full_path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned().into())
|
||||
.unwrap_or_else(|| full_path_string.clone());
|
||||
let parent = full_path
|
||||
.parent()
|
||||
.and_then(|p| p.file_name())
|
||||
.map(|n| n.to_string_lossy().into_owned().into());
|
||||
AddedContext {
|
||||
kind: ContextKind::File,
|
||||
name,
|
||||
parent,
|
||||
tooltip: Some(full_path_string),
|
||||
icon_path: FileIcons::get_icon(&full_path, cx),
|
||||
status: ContextStatus::Ready,
|
||||
render_hover: None,
|
||||
handle: AgentContextHandle::File(handle),
|
||||
}
|
||||
}
|
||||
|
||||
fn pending_directory(
|
||||
handle: DirectoryContextHandle,
|
||||
project: &Project,
|
||||
cx: &App,
|
||||
) -> Option<AddedContext> {
|
||||
let worktree = project.worktree_for_entry(handle.entry_id, cx)?.read(cx);
|
||||
let entry = worktree.entry_for_id(handle.entry_id)?;
|
||||
let full_path = worktree.full_path(&entry.path);
|
||||
Some(Self::directory(handle, &full_path))
|
||||
}
|
||||
|
||||
fn attached_directory(context: &DirectoryContext) -> AddedContext {
|
||||
Self::directory(context.handle.clone(), &context.full_path)
|
||||
}
|
||||
|
||||
fn directory(handle: DirectoryContextHandle, full_path: &Path) -> AddedContext {
|
||||
let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into();
|
||||
let name = full_path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned().into())
|
||||
.unwrap_or_else(|| full_path_string.clone());
|
||||
let parent = full_path
|
||||
.parent()
|
||||
.and_then(|p| p.file_name())
|
||||
.map(|n| n.to_string_lossy().into_owned().into());
|
||||
AddedContext {
|
||||
kind: ContextKind::Directory,
|
||||
name,
|
||||
parent,
|
||||
tooltip: Some(full_path_string),
|
||||
icon_path: None,
|
||||
status: ContextStatus::Ready,
|
||||
render_hover: None,
|
||||
handle: AgentContextHandle::Directory(handle),
|
||||
}
|
||||
}
|
||||
|
||||
fn pending_symbol(handle: SymbolContextHandle, cx: &App) -> Option<AddedContext> {
|
||||
let excerpt =
|
||||
ContextFileExcerpt::new(&handle.full_path(cx)?, handle.enclosing_line_range(cx), cx);
|
||||
Some(AddedContext {
|
||||
kind: ContextKind::Symbol,
|
||||
name: handle.symbol.clone(),
|
||||
parent: Some(excerpt.file_name_and_range.clone()),
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
status: ContextStatus::Ready,
|
||||
render_hover: {
|
||||
let handle = handle.clone();
|
||||
Some(Rc::new(move |_, cx| {
|
||||
excerpt.hover_view(handle.text(cx), cx).into()
|
||||
}))
|
||||
},
|
||||
handle: AgentContextHandle::Symbol(handle),
|
||||
})
|
||||
}
|
||||
|
||||
fn attached_symbol(context: &SymbolContext, cx: &App) -> AddedContext {
|
||||
let excerpt = ContextFileExcerpt::new(&context.full_path, context.line_range.clone(), cx);
|
||||
AddedContext {
|
||||
kind: ContextKind::Symbol,
|
||||
name: context.handle.symbol.clone(),
|
||||
parent: Some(excerpt.file_name_and_range.clone()),
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
status: ContextStatus::Ready,
|
||||
render_hover: {
|
||||
let text = context.text.clone();
|
||||
Some(Rc::new(move |_, cx| {
|
||||
excerpt.hover_view(text.clone(), cx).into()
|
||||
}))
|
||||
},
|
||||
handle: AgentContextHandle::Symbol(context.handle.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn pending_selection(handle: SelectionContextHandle, cx: &App) -> Option<AddedContext> {
|
||||
let excerpt = ContextFileExcerpt::new(&handle.full_path(cx)?, handle.line_range(cx), cx);
|
||||
Some(AddedContext {
|
||||
kind: ContextKind::Selection,
|
||||
name: excerpt.file_name_and_range.clone(),
|
||||
parent: excerpt.parent_name.clone(),
|
||||
tooltip: None,
|
||||
icon_path: excerpt.icon_path.clone(),
|
||||
status: ContextStatus::Ready,
|
||||
render_hover: {
|
||||
let handle = handle.clone();
|
||||
Some(Rc::new(move |_, cx| {
|
||||
excerpt.hover_view(handle.text(cx), cx).into()
|
||||
}))
|
||||
},
|
||||
handle: AgentContextHandle::Selection(handle),
|
||||
})
|
||||
}
|
||||
|
||||
fn attached_selection(context: &SelectionContext, cx: &App) -> AddedContext {
|
||||
let excerpt = ContextFileExcerpt::new(&context.full_path, context.line_range.clone(), cx);
|
||||
AddedContext {
|
||||
kind: ContextKind::Selection,
|
||||
name: excerpt.file_name_and_range.clone(),
|
||||
parent: excerpt.parent_name.clone(),
|
||||
tooltip: None,
|
||||
icon_path: excerpt.icon_path.clone(),
|
||||
status: ContextStatus::Ready,
|
||||
render_hover: {
|
||||
let text = context.text.clone();
|
||||
Some(Rc::new(move |_, cx| {
|
||||
excerpt.hover_view(text.clone(), cx).into()
|
||||
}))
|
||||
},
|
||||
handle: AgentContextHandle::Selection(context.handle.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn fetched_url(context: FetchedUrlContext) -> AddedContext {
|
||||
AddedContext {
|
||||
kind: ContextKind::FetchedUrl,
|
||||
name: context.url.clone(),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
status: ContextStatus::Ready,
|
||||
render_hover: None,
|
||||
handle: AgentContextHandle::FetchedUrl(context),
|
||||
}
|
||||
}
|
||||
|
||||
fn pending_thread(handle: ThreadContextHandle, cx: &App) -> AddedContext {
|
||||
AddedContext {
|
||||
kind: ContextKind::Thread,
|
||||
name: handle.title(cx),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
status: if handle.thread.read(cx).is_generating_detailed_summary() {
|
||||
ContextStatus::Loading {
|
||||
message: "Summarizing…".into(),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ContextStatus::Ready
|
||||
},
|
||||
render_hover: {
|
||||
let thread = handle.thread.clone();
|
||||
Some(Rc::new(move |_, cx| {
|
||||
let text = thread.read(cx).latest_detailed_summary_or_text();
|
||||
ContextPillHover::new_text(text.clone(), cx).into()
|
||||
}))
|
||||
},
|
||||
handle: AgentContextHandle::Thread(handle),
|
||||
}
|
||||
}
|
||||
|
||||
AssistantContext::Directory(directory_context) => {
|
||||
// TODO: handle worktree disambiguation. Maybe by storing an `Arc<dyn File>` to also
|
||||
// handle renames?
|
||||
let full_path = &directory_context.project_path.path;
|
||||
let full_path_string: SharedString =
|
||||
full_path.to_string_lossy().into_owned().into();
|
||||
let name = full_path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned().into())
|
||||
.unwrap_or_else(|| full_path_string.clone());
|
||||
let parent = full_path
|
||||
.parent()
|
||||
.and_then(|p| p.file_name())
|
||||
.map(|n| n.to_string_lossy().into_owned().into());
|
||||
AddedContext {
|
||||
id: directory_context.id,
|
||||
kind: ContextKind::Directory,
|
||||
name,
|
||||
parent,
|
||||
tooltip: Some(full_path_string),
|
||||
icon_path: None,
|
||||
summarizing: false,
|
||||
fn attached_thread(context: &ThreadContext) -> AddedContext {
|
||||
AddedContext {
|
||||
kind: ContextKind::Thread,
|
||||
name: context.title.clone(),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
status: ContextStatus::Ready,
|
||||
render_hover: {
|
||||
let text = context.text.clone();
|
||||
Some(Rc::new(move |_, cx| {
|
||||
ContextPillHover::new_text(text.clone(), cx).into()
|
||||
}))
|
||||
},
|
||||
handle: AgentContextHandle::Thread(context.handle.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn pending_rules(
|
||||
handle: RulesContextHandle,
|
||||
prompt_store: Option<&Entity<PromptStore>>,
|
||||
cx: &App,
|
||||
) -> Option<AddedContext> {
|
||||
let title = prompt_store
|
||||
.as_ref()?
|
||||
.read(cx)
|
||||
.metadata(handle.prompt_id.into())?
|
||||
.title
|
||||
.unwrap_or_else(|| "Unnamed Rule".into());
|
||||
Some(AddedContext {
|
||||
kind: ContextKind::Rules,
|
||||
name: title.clone(),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
status: ContextStatus::Ready,
|
||||
render_hover: None,
|
||||
handle: AgentContextHandle::Rules(handle),
|
||||
})
|
||||
}
|
||||
|
||||
fn attached_rules(context: &RulesContext) -> AddedContext {
|
||||
let title = context
|
||||
.title
|
||||
.clone()
|
||||
.unwrap_or_else(|| "Unnamed Rule".into());
|
||||
AddedContext {
|
||||
kind: ContextKind::Rules,
|
||||
name: title,
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
status: ContextStatus::Ready,
|
||||
render_hover: {
|
||||
let text = context.text.clone();
|
||||
Some(Rc::new(move |_, cx| {
|
||||
ContextPillHover::new_text(text.clone(), cx).into()
|
||||
}))
|
||||
},
|
||||
handle: AgentContextHandle::Rules(context.handle.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn image(context: ImageContext) -> AddedContext {
|
||||
AddedContext {
|
||||
kind: ContextKind::Image,
|
||||
name: "Image".into(),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
status: match context.status() {
|
||||
ImageStatus::Loading => ContextStatus::Loading {
|
||||
message: "Loading…".into(),
|
||||
},
|
||||
ImageStatus::Error => ContextStatus::Error {
|
||||
message: "Failed to load image".into(),
|
||||
},
|
||||
ImageStatus::Ready => ContextStatus::Ready,
|
||||
},
|
||||
render_hover: Some(Rc::new({
|
||||
let image = context.original_image.clone();
|
||||
move |_, cx| {
|
||||
let image = image.clone();
|
||||
ContextPillHover::new(cx, move |_, _| {
|
||||
gpui::img(image.clone())
|
||||
.max_w_96()
|
||||
.max_h_96()
|
||||
.into_any_element()
|
||||
})
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
AssistantContext::Symbol(symbol_context) => AddedContext {
|
||||
id: symbol_context.id,
|
||||
kind: ContextKind::Symbol,
|
||||
name: symbol_context.context_symbol.id.name.clone(),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
summarizing: false,
|
||||
},
|
||||
|
||||
AssistantContext::FetchedUrl(fetched_url_context) => AddedContext {
|
||||
id: fetched_url_context.id,
|
||||
kind: ContextKind::FetchedUrl,
|
||||
name: fetched_url_context.url.clone(),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
summarizing: false,
|
||||
},
|
||||
|
||||
AssistantContext::Thread(thread_context) => AddedContext {
|
||||
id: thread_context.id,
|
||||
kind: ContextKind::Thread,
|
||||
name: thread_context.summary(cx),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
summarizing: thread_context
|
||||
.thread
|
||||
.read(cx)
|
||||
.is_generating_detailed_summary(),
|
||||
},
|
||||
})),
|
||||
handle: AgentContextHandle::Image(context),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct ContextFileExcerpt {
|
||||
pub file_name_and_range: SharedString,
|
||||
pub full_path_and_range: SharedString,
|
||||
pub parent_name: Option<SharedString>,
|
||||
pub icon_path: Option<SharedString>,
|
||||
}
|
||||
|
||||
impl ContextFileExcerpt {
|
||||
pub fn new(full_path: &Path, line_range: Range<Point>, cx: &App) -> Self {
|
||||
let full_path_string = full_path.to_string_lossy().into_owned();
|
||||
let file_name = full_path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| full_path_string.clone());
|
||||
|
||||
let line_range_text = format!(" ({}-{})", line_range.start.row + 1, line_range.end.row + 1);
|
||||
let mut full_path_and_range = full_path_string;
|
||||
full_path_and_range.push_str(&line_range_text);
|
||||
let mut file_name_and_range = file_name;
|
||||
file_name_and_range.push_str(&line_range_text);
|
||||
|
||||
let parent_name = full_path
|
||||
.parent()
|
||||
.and_then(|p| p.file_name())
|
||||
.map(|n| n.to_string_lossy().into_owned().into());
|
||||
|
||||
let icon_path = FileIcons::get_icon(&full_path, cx);
|
||||
|
||||
ContextFileExcerpt {
|
||||
file_name_and_range: file_name_and_range.into(),
|
||||
full_path_and_range: full_path_and_range.into(),
|
||||
parent_name,
|
||||
icon_path,
|
||||
}
|
||||
}
|
||||
|
||||
fn hover_view(&self, text: SharedString, cx: &mut App) -> Entity<ContextPillHover> {
|
||||
let icon_path = self.icon_path.clone();
|
||||
let full_path_and_range = self.full_path_and_range.clone();
|
||||
ContextPillHover::new(cx, move |_, cx| {
|
||||
v_flex()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.w_full()
|
||||
.max_w_full()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border.opacity(0.6))
|
||||
.children(
|
||||
icon_path
|
||||
.clone()
|
||||
.map(Icon::from_path)
|
||||
.map(|icon| icon.color(Color::Muted).size(IconSize::XSmall)),
|
||||
)
|
||||
.child(
|
||||
// TODO: make this truncate on the left.
|
||||
Label::new(full_path_and_range.clone())
|
||||
.size(LabelSize::Small)
|
||||
.ml_1(),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("context-pill-hover-contents")
|
||||
.overflow_scroll()
|
||||
.max_w_128()
|
||||
.max_h_96()
|
||||
.child(Label::new(text.clone()).buffer_font(cx)),
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct ContextPillHover {
|
||||
render_hover: Box<dyn Fn(&mut Window, &mut App) -> AnyElement>,
|
||||
}
|
||||
|
||||
impl ContextPillHover {
|
||||
fn new(
|
||||
cx: &mut App,
|
||||
render_hover: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
|
||||
) -> Entity<Self> {
|
||||
cx.new(|_| Self {
|
||||
render_hover: Box::new(render_hover),
|
||||
})
|
||||
}
|
||||
|
||||
fn new_text(content: SharedString, cx: &mut App) -> Entity<Self> {
|
||||
Self::new(cx, move |_, _| {
|
||||
div()
|
||||
.id("context-pill-hover-contents")
|
||||
.overflow_scroll()
|
||||
.max_w_128()
|
||||
.max_h_96()
|
||||
.child(content.clone())
|
||||
.into_any_element()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ContextPillHover {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
tooltip_container(window, cx, move |this, window, cx| {
|
||||
this.occlude()
|
||||
.on_mouse_move(|_, _, cx| cx.stop_propagation())
|
||||
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
||||
.child((self.render_hover)(window, cx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for AddedContext {
|
||||
fn scope() -> ComponentScope {
|
||||
ComponentScope::Agent
|
||||
}
|
||||
|
||||
fn sort_name() -> &'static str {
|
||||
"AddedContext"
|
||||
}
|
||||
|
||||
fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
|
||||
let mut next_context_id = ContextId::zero();
|
||||
let image_ready = (
|
||||
"Ready",
|
||||
AddedContext::image(ImageContext {
|
||||
context_id: next_context_id.post_inc(),
|
||||
project_path: None,
|
||||
original_image: Arc::new(Image::empty()),
|
||||
image_task: Task::ready(Some(LanguageModelImage::empty())).shared(),
|
||||
}),
|
||||
);
|
||||
|
||||
let image_loading = (
|
||||
"Loading",
|
||||
AddedContext::image(ImageContext {
|
||||
context_id: next_context_id.post_inc(),
|
||||
project_path: None,
|
||||
original_image: Arc::new(Image::empty()),
|
||||
image_task: cx
|
||||
.background_spawn(async move {
|
||||
smol::Timer::after(Duration::from_secs(60 * 5)).await;
|
||||
Some(LanguageModelImage::empty())
|
||||
})
|
||||
.shared(),
|
||||
}),
|
||||
);
|
||||
|
||||
let image_error = (
|
||||
"Error",
|
||||
AddedContext::image(ImageContext {
|
||||
context_id: next_context_id.post_inc(),
|
||||
project_path: None,
|
||||
original_image: Arc::new(Image::empty()),
|
||||
image_task: Task::ready(None).shared(),
|
||||
}),
|
||||
);
|
||||
|
||||
Some(
|
||||
v_flex()
|
||||
.gap_6()
|
||||
.children(
|
||||
vec![image_ready, image_loading, image_error]
|
||||
.into_iter()
|
||||
.map(|(text, context)| {
|
||||
single_example(
|
||||
text,
|
||||
ContextPill::added(context, false, false, None).into_any_element(),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.into_any(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
258
crates/agent/src/ui/usage_banner.rs
Normal file
@@ -0,0 +1,258 @@
|
||||
use client::zed_urls;
|
||||
use language_model::RequestUsage;
|
||||
use ui::{Banner, ProgressBar, Severity, prelude::*};
|
||||
use zed_llm_client::{Plan, UsageLimit};
|
||||
|
||||
#[derive(IntoElement, RegisterComponent)]
|
||||
pub struct UsageBanner {
|
||||
plan: Plan,
|
||||
usage: RequestUsage,
|
||||
}
|
||||
|
||||
impl UsageBanner {
|
||||
pub fn new(plan: Plan, usage: RequestUsage) -> Self {
|
||||
Self { plan, usage }
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for UsageBanner {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let used_percentage = match self.usage.limit {
|
||||
UsageLimit::Limited(limit) => Some((self.usage.amount as f32 / limit as f32) * 100.),
|
||||
UsageLimit::Unlimited => None,
|
||||
};
|
||||
|
||||
let (severity, message) = match self.usage.limit {
|
||||
UsageLimit::Limited(limit) => {
|
||||
if self.usage.amount >= limit {
|
||||
let message = match self.plan {
|
||||
Plan::ZedPro => "Monthly request limit reached",
|
||||
Plan::ZedProTrial => "Trial request limit reached",
|
||||
Plan::Free => "Free tier request limit reached",
|
||||
};
|
||||
|
||||
(Severity::Error, message)
|
||||
} else if (self.usage.amount as f32 / limit as f32) >= 0.9 {
|
||||
(Severity::Warning, "Approaching request limit")
|
||||
} else {
|
||||
let message = match self.plan {
|
||||
Plan::ZedPro => "Zed Pro",
|
||||
Plan::ZedProTrial => "Zed Pro (Trial)",
|
||||
Plan::Free => "Zed Free",
|
||||
};
|
||||
|
||||
(Severity::Info, message)
|
||||
}
|
||||
}
|
||||
UsageLimit::Unlimited => {
|
||||
let message = match self.plan {
|
||||
Plan::ZedPro => "Zed Pro",
|
||||
Plan::ZedProTrial => "Zed Pro (Trial)",
|
||||
Plan::Free => "Zed Free",
|
||||
};
|
||||
|
||||
(Severity::Info, message)
|
||||
}
|
||||
};
|
||||
|
||||
let action = match self.plan {
|
||||
Plan::ZedProTrial | Plan::Free => {
|
||||
Button::new("upgrade", "Upgrade").on_click(|_, _window, cx| {
|
||||
cx.open_url(&zed_urls::account_url(cx));
|
||||
})
|
||||
}
|
||||
Plan::ZedPro => Button::new("manage", "Manage").on_click(|_, _window, cx| {
|
||||
cx.open_url(&zed_urls::account_url(cx));
|
||||
}),
|
||||
};
|
||||
|
||||
Banner::new().severity(severity).children(
|
||||
h_flex().flex_1().gap_1().child(Label::new(message)).child(
|
||||
h_flex()
|
||||
.flex_1()
|
||||
.justify_end()
|
||||
.gap_1p5()
|
||||
.children(used_percentage.map(|percent| {
|
||||
h_flex()
|
||||
.items_center()
|
||||
.w_full()
|
||||
.max_w(px(180.))
|
||||
.child(ProgressBar::new("usage", percent, 100., cx))
|
||||
}))
|
||||
.child(
|
||||
Label::new(match self.usage.limit {
|
||||
UsageLimit::Limited(limit) => {
|
||||
format!("{} / {limit}", self.usage.amount)
|
||||
}
|
||||
UsageLimit::Unlimited => format!("{} / ∞", self.usage.amount),
|
||||
})
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
// Note: This should go in the banner's `action_slot`, but doing that messes with the size of the
|
||||
// progress bar.
|
||||
.child(action),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for UsageBanner {
|
||||
fn sort_name() -> &'static str {
|
||||
"AgentUsageBanner"
|
||||
}
|
||||
|
||||
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
|
||||
let trial_limit = Plan::ZedProTrial.model_requests_limit();
|
||||
let trial_examples = vec![
|
||||
single_example(
|
||||
"Zed Pro Trial - New User",
|
||||
div()
|
||||
.size_full()
|
||||
.child(UsageBanner::new(
|
||||
Plan::ZedProTrial,
|
||||
RequestUsage {
|
||||
limit: trial_limit,
|
||||
amount: 10,
|
||||
},
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Zed Pro Trial - Approaching Limit",
|
||||
div()
|
||||
.size_full()
|
||||
.child(UsageBanner::new(
|
||||
Plan::ZedProTrial,
|
||||
RequestUsage {
|
||||
limit: trial_limit,
|
||||
amount: 135,
|
||||
},
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Zed Pro Trial - Request Limit Reached",
|
||||
div()
|
||||
.size_full()
|
||||
.child(UsageBanner::new(
|
||||
Plan::ZedProTrial,
|
||||
RequestUsage {
|
||||
limit: trial_limit,
|
||||
amount: 150,
|
||||
},
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
];
|
||||
|
||||
let free_limit = Plan::Free.model_requests_limit();
|
||||
let free_examples = vec![
|
||||
single_example(
|
||||
"Free - Normal Usage",
|
||||
div()
|
||||
.size_full()
|
||||
.child(UsageBanner::new(
|
||||
Plan::Free,
|
||||
RequestUsage {
|
||||
limit: free_limit,
|
||||
amount: 25,
|
||||
},
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Free - Approaching Limit",
|
||||
div()
|
||||
.size_full()
|
||||
.child(UsageBanner::new(
|
||||
Plan::Free,
|
||||
RequestUsage {
|
||||
limit: free_limit,
|
||||
amount: 45,
|
||||
},
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Free - Request Limit Reached",
|
||||
div()
|
||||
.size_full()
|
||||
.child(UsageBanner::new(
|
||||
Plan::Free,
|
||||
RequestUsage {
|
||||
limit: free_limit,
|
||||
amount: 50,
|
||||
},
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
];
|
||||
|
||||
let zed_pro_limit = Plan::ZedPro.model_requests_limit();
|
||||
let zed_pro_examples = vec![
|
||||
single_example(
|
||||
"Zed Pro - Normal Usage",
|
||||
div()
|
||||
.size_full()
|
||||
.child(UsageBanner::new(
|
||||
Plan::ZedPro,
|
||||
RequestUsage {
|
||||
limit: zed_pro_limit,
|
||||
amount: 250,
|
||||
},
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Zed Pro - Approaching Limit",
|
||||
div()
|
||||
.size_full()
|
||||
.child(UsageBanner::new(
|
||||
Plan::ZedPro,
|
||||
RequestUsage {
|
||||
limit: zed_pro_limit,
|
||||
amount: 450,
|
||||
},
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Zed Pro - Request Limit Reached",
|
||||
div()
|
||||
.size_full()
|
||||
.child(UsageBanner::new(
|
||||
Plan::ZedPro,
|
||||
RequestUsage {
|
||||
limit: zed_pro_limit,
|
||||
amount: 500,
|
||||
},
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
];
|
||||
|
||||
Some(
|
||||
v_flex()
|
||||
.gap_6()
|
||||
.p_4()
|
||||
.children(vec![
|
||||
Label::new("Trial Plan")
|
||||
.size(LabelSize::Large)
|
||||
.into_any_element(),
|
||||
example_group(trial_examples).vertical().into_any_element(),
|
||||
Label::new("Free Plan")
|
||||
.size(LabelSize::Large)
|
||||
.into_any_element(),
|
||||
example_group(free_examples).vertical().into_any_element(),
|
||||
Label::new("Pro Plan")
|
||||
.size(LabelSize::Large)
|
||||
.into_any_element(),
|
||||
example_group(zed_pro_examples)
|
||||
.vertical()
|
||||
.into_any_element(),
|
||||
])
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
// Copied from `crates/zed/build.rs`, with removal of code for including the zed icon on windows.
|
||||
|
||||
use std::process::Command;
|
||||
|
||||
fn main() {
|
||||
if cfg!(target_os = "macos") {
|
||||
println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.15.7");
|
||||
|
||||
// Weakly link ReplayKit to ensure Zed can be used on macOS 10.15+.
|
||||
println!("cargo:rustc-link-arg=-Wl,-weak_framework,ReplayKit");
|
||||
|
||||
// Seems to be required to enable Swift concurrency
|
||||
println!("cargo:rustc-link-arg=-Wl,-rpath,/usr/lib/swift");
|
||||
|
||||
// Register exported Objective-C selectors, protocols, etc
|
||||
println!("cargo:rustc-link-arg=-Wl,-ObjC");
|
||||
}
|
||||
|
||||
// Populate git sha environment variable if git is available
|
||||
println!("cargo:rerun-if-changed=../../.git/logs/HEAD");
|
||||
println!(
|
||||
"cargo:rustc-env=TARGET={}",
|
||||
std::env::var("TARGET").unwrap()
|
||||
);
|
||||
if let Ok(output) = Command::new("git").args(["rev-parse", "HEAD"]).output() {
|
||||
if output.status.success() {
|
||||
let git_sha = String::from_utf8_lossy(&output.stdout);
|
||||
let git_sha = git_sha.trim();
|
||||
|
||||
println!("cargo:rustc-env=ZED_COMMIT_SHA={git_sha}");
|
||||
|
||||
if let Ok(build_profile) = std::env::var("PROFILE") {
|
||||
if build_profile == "release" {
|
||||
// This is currently the best way to make `cargo build ...`'s build script
|
||||
// to print something to stdout without extra verbosity.
|
||||
println!(
|
||||
"cargo:warning=Info: using '{git_sha}' hash for ZED_COMMIT_SHA env var"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
#[cfg(target_env = "msvc")]
|
||||
{
|
||||
// todo(windows): This is to avoid stack overflow. Remove it when solved.
|
||||
println!("cargo:rustc-link-arg=/stack:{}", 8 * 1024 * 1024);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,384 +0,0 @@
|
||||
use crate::git_commands::{run_git, setup_temp_repo};
|
||||
use crate::headless_assistant::{HeadlessAppState, HeadlessAssistant};
|
||||
use crate::{get_exercise_language, get_exercise_name};
|
||||
use agent::RequestKind;
|
||||
use anyhow::{Result, anyhow};
|
||||
use collections::HashMap;
|
||||
use gpui::{App, Task};
|
||||
use language_model::{LanguageModel, TokenUsage};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
fs,
|
||||
io::Write,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct EvalResult {
|
||||
pub exercise_name: String,
|
||||
pub diff: String,
|
||||
pub assistant_response: String,
|
||||
pub elapsed_time_ms: u128,
|
||||
pub timestamp: u128,
|
||||
// Token usage fields
|
||||
pub input_tokens: usize,
|
||||
pub output_tokens: usize,
|
||||
pub total_tokens: usize,
|
||||
pub tool_use_counts: usize,
|
||||
}
|
||||
|
||||
pub struct EvalOutput {
|
||||
pub diff: String,
|
||||
pub last_message: String,
|
||||
pub elapsed_time: Duration,
|
||||
pub assistant_response_count: usize,
|
||||
pub tool_use_counts: HashMap<Arc<str>, u32>,
|
||||
pub token_usage: TokenUsage,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct EvalSetup {
|
||||
pub url: String,
|
||||
pub base_sha: String,
|
||||
}
|
||||
|
||||
pub struct Eval {
|
||||
pub repo_path: PathBuf,
|
||||
pub eval_setup: EvalSetup,
|
||||
pub user_prompt: String,
|
||||
}
|
||||
|
||||
impl Eval {
|
||||
// Keep this method for potential future use, but mark it as intentionally unused
|
||||
#[allow(dead_code)]
|
||||
pub async fn load(_name: String, path: PathBuf, repos_dir: &Path) -> Result<Self> {
|
||||
let prompt_path = path.join("prompt.txt");
|
||||
let user_prompt = smol::unblock(|| std::fs::read_to_string(prompt_path)).await?;
|
||||
let setup_path = path.join("setup.json");
|
||||
let setup_contents = smol::unblock(|| std::fs::read_to_string(setup_path)).await?;
|
||||
let eval_setup = serde_json_lenient::from_str_lenient::<EvalSetup>(&setup_contents)?;
|
||||
|
||||
// Move this internal function inside the load method since it's only used here
|
||||
fn repo_dir_name(url: &str) -> String {
|
||||
url.trim_start_matches("https://")
|
||||
.replace(|c: char| !c.is_alphanumeric(), "_")
|
||||
}
|
||||
|
||||
let repo_path = repos_dir.join(repo_dir_name(&eval_setup.url));
|
||||
|
||||
Ok(Eval {
|
||||
repo_path,
|
||||
eval_setup,
|
||||
user_prompt,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn run(
|
||||
self,
|
||||
app_state: Arc<HeadlessAppState>,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<EvalOutput>> {
|
||||
cx.spawn(async move |cx| {
|
||||
run_git(&self.repo_path, &["checkout", &self.eval_setup.base_sha]).await?;
|
||||
|
||||
let (assistant, done_rx) =
|
||||
cx.update(|cx| HeadlessAssistant::new(app_state.clone(), cx))??;
|
||||
|
||||
let _worktree = assistant
|
||||
.update(cx, |assistant, cx| {
|
||||
assistant.project.update(cx, |project, cx| {
|
||||
project.create_worktree(&self.repo_path, true, cx)
|
||||
})
|
||||
})?
|
||||
.await?;
|
||||
|
||||
let start_time = std::time::SystemTime::now();
|
||||
|
||||
let (system_prompt_context, load_error) = cx
|
||||
.update(|cx| {
|
||||
assistant
|
||||
.read(cx)
|
||||
.thread
|
||||
.read(cx)
|
||||
.load_system_prompt_context(cx)
|
||||
})?
|
||||
.await;
|
||||
|
||||
if let Some(load_error) = load_error {
|
||||
return Err(anyhow!("{:?}", load_error));
|
||||
};
|
||||
|
||||
assistant.update(cx, |assistant, cx| {
|
||||
assistant.thread.update(cx, |thread, cx| {
|
||||
let context = vec![];
|
||||
thread.insert_user_message(self.user_prompt.clone(), context, None, cx);
|
||||
thread.set_system_prompt_context(system_prompt_context);
|
||||
thread.send_to_model(model, RequestKind::Chat, cx);
|
||||
});
|
||||
})?;
|
||||
|
||||
done_rx.recv().await??;
|
||||
|
||||
// Add this section to check untracked files
|
||||
println!("Checking for untracked files:");
|
||||
let untracked = run_git(
|
||||
&self.repo_path,
|
||||
&["ls-files", "--others", "--exclude-standard"],
|
||||
)
|
||||
.await?;
|
||||
if untracked.is_empty() {
|
||||
println!("No untracked files found");
|
||||
} else {
|
||||
// Add all files to git so they appear in the diff
|
||||
println!("Adding untracked files to git");
|
||||
run_git(&self.repo_path, &["add", "."]).await?;
|
||||
}
|
||||
|
||||
// get git status
|
||||
let _status = run_git(&self.repo_path, &["status", "--short"]).await?;
|
||||
|
||||
let elapsed_time = start_time.elapsed()?;
|
||||
|
||||
// Get diff of staged changes (the files we just added)
|
||||
let staged_diff = run_git(&self.repo_path, &["diff", "--staged"]).await?;
|
||||
|
||||
// Get diff of unstaged changes
|
||||
let unstaged_diff = run_git(&self.repo_path, &["diff"]).await?;
|
||||
|
||||
// Combine both diffs
|
||||
let diff = if unstaged_diff.is_empty() {
|
||||
staged_diff
|
||||
} else if staged_diff.is_empty() {
|
||||
unstaged_diff
|
||||
} else {
|
||||
format!(
|
||||
"# Staged changes\n{}\n\n# Unstaged changes\n{}",
|
||||
staged_diff, unstaged_diff
|
||||
)
|
||||
};
|
||||
|
||||
assistant.update(cx, |assistant, cx| {
|
||||
let thread = assistant.thread.read(cx);
|
||||
let last_message = thread.messages().last().unwrap();
|
||||
if last_message.role != language_model::Role::Assistant {
|
||||
return Err(anyhow!("Last message is not from assistant"));
|
||||
}
|
||||
let assistant_response_count = thread
|
||||
.messages()
|
||||
.filter(|message| message.role == language_model::Role::Assistant)
|
||||
.count();
|
||||
Ok(EvalOutput {
|
||||
diff,
|
||||
last_message: last_message.to_string(),
|
||||
elapsed_time,
|
||||
assistant_response_count,
|
||||
tool_use_counts: assistant.tool_use_counts.clone(),
|
||||
token_usage: thread.cumulative_token_usage(),
|
||||
})
|
||||
})?
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl EvalOutput {
|
||||
// Keep this method for potential future use, but mark it as intentionally unused
|
||||
#[allow(dead_code)]
|
||||
pub fn save_to_directory(&self, output_dir: &Path, eval_output_value: String) -> Result<()> {
|
||||
// Create the output directory if it doesn't exist
|
||||
fs::create_dir_all(&output_dir)?;
|
||||
|
||||
// Save the diff to a file
|
||||
let diff_path = output_dir.join("diff.patch");
|
||||
let mut diff_file = fs::File::create(&diff_path)?;
|
||||
diff_file.write_all(self.diff.as_bytes())?;
|
||||
|
||||
// Save the last message to a file
|
||||
let message_path = output_dir.join("assistant_response.txt");
|
||||
let mut message_file = fs::File::create(&message_path)?;
|
||||
message_file.write_all(self.last_message.as_bytes())?;
|
||||
|
||||
// Current metrics for this run
|
||||
let current_metrics = serde_json::json!({
|
||||
"elapsed_time_ms": self.elapsed_time.as_millis(),
|
||||
"assistant_response_count": self.assistant_response_count,
|
||||
"tool_use_counts": self.tool_use_counts,
|
||||
"token_usage": self.token_usage,
|
||||
"eval_output_value": eval_output_value,
|
||||
});
|
||||
|
||||
// Get current timestamp in milliseconds
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)?
|
||||
.as_millis()
|
||||
.to_string();
|
||||
|
||||
// Path to metrics file
|
||||
let metrics_path = output_dir.join("metrics.json");
|
||||
|
||||
// Load existing metrics if the file exists, or create a new object
|
||||
let mut historical_metrics = if metrics_path.exists() {
|
||||
let metrics_content = fs::read_to_string(&metrics_path)?;
|
||||
serde_json::from_str::<serde_json::Value>(&metrics_content)
|
||||
.unwrap_or_else(|_| serde_json::json!({}))
|
||||
} else {
|
||||
serde_json::json!({})
|
||||
};
|
||||
|
||||
// Add new run with timestamp as key
|
||||
if let serde_json::Value::Object(ref mut map) = historical_metrics {
|
||||
map.insert(timestamp, current_metrics);
|
||||
}
|
||||
|
||||
// Write updated metrics back to file
|
||||
let metrics_json = serde_json::to_string_pretty(&historical_metrics)?;
|
||||
let mut metrics_file = fs::File::create(&metrics_path)?;
|
||||
metrics_file.write_all(metrics_json.as_bytes())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn read_instructions(exercise_path: &Path) -> Result<String> {
|
||||
let instructions_path = exercise_path.join(".docs").join("instructions.md");
|
||||
println!("Reading instructions from: {}", instructions_path.display());
|
||||
let instructions = smol::unblock(move || std::fs::read_to_string(&instructions_path)).await?;
|
||||
Ok(instructions)
|
||||
}
|
||||
|
||||
pub async fn save_eval_results(exercise_path: &Path, results: Vec<EvalResult>) -> Result<()> {
|
||||
let eval_dir = exercise_path.join("evaluation");
|
||||
fs::create_dir_all(&eval_dir)?;
|
||||
|
||||
let eval_file = eval_dir.join("evals.json");
|
||||
|
||||
println!("Saving evaluation results to: {}", eval_file.display());
|
||||
println!(
|
||||
"Results to save: {} evaluations for exercise path: {}",
|
||||
results.len(),
|
||||
exercise_path.display()
|
||||
);
|
||||
|
||||
// Check file existence before reading/writing
|
||||
if eval_file.exists() {
|
||||
println!("Existing evals.json file found, will update it");
|
||||
} else {
|
||||
println!("No existing evals.json file found, will create new one");
|
||||
}
|
||||
|
||||
// Structure to organize evaluations by test name and timestamp
|
||||
let mut eval_data: serde_json::Value = if eval_file.exists() {
|
||||
let content = fs::read_to_string(&eval_file)?;
|
||||
serde_json::from_str(&content).unwrap_or_else(|_| serde_json::json!({}))
|
||||
} else {
|
||||
serde_json::json!({})
|
||||
};
|
||||
|
||||
// Get current timestamp for this batch of results
|
||||
let timestamp = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)?
|
||||
.as_millis()
|
||||
.to_string();
|
||||
|
||||
// Group the new results by test name (exercise name)
|
||||
for result in results {
|
||||
let exercise_name = &result.exercise_name;
|
||||
|
||||
println!("Adding result: exercise={}", exercise_name);
|
||||
|
||||
// Ensure the exercise entry exists
|
||||
if eval_data.get(exercise_name).is_none() {
|
||||
eval_data[exercise_name] = serde_json::json!({});
|
||||
}
|
||||
|
||||
// Ensure the timestamp entry exists as an object
|
||||
if eval_data[exercise_name].get(×tamp).is_none() {
|
||||
eval_data[exercise_name][×tamp] = serde_json::json!({});
|
||||
}
|
||||
|
||||
// Add this result under the timestamp with template name as key
|
||||
eval_data[exercise_name][×tamp] = serde_json::to_value(&result)?;
|
||||
}
|
||||
|
||||
// Write back to file with pretty formatting
|
||||
let json_content = serde_json::to_string_pretty(&eval_data)?;
|
||||
match fs::write(&eval_file, json_content) {
|
||||
Ok(_) => println!("✓ Successfully saved results to {}", eval_file.display()),
|
||||
Err(e) => println!("✗ Failed to write results file: {}", e),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn run_exercise_eval(
|
||||
exercise_path: PathBuf,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
app_state: Arc<HeadlessAppState>,
|
||||
base_sha: String,
|
||||
_framework_path: PathBuf,
|
||||
cx: gpui::AsyncApp,
|
||||
) -> Result<EvalResult> {
|
||||
let exercise_name = get_exercise_name(&exercise_path);
|
||||
let language = get_exercise_language(&exercise_path)?;
|
||||
let mut instructions = read_instructions(&exercise_path).await?;
|
||||
instructions.push_str(&format!(
|
||||
"\n\nWhen writing the code for this prompt, use {} to achieve the goal.",
|
||||
language
|
||||
));
|
||||
|
||||
println!("Running evaluation for exercise: {}", exercise_name);
|
||||
|
||||
// Create temporary directory with exercise files
|
||||
let temp_dir = setup_temp_repo(&exercise_path, &base_sha).await?;
|
||||
let temp_path = temp_dir.path().to_path_buf();
|
||||
|
||||
let local_commit_sha = run_git(&temp_path, &["rev-parse", "HEAD"]).await?;
|
||||
|
||||
let start_time = SystemTime::now();
|
||||
|
||||
// Create a basic eval struct to work with the existing system
|
||||
let eval = Eval {
|
||||
repo_path: temp_path.clone(),
|
||||
eval_setup: EvalSetup {
|
||||
url: format!("file://{}", temp_path.display()),
|
||||
base_sha: local_commit_sha, // Use the local commit SHA instead of the framework base SHA
|
||||
},
|
||||
user_prompt: instructions.clone(),
|
||||
};
|
||||
|
||||
// Run the evaluation
|
||||
let eval_output = cx
|
||||
.update(|cx| eval.run(app_state.clone(), model.clone(), cx))?
|
||||
.await?;
|
||||
|
||||
// Get diff from git
|
||||
let diff = eval_output.diff.clone();
|
||||
|
||||
let elapsed_time = start_time.elapsed()?;
|
||||
|
||||
// Calculate total tokens as the sum of input and output tokens
|
||||
let input_tokens = eval_output.token_usage.input_tokens;
|
||||
let output_tokens = eval_output.token_usage.output_tokens;
|
||||
let tool_use_counts = eval_output.tool_use_counts.values().sum::<u32>();
|
||||
let total_tokens = input_tokens + output_tokens;
|
||||
|
||||
// Save results to evaluation directory
|
||||
let result = EvalResult {
|
||||
exercise_name: exercise_name.clone(),
|
||||
diff,
|
||||
assistant_response: eval_output.last_message.clone(),
|
||||
elapsed_time_ms: elapsed_time.as_millis(),
|
||||
timestamp: SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)?
|
||||
.as_millis(),
|
||||
// Convert u32 token counts to usize
|
||||
input_tokens: input_tokens.try_into().unwrap(),
|
||||
output_tokens: output_tokens.try_into().unwrap(),
|
||||
total_tokens: total_tokens.try_into().unwrap(),
|
||||
tool_use_counts: tool_use_counts.try_into().unwrap(),
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
pub fn get_exercise_name(exercise_path: &Path) -> String {
|
||||
exercise_path
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
pub fn get_exercise_language(exercise_path: &Path) -> Result<String> {
|
||||
// Extract the language from path (data/python/exercises/... => python)
|
||||
let parts: Vec<_> = exercise_path.components().collect();
|
||||
|
||||
for (i, part) in parts.iter().enumerate() {
|
||||
if i > 0 && part.as_os_str() == "eval_code" {
|
||||
if i + 1 < parts.len() {
|
||||
let language = parts[i + 1].as_os_str().to_string_lossy().to_string();
|
||||
return Ok(language);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(anyhow!(
|
||||
"Could not determine language from path: {:?}",
|
||||
exercise_path
|
||||
))
|
||||
}
|
||||
|
||||
pub fn find_exercises(
|
||||
framework_path: &Path,
|
||||
languages: &[&str],
|
||||
max_per_language: Option<usize>,
|
||||
) -> Result<Vec<PathBuf>> {
|
||||
let mut all_exercises = Vec::new();
|
||||
|
||||
println!("Searching for exercises in languages: {:?}", languages);
|
||||
|
||||
for language in languages {
|
||||
let language_dir = framework_path
|
||||
.join("eval_code")
|
||||
.join(language)
|
||||
.join("exercises")
|
||||
.join("practice");
|
||||
|
||||
println!("Checking language directory: {:?}", language_dir);
|
||||
if !language_dir.exists() {
|
||||
println!("Warning: Language directory not found: {:?}", language_dir);
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut exercises = Vec::new();
|
||||
match fs::read_dir(&language_dir) {
|
||||
Ok(entries) => {
|
||||
for entry_result in entries {
|
||||
match entry_result {
|
||||
Ok(entry) => {
|
||||
let path = entry.path();
|
||||
|
||||
if path.is_dir() {
|
||||
// Special handling for "internal" directory
|
||||
if *language == "internal" {
|
||||
// Check for repo_info.json to validate it's an internal exercise
|
||||
let repo_info_path = path.join(".meta").join("repo_info.json");
|
||||
let instructions_path =
|
||||
path.join(".docs").join("instructions.md");
|
||||
|
||||
if repo_info_path.exists() && instructions_path.exists() {
|
||||
exercises.push(path);
|
||||
}
|
||||
} else {
|
||||
// Map the language to the file extension - original code
|
||||
let language_extension = match *language {
|
||||
"python" => "py",
|
||||
"go" => "go",
|
||||
"rust" => "rs",
|
||||
"typescript" => "ts",
|
||||
"javascript" => "js",
|
||||
"ruby" => "rb",
|
||||
"php" => "php",
|
||||
"bash" => "sh",
|
||||
"multi" => "diff",
|
||||
_ => continue, // Skip unsupported languages
|
||||
};
|
||||
|
||||
// Check if this is a valid exercise with instructions and example
|
||||
let instructions_path =
|
||||
path.join(".docs").join("instructions.md");
|
||||
let has_instructions = instructions_path.exists();
|
||||
let example_path = path
|
||||
.join(".meta")
|
||||
.join(format!("example.{}", language_extension));
|
||||
let has_example = example_path.exists();
|
||||
|
||||
if has_instructions && has_example {
|
||||
exercises.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => println!("Error reading directory entry: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => println!(
|
||||
"Error reading directory {}: {}",
|
||||
language_dir.display(),
|
||||
err
|
||||
),
|
||||
}
|
||||
|
||||
// Sort exercises by name for consistent selection
|
||||
exercises.sort_by(|a, b| {
|
||||
let a_name = a.file_name().unwrap_or_default().to_string_lossy();
|
||||
let b_name = b.file_name().unwrap_or_default().to_string_lossy();
|
||||
a_name.cmp(&b_name)
|
||||
});
|
||||
|
||||
// Apply the limit if specified
|
||||
if let Some(limit) = max_per_language {
|
||||
if exercises.len() > limit {
|
||||
println!(
|
||||
"Limiting {} exercises to {} for language {}",
|
||||
exercises.len(),
|
||||
limit,
|
||||
language
|
||||
);
|
||||
exercises.truncate(limit);
|
||||
}
|
||||
}
|
||||
|
||||
println!(
|
||||
"Found {} exercises for language {}: {:?}",
|
||||
exercises.len(),
|
||||
language,
|
||||
exercises
|
||||
.iter()
|
||||
.map(|p| p.file_name().unwrap_or_default().to_string_lossy())
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
all_exercises.extend(exercises);
|
||||
}
|
||||
|
||||
Ok(all_exercises)
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use serde::Deserialize;
|
||||
use std::{fs, path::Path};
|
||||
use tempfile::TempDir;
|
||||
use util::command::new_smol_command;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SetupConfig {
|
||||
#[serde(rename = "base.sha")]
|
||||
pub base_sha: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct RepoInfo {
|
||||
pub remote_url: String,
|
||||
pub head_sha: String,
|
||||
}
|
||||
|
||||
pub async fn run_git(repo_path: &Path, args: &[&str]) -> Result<String> {
|
||||
let output = new_smol_command("git")
|
||||
.current_dir(repo_path)
|
||||
.args(args)
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
if output.status.success() {
|
||||
Ok(String::from_utf8(output.stdout)?.trim().to_string())
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"Git command failed: {} with status: {}",
|
||||
args.join(" "),
|
||||
output.status
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn read_base_sha(framework_path: &Path) -> Result<String> {
|
||||
let setup_path = framework_path.join("setup.json");
|
||||
let setup_content = smol::unblock(move || std::fs::read_to_string(&setup_path)).await?;
|
||||
let setup_config: SetupConfig = serde_json_lenient::from_str_lenient(&setup_content)?;
|
||||
Ok(setup_config.base_sha)
|
||||
}
|
||||
|
||||
pub async fn read_repo_info(exercise_path: &Path) -> Result<RepoInfo> {
|
||||
let repo_info_path = exercise_path.join(".meta").join("repo_info.json");
|
||||
println!("Reading repo info from: {}", repo_info_path.display());
|
||||
let repo_info_content = smol::unblock(move || std::fs::read_to_string(&repo_info_path)).await?;
|
||||
let repo_info: RepoInfo = serde_json_lenient::from_str_lenient(&repo_info_content)?;
|
||||
|
||||
// Remove any quotes from the strings
|
||||
let remote_url = repo_info.remote_url.trim_matches('"').to_string();
|
||||
let head_sha = repo_info.head_sha.trim_matches('"').to_string();
|
||||
|
||||
Ok(RepoInfo {
|
||||
remote_url,
|
||||
head_sha,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn setup_temp_repo(exercise_path: &Path, _base_sha: &str) -> Result<TempDir> {
|
||||
let temp_dir = TempDir::new()?;
|
||||
|
||||
// Check if this is an internal exercise by looking for repo_info.json
|
||||
let repo_info_path = exercise_path.join(".meta").join("repo_info.json");
|
||||
if repo_info_path.exists() {
|
||||
// This is an internal exercise, handle it differently
|
||||
let repo_info = read_repo_info(exercise_path).await?;
|
||||
|
||||
// Clone the repository to the temp directory
|
||||
let url = repo_info.remote_url;
|
||||
let clone_path = temp_dir.path();
|
||||
println!(
|
||||
"Cloning repository from {} to {}",
|
||||
url,
|
||||
clone_path.display()
|
||||
);
|
||||
run_git(
|
||||
&std::env::current_dir()?,
|
||||
&["clone", &url, &clone_path.to_string_lossy()],
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Checkout the specified commit
|
||||
println!("Checking out commit: {}", repo_info.head_sha);
|
||||
run_git(temp_dir.path(), &["checkout", &repo_info.head_sha]).await?;
|
||||
|
||||
println!("Successfully set up internal repository");
|
||||
} else {
|
||||
// Original code for regular exercises
|
||||
// Copy the exercise files to the temp directory, excluding .docs and .meta
|
||||
for entry in WalkDir::new(exercise_path).min_depth(0).max_depth(10) {
|
||||
let entry = entry?;
|
||||
let source_path = entry.path();
|
||||
|
||||
// Skip .docs and .meta directories completely
|
||||
if source_path.starts_with(exercise_path.join(".docs"))
|
||||
|| source_path.starts_with(exercise_path.join(".meta"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if source_path.is_file() {
|
||||
let relative_path = source_path.strip_prefix(exercise_path)?;
|
||||
let dest_path = temp_dir.path().join(relative_path);
|
||||
|
||||
// Make sure parent directories exist
|
||||
if let Some(parent) = dest_path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
fs::copy(source_path, dest_path)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize git repo in the temp directory
|
||||
run_git(temp_dir.path(), &["init"]).await?;
|
||||
run_git(temp_dir.path(), &["add", "."]).await?;
|
||||
run_git(temp_dir.path(), &["commit", "-m", "Initial commit"]).await?;
|
||||
|
||||
println!("Created temp repo without .docs and .meta directories");
|
||||
}
|
||||
|
||||
Ok(temp_dir)
|
||||
}
|
||||
@@ -1,246 +0,0 @@
|
||||
use agent::{RequestKind, Thread, ThreadEvent, ThreadStore};
|
||||
use anyhow::anyhow;
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use client::{Client, UserStore};
|
||||
use collections::HashMap;
|
||||
use dap::DapRegistry;
|
||||
use gpui::{App, Entity, SemanticVersion, Subscription, Task, prelude::*};
|
||||
use language::LanguageRegistry;
|
||||
use language_model::{
|
||||
AuthenticateError, LanguageModel, LanguageModelProviderId, LanguageModelRegistry,
|
||||
};
|
||||
use node_runtime::NodeRuntime;
|
||||
use project::{Project, RealFs};
|
||||
use prompt_store::PromptBuilder;
|
||||
use settings::SettingsStore;
|
||||
use smol::channel;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Subset of `workspace::AppState` needed by `HeadlessAssistant`, with additional fields.
|
||||
pub struct HeadlessAppState {
|
||||
pub languages: Arc<LanguageRegistry>,
|
||||
pub client: Arc<Client>,
|
||||
pub user_store: Entity<UserStore>,
|
||||
pub fs: Arc<dyn fs::Fs>,
|
||||
pub node_runtime: NodeRuntime,
|
||||
|
||||
// Additional fields not present in `workspace::AppState`.
|
||||
pub prompt_builder: Arc<PromptBuilder>,
|
||||
}
|
||||
|
||||
pub struct HeadlessAssistant {
|
||||
pub thread: Entity<Thread>,
|
||||
pub project: Entity<Project>,
|
||||
#[allow(dead_code)]
|
||||
pub thread_store: Entity<ThreadStore>,
|
||||
pub tool_use_counts: HashMap<Arc<str>, u32>,
|
||||
pub done_tx: channel::Sender<anyhow::Result<()>>,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl HeadlessAssistant {
|
||||
pub fn new(
|
||||
app_state: Arc<HeadlessAppState>,
|
||||
cx: &mut App,
|
||||
) -> anyhow::Result<(Entity<Self>, channel::Receiver<anyhow::Result<()>>)> {
|
||||
let env = None;
|
||||
let project = Project::local(
|
||||
app_state.client.clone(),
|
||||
app_state.node_runtime.clone(),
|
||||
app_state.user_store.clone(),
|
||||
app_state.languages.clone(),
|
||||
Arc::new(DapRegistry::default()),
|
||||
app_state.fs.clone(),
|
||||
env,
|
||||
cx,
|
||||
);
|
||||
|
||||
let tools = Arc::new(ToolWorkingSet::default());
|
||||
let thread_store =
|
||||
ThreadStore::new(project.clone(), tools, app_state.prompt_builder.clone(), cx)?;
|
||||
|
||||
let thread = thread_store.update(cx, |thread_store, cx| thread_store.create_thread(cx));
|
||||
|
||||
let (done_tx, done_rx) = channel::unbounded::<anyhow::Result<()>>();
|
||||
|
||||
let headless_thread = cx.new(move |cx| Self {
|
||||
_subscription: cx.subscribe(&thread, Self::handle_thread_event),
|
||||
thread,
|
||||
project,
|
||||
thread_store,
|
||||
tool_use_counts: HashMap::default(),
|
||||
done_tx,
|
||||
});
|
||||
|
||||
Ok((headless_thread, done_rx))
|
||||
}
|
||||
|
||||
fn handle_thread_event(
|
||||
&mut self,
|
||||
thread: Entity<Thread>,
|
||||
event: &ThreadEvent,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
ThreadEvent::ShowError(err) => self
|
||||
.done_tx
|
||||
.send_blocking(Err(anyhow!("{:?}", err)))
|
||||
.unwrap(),
|
||||
ThreadEvent::DoneStreaming => {
|
||||
let thread = thread.read(cx);
|
||||
if let Some(message) = thread.messages().last() {
|
||||
println!("Message: {}", message.to_string());
|
||||
}
|
||||
if thread.all_tools_finished() {
|
||||
self.done_tx.send_blocking(Ok(())).unwrap()
|
||||
}
|
||||
}
|
||||
ThreadEvent::UsePendingTools => {
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.use_pending_tools(cx);
|
||||
});
|
||||
}
|
||||
ThreadEvent::ToolConfirmationNeeded => {
|
||||
// Automatically approve all tools that need confirmation in headless mode
|
||||
println!("Tool confirmation needed - automatically approving in headless mode");
|
||||
|
||||
// Get the tools needing confirmation
|
||||
let tools_needing_confirmation: Vec<_> = thread
|
||||
.read(cx)
|
||||
.tools_needing_confirmation()
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
// Run each tool that needs confirmation
|
||||
for tool_use in tools_needing_confirmation {
|
||||
if let Some(tool) = thread.read(cx).tools().tool(&tool_use.name, cx) {
|
||||
thread.update(cx, |thread, cx| {
|
||||
println!("Auto-approving tool: {}", tool_use.name);
|
||||
|
||||
// Create a request to send to the tool
|
||||
let request = thread.to_completion_request(RequestKind::Chat, cx);
|
||||
let messages = Arc::new(request.messages);
|
||||
|
||||
// Run the tool
|
||||
thread.run_tool(
|
||||
tool_use.id.clone(),
|
||||
tool_use.ui_text.clone(),
|
||||
tool_use.input.clone(),
|
||||
&messages,
|
||||
tool,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
ThreadEvent::ToolFinished {
|
||||
tool_use_id,
|
||||
pending_tool_use,
|
||||
..
|
||||
} => {
|
||||
if let Some(pending_tool_use) = pending_tool_use {
|
||||
println!(
|
||||
"Used tool {} with input: {}",
|
||||
pending_tool_use.name, pending_tool_use.input
|
||||
);
|
||||
*self
|
||||
.tool_use_counts
|
||||
.entry(pending_tool_use.name.clone())
|
||||
.or_insert(0) += 1;
|
||||
}
|
||||
if let Some(tool_result) = thread.read(cx).tool_result(tool_use_id) {
|
||||
println!("Tool result: {:?}", tool_result);
|
||||
}
|
||||
if thread.read(cx).all_tools_finished() {
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
if let Some(model) = model_registry.default_model() {
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.attach_tool_results(cx);
|
||||
thread.send_to_model(model.model, RequestKind::Chat, cx);
|
||||
});
|
||||
} else {
|
||||
println!(
|
||||
"Warning: No active language model available to continue conversation"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(cx: &mut App) -> Arc<HeadlessAppState> {
|
||||
release_channel::init(SemanticVersion::default(), cx);
|
||||
gpui_tokio::init(cx);
|
||||
|
||||
let mut settings_store = SettingsStore::new(cx);
|
||||
settings_store
|
||||
.set_default_settings(settings::default_settings().as_ref(), cx)
|
||||
.unwrap();
|
||||
cx.set_global(settings_store);
|
||||
client::init_settings(cx);
|
||||
Project::init_settings(cx);
|
||||
|
||||
let client = Client::production(cx);
|
||||
cx.set_http_client(client.http_client().clone());
|
||||
|
||||
let git_binary_path = None;
|
||||
let fs = Arc::new(RealFs::new(
|
||||
git_binary_path,
|
||||
cx.background_executor().clone(),
|
||||
));
|
||||
|
||||
let languages = Arc::new(LanguageRegistry::new(cx.background_executor().clone()));
|
||||
|
||||
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
|
||||
|
||||
language::init(cx);
|
||||
language_model::init(client.clone(), cx);
|
||||
language_models::init(user_store.clone(), client.clone(), fs.clone(), cx);
|
||||
assistant_tools::init(client.http_client().clone(), cx);
|
||||
context_server::init(cx);
|
||||
let stdout_is_a_pty = false;
|
||||
let prompt_builder = PromptBuilder::load(fs.clone(), stdout_is_a_pty, cx);
|
||||
agent::init(fs.clone(), client.clone(), prompt_builder.clone(), cx);
|
||||
|
||||
Arc::new(HeadlessAppState {
|
||||
languages,
|
||||
client,
|
||||
user_store,
|
||||
fs,
|
||||
node_runtime: NodeRuntime::unavailable(),
|
||||
prompt_builder,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn find_model(model_name: &str, cx: &App) -> anyhow::Result<Arc<dyn LanguageModel>> {
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
let model = model_registry
|
||||
.available_models(cx)
|
||||
.find(|model| model.id().0 == model_name);
|
||||
|
||||
let Some(model) = model else {
|
||||
return Err(anyhow!(
|
||||
"No language model named {} was available. Available models: {}",
|
||||
model_name,
|
||||
model_registry
|
||||
.available_models(cx)
|
||||
.map(|model| model.id().0.clone())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
));
|
||||
};
|
||||
|
||||
Ok(model)
|
||||
}
|
||||
|
||||
pub fn authenticate_model_provider(
|
||||
provider_id: LanguageModelProviderId,
|
||||
cx: &mut App,
|
||||
) -> Task<std::result::Result<(), AuthenticateError>> {
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
let model_provider = model_registry.provider(&provider_id).unwrap();
|
||||
model_provider.authenticate(cx)
|
||||
}
|
||||
@@ -1,205 +0,0 @@
|
||||
mod eval;
|
||||
mod get_exercise;
|
||||
mod git_commands;
|
||||
mod headless_assistant;
|
||||
|
||||
use clap::Parser;
|
||||
use eval::{run_exercise_eval, save_eval_results};
|
||||
use futures::stream::{self, StreamExt};
|
||||
use get_exercise::{find_exercises, get_exercise_language, get_exercise_name};
|
||||
use git_commands::read_base_sha;
|
||||
use gpui::Application;
|
||||
use headless_assistant::{authenticate_model_provider, find_model};
|
||||
use language_model::LanguageModelRegistry;
|
||||
use reqwest_client::ReqwestClient;
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(
|
||||
name = "agent_eval",
|
||||
disable_version_flag = true,
|
||||
before_help = "Tool eval runner"
|
||||
)]
|
||||
struct Args {
|
||||
/// Match the names of evals to run.
|
||||
#[arg(long)]
|
||||
exercise_names: Vec<String>,
|
||||
/// Runs all exercises, causes the exercise_names to be ignored.
|
||||
#[arg(long)]
|
||||
all: bool,
|
||||
/// Supported language types to evaluate (default: internal).
|
||||
/// Internal is data generated from the agent panel
|
||||
#[arg(long, default_value = "internal")]
|
||||
languages: String,
|
||||
/// Name of the model (default: "claude-3-7-sonnet-latest")
|
||||
#[arg(long, default_value = "claude-3-7-sonnet-latest")]
|
||||
model_name: String,
|
||||
/// Name of the editor model (default: value of `--model_name`).
|
||||
#[arg(long)]
|
||||
editor_model_name: Option<String>,
|
||||
/// Number of evaluations to run concurrently (default: 3)
|
||||
#[arg(short, long, default_value = "5")]
|
||||
concurrency: usize,
|
||||
/// Maximum number of exercises to evaluate per language
|
||||
#[arg(long)]
|
||||
max_exercises_per_language: Option<usize>,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
let args = Args::parse();
|
||||
let http_client = Arc::new(ReqwestClient::new());
|
||||
let app = Application::headless().with_http_client(http_client.clone());
|
||||
|
||||
// Path to the zed-ace-framework repo
|
||||
let framework_path = PathBuf::from("../zed-ace-framework")
|
||||
.canonicalize()
|
||||
.unwrap();
|
||||
|
||||
// Fix the 'languages' lifetime issue by creating owned Strings instead of slices
|
||||
let languages: Vec<String> = args.languages.split(',').map(|s| s.to_string()).collect();
|
||||
|
||||
println!("Using zed-ace-framework at: {:?}", framework_path);
|
||||
println!("Evaluating languages: {:?}", languages);
|
||||
|
||||
app.run(move |cx| {
|
||||
let app_state = headless_assistant::init(cx);
|
||||
|
||||
let model = find_model(&args.model_name, cx).unwrap();
|
||||
let editor_model = if let Some(model_name) = &args.editor_model_name {
|
||||
find_model(model_name, cx).unwrap()
|
||||
} else {
|
||||
model.clone()
|
||||
};
|
||||
|
||||
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
|
||||
registry.set_default_model(Some(model.clone()), cx);
|
||||
});
|
||||
|
||||
let model_provider_id = model.provider_id();
|
||||
let editor_model_provider_id = editor_model.provider_id();
|
||||
|
||||
let framework_path_clone = framework_path.clone();
|
||||
let languages_clone = languages.clone();
|
||||
let exercise_names = args.exercise_names.clone();
|
||||
let all_flag = args.all;
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
// Authenticate all model providers first
|
||||
cx.update(|cx| authenticate_model_provider(model_provider_id.clone(), cx))
|
||||
.unwrap()
|
||||
.await
|
||||
.unwrap();
|
||||
cx.update(|cx| authenticate_model_provider(editor_model_provider_id.clone(), cx))
|
||||
.unwrap()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("framework path: {}", framework_path_clone.display());
|
||||
|
||||
let base_sha = read_base_sha(&framework_path_clone).await.unwrap();
|
||||
|
||||
println!("base sha: {}", base_sha);
|
||||
|
||||
let all_exercises = find_exercises(
|
||||
&framework_path_clone,
|
||||
&languages_clone
|
||||
.iter()
|
||||
.map(|s| s.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
args.max_exercises_per_language,
|
||||
)
|
||||
.unwrap();
|
||||
println!("Found {} exercises total", all_exercises.len());
|
||||
|
||||
// Filter exercises if specific ones were requested
|
||||
let exercises_to_run = if !exercise_names.is_empty() {
|
||||
// If exercise names are specified, filter by them regardless of --all flag
|
||||
all_exercises
|
||||
.into_iter()
|
||||
.filter(|path| {
|
||||
let name = get_exercise_name(path);
|
||||
exercise_names.iter().any(|filter| name.contains(filter))
|
||||
})
|
||||
.collect()
|
||||
} else if all_flag {
|
||||
// Only use all_flag if no exercise names are specified
|
||||
all_exercises
|
||||
} else {
|
||||
// Default behavior (no filters)
|
||||
all_exercises
|
||||
};
|
||||
|
||||
println!("Will run {} exercises", exercises_to_run.len());
|
||||
|
||||
// Create exercise eval tasks - each exercise is a single task that will run templates sequentially
|
||||
let exercise_tasks: Vec<_> = exercises_to_run
|
||||
.into_iter()
|
||||
.map(|exercise_path| {
|
||||
let exercise_name = get_exercise_name(&exercise_path);
|
||||
let model_clone = model.clone();
|
||||
let app_state_clone = app_state.clone();
|
||||
let base_sha_clone = base_sha.clone();
|
||||
let framework_path_clone = framework_path_clone.clone();
|
||||
let cx_clone = cx.clone();
|
||||
|
||||
async move {
|
||||
println!("Processing exercise: {}", exercise_name);
|
||||
let mut exercise_results = Vec::new();
|
||||
|
||||
match run_exercise_eval(
|
||||
exercise_path.clone(),
|
||||
model_clone.clone(),
|
||||
app_state_clone.clone(),
|
||||
base_sha_clone.clone(),
|
||||
framework_path_clone.clone(),
|
||||
cx_clone.clone(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
println!("Completed {}", exercise_name);
|
||||
exercise_results.push(result);
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Error running {}: {}", exercise_name, err);
|
||||
}
|
||||
}
|
||||
|
||||
// Save results for this exercise
|
||||
if !exercise_results.is_empty() {
|
||||
if let Err(err) =
|
||||
save_eval_results(&exercise_path, exercise_results.clone()).await
|
||||
{
|
||||
println!("Error saving results for {}: {}", exercise_name, err);
|
||||
} else {
|
||||
println!("Saved results for {}", exercise_name);
|
||||
}
|
||||
}
|
||||
|
||||
exercise_results
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
println!(
|
||||
"Running {} exercises with concurrency: {}",
|
||||
exercise_tasks.len(),
|
||||
args.concurrency
|
||||
);
|
||||
|
||||
// Run exercises concurrently, with each exercise running its templates sequentially
|
||||
let all_results = stream::iter(exercise_tasks)
|
||||
.buffer_unordered(args.concurrency)
|
||||
.flat_map(stream::iter)
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
|
||||
println!("Completed {} evaluation runs", all_results.len());
|
||||
cx.update(|cx| cx.quit()).unwrap();
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
|
||||
println!("Done running evals");
|
||||
}
|
||||
@@ -25,5 +25,4 @@ serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
strum.workspace = true
|
||||
thiserror.workspace = true
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
mod supported_countries;
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
@@ -10,9 +8,6 @@ use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::{EnumIter, EnumString};
|
||||
use thiserror::Error;
|
||||
use util::ResultExt as _;
|
||||
|
||||
pub use supported_countries::*;
|
||||
|
||||
pub const ANTHROPIC_API_URL: &str = "https://api.anthropic.com";
|
||||
|
||||
@@ -37,9 +32,9 @@ pub enum AnthropicModelMode {
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)]
|
||||
pub enum Model {
|
||||
#[default]
|
||||
#[serde(rename = "claude-3-5-sonnet", alias = "claude-3-5-sonnet-latest")]
|
||||
Claude3_5Sonnet,
|
||||
#[default]
|
||||
#[serde(rename = "claude-3-7-sonnet", alias = "claude-3-7-sonnet-latest")]
|
||||
Claude3_7Sonnet,
|
||||
#[serde(
|
||||
@@ -75,6 +70,10 @@ pub enum Model {
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub fn default_fast() -> Self {
|
||||
Self::Claude3_5Haiku
|
||||
}
|
||||
|
||||
pub fn from_id(id: &str) -> Result<Self> {
|
||||
if id.starts_with("claude-3-5-sonnet") {
|
||||
Ok(Self::Claude3_5Sonnet)
|
||||
@@ -363,11 +362,25 @@ pub struct RateLimitInfo {
|
||||
|
||||
impl RateLimitInfo {
|
||||
fn from_headers(headers: &HeaderMap<HeaderValue>) -> Self {
|
||||
// Check if any rate limit headers exist
|
||||
let has_rate_limit_headers = headers
|
||||
.keys()
|
||||
.any(|k| k.as_str().starts_with("anthropic-ratelimit-"));
|
||||
|
||||
if !has_rate_limit_headers {
|
||||
return Self {
|
||||
requests: None,
|
||||
tokens: None,
|
||||
input_tokens: None,
|
||||
output_tokens: None,
|
||||
};
|
||||
}
|
||||
|
||||
Self {
|
||||
requests: RateLimit::from_headers("requests", headers).log_err(),
|
||||
tokens: RateLimit::from_headers("tokens", headers).log_err(),
|
||||
input_tokens: RateLimit::from_headers("input-tokens", headers).log_err(),
|
||||
output_tokens: RateLimit::from_headers("output-tokens", headers).log_err(),
|
||||
requests: RateLimit::from_headers("requests", headers).ok(),
|
||||
tokens: RateLimit::from_headers("tokens", headers).ok(),
|
||||
input_tokens: RateLimit::from_headers("input-tokens", headers).ok(),
|
||||
output_tokens: RateLimit::from_headers("output-tokens", headers).ok(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -494,6 +507,15 @@ pub enum RequestContent {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
cache_control: Option<CacheControl>,
|
||||
},
|
||||
#[serde(rename = "thinking")]
|
||||
Thinking {
|
||||
thinking: String,
|
||||
signature: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
cache_control: Option<CacheControl>,
|
||||
},
|
||||
#[serde(rename = "redacted_thinking")]
|
||||
RedactedThinking { data: String },
|
||||
#[serde(rename = "image")]
|
||||
Image {
|
||||
source: ImageSource,
|
||||
@@ -724,4 +746,54 @@ impl ApiError {
|
||||
pub fn is_rate_limit_error(&self) -> bool {
|
||||
matches!(self.error_type.as_str(), "rate_limit_error")
|
||||
}
|
||||
|
||||
pub fn match_window_exceeded(&self) -> Option<usize> {
|
||||
let Some(ApiErrorCode::InvalidRequestError) = self.code() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
parse_prompt_too_long(&self.message)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_prompt_too_long(message: &str) -> Option<usize> {
|
||||
message
|
||||
.strip_prefix("prompt is too long: ")?
|
||||
.split_once(" tokens")?
|
||||
.0
|
||||
.parse::<usize>()
|
||||
.ok()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_match_window_exceeded() {
|
||||
let error = ApiError {
|
||||
error_type: "invalid_request_error".to_string(),
|
||||
message: "prompt is too long: 220000 tokens > 200000".to_string(),
|
||||
};
|
||||
assert_eq!(error.match_window_exceeded(), Some(220_000));
|
||||
|
||||
let error = ApiError {
|
||||
error_type: "invalid_request_error".to_string(),
|
||||
message: "prompt is too long: 1234953 tokens".to_string(),
|
||||
};
|
||||
assert_eq!(error.match_window_exceeded(), Some(1234953));
|
||||
|
||||
let error = ApiError {
|
||||
error_type: "invalid_request_error".to_string(),
|
||||
message: "not a prompt length error".to_string(),
|
||||
};
|
||||
assert_eq!(error.match_window_exceeded(), None);
|
||||
|
||||
let error = ApiError {
|
||||
error_type: "rate_limit_error".to_string(),
|
||||
message: "prompt is too long: 12345 tokens".to_string(),
|
||||
};
|
||||
assert_eq!(error.match_window_exceeded(), None);
|
||||
|
||||
let error = ApiError {
|
||||
error_type: "invalid_request_error".to_string(),
|
||||
message: "prompt is too long: invalid tokens".to_string(),
|
||||
};
|
||||
assert_eq!(error.match_window_exceeded(), None);
|
||||
}
|
||||
|
||||
@@ -1,225 +0,0 @@
|
||||
use std::collections::HashSet;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
/// Returns whether the given country code is supported by Anthropic.
|
||||
///
|
||||
/// <https://www.anthropic.com/supported-countries>
|
||||
pub fn is_supported_country(country_code: &str) -> bool {
|
||||
SUPPORTED_COUNTRIES.contains(&country_code)
|
||||
}
|
||||
|
||||
/// The list of country codes supported by Anthropic.
|
||||
///
|
||||
/// https://www.anthropic.com/supported-countries
|
||||
static SUPPORTED_COUNTRIES: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
|
||||
vec![
|
||||
"AL", // Albania
|
||||
"DZ", // Algeria
|
||||
"AS", // American Samoa (US)
|
||||
"AD", // Andorra
|
||||
"AO", // Angola
|
||||
"AI", // Anguilla (UK)
|
||||
"AG", // Antigua and Barbuda
|
||||
"AR", // Argentina
|
||||
"AM", // Armenia
|
||||
"AU", // Australia
|
||||
"AT", // Austria
|
||||
"AZ", // Azerbaijan
|
||||
"BS", // Bahamas
|
||||
"BH", // Bahrain
|
||||
"BD", // Bangladesh
|
||||
"BB", // Barbados
|
||||
"BE", // Belgium
|
||||
"BZ", // Belize
|
||||
"BJ", // Benin
|
||||
"BM", // Bermuda (UK)
|
||||
"BT", // Bhutan
|
||||
"BO", // Bolivia
|
||||
"BA", // Bosnia and Herzegovina
|
||||
"BW", // Botswana
|
||||
"BR", // Brazil
|
||||
"IO", // British Indian Ocean Territory (UK)
|
||||
"BN", // Brunei
|
||||
"BG", // Bulgaria
|
||||
"BF", // Burkina Faso
|
||||
"BI", // Burundi
|
||||
"CV", // Cabo Verde
|
||||
"KH", // Cambodia
|
||||
"CM", // Cameroon
|
||||
"CA", // Canada
|
||||
"KY", // Cayman Islands (UK)
|
||||
"TD", // Chad
|
||||
"CL", // Chile
|
||||
"CX", // Christmas Island (AU)
|
||||
"CC", // Cocos (Keeling) Islands (AU)
|
||||
"CO", // Colombia
|
||||
"KM", // Comoros
|
||||
"CG", // Congo (Brazzaville)
|
||||
"CK", // Cook Islands (NZ)
|
||||
"CR", // Costa Rica
|
||||
"CI", // Côte d'Ivoire
|
||||
"HR", // Croatia
|
||||
"CY", // Cyprus
|
||||
"CZ", // Czechia (Czech Republic)
|
||||
"DK", // Denmark
|
||||
"DJ", // Djibouti
|
||||
"DM", // Dominica
|
||||
"DO", // Dominican Republic
|
||||
"EC", // Ecuador
|
||||
"EG", // Egypt
|
||||
"SV", // El Salvador
|
||||
"GQ", // Equatorial Guinea
|
||||
"EE", // Estonia
|
||||
"SZ", // Eswatini
|
||||
"FK", // Falkland Islands (UK)
|
||||
"FJ", // Fiji
|
||||
"FI", // Finland
|
||||
"FR", // France
|
||||
"GF", // French Guiana (FR)
|
||||
"PF", // French Polynesia (FR)
|
||||
"TF", // French Southern Territories
|
||||
"GA", // Gabon
|
||||
"GM", // Gambia
|
||||
"GE", // Georgia
|
||||
"DE", // Germany
|
||||
"GH", // Ghana
|
||||
"GI", // Gibraltar (UK)
|
||||
"GR", // Greece
|
||||
"GD", // Grenada
|
||||
"GT", // Guatemala
|
||||
"GU", // Guam (US)
|
||||
"GN", // Guinea
|
||||
"GW", // Guinea-Bissau
|
||||
"GY", // Guyana
|
||||
"HT", // Haiti
|
||||
"HM", // Heard Island and McDonald Islands (AU)
|
||||
"HN", // Honduras
|
||||
"HU", // Hungary
|
||||
"IS", // Iceland
|
||||
"IN", // India
|
||||
"ID", // Indonesia
|
||||
"IQ", // Iraq
|
||||
"IE", // Ireland
|
||||
"IL", // Israel
|
||||
"IT", // Italy
|
||||
"JM", // Jamaica
|
||||
"JP", // Japan
|
||||
"JO", // Jordan
|
||||
"KZ", // Kazakhstan
|
||||
"KE", // Kenya
|
||||
"KI", // Kiribati
|
||||
"KW", // Kuwait
|
||||
"KG", // Kyrgyzstan
|
||||
"LA", // Laos
|
||||
"LV", // Latvia
|
||||
"LB", // Lebanon
|
||||
"LS", // Lesotho
|
||||
"LR", // Liberia
|
||||
"LI", // Liechtenstein
|
||||
"LT", // Lithuania
|
||||
"LU", // Luxembourg
|
||||
"MG", // Madagascar
|
||||
"MW", // Malawi
|
||||
"MY", // Malaysia
|
||||
"MV", // Maldives
|
||||
"MT", // Malta
|
||||
"MH", // Marshall Islands
|
||||
"MR", // Mauritania
|
||||
"MU", // Mauritius
|
||||
"MX", // Mexico
|
||||
"FM", // Micronesia
|
||||
"MD", // Moldova
|
||||
"MC", // Monaco
|
||||
"MN", // Mongolia
|
||||
"MS", // Montserrat (UK)
|
||||
"ME", // Montenegro
|
||||
"MA", // Morocco
|
||||
"MZ", // Mozambique
|
||||
"NA", // Namibia
|
||||
"NR", // Nauru
|
||||
"NP", // Nepal
|
||||
"NL", // Netherlands
|
||||
"NZ", // New Zealand
|
||||
"NE", // Niger
|
||||
"NG", // Nigeria
|
||||
"NF", // Norfolk Island (AU)
|
||||
"MK", // North Macedonia
|
||||
"MI", // Northern Mariana Islands (UK)
|
||||
"NO", // Norway
|
||||
"NU", // Niue (NZ)
|
||||
"OM", // Oman
|
||||
"PK", // Pakistan
|
||||
"PW", // Palau
|
||||
"PS", // Palestine
|
||||
"PA", // Panama
|
||||
"PG", // Papua New Guinea
|
||||
"PY", // Paraguay
|
||||
"PE", // Peru
|
||||
"PH", // Philippines
|
||||
"PN", // Pitcairn (UK)
|
||||
"PL", // Poland
|
||||
"PT", // Portugal
|
||||
"PR", // Puerto Rico (US)
|
||||
"QA", // Qatar
|
||||
"RO", // Romania
|
||||
"RW", // Rwanda
|
||||
"BL", // Saint Barthélemy (FR)
|
||||
"KN", // Saint Kitts and Nevis
|
||||
"LC", // Saint Lucia
|
||||
"MF", // Saint Martin (FR)
|
||||
"PM", // Saint Pierre and Miquelon (FR)
|
||||
"VC", // Saint Vincent and the Grenadines
|
||||
"WS", // Samoa
|
||||
"SM", // San Marino
|
||||
"ST", // São Tomé and Príncipe
|
||||
"SA", // Saudi Arabia
|
||||
"SN", // Senegal
|
||||
"RS", // Serbia
|
||||
"SC", // Seychelles
|
||||
"SH", // Saint Helena, Ascension and Tristan da Cunha (UK)
|
||||
"SL", // Sierra Leone
|
||||
"SG", // Singapore
|
||||
"SK", // Slovakia
|
||||
"SI", // Slovenia
|
||||
"SB", // Solomon Islands
|
||||
"ZA", // South Africa
|
||||
"KR", // South Korea
|
||||
"ES", // Spain
|
||||
"LK", // Sri Lanka
|
||||
"SR", // Suriname
|
||||
"SE", // Sweden
|
||||
"CH", // Switzerland
|
||||
"TW", // Taiwan
|
||||
"TJ", // Tajikistan
|
||||
"TZ", // Tanzania
|
||||
"TH", // Thailand
|
||||
"TL", // Timor-Leste
|
||||
"TG", // Togo
|
||||
"TK", // Tokelau (NZ)
|
||||
"TO", // Tonga
|
||||
"TT", // Trinidad and Tobago
|
||||
"TN", // Tunisia
|
||||
"TR", // Türkiye (Turkey)
|
||||
"TM", // Turkmenistan
|
||||
"TC", // Turks and Caicos Islands (UK)
|
||||
"TV", // Tuvalu
|
||||
"UG", // Uganda
|
||||
"UA", // Ukraine (except Crimea, Donetsk, and Luhansk regions)
|
||||
"AE", // United Arab Emirates
|
||||
"GB", // United Kingdom
|
||||
"UM", // United States Minor Outlying Islands (US)
|
||||
"US", // United States of America
|
||||
"UY", // Uruguay
|
||||
"UZ", // Uzbekistan
|
||||
"VU", // Vanuatu
|
||||
"VA", // Vatican City
|
||||
"VN", // Vietnam
|
||||
"VI", // Virgin Islands (US)
|
||||
"VG", // Virgin Islands (UK)
|
||||
"WF", // Wallis and Futuna (FR)
|
||||
"ZM", // Zambia
|
||||
"ZW", // Zimbabwe
|
||||
]
|
||||
.into_iter()
|
||||
.collect()
|
||||
});
|
||||
@@ -15,8 +15,8 @@ path = "src/askpass.rs"
|
||||
anyhow.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
shlex.workspace = true
|
||||
smol.workspace = true
|
||||
tempfile.workspace = true
|
||||
util.workspace = true
|
||||
which.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
@@ -72,6 +72,7 @@ impl AskPassSession {
|
||||
let (askpass_opened_tx, askpass_opened_rx) = oneshot::channel::<()>();
|
||||
let listener =
|
||||
UnixListener::bind(&askpass_socket).context("failed to create askpass socket")?;
|
||||
let zed_path = get_shell_safe_zed_path()?;
|
||||
|
||||
let (askpass_kill_master_tx, askpass_kill_master_rx) = oneshot::channel::<()>();
|
||||
let mut kill_tx = Some(askpass_kill_master_tx);
|
||||
@@ -110,21 +111,10 @@ impl AskPassSession {
|
||||
drop(temp_dir)
|
||||
});
|
||||
|
||||
anyhow::ensure!(
|
||||
which::which("nc").is_ok(),
|
||||
"Cannot find `nc` command (netcat), which is required to connect over SSH."
|
||||
);
|
||||
|
||||
// Create an askpass script that communicates back to this process.
|
||||
let askpass_script = format!(
|
||||
"{shebang}\n{print_args} | {nc} -U {askpass_socket} 2> /dev/null \n",
|
||||
// on macOS `brew install netcat` provides the GNU netcat implementation
|
||||
// which does not support -U.
|
||||
nc = if cfg!(target_os = "macos") {
|
||||
"/usr/bin/nc"
|
||||
} else {
|
||||
"nc"
|
||||
},
|
||||
"{shebang}\n{print_args} | {zed_exe} --askpass={askpass_socket} 2> /dev/null \n",
|
||||
zed_exe = zed_path,
|
||||
askpass_socket = askpass_socket.display(),
|
||||
print_args = "printf '%s\\0' \"$@\"",
|
||||
shebang = "#!/bin/sh",
|
||||
@@ -170,6 +160,77 @@ impl AskPassSession {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn get_shell_safe_zed_path() -> anyhow::Result<String> {
|
||||
let zed_path = std::env::current_exe()
|
||||
.context("Failed to figure out current executable path for use in askpass")?
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
// sanity check on unix systems that the path exists and is executable
|
||||
// todo(windows): implement this check for windows (or just use `is-executable` crate)
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
let metadata = std::fs::metadata(&zed_path)
|
||||
.context("Failed to check metadata of Zed executable path for use in askpass")?;
|
||||
let is_executable = metadata.is_file() && metadata.mode() & 0o111 != 0;
|
||||
anyhow::ensure!(
|
||||
is_executable,
|
||||
"Failed to verify Zed executable path for use in askpass"
|
||||
);
|
||||
// As of writing, this can only be fail if the path contains a null byte, which shouldn't be possible
|
||||
// but shlex has annotated the error as #[non_exhaustive] so we can't make it a compile error if other
|
||||
// errors are introduced in the future :(
|
||||
let zed_path_escaped = shlex::try_quote(&zed_path)
|
||||
.context("Failed to shell-escape Zed executable path for use in askpass")?;
|
||||
|
||||
return Ok(zed_path_escaped.to_string());
|
||||
}
|
||||
|
||||
/// The main function for when Zed is running in netcat mode for use in askpass.
|
||||
/// Called from both the remote server binary and the zed binary in their respective main functions.
|
||||
#[cfg(unix)]
|
||||
pub fn main(socket: &str) {
|
||||
use std::io::{self, Read, Write};
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::process::exit;
|
||||
|
||||
let mut stream = match UnixStream::connect(socket) {
|
||||
Ok(stream) => stream,
|
||||
Err(err) => {
|
||||
eprintln!("Error connecting to socket {}: {}", socket, err);
|
||||
exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
if let Err(err) = io::stdin().read_to_end(&mut buffer) {
|
||||
eprintln!("Error reading from stdin: {}", err);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if buffer.last() != Some(&b'\0') {
|
||||
buffer.push(b'\0');
|
||||
}
|
||||
|
||||
if let Err(err) = stream.write_all(&buffer) {
|
||||
eprintln!("Error writing to socket: {}", err);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
let mut response = Vec::new();
|
||||
if let Err(err) = stream.read_to_end(&mut response) {
|
||||
eprintln!("Error reading from socket: {}", err);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if let Err(err) = io::stdout().write_all(&response) {
|
||||
eprintln!("Error writing to stdout: {}", err);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
pub fn main(_socket: &str) {}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
pub struct AskPassSession {
|
||||
path: PathBuf,
|
||||
|
||||
@@ -49,7 +49,7 @@ menu.workspace = true
|
||||
multi_buffer.workspace = true
|
||||
parking_lot.workspace = true
|
||||
project.workspace = true
|
||||
prompt_library.workspace = true
|
||||
rules_library.workspace = true
|
||||
prompt_store.workspace = true
|
||||
proto.workspace = true
|
||||
rope.workspace = true
|
||||
|
||||
@@ -8,7 +8,7 @@ mod terminal_inline_assistant;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_settings::AssistantSettings;
|
||||
use assistant_settings::{AssistantSettings, LanguageModelSelection};
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use client::Client;
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
@@ -101,7 +101,7 @@ pub fn init(
|
||||
SlashCommandSettings::register(cx);
|
||||
|
||||
assistant_context_editor::init(client.clone(), cx);
|
||||
prompt_library::init(cx);
|
||||
rules_library::init(cx);
|
||||
init_language_model_settings(cx);
|
||||
assistant_slash_command::init(cx);
|
||||
assistant_tool::init(cx);
|
||||
@@ -161,71 +161,38 @@ fn init_language_model_settings(cx: &mut App) {
|
||||
|
||||
fn update_active_language_model_from_settings(cx: &mut App) {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
// Default model - used as fallback
|
||||
let active_model_provider_name =
|
||||
LanguageModelProviderId::from(settings.default_model.provider.clone());
|
||||
let active_model_id = LanguageModelId::from(settings.default_model.model.clone());
|
||||
|
||||
// Inline assistant model
|
||||
let inline_assistant_model = settings
|
||||
fn to_selected_model(selection: &LanguageModelSelection) -> language_model::SelectedModel {
|
||||
language_model::SelectedModel {
|
||||
provider: LanguageModelProviderId::from(selection.provider.clone()),
|
||||
model: LanguageModelId::from(selection.model.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
let default = to_selected_model(&settings.default_model);
|
||||
let inline_assistant = settings
|
||||
.inline_assistant_model
|
||||
.as_ref()
|
||||
.unwrap_or(&settings.default_model);
|
||||
let inline_assistant_provider_name =
|
||||
LanguageModelProviderId::from(inline_assistant_model.provider.clone());
|
||||
let inline_assistant_model_id = LanguageModelId::from(inline_assistant_model.model.clone());
|
||||
|
||||
// Commit message model
|
||||
let commit_message_model = settings
|
||||
.map(to_selected_model);
|
||||
let commit_message = settings
|
||||
.commit_message_model
|
||||
.as_ref()
|
||||
.unwrap_or(&settings.default_model);
|
||||
let commit_message_provider_name =
|
||||
LanguageModelProviderId::from(commit_message_model.provider.clone());
|
||||
let commit_message_model_id = LanguageModelId::from(commit_message_model.model.clone());
|
||||
|
||||
// Thread summary model
|
||||
let thread_summary_model = settings
|
||||
.map(to_selected_model);
|
||||
let thread_summary = settings
|
||||
.thread_summary_model
|
||||
.as_ref()
|
||||
.unwrap_or(&settings.default_model);
|
||||
let thread_summary_provider_name =
|
||||
LanguageModelProviderId::from(thread_summary_model.provider.clone());
|
||||
let thread_summary_model_id = LanguageModelId::from(thread_summary_model.model.clone());
|
||||
|
||||
.map(to_selected_model);
|
||||
let inline_alternatives = settings
|
||||
.inline_alternatives
|
||||
.iter()
|
||||
.map(|alternative| {
|
||||
(
|
||||
LanguageModelProviderId::from(alternative.provider.clone()),
|
||||
LanguageModelId::from(alternative.model.clone()),
|
||||
)
|
||||
})
|
||||
.map(to_selected_model)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
|
||||
// Set the default model
|
||||
registry.select_default_model(&active_model_provider_name, &active_model_id, cx);
|
||||
|
||||
// Set the specific models
|
||||
registry.select_inline_assistant_model(
|
||||
&inline_assistant_provider_name,
|
||||
&inline_assistant_model_id,
|
||||
cx,
|
||||
);
|
||||
registry.select_commit_message_model(
|
||||
&commit_message_provider_name,
|
||||
&commit_message_model_id,
|
||||
cx,
|
||||
);
|
||||
registry.select_thread_summary_model(
|
||||
&thread_summary_provider_name,
|
||||
&thread_summary_model_id,
|
||||
cx,
|
||||
);
|
||||
|
||||
// Set the alternatives
|
||||
registry.select_default_model(Some(&default), cx);
|
||||
registry.select_inline_assistant_model(inline_assistant.as_ref(), cx);
|
||||
registry.select_commit_message_model(commit_message.as_ref(), cx);
|
||||
registry.select_thread_summary_model(thread_summary.as_ref(), cx);
|
||||
registry.select_inline_alternative_models(inline_alternatives, cx);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -193,7 +193,7 @@ impl Focusable for ConfigurationView {
|
||||
impl Item for ConfigurationView {
|
||||
type Event = ConfigurationViewEvent;
|
||||
|
||||
fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option<SharedString> {
|
||||
Some("Configuration".into())
|
||||
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
|
||||
"Configuration".into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ use assistant_context_editor::{
|
||||
use assistant_settings::{AssistantDockPosition, AssistantSettings};
|
||||
use assistant_slash_command::SlashCommandWorkingSet;
|
||||
use client::{Client, Status, proto};
|
||||
use editor::{Editor, EditorEvent};
|
||||
use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
Action, App, AsyncWindowContext, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable,
|
||||
@@ -23,14 +23,17 @@ use gpui::{
|
||||
use language::LanguageRegistry;
|
||||
use language_model::{
|
||||
AuthenticateError, ConfiguredModel, LanguageModelProviderId, LanguageModelRegistry,
|
||||
ZED_CLOUD_PROVIDER_ID,
|
||||
};
|
||||
use project::Project;
|
||||
use prompt_library::{PromptLibrary, open_prompt_library};
|
||||
use prompt_store::PromptBuilder;
|
||||
use prompt_store::{PromptBuilder, UserPromptId};
|
||||
use rules_library::{RulesLibrary, open_rules_library};
|
||||
|
||||
use search::{BufferSearchBar, buffer_search::DivRegistrar};
|
||||
use settings::{Settings, update_settings_file};
|
||||
use smol::stream::StreamExt;
|
||||
|
||||
use std::ops::Range;
|
||||
use std::path::Path;
|
||||
use std::{ops::ControlFlow, path::PathBuf, sync::Arc};
|
||||
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
|
||||
use ui::{ContextMenu, PopoverMenu, Tooltip, prelude::*};
|
||||
@@ -41,7 +44,7 @@ use workspace::{
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
pane,
|
||||
};
|
||||
use zed_actions::assistant::{InlineAssist, OpenPromptLibrary, ShowConfiguration, ToggleFocus};
|
||||
use zed_actions::assistant::{InlineAssist, OpenRulesLibrary, ShowConfiguration, ToggleFocus};
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
workspace::FollowableViewRegistry::register::<ContextEditor>(cx);
|
||||
@@ -55,11 +58,11 @@ pub fn init(cx: &mut App) {
|
||||
.register_action(AssistantPanel::show_configuration)
|
||||
.register_action(AssistantPanel::create_new_context)
|
||||
.register_action(AssistantPanel::restart_context_servers)
|
||||
.register_action(|workspace, _: &OpenPromptLibrary, window, cx| {
|
||||
.register_action(|workspace, action: &OpenRulesLibrary, window, cx| {
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
workspace.focus_panel::<AssistantPanel>(window, cx);
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.deploy_prompt_library(&OpenPromptLibrary, window, cx)
|
||||
panel.deploy_rules_library(action, window, cx)
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -269,7 +272,10 @@ impl AssistantPanel {
|
||||
menu.context(focus_handle.clone())
|
||||
.action("New Chat", Box::new(NewChat))
|
||||
.action("History", Box::new(DeployHistory))
|
||||
.action("Prompt Library", Box::new(OpenPromptLibrary))
|
||||
.action(
|
||||
"Rules Library",
|
||||
Box::new(OpenRulesLibrary::default()),
|
||||
)
|
||||
.action("Configure", Box::new(ShowConfiguration))
|
||||
.action(zoom_label, Box::new(ToggleZoom))
|
||||
}))
|
||||
@@ -471,7 +477,7 @@ impl AssistantPanel {
|
||||
{
|
||||
return;
|
||||
}
|
||||
context.custom_summary(new_summary, cx)
|
||||
context.set_custom_summary(new_summary, cx)
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -483,8 +489,8 @@ impl AssistantPanel {
|
||||
|
||||
// If we're signed out and don't have a provider configured, or we're signed-out AND Zed.dev is
|
||||
// the provider, we want to show a nudge to sign in.
|
||||
let show_zed_ai_notice = client_status.is_signed_out()
|
||||
&& model.map_or(true, |model| model.provider.id().0 == ZED_CLOUD_PROVIDER_ID);
|
||||
let show_zed_ai_notice =
|
||||
client_status.is_signed_out() && model.map_or(true, |model| model.is_provided_by_zed());
|
||||
|
||||
self.show_zed_ai_notice = show_zed_ai_notice;
|
||||
cx.notify();
|
||||
@@ -1038,13 +1044,13 @@ impl AssistantPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn deploy_prompt_library(
|
||||
fn deploy_rules_library(
|
||||
&mut self,
|
||||
_: &OpenPromptLibrary,
|
||||
action: &OpenRulesLibrary,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
open_prompt_library(
|
||||
open_rules_library(
|
||||
self.languages.clone(),
|
||||
Box::new(PromptLibraryInlineAssist),
|
||||
Arc::new(|| {
|
||||
@@ -1054,6 +1060,9 @@ impl AssistantPanel {
|
||||
None,
|
||||
))
|
||||
}),
|
||||
action
|
||||
.prompt_to_select
|
||||
.map(|uuid| UserPromptId(uuid).into()),
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx);
|
||||
@@ -1072,7 +1081,7 @@ impl AssistantPanel {
|
||||
|
||||
pub fn open_saved_context(
|
||||
&mut self,
|
||||
path: PathBuf,
|
||||
path: Arc<Path>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
@@ -1227,7 +1236,7 @@ impl Render for AssistantPanel {
|
||||
this.show_configuration_tab(window, cx)
|
||||
}))
|
||||
.on_action(cx.listener(AssistantPanel::deploy_history))
|
||||
.on_action(cx.listener(AssistantPanel::deploy_prompt_library))
|
||||
.on_action(cx.listener(AssistantPanel::deploy_rules_library))
|
||||
.child(registrar.size_full().child(self.pane.clone()))
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -1342,13 +1351,13 @@ impl Focusable for AssistantPanel {
|
||||
|
||||
struct PromptLibraryInlineAssist;
|
||||
|
||||
impl prompt_library::InlineAssistDelegate for PromptLibraryInlineAssist {
|
||||
impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
|
||||
fn assist(
|
||||
&self,
|
||||
prompt_editor: &Entity<Editor>,
|
||||
initial_prompt: Option<String>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<PromptLibrary>,
|
||||
cx: &mut Context<RulesLibrary>,
|
||||
) {
|
||||
InlineAssistant::update_global(cx, |assistant, cx| {
|
||||
assistant.assist(&prompt_editor, None, None, initial_prompt, window, cx)
|
||||
@@ -1383,7 +1392,7 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
|
||||
fn open_saved_context(
|
||||
&self,
|
||||
workspace: &mut Workspace,
|
||||
path: PathBuf,
|
||||
path: Arc<Path>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) -> Task<Result<()>> {
|
||||
@@ -1413,7 +1422,8 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
|
||||
fn quote_selection(
|
||||
&self,
|
||||
workspace: &mut Workspace,
|
||||
creases: Vec<(String, String)>,
|
||||
selection_ranges: Vec<Range<Anchor>>,
|
||||
buffer: Entity<MultiBuffer>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
@@ -1425,6 +1435,12 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
|
||||
workspace.toggle_panel_focus::<AssistantPanel>(window, cx);
|
||||
}
|
||||
|
||||
let snapshot = buffer.read(cx).snapshot(cx);
|
||||
let selection_ranges = selection_ranges
|
||||
.into_iter()
|
||||
.map(|range| range.to_point(&snapshot))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
panel.update(cx, |_, cx| {
|
||||
// Wait to create a new context until the workspace is no longer
|
||||
// being updated.
|
||||
@@ -1433,7 +1449,9 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
|
||||
.active_context_editor(cx)
|
||||
.or_else(|| panel.new_context(window, cx))
|
||||
{
|
||||
context.update(cx, |context, cx| context.quote_creases(creases, window, cx));
|
||||
context.update(cx, |context, cx| {
|
||||
context.quote_ranges(selection_ranges, snapshot, window, cx)
|
||||
});
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,11 +18,11 @@ use editor::{
|
||||
},
|
||||
};
|
||||
use feature_flags::{
|
||||
Assistant2FeatureFlag, FeatureFlagAppExt as _, FeatureFlagViewExt as _, ZedPro,
|
||||
Assistant2FeatureFlag, FeatureFlagAppExt as _, FeatureFlagViewExt as _, ZedProFeatureFlag,
|
||||
};
|
||||
use fs::Fs;
|
||||
use futures::{
|
||||
SinkExt, Stream, StreamExt,
|
||||
SinkExt, Stream, StreamExt, TryStreamExt as _,
|
||||
channel::mpsc,
|
||||
future::{BoxFuture, LocalBoxFuture},
|
||||
join,
|
||||
@@ -1226,7 +1226,7 @@ impl InlineAssistant {
|
||||
editor.highlight_rows::<InlineAssist>(
|
||||
row_range,
|
||||
cx.theme().status().info_background,
|
||||
false,
|
||||
Default::default(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
@@ -1291,7 +1291,7 @@ impl InlineAssistant {
|
||||
editor.highlight_rows::<DeletedLines>(
|
||||
Anchor::min()..Anchor::max(),
|
||||
cx.theme().status().deleted_background,
|
||||
false,
|
||||
Default::default(),
|
||||
cx,
|
||||
);
|
||||
editor
|
||||
@@ -1652,7 +1652,7 @@ impl Render for PromptEditor {
|
||||
|
||||
let error_message = SharedString::from(error.to_string());
|
||||
if error.error_code() == proto::ErrorCode::RateLimitExceeded
|
||||
&& cx.has_flag::<ZedPro>()
|
||||
&& cx.has_flag::<ZedProFeatureFlag>()
|
||||
{
|
||||
el.child(
|
||||
v_flex()
|
||||
@@ -1759,6 +1759,7 @@ impl PromptEditor {
|
||||
language_model_selector: cx.new(|cx| {
|
||||
let fs = fs.clone();
|
||||
LanguageModelSelector::new(
|
||||
|cx| LanguageModelRegistry::read_global(cx).default_model(),
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
@@ -1965,7 +1966,7 @@ impl PromptEditor {
|
||||
.update(cx, |editor, _| editor.set_read_only(false));
|
||||
}
|
||||
CodegenStatus::Error(error) => {
|
||||
if cx.has_flag::<ZedPro>()
|
||||
if cx.has_flag::<ZedProFeatureFlag>()
|
||||
&& error.error_code() == proto::ErrorCode::RateLimitExceeded
|
||||
&& !dismissed_rate_limit_notice()
|
||||
{
|
||||
@@ -2978,6 +2979,9 @@ impl CodegenAlternative {
|
||||
});
|
||||
|
||||
Ok(LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
mode: None,
|
||||
messages,
|
||||
tools: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
@@ -3020,7 +3024,7 @@ impl CodegenAlternative {
|
||||
}
|
||||
}
|
||||
|
||||
let http_client = cx.http_client().clone();
|
||||
let http_client = cx.http_client();
|
||||
let telemetry = self.telemetry.clone();
|
||||
let language_name = {
|
||||
let multibuffer = self.buffer.read(cx);
|
||||
@@ -3053,7 +3057,8 @@ impl CodegenAlternative {
|
||||
let mut response_latency = None;
|
||||
let request_start = Instant::now();
|
||||
let diff = async {
|
||||
let chunks = StripInvalidSpans::new(stream?.stream);
|
||||
let chunks =
|
||||
StripInvalidSpans::new(stream?.stream.map_err(|e| e.into()));
|
||||
futures::pin_mut!(chunks);
|
||||
let mut diff = StreamingDiff::new(selected_text.to_string());
|
||||
let mut line_diff = LineDiff::default();
|
||||
|
||||
@@ -44,4 +44,6 @@ impl Settings for SlashCommandSettings {
|
||||
.chain(sources.server),
|
||||
)
|
||||
}
|
||||
|
||||
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
|
||||
}
|
||||
|
||||
@@ -292,6 +292,9 @@ impl TerminalInlineAssistant {
|
||||
});
|
||||
|
||||
Ok(LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
mode: None,
|
||||
messages,
|
||||
tools: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
@@ -746,6 +749,7 @@ impl PromptEditor {
|
||||
language_model_selector: cx.new(|cx| {
|
||||
let fs = fs.clone();
|
||||
LanguageModelSelector::new(
|
||||
|cx| LanguageModelRegistry::read_global(cx).default_model(),
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
|
||||
@@ -35,7 +35,7 @@ use std::{
|
||||
fmt::Debug,
|
||||
iter, mem,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
path::Path,
|
||||
str::FromStr as _,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
@@ -46,7 +46,7 @@ use ui::IconName;
|
||||
use util::{ResultExt, TryFutureExt, post_inc};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
pub struct ContextId(String);
|
||||
|
||||
impl ContextId {
|
||||
@@ -459,6 +459,7 @@ pub enum ContextEvent {
|
||||
ShowMaxMonthlySpendReachedError,
|
||||
MessagesEdited,
|
||||
SummaryChanged,
|
||||
SummaryGenerated,
|
||||
StreamedCompletion,
|
||||
StartedThoughtProcess(Range<language::Anchor>),
|
||||
EndedThoughtProcess(language::Anchor),
|
||||
@@ -482,7 +483,7 @@ pub enum ContextEvent {
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct ContextSummary {
|
||||
pub text: String,
|
||||
done: bool,
|
||||
pub done: bool,
|
||||
timestamp: clock::Lamport,
|
||||
}
|
||||
|
||||
@@ -640,14 +641,14 @@ pub struct AssistantContext {
|
||||
contents: Vec<Content>,
|
||||
messages_metadata: HashMap<MessageId, MessageMetadata>,
|
||||
summary: Option<ContextSummary>,
|
||||
pending_summary: Task<Option<()>>,
|
||||
summary_task: Task<Option<()>>,
|
||||
completion_count: usize,
|
||||
pending_completions: Vec<PendingCompletion>,
|
||||
token_count: Option<usize>,
|
||||
pending_token_count: Task<Option<()>>,
|
||||
pending_save: Task<Result<()>>,
|
||||
pending_cache_warming_task: Task<Option<()>>,
|
||||
path: Option<PathBuf>,
|
||||
path: Option<Arc<Path>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
@@ -741,7 +742,7 @@ impl AssistantContext {
|
||||
thought_process_output_sections: Vec::new(),
|
||||
edits_since_last_parse: edits_since_last_slash_command_parse,
|
||||
summary: None,
|
||||
pending_summary: Task::ready(None),
|
||||
summary_task: Task::ready(None),
|
||||
completion_count: Default::default(),
|
||||
pending_completions: Default::default(),
|
||||
token_count: None,
|
||||
@@ -838,7 +839,7 @@ impl AssistantContext {
|
||||
|
||||
pub fn deserialize(
|
||||
saved_context: SavedContext,
|
||||
path: PathBuf,
|
||||
path: Arc<Path>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
slash_commands: Arc<SlashCommandWorkingSet>,
|
||||
@@ -951,7 +952,7 @@ impl AssistantContext {
|
||||
|
||||
fn flush_ops(&mut self, cx: &mut Context<AssistantContext>) {
|
||||
let mut changed_messages = HashSet::default();
|
||||
let mut summary_changed = false;
|
||||
let mut summary_generated = false;
|
||||
|
||||
self.pending_ops.sort_unstable_by_key(|op| op.timestamp());
|
||||
for op in mem::take(&mut self.pending_ops) {
|
||||
@@ -993,7 +994,7 @@ impl AssistantContext {
|
||||
.map_or(true, |summary| new_summary.timestamp > summary.timestamp)
|
||||
{
|
||||
self.summary = Some(new_summary);
|
||||
summary_changed = true;
|
||||
summary_generated = true;
|
||||
}
|
||||
}
|
||||
ContextOperation::SlashCommandStarted {
|
||||
@@ -1072,8 +1073,9 @@ impl AssistantContext {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
if summary_changed {
|
||||
if summary_generated {
|
||||
cx.emit(ContextEvent::SummaryChanged);
|
||||
cx.emit(ContextEvent::SummaryGenerated);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
@@ -1145,8 +1147,8 @@ impl AssistantContext {
|
||||
self.prompt_builder.clone()
|
||||
}
|
||||
|
||||
pub fn path(&self) -> Option<&Path> {
|
||||
self.path.as_deref()
|
||||
pub fn path(&self) -> Option<&Arc<Path>> {
|
||||
self.path.as_ref()
|
||||
}
|
||||
|
||||
pub fn summary(&self) -> Option<&ContextSummary> {
|
||||
@@ -2373,7 +2375,7 @@ impl AssistantContext {
|
||||
LanguageModelCompletionEvent::Stop(reason) => {
|
||||
stop_reason = reason;
|
||||
}
|
||||
LanguageModelCompletionEvent::Thinking(chunk) => {
|
||||
LanguageModelCompletionEvent::Thinking { text: chunk, .. } => {
|
||||
if thought_process_stack.is_empty() {
|
||||
let start =
|
||||
buffer.anchor_before(message_old_end_offset);
|
||||
@@ -2555,6 +2557,9 @@ impl AssistantContext {
|
||||
}
|
||||
|
||||
let mut completion_request = LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
mode: None,
|
||||
messages: Vec::new(),
|
||||
tools: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
@@ -2609,7 +2614,9 @@ impl AssistantContext {
|
||||
.map(MessageContent::Text),
|
||||
);
|
||||
|
||||
completion_request.messages.push(request_message);
|
||||
if !request_message.contents_empty() {
|
||||
completion_request.messages.push(request_message);
|
||||
}
|
||||
}
|
||||
|
||||
if let RequestType::SuggestEdits = request_type {
|
||||
@@ -2943,7 +2950,7 @@ impl AssistantContext {
|
||||
self.message_anchors.insert(insertion_ix, new_anchor);
|
||||
}
|
||||
|
||||
pub fn summarize(&mut self, replace_old: bool, cx: &mut Context<Self>) {
|
||||
pub fn summarize(&mut self, mut replace_old: bool, cx: &mut Context<Self>) {
|
||||
let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
|
||||
return;
|
||||
};
|
||||
@@ -2963,7 +2970,18 @@ impl AssistantContext {
|
||||
cache: false,
|
||||
});
|
||||
|
||||
self.pending_summary = cx.spawn(async move |this, cx| {
|
||||
// If there is no summary, it is set with `done: false` so that "Loading Summary…" can
|
||||
// be displayed.
|
||||
if self.summary.is_none() {
|
||||
self.summary = Some(ContextSummary {
|
||||
text: "".to_string(),
|
||||
done: false,
|
||||
timestamp: clock::Lamport::default(),
|
||||
});
|
||||
replace_old = true;
|
||||
}
|
||||
|
||||
self.summary_task = cx.spawn(async move |this, cx| {
|
||||
async move {
|
||||
let stream = model.model.stream_completion_text(request, &cx);
|
||||
let mut messages = stream.await?;
|
||||
@@ -2988,6 +3006,7 @@ impl AssistantContext {
|
||||
};
|
||||
this.push_op(operation, cx);
|
||||
cx.emit(ContextEvent::SummaryChanged);
|
||||
cx.emit(ContextEvent::SummaryGenerated);
|
||||
})?;
|
||||
|
||||
// Stop if the LLM generated multiple lines.
|
||||
@@ -3008,6 +3027,7 @@ impl AssistantContext {
|
||||
};
|
||||
this.push_op(operation, cx);
|
||||
cx.emit(ContextEvent::SummaryChanged);
|
||||
cx.emit(ContextEvent::SummaryGenerated);
|
||||
}
|
||||
})?;
|
||||
|
||||
@@ -3161,7 +3181,7 @@ impl AssistantContext {
|
||||
fs.atomic_write(new_path.clone(), serde_json::to_string(&context).unwrap())
|
||||
.await?;
|
||||
if let Some(old_path) = old_path {
|
||||
if new_path != old_path {
|
||||
if new_path.as_path() != old_path.as_ref() {
|
||||
fs.remove_file(
|
||||
&old_path,
|
||||
RemoveOptions {
|
||||
@@ -3173,14 +3193,14 @@ impl AssistantContext {
|
||||
}
|
||||
}
|
||||
|
||||
this.update(cx, |this, _| this.path = Some(new_path))?;
|
||||
this.update(cx, |this, _| this.path = Some(new_path.into()))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
pub fn custom_summary(&mut self, custom_summary: String, cx: &mut Context<Self>) {
|
||||
pub fn set_custom_summary(&mut self, custom_summary: String, cx: &mut Context<Self>) {
|
||||
let timestamp = self.next_timestamp();
|
||||
let summary = self.summary.get_or_insert(ContextSummary::default());
|
||||
summary.timestamp = timestamp;
|
||||
@@ -3188,6 +3208,15 @@ impl AssistantContext {
|
||||
summary.text = custom_summary;
|
||||
cx.emit(ContextEvent::SummaryChanged);
|
||||
}
|
||||
|
||||
pub const DEFAULT_SUMMARY: SharedString = SharedString::new_static("New Text Thread");
|
||||
|
||||
pub fn summary_or_default(&self) -> SharedString {
|
||||
self.summary
|
||||
.as_ref()
|
||||
.map(|summary| summary.text.clone().into())
|
||||
.unwrap_or(Self::DEFAULT_SUMMARY)
|
||||
}
|
||||
}
|
||||
|
||||
fn trimmed_text_in_range(buffer: &BufferSnapshot, range: Range<text::Anchor>) -> String {
|
||||
@@ -3560,6 +3589,6 @@ impl SavedContextV0_1_0 {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SavedContextMetadata {
|
||||
pub title: String,
|
||||
pub path: PathBuf,
|
||||
pub path: Arc<Path>,
|
||||
pub mtime: chrono::DateTime<chrono::Local>,
|
||||
}
|
||||
|
||||
@@ -959,7 +959,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
let deserialized_context = cx.new(|cx| {
|
||||
AssistantContext::deserialize(
|
||||
serialized_context,
|
||||
Default::default(),
|
||||
Path::new("").into(),
|
||||
registry.clone(),
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
@@ -1120,7 +1120,7 @@ async fn test_serialization(cx: &mut TestAppContext) {
|
||||
let deserialized_context = cx.new(|cx| {
|
||||
AssistantContext::deserialize(
|
||||
serialized_context,
|
||||
Default::default(),
|
||||
Path::new("").into(),
|
||||
registry.clone(),
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
|
||||
@@ -8,9 +8,9 @@ use assistant_slash_commands::{
|
||||
use client::{proto, zed_urls};
|
||||
use collections::{BTreeSet, HashMap, HashSet, hash_map};
|
||||
use editor::{
|
||||
Anchor, Editor, EditorEvent, MenuInlineCompletionsPolicy, ProposedChangeLocation,
|
||||
ProposedChangesEditor, RowExt, ToOffset as _, ToPoint,
|
||||
actions::{FoldAt, MoveToEndOfLine, Newline, ShowCompletions, UnfoldAt},
|
||||
Anchor, Editor, EditorEvent, MenuInlineCompletionsPolicy, MultiBuffer, MultiBufferSnapshot,
|
||||
ProposedChangeLocation, ProposedChangesEditor, RowExt, ToOffset as _, ToPoint,
|
||||
actions::{MoveToEndOfLine, Newline, ShowCompletions},
|
||||
display_map::{
|
||||
BlockContext, BlockId, BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata,
|
||||
CustomBlockId, FoldId, RenderBlock, ToDisplayPoint,
|
||||
@@ -48,7 +48,14 @@ use project::{Project, Worktree};
|
||||
use rope::Point;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsStore, update_settings_file};
|
||||
use std::{any::TypeId, borrow::Cow, cmp, ops::Range, path::PathBuf, sync::Arc, time::Duration};
|
||||
use std::{
|
||||
any::TypeId,
|
||||
cmp,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use text::SelectionGoal;
|
||||
use ui::{
|
||||
ButtonLike, Disclosure, ElevationIndex, KeyBinding, PopoverMenuHandle, TintColor, Tooltip,
|
||||
@@ -139,7 +146,7 @@ pub trait AssistantPanelDelegate {
|
||||
fn open_saved_context(
|
||||
&self,
|
||||
workspace: &mut Workspace,
|
||||
path: PathBuf,
|
||||
path: Arc<Path>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) -> Task<Result<()>>;
|
||||
@@ -155,7 +162,8 @@ pub trait AssistantPanelDelegate {
|
||||
fn quote_selection(
|
||||
&self,
|
||||
workspace: &mut Workspace,
|
||||
creases: Vec<(String, String)>,
|
||||
selection_ranges: Vec<Range<Anchor>>,
|
||||
buffer: Entity<MultiBuffer>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
);
|
||||
@@ -290,6 +298,7 @@ impl ContextEditor {
|
||||
dragged_file_worktrees: Vec::new(),
|
||||
language_model_selector: cx.new(|cx| {
|
||||
LanguageModelSelector::new(
|
||||
|cx| LanguageModelRegistry::read_global(cx).default_model(),
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
@@ -616,6 +625,7 @@ impl ContextEditor {
|
||||
context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
|
||||
});
|
||||
}
|
||||
ContextEvent::SummaryGenerated => {}
|
||||
ContextEvent::StartedThoughtProcess(range) => {
|
||||
let creases = self.insert_thought_process_output_sections(
|
||||
[(
|
||||
@@ -1053,7 +1063,7 @@ impl ContextEditor {
|
||||
let creases = editor.insert_creases(creases, cx);
|
||||
|
||||
for buffer_row in buffer_rows_to_fold.into_iter().rev() {
|
||||
editor.fold_at(&FoldAt { buffer_row }, window, cx);
|
||||
editor.fold_at(buffer_row, window, cx);
|
||||
}
|
||||
|
||||
creases
|
||||
@@ -1109,7 +1119,7 @@ impl ContextEditor {
|
||||
buffer_rows_to_fold.clear();
|
||||
}
|
||||
for buffer_row in buffer_rows_to_fold.into_iter().rev() {
|
||||
editor.fold_at(&FoldAt { buffer_row }, window, cx);
|
||||
editor.fold_at(buffer_row, window, cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1800,23 +1810,45 @@ impl ContextEditor {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(creases) = selections_creases(workspace, cx) else {
|
||||
let Some((selections, buffer)) = maybe!({
|
||||
let editor = workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.act_as::<Editor>(cx))?;
|
||||
|
||||
let buffer = editor.read(cx).buffer().clone();
|
||||
let snapshot = buffer.read(cx).snapshot(cx);
|
||||
let selections = editor.update(cx, |editor, cx| {
|
||||
editor
|
||||
.selections
|
||||
.all_adjusted(cx)
|
||||
.into_iter()
|
||||
.filter_map(|s| {
|
||||
(!s.is_empty())
|
||||
.then(|| snapshot.anchor_after(s.start)..snapshot.anchor_before(s.end))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
Some((selections, buffer))
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if creases.is_empty() {
|
||||
if selections.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
assistant_panel_delegate.quote_selection(workspace, creases, window, cx);
|
||||
assistant_panel_delegate.quote_selection(workspace, selections, buffer, window, cx);
|
||||
}
|
||||
|
||||
pub fn quote_creases(
|
||||
pub fn quote_ranges(
|
||||
&mut self,
|
||||
creases: Vec<(String, String)>,
|
||||
ranges: Vec<Range<Point>>,
|
||||
snapshot: MultiBufferSnapshot,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let creases = selections_creases(ranges, snapshot, cx);
|
||||
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.insert("\n", window, cx);
|
||||
for (text, crease_title) in creases {
|
||||
@@ -1844,13 +1876,7 @@ impl ContextEditor {
|
||||
|_, _, _, _| Empty.into_any(),
|
||||
);
|
||||
editor.insert_creases(vec![crease], cx);
|
||||
editor.fold_at(
|
||||
&FoldAt {
|
||||
buffer_row: start_row,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
editor.fold_at(start_row, window, cx);
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -2042,7 +2068,7 @@ impl ContextEditor {
|
||||
cx,
|
||||
);
|
||||
for buffer_row in buffer_rows_to_fold.into_iter().rev() {
|
||||
editor.fold_at(&FoldAt { buffer_row }, window, cx);
|
||||
editor.fold_at(buffer_row, window, cx);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -2071,7 +2097,7 @@ impl ContextEditor {
|
||||
continue;
|
||||
};
|
||||
let image_id = image.id();
|
||||
let image_task = LanguageModelImage::from_image(image, cx).shared();
|
||||
let image_task = LanguageModelImage::from_image(Arc::new(image), cx).shared();
|
||||
|
||||
for image_position in image_positions.iter() {
|
||||
context.insert_content(
|
||||
@@ -2161,13 +2187,8 @@ impl ContextEditor {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn title(&self, cx: &App) -> Cow<str> {
|
||||
self.context
|
||||
.read(cx)
|
||||
.summary()
|
||||
.map(|summary| summary.text.clone())
|
||||
.map(Cow::Owned)
|
||||
.unwrap_or_else(|| Cow::Borrowed(DEFAULT_TAB_TITLE))
|
||||
pub fn title(&self, cx: &App) -> SharedString {
|
||||
self.context.read(cx).summary_or_default()
|
||||
}
|
||||
|
||||
fn render_patch_block(
|
||||
@@ -2793,7 +2814,7 @@ fn render_thought_process_fold_icon_button(
|
||||
let button = match status {
|
||||
ThoughtProcessStatus::Pending => button
|
||||
.child(
|
||||
Icon::new(IconName::Brain)
|
||||
Icon::new(IconName::LightBulb)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
@@ -2808,7 +2829,7 @@ fn render_thought_process_fold_icon_button(
|
||||
),
|
||||
ThoughtProcessStatus::Completed => button
|
||||
.style(ButtonStyle::Filled)
|
||||
.child(Icon::new(IconName::Brain).size(IconSize::Small))
|
||||
.child(Icon::new(IconName::LightBulb).size(IconSize::Small))
|
||||
.child(Label::new("Thought Process").single_line()),
|
||||
};
|
||||
|
||||
@@ -2820,7 +2841,7 @@ fn render_thought_process_fold_icon_button(
|
||||
.start
|
||||
.to_point(&editor.buffer().read(cx).read(cx));
|
||||
let buffer_row = MultiBufferRow(buffer_start.row);
|
||||
editor.unfold_at(&UnfoldAt { buffer_row }, window, cx);
|
||||
editor.unfold_at(buffer_row, window, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
@@ -2847,7 +2868,7 @@ fn render_fold_icon_button(
|
||||
.start
|
||||
.to_point(&editor.buffer().read(cx).read(cx));
|
||||
let buffer_row = MultiBufferRow(buffer_start.row);
|
||||
editor.unfold_at(&UnfoldAt { buffer_row }, window, cx);
|
||||
editor.unfold_at(buffer_row, window, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
@@ -2907,7 +2928,7 @@ fn quote_selection_fold_placeholder(title: String, editor: WeakEntity<Editor>) -
|
||||
.start
|
||||
.to_point(&editor.buffer().read(cx).read(cx));
|
||||
let buffer_row = MultiBufferRow(buffer_start.row);
|
||||
editor.unfold_at(&UnfoldAt { buffer_row }, window, cx);
|
||||
editor.unfold_at(buffer_row, window, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
@@ -3142,8 +3163,8 @@ impl Focusable for ContextEditor {
|
||||
impl Item for ContextEditor {
|
||||
type Event = editor::EditorEvent;
|
||||
|
||||
fn tab_content_text(&self, _window: &Window, cx: &App) -> Option<SharedString> {
|
||||
Some(util::truncate_and_trailoff(&self.title(cx), MAX_TAB_TITLE_LEN).into())
|
||||
fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString {
|
||||
util::truncate_and_trailoff(&self.title(cx), MAX_TAB_TITLE_LEN).into()
|
||||
}
|
||||
|
||||
fn to_item_events(event: &Self::Event, mut f: impl FnMut(item::ItemEvent)) {
|
||||
@@ -3750,7 +3771,7 @@ pub fn make_lsp_adapter_delegate(
|
||||
let Some(worktree) = project.worktrees(cx).next() else {
|
||||
return Ok(None::<Arc<dyn LspAdapterDelegate>>);
|
||||
};
|
||||
let http_client = project.client().http_client().clone();
|
||||
let http_client = project.client().http_client();
|
||||
project.lsp_store().update(cx, |_, cx| {
|
||||
Ok(Some(LocalLspAdapterDelegate::new(
|
||||
project.languages().clone(),
|
||||
|
||||
@@ -108,8 +108,8 @@ impl EventEmitter<()> for ContextHistory {}
|
||||
impl Item for ContextHistory {
|
||||
type Event = ();
|
||||
|
||||
fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option<SharedString> {
|
||||
Some("History".into())
|
||||
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
|
||||
"History".into()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||