Compare commits
588
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89ca7986bf | ||
|
|
fa97475ee8 | ||
|
|
0eaeb4588e | ||
|
|
1413d77b1f | ||
|
|
624dd94b5d | ||
|
|
d86f24b6b3 | ||
|
|
03de51bd3c | ||
|
|
8f9742d988 | ||
|
|
f6e7aefa72 | ||
|
|
e269788a8f | ||
|
|
66780195dc | ||
|
|
ad2087094d | ||
|
|
5bdf1c4b96 | ||
|
|
135f8ffb2a | ||
|
|
bf5a01edd9 | ||
|
|
81ca2df6ad | ||
|
|
aea68c386a | ||
|
|
8eea53a41e | ||
|
|
3befd0c6c5 | ||
|
|
8577eb8ec9 | ||
|
|
c856f875a1 | ||
|
|
264dd213f9 | ||
|
|
125727d09c | ||
|
|
8c7b35ad05 | ||
|
|
e2a33f75e1 | ||
|
|
006d673ed2 | ||
|
|
6b4d617df0 | ||
|
|
e3471526f4 | ||
|
|
6b30e0b752 | ||
|
|
6413e91947 | ||
|
|
9106d315e4 | ||
|
|
fbabce1125 | ||
|
|
8f56ed5b85 | ||
|
|
81b5a6a08b | ||
|
|
94cb6390aa | ||
|
|
42bea5d297 | ||
|
|
f252e3234c | ||
|
|
eef3ae3e1f | ||
|
|
fc88dde63f | ||
|
|
4619e9d183 | ||
|
|
4dc363f306 | ||
|
|
2e8082dd21 | ||
|
|
a52fe28246 | ||
|
|
8c5ba8aeb0 | ||
|
|
50330820c0 | ||
|
|
7222fc0ba0 | ||
|
|
17bdb5d56a | ||
|
|
7a463cd193 | ||
|
|
352a54c698 | ||
|
|
93957da2c9 | ||
|
|
edcfd562af | ||
|
|
ef5ec5dc28 | ||
|
|
c426cb0f14 | ||
|
|
dd1862cc2b | ||
|
|
a25b2af05a | ||
|
|
8bfd6fdba2 | ||
|
|
cf7a1b8d80 | ||
|
|
5ba4c0e024 | ||
|
|
567e094e6c | ||
|
|
b523998329 | ||
|
|
7e1247c420 | ||
|
|
783888131e | ||
|
|
213a87234d | ||
|
|
d98bd4bd52 | ||
|
|
22125d1347 | ||
|
|
8c120f2fab | ||
|
|
c6ec2f47ef | ||
|
|
0fd6f365be | ||
|
|
60bdb6e9ba | ||
|
|
6e9cd576ea | ||
|
|
53ec15a56a | ||
|
|
a90b62267f | ||
|
|
24556331c8 | ||
|
|
39145b99e8 | ||
|
|
0afa6e03a8 | ||
|
|
7a3c775dc1 | ||
|
|
3ea58bb790 | ||
|
|
50c705cd2d | ||
|
|
3894c217cc | ||
|
|
66c2bb8f37 | ||
|
|
1bbbd51d48 | ||
|
|
50f3e74d05 | ||
|
|
21475a1dfd | ||
|
|
dce4c05fa9 | ||
|
|
8c56571ef9 | ||
|
|
92a77b72fb | ||
|
|
4f6b929784 | ||
|
|
55119559b3 | ||
|
|
fd5531316f | ||
|
|
fbc41475b4 | ||
|
|
a0673256db | ||
|
|
fc37337a3e | ||
|
|
80220cebe4 | ||
|
|
8bdf6fa359 | ||
|
|
1d11a0adfd | ||
|
|
1e2f664410 | ||
|
|
a3aad9c9bf | ||
|
|
eb2587844b | ||
|
|
d863a9cf4e | ||
|
|
7d5be1556a | ||
|
|
659f15aa9b | ||
|
|
d1f5b9e911 | ||
|
|
284b00ff23 | ||
|
|
2c5760742b | ||
|
|
70c794e913 | ||
|
|
3929f0b5bd | ||
|
|
6f5dfe125a | ||
|
|
27fa9dc843 | ||
|
|
1e03a55acd | ||
|
|
65c9669283 | ||
|
|
18b6257119 | ||
|
|
c607c01fb9 | ||
|
|
4c4e30cd71 | ||
|
|
19ad7ad809 | ||
|
|
87795384de | ||
|
|
0732ab3393 | ||
|
|
2bccfd7462 | ||
|
|
83853cc5e6 | ||
|
|
4a73d51acd | ||
|
|
63cd763418 | ||
|
|
32394b699e | ||
|
|
12262862cd | ||
|
|
56a752092e | ||
|
|
439e7ec1fd | ||
|
|
20cf3fc679 | ||
|
|
949f61075f | ||
|
|
705200e199 | ||
|
|
85fa8abd50 | ||
|
|
3118cab2d8 | ||
|
|
31f893f8cb | ||
|
|
056d0c1197 | ||
|
|
832902c8e3 | ||
|
|
3d6fb29f0c | ||
|
|
9824370f82 | ||
|
|
371e106faa | ||
|
|
19809e7680 | ||
|
|
389afef336 | ||
|
|
274bb948e7 | ||
|
|
d9b4535d64 | ||
|
|
3dc720ff9c | ||
|
|
56b340b5d5 | ||
|
|
ba740eaefd | ||
|
|
39c5da4405 | ||
|
|
83708c295c | ||
|
|
a84bdd7cd7 | ||
|
|
110f6804fb | ||
|
|
e5ec2f9991 | ||
|
|
7bca3fbf18 | ||
|
|
d578f80f00 | ||
|
|
dc53086c1e | ||
|
|
f74c0339cc | ||
|
|
fb94b4f8e8 | ||
|
|
8ad4768ecd | ||
|
|
24fd8c166d | ||
|
|
a7c5d5ac4c | ||
|
|
5be1202eea | ||
|
|
173c16581d | ||
|
|
373b2270e7 | ||
|
|
05355a6b5c | ||
|
|
7ff51183ce | ||
|
|
bda0cbdec7 | ||
|
|
acc53d9f61 | ||
|
|
30f0d3b394 | ||
|
|
03f3029dc6 | ||
|
|
aed7bb8c09 | ||
|
|
dd2d232a9d | ||
|
|
993ac55e39 | ||
|
|
93a11ddedf | ||
|
|
94feb811ca | ||
|
|
b0ceec9b19 | ||
|
|
40b111d92c | ||
|
|
520110e864 | ||
|
|
d4a68b0f4e | ||
|
|
019cfd4a52 | ||
|
|
687210a55d | ||
|
|
b12eab782f | ||
|
|
99ea1351ce | ||
|
|
d40dffb854 | ||
|
|
0cd52f830c | ||
|
|
62f38087b8 | ||
|
|
79879b43ce | ||
|
|
a598ecac1f | ||
|
|
76bcd22802 | ||
|
|
e06fcfdc43 | ||
|
|
246430cb8f | ||
|
|
f689fc7f75 | ||
|
|
9b2fd57e6e | ||
|
|
0d365fa613 | ||
|
|
601e631624 | ||
|
|
2fef02f487 | ||
|
|
4cc9104942 | ||
|
|
0eaa6b5fc8 | ||
|
|
e563cff034 | ||
|
|
3b2550106b | ||
|
|
3ec6bff038 | ||
|
|
aab2a6df3b | ||
|
|
84171018f2 | ||
|
|
280d7e6f91 | ||
|
|
9ac54adbb2 | ||
|
|
6490fb0148 | ||
|
|
43811b62d2 | ||
|
|
de0f4ef80b | ||
|
|
9a7f54f21a | ||
|
|
27c8a08144 | ||
|
|
80c1c59ed3 | ||
|
|
7c6b8d7a8a | ||
|
|
4187a5fe7f | ||
|
|
d5c86b03ba | ||
|
|
bc25efdf72 | ||
|
|
c639200ede | ||
|
|
d5036cf01f | ||
|
|
d1ebe0767c | ||
|
|
19b1222cd8 | ||
|
|
7631060a82 | ||
|
|
85d0ed5989 | ||
|
|
3408c10057 | ||
|
|
ecaeb9e602 | ||
|
|
e772fc6e23 | ||
|
|
4b7abc0a2c | ||
|
|
805207e096 | ||
|
|
0e1f543646 | ||
|
|
fb331f6cb8 | ||
|
|
6bdd3528ac | ||
|
|
4efbfcd087 | ||
|
|
9401029b1d | ||
|
|
04f216902b | ||
|
|
8ad5262a87 | ||
|
|
79a706c664 | ||
|
|
515ef8e554 | ||
|
|
fedf9feba8 | ||
|
|
b5b93aea42 | ||
|
|
4abf8049c9 | ||
|
|
fbc08709d1 | ||
|
|
576a681a4f | ||
|
|
95d2d4d3a7 | ||
|
|
a486b74b14 | ||
|
|
7249b87bf6 | ||
|
|
c42d2602b4 | ||
|
|
89064c34c5 | ||
|
|
fde0b39b7c | ||
|
|
e9a3cfc083 | ||
|
|
e767801db2 | ||
|
|
898778daa9 | ||
|
|
13381580af | ||
|
|
def907ae4b | ||
|
|
71930621fd | ||
|
|
288a491651 | ||
|
|
24ed2d3a1d | ||
|
|
a25cd2da72 | ||
|
|
918795d868 | ||
|
|
84c5df19c7 | ||
|
|
e5b355e458 | ||
|
|
89f0a447f6 | ||
|
|
fcc927ee76 | ||
|
|
f256a65b59 | ||
|
|
6a5c1a74f1 | ||
|
|
6324d1c351 | ||
|
|
95ad6758af | ||
|
|
24cd84cda5 | ||
|
|
8069197329 | ||
|
|
3f7ca0494b | ||
|
|
18749c1f4e | ||
|
|
9497cfdf45 | ||
|
|
22353f0169 | ||
|
|
449c5b44b7 | ||
|
|
24dbc46548 | ||
|
|
83156e5153 | ||
|
|
53298145a2 | ||
|
|
2c58dd6203 | ||
|
|
a4bc883595 | ||
|
|
c07077f96c | ||
|
|
e31c27c26b | ||
|
|
12e8ef3178 | ||
|
|
b7ad8e459c | ||
|
|
5a1bf3a968 | ||
|
|
812597bb8b | ||
|
|
0ec5f6608b | ||
|
|
0e73869580 | ||
|
|
732a3dab8c | ||
|
|
400bc7973a | ||
|
|
ac88c6b637 | ||
|
|
d4fcc1b863 | ||
|
|
6c0dce6711 | ||
|
|
4afec6731d | ||
|
|
5d92219812 | ||
|
|
80a5c3d7ed | ||
|
|
981b80d40b | ||
|
|
3c5e1a98fc | ||
|
|
5ae4463b63 | ||
|
|
e0e32ed3a8 | ||
|
|
9b20679a61 | ||
|
|
26e1901bd0 | ||
|
|
9f00b8c8dc | ||
|
|
c35bd39829 | ||
|
|
266de27a0b | ||
|
|
0f1fdeceda | ||
|
|
8bf97ef9e5 | ||
|
|
683d234d80 | ||
|
|
229cdafcc4 | ||
|
|
154d0ebf53 | ||
|
|
2e9a63fe4f | ||
|
|
579902ace6 | ||
|
|
3da2f2f33b | ||
|
|
9ff423bebf | ||
|
|
1824db13cf | ||
|
|
36637b3be0 | ||
|
|
a45841396f | ||
|
|
102d8e72bb | ||
|
|
09a0e921ce | ||
|
|
28c8182bd5 | ||
|
|
bccd568993 | ||
|
|
fd8c2fb0cd | ||
|
|
d160cca179 | ||
|
|
b551195818 | ||
|
|
d7c2d5db3b | ||
|
|
1dd88aeae6 | ||
|
|
2875405514 | ||
|
|
8c0300c021 | ||
|
|
26b786dd3f | ||
|
|
afce869d3b | ||
|
|
b738d88ec4 | ||
|
|
83646e0366 | ||
|
|
c40ce47e92 | ||
|
|
b1c44c7e5c | ||
|
|
081f065942 | ||
|
|
8ddef975b7 | ||
|
|
2f78705f6e | ||
|
|
a0bc656215 | ||
|
|
47f00d23b3 | ||
|
|
40ebc34909 | ||
|
|
e08705f4ef | ||
|
|
7c748ef089 | ||
|
|
fba5a79c45 | ||
|
|
dbde377ab0 | ||
|
|
9adcf524e2 | ||
|
|
531b1941a0 | ||
|
|
a1c46e05ee | ||
|
|
1fe1457cfa | ||
|
|
aedd85d885 | ||
|
|
5b3d94ebaa | ||
|
|
1a6a3f4b54 | ||
|
|
3116cfc167 | ||
|
|
05529f66d7 | ||
|
|
ef09dddaa5 | ||
|
|
bf7af99a3f | ||
|
|
7555742bf0 | ||
|
|
fa20bc296b | ||
|
|
195731f347 | ||
|
|
72de9fe7a6 | ||
|
|
bec1148192 | ||
|
|
8c8d888140 | ||
|
|
d3a247bfff | ||
|
|
9ef319f25f | ||
|
|
64e2bf8bf0 | ||
|
|
556adad67b | ||
|
|
843bbc973a | ||
|
|
173804c097 | ||
|
|
4086a9ae8e | ||
|
|
2614342f97 | ||
|
|
4387f9fb9a | ||
|
|
31e2feb347 | ||
|
|
2896b8a863 | ||
|
|
0d38e69038 | ||
|
|
9679e0c59c | ||
|
|
41bc4ec7f0 | ||
|
|
912098928a | ||
|
|
222bddc41a | ||
|
|
9436cb575b | ||
|
|
d1686661c0 | ||
|
|
305007aa0c | ||
|
|
a2c28fc8d7 | ||
|
|
ce87121067 | ||
|
|
ecd7854853 | ||
|
|
57b8c62909 | ||
|
|
28dc5de6a8 | ||
|
|
c875a1fc90 | ||
|
|
1721c6efdf | ||
|
|
93592702c3 | ||
|
|
61d3f788b8 | ||
|
|
a3b281b2f3 | ||
|
|
c8622df762 | ||
|
|
c277ee8cbf | ||
|
|
a2face30f4 | ||
|
|
a219615fe5 | ||
|
|
af06175b1f | ||
|
|
2e8d8de58b | ||
|
|
310de8b1ea | ||
|
|
891875402c | ||
|
|
154cbf6996 | ||
|
|
64bafce665 | ||
|
|
5588453cbe | ||
|
|
5aaf8f8247 | ||
|
|
8c1f1f13dc | ||
|
|
b942e0b4dc | ||
|
|
f282613746 | ||
|
|
7c440ae82c | ||
|
|
b7bd561eaa | ||
|
|
6daa962aaa | ||
|
|
93a07e5a2a | ||
|
|
93e060272a | ||
|
|
acac05f22e | ||
|
|
b5a4671c64 | ||
|
|
a68fedd4a6 | ||
|
|
015cd404e4 | ||
|
|
9921809565 | ||
|
|
137336f373 | ||
|
|
d940d17918 | ||
|
|
0a5d5bc524 | ||
|
|
a30696f9bf | ||
|
|
25bdd77b1d | ||
|
|
2f12e8ee92 | ||
|
|
95211a8854 | ||
|
|
6b5cf936a2 | ||
|
|
17e62b050f | ||
|
|
ee84eb44ee | ||
|
|
82dd4b6908 | ||
|
|
185858749b | ||
|
|
39a504773c | ||
|
|
b7b734f51f | ||
|
|
dcff5b6596 | ||
|
|
4f7da2b757 | ||
|
|
60e616ec81 | ||
|
|
416964acd0 | ||
|
|
017ad2c7f7 | ||
|
|
e33eb1b058 | ||
|
|
857b8a4b56 | ||
|
|
3975329629 | ||
|
|
54e14c1a17 | ||
|
|
3741516fe3 | ||
|
|
76381f33d5 | ||
|
|
95d0d476e3 | ||
|
|
7508839b70 | ||
|
|
e88cbefabe | ||
|
|
a38bae684f | ||
|
|
08671e3155 | ||
|
|
0d557721cf | ||
|
|
e709808b32 | ||
|
|
0d22068c90 | ||
|
|
d116c227e0 | ||
|
|
3f07dffbb0 | ||
|
|
801e4a8a9d | ||
|
|
3adeed8f97 | ||
|
|
1275c71a63 | ||
|
|
ca8c23dd71 | ||
|
|
acc2bf5db9 | ||
|
|
96fbc30945 | ||
|
|
ba545ba9b3 | ||
|
|
188cc24bfc | ||
|
|
5e3162b7f4 | ||
|
|
f9aa209131 | ||
|
|
f86f654cda | ||
|
|
aadd2e13d7 | ||
|
|
531357b40c | ||
|
|
aa6b552c39 | ||
|
|
a3f1918489 | ||
|
|
a9fca05d8b | ||
|
|
824165eb79 | ||
|
|
562c9d76d9 | ||
|
|
c002ca03ba | ||
|
|
befb5d54fb | ||
|
|
69f5f657f2 | ||
|
|
0405b425f5 | ||
|
|
70cf609ce9 | ||
|
|
2f76b49df3 | ||
|
|
dfd5f38408 | ||
|
|
3b93e8d95c | ||
|
|
23631a9393 | ||
|
|
f1e0c31b8f | ||
|
|
30a25e4edc | ||
|
|
ea1aba4192 | ||
|
|
b9aad20be6 | ||
|
|
965f32ad63 | ||
|
|
cf828fff85 | ||
|
|
cf8b033be1 | ||
|
|
1bd5dc5382 | ||
|
|
06d63ca54c | ||
|
|
423778c93a | ||
|
|
8de9e47a5b | ||
|
|
d63ed3bbe3 | ||
|
|
4369d79636 | ||
|
|
3408f1a6ae | ||
|
|
34c58af796 | ||
|
|
37979ea44f | ||
|
|
50b5168c16 | ||
|
|
6b17645f2e | ||
|
|
52006c2fd9 | ||
|
|
26197ec95b | ||
|
|
43bb389e35 | ||
|
|
985090ef3c | ||
|
|
52eb8a7a8c | ||
|
|
1cabeb00d0 | ||
|
|
9564c1d6be | ||
|
|
1832eeffc9 | ||
|
|
e6d8315e29 | ||
|
|
784a17f7b3 | ||
|
|
04aef44fc3 | ||
|
|
c02dd067b2 | ||
|
|
141fdef588 | ||
|
|
3982c7d99a | ||
|
|
76745d0594 | ||
|
|
4850ecc419 | ||
|
|
43354eeabd | ||
|
|
7a9290dc9b | ||
|
|
cfbe9d329f | ||
|
|
f02499fa44 | ||
|
|
bd9d7b3221 | ||
|
|
c69474846f | ||
|
|
0dc80df6fd | ||
|
|
8e985e0a75 | ||
|
|
a4d31b6f95 | ||
|
|
c5dc075a88 | ||
|
|
eade8ee07b | ||
|
|
8fbba8de73 | ||
|
|
826664b559 | ||
|
|
d1f884033f | ||
|
|
0f3630d936 | ||
|
|
83d0e48e38 | ||
|
|
6c9b2c37a5 | ||
|
|
3ab41d548f | ||
|
|
d3d783e23d | ||
|
|
7aad2ee9ae | ||
|
|
f390ac251d | ||
|
|
7837bbc639 | ||
|
|
372dcc033c | ||
|
|
4158d7cda8 | ||
|
|
425abe2fbf | ||
|
|
744fb6aed0 | ||
|
|
e9f8e6aeec | ||
|
|
5dee3328d4 | ||
|
|
2f63152af3 | ||
|
|
c9891fe071 | ||
|
|
d35956fd92 | ||
|
|
7417e6eb38 | ||
|
|
5db089070a | ||
|
|
612b656d36 | ||
|
|
cb6ec0a564 | ||
|
|
12b8c42387 | ||
|
|
fa75d922ed | ||
|
|
e445dc0746 | ||
|
|
e84d441b82 | ||
|
|
377bf7ff21 | ||
|
|
b39c1f158f | ||
|
|
f23d8d343b | ||
|
|
91f2ac3cb2 | ||
|
|
ec720145fa | ||
|
|
f6948d0ffa | ||
|
|
d52ee41b3a | ||
|
|
ca5e85d6ea | ||
|
|
01cec84789 | ||
|
|
e62a15d421 | ||
|
|
d29dfe31e4 | ||
|
|
f15755684f | ||
|
|
16145af480 | ||
|
|
eace76e525 | ||
|
|
cc1d3732bc | ||
|
|
1798af72b0 | ||
|
|
2c82e6c6ae | ||
|
|
3577d829c2 | ||
|
|
29d02d643b | ||
|
|
23c803707d | ||
|
|
b51005ec4a | ||
|
|
dfbe553626 | ||
|
|
2af1ca7290 | ||
|
|
3e67104257 | ||
|
|
d1d7447493 | ||
|
|
c3faeae9d0 | ||
|
|
94baf1f721 | ||
|
|
9b8b9e28e2 | ||
|
|
2a56a1d6ef | ||
|
|
9e45313b0a | ||
|
|
d4c90b2dfb | ||
|
|
5b784871f0 | ||
|
|
e5f677dfb5 | ||
|
|
6a96810249 | ||
|
|
8b7fe7c09f | ||
|
|
0961632a9c | ||
|
|
abbf60080d | ||
|
|
33252a65b4 | ||
|
|
e70d984320 | ||
|
|
da7c874808 | ||
|
|
a19ef17bcb | ||
|
|
121d6a72c0 | ||
|
|
35f64b80fa | ||
|
|
feca42b025 | ||
|
|
53f118c57a | ||
|
|
786ae0a584 | ||
|
|
f73f88fb56 | ||
|
|
ac254fb442 |
@@ -1,4 +1,5 @@
|
|||||||
# web + desktop packages
|
# web + desktop packages
|
||||||
packages/app/ @adamdotdevin
|
packages/app/ @adamdotdevin
|
||||||
packages/tauri/ @adamdotdevin
|
packages/tauri/ @adamdotdevin
|
||||||
|
packages/desktop/src-tauri/ @brendonovich
|
||||||
packages/desktop/ @adamdotdevin
|
packages/desktop/ @adamdotdevin
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
blank_issues_enabled: true
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: 💬 Discord Community
|
- name: 💬 Discord Community
|
||||||
url: https://discord.gg/opencode
|
url: https://discord.gg/opencode
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Vouched contributors for this project.
|
||||||
|
#
|
||||||
|
# See https://github.com/mitchellh/vouch for details.
|
||||||
|
#
|
||||||
|
# Syntax:
|
||||||
|
# - One handle per line (without @), sorted alphabetically.
|
||||||
|
# - Optional platform prefix: platform:username (e.g., github:user).
|
||||||
|
# - Denounce with minus prefix: -username or -platform:username.
|
||||||
|
# - Optional details after a space following the handle.
|
||||||
|
adamdotdevin
|
||||||
|
ariane-emory
|
||||||
|
-florianleibert
|
||||||
|
fwang
|
||||||
|
iamdavidhill
|
||||||
|
jayair
|
||||||
|
kitlangton
|
||||||
|
kommander
|
||||||
|
r44vc0rp
|
||||||
|
rekram1-node
|
||||||
|
-spider-yamet clawdbot/llm psychosis, spam pinging the team
|
||||||
|
thdxr
|
||||||
@@ -6,7 +6,7 @@ runs:
|
|||||||
- name: Mount Bun Cache
|
- name: Mount Bun Cache
|
||||||
uses: useblacksmith/stickydisk@v1
|
uses: useblacksmith/stickydisk@v1
|
||||||
with:
|
with:
|
||||||
key: ${{ github.repository }}-bun-cache
|
key: ${{ github.repository }}-bun-cache-${{ runner.os }}
|
||||||
path: ~/.bun
|
path: ~/.bun
|
||||||
|
|
||||||
- name: Setup Bun
|
- name: Setup Bun
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
### What does this PR do?
|
### What does this PR do?
|
||||||
|
|
||||||
Please provide a description of the issue (if there is one), the changes you made to fix it, and why they work. It is expected that you understand why your changes work and if you do not understand why at least say as much so a maintainer knows how much to value the pr.
|
Please provide a description of the issue (if there is one), the changes you made to fix it, and why they work. It is expected that you understand why your changes work and if you do not understand why at least say as much so a maintainer knows how much to value the PR.
|
||||||
|
|
||||||
**If you paste a large clearly AI generated description here your PR may be IGNORED or CLOSED!**
|
**If you paste a large clearly AI generated description here your PR may be IGNORED or CLOSED!**
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ jobs:
|
|||||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
close-stale-prs:
|
close-stale-prs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
steps:
|
steps:
|
||||||
- name: Close inactive PRs
|
- name: Close inactive PRs
|
||||||
uses: actions/github-script@v8
|
uses: actions/github-script@v8
|
||||||
@@ -25,59 +26,210 @@ jobs:
|
|||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
script: |
|
script: |
|
||||||
const DAYS_INACTIVE = 60
|
const DAYS_INACTIVE = 60
|
||||||
|
const MAX_RETRIES = 3
|
||||||
|
|
||||||
|
// Adaptive delay: fast for small batches, slower for large to respect
|
||||||
|
// GitHub's 80 content-generating requests/minute limit
|
||||||
|
const SMALL_BATCH_THRESHOLD = 10
|
||||||
|
const SMALL_BATCH_DELAY_MS = 1000 // 1s for daily operations (≤10 PRs)
|
||||||
|
const LARGE_BATCH_DELAY_MS = 2000 // 2s for backlog (>10 PRs) = ~30 ops/min, well under 80 limit
|
||||||
|
|
||||||
|
const startTime = Date.now()
|
||||||
const cutoff = new Date(Date.now() - DAYS_INACTIVE * 24 * 60 * 60 * 1000)
|
const cutoff = new Date(Date.now() - DAYS_INACTIVE * 24 * 60 * 60 * 1000)
|
||||||
const { owner, repo } = context.repo
|
const { owner, repo } = context.repo
|
||||||
const dryRun = context.payload.inputs?.dryRun === "true"
|
const dryRun = context.payload.inputs?.dryRun === "true"
|
||||||
const stalePrs = []
|
|
||||||
|
|
||||||
core.info(`Dry run mode: ${dryRun}`)
|
core.info(`Dry run mode: ${dryRun}`)
|
||||||
|
core.info(`Cutoff date: ${cutoff.toISOString()}`)
|
||||||
|
|
||||||
const prs = await github.paginate(github.rest.pulls.list, {
|
function sleep(ms) {
|
||||||
owner,
|
return new Promise(resolve => setTimeout(resolve, ms))
|
||||||
repo,
|
}
|
||||||
state: "open",
|
|
||||||
per_page: 100,
|
|
||||||
sort: "updated",
|
|
||||||
direction: "asc",
|
|
||||||
})
|
|
||||||
|
|
||||||
for (const pr of prs) {
|
async function withRetry(fn, description = 'API call') {
|
||||||
const lastUpdated = new Date(pr.updated_at)
|
let lastError
|
||||||
if (lastUpdated > cutoff) {
|
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
||||||
core.info(`PR ${pr.number} is fresh`)
|
try {
|
||||||
continue
|
const result = await fn()
|
||||||
|
return result
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error
|
||||||
|
const isRateLimited = error.status === 403 &&
|
||||||
|
(error.message?.includes('rate limit') || error.message?.includes('secondary'))
|
||||||
|
|
||||||
|
if (!isRateLimited) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse retry-after header, default to 60 seconds
|
||||||
|
const retryAfter = error.response?.headers?.['retry-after']
|
||||||
|
? parseInt(error.response.headers['retry-after'])
|
||||||
|
: 60
|
||||||
|
|
||||||
|
// Exponential backoff: retryAfter * 2^attempt
|
||||||
|
const backoffMs = retryAfter * 1000 * Math.pow(2, attempt)
|
||||||
|
|
||||||
|
core.warning(`${description}: Rate limited (attempt ${attempt + 1}/${MAX_RETRIES}). Waiting ${backoffMs / 1000}s before retry...`)
|
||||||
|
|
||||||
|
await sleep(backoffMs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
core.error(`${description}: Max retries (${MAX_RETRIES}) exceeded`)
|
||||||
|
throw lastError
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
query($owner: String!, $repo: String!, $cursor: String) {
|
||||||
|
repository(owner: $owner, name: $repo) {
|
||||||
|
pullRequests(first: 100, states: OPEN, after: $cursor) {
|
||||||
|
pageInfo {
|
||||||
|
hasNextPage
|
||||||
|
endCursor
|
||||||
|
}
|
||||||
|
nodes {
|
||||||
|
number
|
||||||
|
title
|
||||||
|
author {
|
||||||
|
login
|
||||||
|
}
|
||||||
|
createdAt
|
||||||
|
commits(last: 1) {
|
||||||
|
nodes {
|
||||||
|
commit {
|
||||||
|
committedDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
comments(last: 1) {
|
||||||
|
nodes {
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reviews(last: 1) {
|
||||||
|
nodes {
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const allPrs = []
|
||||||
|
let cursor = null
|
||||||
|
let hasNextPage = true
|
||||||
|
let pageCount = 0
|
||||||
|
|
||||||
|
while (hasNextPage) {
|
||||||
|
pageCount++
|
||||||
|
core.info(`Fetching page ${pageCount} of open PRs...`)
|
||||||
|
|
||||||
|
const result = await withRetry(
|
||||||
|
() => github.graphql(query, { owner, repo, cursor }),
|
||||||
|
`GraphQL page ${pageCount}`
|
||||||
|
)
|
||||||
|
|
||||||
|
allPrs.push(...result.repository.pullRequests.nodes)
|
||||||
|
hasNextPage = result.repository.pullRequests.pageInfo.hasNextPage
|
||||||
|
cursor = result.repository.pullRequests.pageInfo.endCursor
|
||||||
|
|
||||||
|
core.info(`Page ${pageCount}: fetched ${result.repository.pullRequests.nodes.length} PRs (total: ${allPrs.length})`)
|
||||||
|
|
||||||
|
// Delay between pagination requests (use small batch delay for reads)
|
||||||
|
if (hasNextPage) {
|
||||||
|
await sleep(SMALL_BATCH_DELAY_MS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
core.info(`Found ${allPrs.length} open pull requests`)
|
||||||
|
|
||||||
|
const stalePrs = allPrs.filter((pr) => {
|
||||||
|
const dates = [
|
||||||
|
new Date(pr.createdAt),
|
||||||
|
pr.commits.nodes[0] ? new Date(pr.commits.nodes[0].commit.committedDate) : null,
|
||||||
|
pr.comments.nodes[0] ? new Date(pr.comments.nodes[0].createdAt) : null,
|
||||||
|
pr.reviews.nodes[0] ? new Date(pr.reviews.nodes[0].createdAt) : null,
|
||||||
|
].filter((d) => d !== null)
|
||||||
|
|
||||||
|
const lastActivity = dates.sort((a, b) => b.getTime() - a.getTime())[0]
|
||||||
|
|
||||||
|
if (!lastActivity || lastActivity > cutoff) {
|
||||||
|
core.info(`PR #${pr.number} is fresh (last activity: ${lastActivity?.toISOString() || "unknown"})`)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
stalePrs.push(pr)
|
core.info(`PR #${pr.number} is STALE (last activity: ${lastActivity.toISOString()})`)
|
||||||
}
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
if (!stalePrs.length) {
|
if (!stalePrs.length) {
|
||||||
core.info("No stale pull requests found.")
|
core.info("No stale pull requests found.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
core.info(`Found ${stalePrs.length} stale pull requests`)
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Close stale PRs
|
||||||
|
// ============================================
|
||||||
|
const requestDelayMs = stalePrs.length > SMALL_BATCH_THRESHOLD
|
||||||
|
? LARGE_BATCH_DELAY_MS
|
||||||
|
: SMALL_BATCH_DELAY_MS
|
||||||
|
|
||||||
|
core.info(`Using ${requestDelayMs}ms delay between operations (${stalePrs.length > SMALL_BATCH_THRESHOLD ? 'large' : 'small'} batch mode)`)
|
||||||
|
|
||||||
|
let closedCount = 0
|
||||||
|
let skippedCount = 0
|
||||||
|
|
||||||
for (const pr of stalePrs) {
|
for (const pr of stalePrs) {
|
||||||
const issue_number = pr.number
|
const issue_number = pr.number
|
||||||
const closeComment = `Closing this pull request because it has had no updates for more than ${DAYS_INACTIVE} days. If you plan to continue working on it, feel free to reopen or open a new PR.`
|
const closeComment = `Closing this pull request because it has had no updates for more than ${DAYS_INACTIVE} days. If you plan to continue working on it, feel free to reopen or open a new PR.`
|
||||||
|
|
||||||
if (dryRun) {
|
if (dryRun) {
|
||||||
core.info(`[dry-run] Would close PR #${issue_number} from ${pr.user.login}`)
|
core.info(`[dry-run] Would close PR #${issue_number} from ${pr.author?.login || 'unknown'}: ${pr.title}`)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
await github.rest.issues.createComment({
|
try {
|
||||||
owner,
|
// Add comment
|
||||||
repo,
|
await withRetry(
|
||||||
issue_number,
|
() => github.rest.issues.createComment({
|
||||||
body: closeComment,
|
owner,
|
||||||
})
|
repo,
|
||||||
|
issue_number,
|
||||||
|
body: closeComment,
|
||||||
|
}),
|
||||||
|
`Comment on PR #${issue_number}`
|
||||||
|
)
|
||||||
|
|
||||||
await github.rest.pulls.update({
|
// Close PR
|
||||||
owner,
|
await withRetry(
|
||||||
repo,
|
() => github.rest.pulls.update({
|
||||||
pull_number: issue_number,
|
owner,
|
||||||
state: "closed",
|
repo,
|
||||||
})
|
pull_number: issue_number,
|
||||||
|
state: "closed",
|
||||||
|
}),
|
||||||
|
`Close PR #${issue_number}`
|
||||||
|
)
|
||||||
|
|
||||||
core.info(`Closed PR #${issue_number} from ${pr.user.login}`)
|
closedCount++
|
||||||
|
core.info(`Closed PR #${issue_number} from ${pr.author?.login || 'unknown'}: ${pr.title}`)
|
||||||
|
|
||||||
|
// Delay before processing next PR
|
||||||
|
await sleep(requestDelayMs)
|
||||||
|
} catch (error) {
|
||||||
|
skippedCount++
|
||||||
|
core.error(`Failed to close PR #${issue_number}: ${error.message}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const elapsed = Math.round((Date.now() - startTime) / 1000)
|
||||||
|
core.info(`\n========== Summary ==========`)
|
||||||
|
core.info(`Total open PRs found: ${allPrs.length}`)
|
||||||
|
core.info(`Stale PRs identified: ${stalePrs.length}`)
|
||||||
|
core.info(`PRs closed: ${closedCount}`)
|
||||||
|
core.info(`PRs skipped (errors): ${skippedCount}`)
|
||||||
|
core.info(`Elapsed time: ${elapsed}s`)
|
||||||
|
core.info(`=============================`)
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
name: compliance-close
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
# Run every 30 minutes to check for expired compliance windows
|
||||||
|
- cron: "*/30 * * * *"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
close-non-compliant:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Close non-compliant issues and PRs after 2 hours
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const { data: items } = await github.rest.issues.listForRepo({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
labels: 'needs:compliance',
|
||||||
|
state: 'open',
|
||||||
|
per_page: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (items.length === 0) {
|
||||||
|
core.info('No open issues/PRs with needs:compliance label');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const twoHours = 2 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const isPR = !!item.pull_request;
|
||||||
|
const kind = isPR ? 'PR' : 'issue';
|
||||||
|
|
||||||
|
const { data: comments } = await github.rest.issues.listComments({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: item.number,
|
||||||
|
});
|
||||||
|
|
||||||
|
const complianceComment = comments.find(c => c.body.includes('<!-- issue-compliance -->'));
|
||||||
|
if (!complianceComment) continue;
|
||||||
|
|
||||||
|
const commentAge = now - new Date(complianceComment.created_at).getTime();
|
||||||
|
if (commentAge < twoHours) {
|
||||||
|
core.info(`${kind} #${item.number} still within 2-hour window (${Math.round(commentAge / 60000)}m elapsed)`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeMessage = isPR
|
||||||
|
? 'This pull request has been automatically closed because it was not updated to meet our [contributing guidelines](../blob/dev/CONTRIBUTING.md) within the 2-hour window.\n\nFeel free to open a new pull request that follows our guidelines.'
|
||||||
|
: 'This issue has been automatically closed because it was not updated to meet our [contributing guidelines](../blob/dev/CONTRIBUTING.md) within the 2-hour window.\n\nFeel free to open a new issue that follows our issue templates.';
|
||||||
|
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: item.number,
|
||||||
|
body: closeMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isPR) {
|
||||||
|
await github.rest.pulls.update({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
pull_number: item.number,
|
||||||
|
state: 'closed',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await github.rest.issues.update({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: item.number,
|
||||||
|
state: 'closed',
|
||||||
|
state_reason: 'not_planned',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
core.info(`Closed non-compliant ${kind} #${item.number} after 2-hour window`);
|
||||||
|
}
|
||||||
@@ -48,8 +48,12 @@ jobs:
|
|||||||
TODAY'S DATE: ${TODAY}
|
TODAY'S DATE: ${TODAY}
|
||||||
|
|
||||||
STEP 1: Gather today's issues
|
STEP 1: Gather today's issues
|
||||||
Search for all issues created today (${TODAY}) using:
|
Search for all OPEN issues created today (${TODAY}) using:
|
||||||
gh issue list --repo ${{ github.repository }} --state all --search \"created:${TODAY}\" --json number,title,body,labels,state,comments,createdAt,author --limit 500
|
gh issue list --repo ${{ github.repository }} --state open --search \"created:${TODAY}\" --json number,title,body,labels,state,comments,createdAt,author --limit 500
|
||||||
|
|
||||||
|
IMPORTANT: EXCLUDE all issues authored by Anomaly team members. Filter out issues where the author login matches ANY of these:
|
||||||
|
adamdotdevin, Brendonovich, fwang, Hona, iamdavidhill, jayair, kitlangton, kommander, MrMushrooooom, R44VC0RP, rekram1-node, thdxr
|
||||||
|
This recap is specifically for COMMUNITY (external) issues only.
|
||||||
|
|
||||||
STEP 2: Analyze and categorize
|
STEP 2: Analyze and categorize
|
||||||
For each issue created today, categorize it:
|
For each issue created today, categorize it:
|
||||||
|
|||||||
@@ -47,14 +47,18 @@ jobs:
|
|||||||
TODAY'S DATE: ${TODAY}
|
TODAY'S DATE: ${TODAY}
|
||||||
|
|
||||||
STEP 1: Gather PR data
|
STEP 1: Gather PR data
|
||||||
Run these commands to gather PR information. ONLY include PRs created or updated TODAY (${TODAY}):
|
Run these commands to gather PR information. ONLY include OPEN PRs created or updated TODAY (${TODAY}):
|
||||||
|
|
||||||
# PRs created today
|
# Open PRs created today
|
||||||
gh pr list --repo ${{ github.repository }} --state all --search \"created:${TODAY}\" --json number,title,author,labels,createdAt,updatedAt,reviewDecision,isDraft,additions,deletions --limit 100
|
gh pr list --repo ${{ github.repository }} --state open --search \"created:${TODAY}\" --json number,title,author,labels,createdAt,updatedAt,reviewDecision,isDraft,additions,deletions --limit 100
|
||||||
|
|
||||||
# PRs with activity today (updated today)
|
# Open PRs with activity today (updated today)
|
||||||
gh pr list --repo ${{ github.repository }} --state open --search \"updated:${TODAY}\" --json number,title,author,labels,createdAt,updatedAt,reviewDecision,isDraft,additions,deletions --limit 100
|
gh pr list --repo ${{ github.repository }} --state open --search \"updated:${TODAY}\" --json number,title,author,labels,createdAt,updatedAt,reviewDecision,isDraft,additions,deletions --limit 100
|
||||||
|
|
||||||
|
IMPORTANT: EXCLUDE all PRs authored by Anomaly team members. Filter out PRs where the author login matches ANY of these:
|
||||||
|
adamdotdevin, Brendonovich, fwang, Hona, iamdavidhill, jayair, kitlangton, kommander, MrMushrooooom, R44VC0RP, rekram1-node, thdxr
|
||||||
|
This recap is specifically for COMMUNITY (external) contributions only.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
STEP 2: For high-activity PRs, check comment counts
|
STEP 2: For high-activity PRs, check comment counts
|
||||||
|
|||||||
@@ -21,6 +21,15 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: "24"
|
node-version: "24"
|
||||||
|
|
||||||
|
# Workaround for Pulumi version conflict:
|
||||||
|
# GitHub runners have Pulumi 3.212.0+ pre-installed, which removed the -root flag
|
||||||
|
# from pulumi-language-nodejs (see https://github.com/pulumi/pulumi/pull/21065).
|
||||||
|
# SST 3.17.x uses Pulumi SDK 3.210.0 which still passes -root, causing a conflict.
|
||||||
|
# Removing the system language plugin forces SST to use its bundled compatible version.
|
||||||
|
# TODO: Remove when sst supports Pulumi >3.210.0
|
||||||
|
- name: Fix Pulumi version conflict
|
||||||
|
run: sudo rm -f /usr/local/bin/pulumi-language-nodejs
|
||||||
|
|
||||||
- run: bun sst deploy --stage=${{ github.ref_name }}
|
- run: bun sst deploy --stage=${{ github.ref_name }}
|
||||||
env:
|
env:
|
||||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
name: docs-locale-sync
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
paths:
|
||||||
|
- packages/web/src/content/docs/*.mdx
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sync-locales:
|
||||||
|
if: github.actor != 'opencode-agent[bot]'
|
||||||
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup Bun
|
||||||
|
uses: ./.github/actions/setup-bun
|
||||||
|
|
||||||
|
- name: Setup git committer
|
||||||
|
id: committer
|
||||||
|
uses: ./.github/actions/setup-git-committer
|
||||||
|
with:
|
||||||
|
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
|
||||||
|
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
|
||||||
|
|
||||||
|
- name: Compute changed English docs
|
||||||
|
id: changes
|
||||||
|
run: |
|
||||||
|
FILES=$(git diff --name-only "${{ github.event.before }}" "${{ github.sha }}" -- 'packages/web/src/content/docs/*.mdx' || true)
|
||||||
|
if [ -z "$FILES" ]; then
|
||||||
|
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "No English docs changed in push range"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "has_changes=true" >> "$GITHUB_OUTPUT"
|
||||||
|
{
|
||||||
|
echo "files<<EOF"
|
||||||
|
echo "$FILES"
|
||||||
|
echo "EOF"
|
||||||
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Sync locale docs with OpenCode
|
||||||
|
if: steps.changes.outputs.has_changes == 'true'
|
||||||
|
uses: sst/opencode/github@latest
|
||||||
|
env:
|
||||||
|
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||||
|
with:
|
||||||
|
model: opencode/gpt-5.2
|
||||||
|
agent: docs
|
||||||
|
prompt: |
|
||||||
|
Update localized docs to match the latest English docs changes.
|
||||||
|
|
||||||
|
Changed English doc files:
|
||||||
|
<changed_english_docs>
|
||||||
|
${{ steps.changes.outputs.files }}
|
||||||
|
</changed_english_docs>
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
1. Update all relevant locale docs under packages/web/src/content/docs/<locale>/ so they reflect these English page changes.
|
||||||
|
2. You MUST use the Task tool for translation work and launch subagents with subagent_type `translator` (defined in .opencode/agent/translator.md).
|
||||||
|
3. Do not translate directly in the primary agent. Use translator subagent output as the source for locale text updates.
|
||||||
|
4. Run translator subagent Task calls in parallel whenever file/locale translation work is independent.
|
||||||
|
5. Preserve frontmatter keys, internal links, code blocks, and existing locale-specific metadata unless the English change requires an update.
|
||||||
|
6. Keep locale docs structure aligned with their corresponding English pages.
|
||||||
|
7. Do not modify English source docs in packages/web/src/content/docs/*.mdx.
|
||||||
|
8. If no locale updates are needed, make no changes.
|
||||||
|
|
||||||
|
- name: Commit and push locale docs updates
|
||||||
|
if: steps.changes.outputs.has_changes == 'true'
|
||||||
|
run: |
|
||||||
|
if [ -z "$(git status --porcelain)" ]; then
|
||||||
|
echo "No locale docs changes to commit"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
git add -A
|
||||||
|
git commit -m "docs(i18n): sync locale docs from english changes"
|
||||||
|
git pull --rebase --autostash origin "$GITHUB_REF_NAME"
|
||||||
|
git push origin HEAD:"$GITHUB_REF_NAME"
|
||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
- name: Install opencode
|
- name: Install opencode
|
||||||
run: curl -fsSL https://opencode.ai/install | bash
|
run: curl -fsSL https://opencode.ai/install | bash
|
||||||
|
|
||||||
- name: Check for duplicate issues
|
- name: Check duplicates and compliance
|
||||||
env:
|
env:
|
||||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -34,30 +34,84 @@ jobs:
|
|||||||
"webfetch": "deny"
|
"webfetch": "deny"
|
||||||
}
|
}
|
||||||
run: |
|
run: |
|
||||||
opencode run -m opencode/claude-haiku-4-5 "A new issue has been created:'
|
opencode run -m opencode/claude-haiku-4-5 "A new issue has been created:
|
||||||
|
|
||||||
Issue number:
|
Issue number: ${{ github.event.issue.number }}
|
||||||
${{ github.event.issue.number }}
|
|
||||||
|
|
||||||
Lookup this issue and search through existing issues (excluding #${{ github.event.issue.number }}) in this repository to find any potential duplicates of this new issue.
|
Lookup this issue with gh issue view ${{ github.event.issue.number }}.
|
||||||
|
|
||||||
|
You have TWO tasks. Perform both, then post a SINGLE comment (if needed).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
TASK 1: CONTRIBUTING GUIDELINES COMPLIANCE CHECK
|
||||||
|
|
||||||
|
Check whether the issue follows our contributing guidelines and issue templates.
|
||||||
|
|
||||||
|
This project has three issue templates that every issue MUST use one of:
|
||||||
|
|
||||||
|
1. Bug Report - requires a Description field with real content
|
||||||
|
2. Feature Request - requires a verification checkbox and description, title should start with [FEATURE]:
|
||||||
|
3. Question - requires the Question field with real content
|
||||||
|
|
||||||
|
Additionally check:
|
||||||
|
- No AI-generated walls of text (long, AI-generated descriptions are not acceptable)
|
||||||
|
- The issue has real content, not just template placeholder text left unchanged
|
||||||
|
- Bug reports should include some context about how to reproduce
|
||||||
|
- Feature requests should explain the problem or need
|
||||||
|
- We want to push for having the user provide system description & information
|
||||||
|
|
||||||
|
Do NOT be nitpicky about optional fields. Only flag real problems like: no template used, required fields empty or placeholder text only, obviously AI-generated walls of text, or completely empty/nonsensical content.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
TASK 2: DUPLICATE CHECK
|
||||||
|
|
||||||
|
Search through existing issues (excluding #${{ github.event.issue.number }}) to find potential duplicates.
|
||||||
Consider:
|
Consider:
|
||||||
1. Similar titles or descriptions
|
1. Similar titles or descriptions
|
||||||
2. Same error messages or symptoms
|
2. Same error messages or symptoms
|
||||||
3. Related functionality or components
|
3. Related functionality or components
|
||||||
4. Similar feature requests
|
4. Similar feature requests
|
||||||
|
|
||||||
If you find any potential duplicates, please comment on the new issue with:
|
Additionally, if the issue mentions keybinds, keyboard shortcuts, or key bindings, note the pinned keybinds issue #4997.
|
||||||
- A brief explanation of why it might be a duplicate
|
|
||||||
- Links to the potentially duplicate issues
|
---
|
||||||
- A suggestion to check those issues first
|
|
||||||
|
POSTING YOUR COMMENT:
|
||||||
|
|
||||||
|
Based on your findings, post a SINGLE comment on issue #${{ github.event.issue.number }}. Build the comment as follows:
|
||||||
|
|
||||||
|
If the issue is NOT compliant, start the comment with:
|
||||||
|
<!-- issue-compliance -->
|
||||||
|
Then explain what needs to be fixed and that they have 2 hours to edit the issue before it is automatically closed. Also add the label needs:compliance to the issue using: gh issue edit ${{ github.event.issue.number }} --add-label needs:compliance
|
||||||
|
|
||||||
|
If duplicates were found, include a section about potential duplicates with links.
|
||||||
|
|
||||||
|
If the issue mentions keybinds/keyboard shortcuts, include a note about #4997.
|
||||||
|
|
||||||
|
If the issue IS compliant AND no duplicates were found AND no keybind reference, do NOT comment at all.
|
||||||
|
|
||||||
Use this format for the comment:
|
Use this format for the comment:
|
||||||
'This issue might be a duplicate of existing issues. Please check:
|
|
||||||
|
[If not compliant:]
|
||||||
|
<!-- issue-compliance -->
|
||||||
|
This issue doesn't fully meet our [contributing guidelines](../blob/dev/CONTRIBUTING.md).
|
||||||
|
|
||||||
|
**What needs to be fixed:**
|
||||||
|
- [specific reasons]
|
||||||
|
|
||||||
|
Please edit this issue to address the above within **2 hours**, or it will be automatically closed.
|
||||||
|
|
||||||
|
[If duplicates found, add:]
|
||||||
|
---
|
||||||
|
This issue might be a duplicate of existing issues. Please check:
|
||||||
- #[issue_number]: [brief description of similarity]
|
- #[issue_number]: [brief description of similarity]
|
||||||
|
|
||||||
Feel free to ignore if none of these address your specific case.'
|
[If keybind-related, add:]
|
||||||
|
For keybind-related issues, please also check our pinned keybinds documentation: #4997
|
||||||
|
|
||||||
Additionally, if the issue mentions keybinds, keyboard shortcuts, or key bindings, please add a comment mentioning the pinned keybinds issue #4997:
|
[End with if not compliant:]
|
||||||
'For keybind-related issues, please also check our pinned keybinds documentation: #4997'
|
If you believe this was flagged incorrectly, please let a maintainer know.
|
||||||
|
|
||||||
If no clear duplicates are found, do not comment."
|
Remember: post at most ONE comment combining all findings. If everything is fine, post nothing."
|
||||||
|
|||||||
@@ -6,135 +6,143 @@ permissions:
|
|||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
|
branches: [dev]
|
||||||
paths:
|
paths:
|
||||||
- "bun.lock"
|
- "bun.lock"
|
||||||
- "package.json"
|
- "package.json"
|
||||||
- "packages/*/package.json"
|
- "packages/*/package.json"
|
||||||
- "flake.lock"
|
- "flake.lock"
|
||||||
- ".github/workflows/nix-hashes.yml"
|
- "nix/node_modules.nix"
|
||||||
pull_request:
|
- "nix/scripts/**"
|
||||||
paths:
|
- "patches/**"
|
||||||
- "bun.lock"
|
|
||||||
- "package.json"
|
|
||||||
- "packages/*/package.json"
|
|
||||||
- "flake.lock"
|
|
||||||
- ".github/workflows/nix-hashes.yml"
|
- ".github/workflows/nix-hashes.yml"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
nix-hashes:
|
# Native runners required: bun install cross-compilation flags (--os/--cpu)
|
||||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
# do not produce byte-identical node_modules as native installs.
|
||||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
compute-hash:
|
||||||
env:
|
strategy:
|
||||||
TITLE: node_modules hashes
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- system: x86_64-linux
|
||||||
|
runner: blacksmith-4vcpu-ubuntu-2404
|
||||||
|
- system: aarch64-linux
|
||||||
|
runner: blacksmith-4vcpu-ubuntu-2404-arm
|
||||||
|
- system: x86_64-darwin
|
||||||
|
runner: macos-15-intel
|
||||||
|
- system: aarch64-darwin
|
||||||
|
runner: macos-latest
|
||||||
|
runs-on: ${{ matrix.runner }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Setup Nix
|
||||||
|
uses: nixbuild/nix-quick-install-action@v34
|
||||||
|
|
||||||
|
- name: Compute node_modules hash
|
||||||
|
id: hash
|
||||||
|
env:
|
||||||
|
SYSTEM: ${{ matrix.system }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
BUILD_LOG=$(mktemp)
|
||||||
|
trap 'rm -f "$BUILD_LOG"' EXIT
|
||||||
|
|
||||||
|
# Build with fakeHash to trigger hash mismatch and reveal correct hash
|
||||||
|
nix build ".#packages.${SYSTEM}.node_modules_updater" --no-link 2>&1 | tee "$BUILD_LOG" || true
|
||||||
|
|
||||||
|
# Extract hash from build log with portability
|
||||||
|
HASH="$(grep -oE 'sha256-[A-Za-z0-9+/=]+' "$BUILD_LOG" | tail -n1 || true)"
|
||||||
|
|
||||||
|
if [ -z "$HASH" ]; then
|
||||||
|
echo "::error::Failed to compute hash for ${SYSTEM}"
|
||||||
|
cat "$BUILD_LOG"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$HASH" > hash.txt
|
||||||
|
echo "Computed hash for ${SYSTEM}: $HASH"
|
||||||
|
|
||||||
|
- name: Upload hash
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
name: hash-${{ matrix.system }}
|
||||||
|
path: hash.txt
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
update-hashes:
|
||||||
|
needs: compute-hash
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
ref: ${{ github.head_ref || github.ref_name }}
|
ref: ${{ github.ref_name }}
|
||||||
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
|
|
||||||
|
|
||||||
- name: Setup git committer
|
- name: Setup git committer
|
||||||
id: committer
|
|
||||||
uses: ./.github/actions/setup-git-committer
|
uses: ./.github/actions/setup-git-committer
|
||||||
with:
|
with:
|
||||||
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
|
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
|
||||||
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
|
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
|
||||||
|
|
||||||
- name: Setup Nix
|
|
||||||
uses: nixbuild/nix-quick-install-action@v34
|
|
||||||
|
|
||||||
- name: Pull latest changes
|
- name: Pull latest changes
|
||||||
env:
|
|
||||||
TARGET_BRANCH: ${{ github.head_ref || github.ref_name }}
|
|
||||||
run: |
|
run: |
|
||||||
BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}"
|
git pull --rebase --autostash origin "$GITHUB_REF_NAME"
|
||||||
git pull --rebase --autostash origin "$BRANCH"
|
|
||||||
|
|
||||||
- name: Compute all node_modules hashes
|
- name: Download hash artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: hashes
|
||||||
|
pattern: hash-*
|
||||||
|
|
||||||
|
- name: Update hashes.json
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
HASH_FILE="nix/hashes.json"
|
HASH_FILE="nix/hashes.json"
|
||||||
SYSTEMS="x86_64-linux aarch64-linux x86_64-darwin aarch64-darwin"
|
|
||||||
|
|
||||||
if [ ! -f "$HASH_FILE" ]; then
|
[ -f "$HASH_FILE" ] || echo '{"nodeModules":{}}' > "$HASH_FILE"
|
||||||
mkdir -p "$(dirname "$HASH_FILE")"
|
|
||||||
echo '{"nodeModules":{}}' > "$HASH_FILE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
for SYSTEM in $SYSTEMS; do
|
for SYSTEM in x86_64-linux aarch64-linux x86_64-darwin aarch64-darwin; do
|
||||||
echo "Computing hash for ${SYSTEM}..."
|
FILE="hashes/hash-${SYSTEM}/hash.txt"
|
||||||
BUILD_LOG=$(mktemp)
|
if [ -f "$FILE" ]; then
|
||||||
trap 'rm -f "$BUILD_LOG"' EXIT
|
HASH="$(tr -d '[:space:]' < "$FILE")"
|
||||||
|
echo "${SYSTEM}: ${HASH}"
|
||||||
# The updater derivations use fakeHash, so they will fail and reveal the correct hash
|
jq --arg sys "$SYSTEM" --arg h "$HASH" '.nodeModules[$sys] = $h' "$HASH_FILE" > tmp.json
|
||||||
UPDATER_ATTR=".#packages.x86_64-linux.${SYSTEM}_node_modules"
|
mv tmp.json "$HASH_FILE"
|
||||||
|
else
|
||||||
nix build "$UPDATER_ATTR" --no-link 2>&1 | tee "$BUILD_LOG" || true
|
echo "::warning::Missing hash for ${SYSTEM}"
|
||||||
|
|
||||||
CORRECT_HASH="$(grep -E 'got:\s+sha256-[A-Za-z0-9+/=]+' "$BUILD_LOG" | awk '{print $2}' | head -n1 || true)"
|
|
||||||
|
|
||||||
if [ -z "$CORRECT_HASH" ]; then
|
|
||||||
CORRECT_HASH="$(grep -A2 'hash mismatch' "$BUILD_LOG" | grep 'got:' | awk '{print $2}' | sed 's/sha256:/sha256-/' || true)"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -z "$CORRECT_HASH" ]; then
|
|
||||||
echo "Failed to determine correct node_modules hash for ${SYSTEM}."
|
|
||||||
cat "$BUILD_LOG"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo " ${SYSTEM}: ${CORRECT_HASH}"
|
|
||||||
jq --arg sys "$SYSTEM" --arg h "$CORRECT_HASH" \
|
|
||||||
'.nodeModules[$sys] = $h' "$HASH_FILE" > "${HASH_FILE}.tmp"
|
|
||||||
mv "${HASH_FILE}.tmp" "$HASH_FILE"
|
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "All hashes computed:"
|
|
||||||
cat "$HASH_FILE"
|
cat "$HASH_FILE"
|
||||||
|
|
||||||
- name: Commit ${{ env.TITLE }} changes
|
- name: Commit changes
|
||||||
env:
|
|
||||||
TARGET_BRANCH: ${{ github.head_ref || github.ref_name }}
|
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
HASH_FILE="nix/hashes.json"
|
HASH_FILE="nix/hashes.json"
|
||||||
echo "Checking for changes..."
|
|
||||||
|
|
||||||
summarize() {
|
if [ -z "$(git status --short -- "$HASH_FILE")" ]; then
|
||||||
local status="$1"
|
echo "No changes to commit"
|
||||||
{
|
echo "### Nix hashes" >> "$GITHUB_STEP_SUMMARY"
|
||||||
echo "### Nix $TITLE"
|
echo "Status: no changes" >> "$GITHUB_STEP_SUMMARY"
|
||||||
echo ""
|
|
||||||
echo "- ref: ${GITHUB_REF_NAME}"
|
|
||||||
echo "- status: ${status}"
|
|
||||||
} >> "$GITHUB_STEP_SUMMARY"
|
|
||||||
if [ -n "${GITHUB_SERVER_URL:-}" ] && [ -n "${GITHUB_REPOSITORY:-}" ] && [ -n "${GITHUB_RUN_ID:-}" ]; then
|
|
||||||
echo "- run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" >> "$GITHUB_STEP_SUMMARY"
|
|
||||||
fi
|
|
||||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
|
||||||
}
|
|
||||||
|
|
||||||
FILES=("$HASH_FILE")
|
|
||||||
STATUS="$(git status --short -- "${FILES[@]}" || true)"
|
|
||||||
if [ -z "$STATUS" ]; then
|
|
||||||
echo "No changes detected."
|
|
||||||
summarize "no changes"
|
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Changes detected:"
|
git add "$HASH_FILE"
|
||||||
echo "$STATUS"
|
|
||||||
git add "${FILES[@]}"
|
|
||||||
git commit -m "chore: update nix node_modules hashes"
|
git commit -m "chore: update nix node_modules hashes"
|
||||||
|
|
||||||
BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}"
|
git pull --rebase --autostash origin "$GITHUB_REF_NAME"
|
||||||
git pull --rebase --autostash origin "$BRANCH"
|
git push origin HEAD:"$GITHUB_REF_NAME"
|
||||||
git push origin HEAD:"$BRANCH"
|
|
||||||
echo "Changes pushed successfully"
|
|
||||||
|
|
||||||
summarize "committed $(git rev-parse --short HEAD)"
|
echo "### Nix hashes" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
echo "Status: committed $(git rev-parse --short HEAD)" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
name: sign-cli
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- brendan/desktop-signpath
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
actions: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sign-cli:
|
||||||
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
|
if: github.repository == 'anomalyco/opencode'
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-tags: true
|
||||||
|
|
||||||
|
- uses: ./.github/actions/setup-bun
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
./packages/opencode/script/build.ts
|
||||||
|
|
||||||
|
- name: Upload unsigned Windows CLI
|
||||||
|
id: upload_unsigned_windows_cli
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: unsigned-opencode-windows-cli
|
||||||
|
path: packages/opencode/dist/opencode-windows-x64/bin/opencode.exe
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Submit SignPath signing request
|
||||||
|
id: submit_signpath_signing_request
|
||||||
|
uses: signpath/github-action-submit-signing-request@v1
|
||||||
|
with:
|
||||||
|
api-token: ${{ secrets.SIGNPATH_API_KEY }}
|
||||||
|
organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }}
|
||||||
|
project-slug: ${{ secrets.SIGNPATH_PROJECT_SLUG }}
|
||||||
|
signing-policy-slug: ${{ secrets.SIGNPATH_SIGNING_POLICY_SLUG }}
|
||||||
|
artifact-configuration-slug: ${{ secrets.SIGNPATH_ARTIFACT_CONFIGURATION_SLUG }}
|
||||||
|
github-artifact-id: ${{ steps.upload_unsigned_windows_cli.outputs.artifact-id }}
|
||||||
|
wait-for-completion: true
|
||||||
|
output-artifact-directory: signed-opencode-cli
|
||||||
|
|
||||||
|
- name: Upload signed Windows CLI
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: signed-opencode-windows-cli
|
||||||
|
path: signed-opencode-cli/*.exe
|
||||||
|
if-no-files-found: error
|
||||||
+46
-89
@@ -7,8 +7,32 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
unit:
|
||||||
name: test (${{ matrix.settings.name }})
|
name: unit (linux)
|
||||||
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Setup Bun
|
||||||
|
uses: ./.github/actions/setup-bun
|
||||||
|
|
||||||
|
- name: Configure git identity
|
||||||
|
run: |
|
||||||
|
git config --global user.email "bot@opencode.ai"
|
||||||
|
git config --global user.name "opencode"
|
||||||
|
|
||||||
|
- name: Run unit tests
|
||||||
|
run: bun turbo test
|
||||||
|
|
||||||
|
e2e:
|
||||||
|
name: e2e (${{ matrix.settings.name }})
|
||||||
|
needs: unit
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -16,17 +40,12 @@ jobs:
|
|||||||
- name: linux
|
- name: linux
|
||||||
host: blacksmith-4vcpu-ubuntu-2404
|
host: blacksmith-4vcpu-ubuntu-2404
|
||||||
playwright: bunx playwright install --with-deps
|
playwright: bunx playwright install --with-deps
|
||||||
workdir: .
|
|
||||||
command: |
|
|
||||||
git config --global user.email "bot@opencode.ai"
|
|
||||||
git config --global user.name "opencode"
|
|
||||||
bun turbo test
|
|
||||||
- name: windows
|
- name: windows
|
||||||
host: windows-latest
|
host: blacksmith-4vcpu-windows-2025
|
||||||
playwright: bunx playwright install
|
playwright: bunx playwright install
|
||||||
workdir: packages/app
|
|
||||||
command: bun test:e2e:local
|
|
||||||
runs-on: ${{ matrix.settings.host }}
|
runs-on: ${{ matrix.settings.host }}
|
||||||
|
env:
|
||||||
|
PLAYWRIGHT_BROWSERS_PATH: 0
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -43,87 +62,10 @@ jobs:
|
|||||||
working-directory: packages/app
|
working-directory: packages/app
|
||||||
run: ${{ matrix.settings.playwright }}
|
run: ${{ matrix.settings.playwright }}
|
||||||
|
|
||||||
- name: Set OS-specific paths
|
- name: Run app e2e tests
|
||||||
run: |
|
run: bun --cwd packages/app test:e2e:local
|
||||||
if [ "${{ runner.os }}" = "Windows" ]; then
|
|
||||||
printf '%s\n' "OPENCODE_E2E_ROOT=${{ runner.temp }}\\opencode-e2e" >> "$GITHUB_ENV"
|
|
||||||
printf '%s\n' "OPENCODE_TEST_HOME=${{ runner.temp }}\\opencode-e2e\\home" >> "$GITHUB_ENV"
|
|
||||||
printf '%s\n' "XDG_DATA_HOME=${{ runner.temp }}\\opencode-e2e\\share" >> "$GITHUB_ENV"
|
|
||||||
printf '%s\n' "XDG_CACHE_HOME=${{ runner.temp }}\\opencode-e2e\\cache" >> "$GITHUB_ENV"
|
|
||||||
printf '%s\n' "XDG_CONFIG_HOME=${{ runner.temp }}\\opencode-e2e\\config" >> "$GITHUB_ENV"
|
|
||||||
printf '%s\n' "XDG_STATE_HOME=${{ runner.temp }}\\opencode-e2e\\state" >> "$GITHUB_ENV"
|
|
||||||
else
|
|
||||||
printf '%s\n' "OPENCODE_E2E_ROOT=${{ runner.temp }}/opencode-e2e" >> "$GITHUB_ENV"
|
|
||||||
printf '%s\n' "OPENCODE_TEST_HOME=${{ runner.temp }}/opencode-e2e/home" >> "$GITHUB_ENV"
|
|
||||||
printf '%s\n' "XDG_DATA_HOME=${{ runner.temp }}/opencode-e2e/share" >> "$GITHUB_ENV"
|
|
||||||
printf '%s\n' "XDG_CACHE_HOME=${{ runner.temp }}/opencode-e2e/cache" >> "$GITHUB_ENV"
|
|
||||||
printf '%s\n' "XDG_CONFIG_HOME=${{ runner.temp }}/opencode-e2e/config" >> "$GITHUB_ENV"
|
|
||||||
printf '%s\n' "XDG_STATE_HOME=${{ runner.temp }}/opencode-e2e/state" >> "$GITHUB_ENV"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Seed opencode data
|
|
||||||
if: matrix.settings.name != 'windows'
|
|
||||||
working-directory: packages/opencode
|
|
||||||
run: bun script/seed-e2e.ts
|
|
||||||
env:
|
|
||||||
OPENCODE_DISABLE_SHARE: "true"
|
|
||||||
OPENCODE_DISABLE_LSP_DOWNLOAD: "true"
|
|
||||||
OPENCODE_DISABLE_DEFAULT_PLUGINS: "true"
|
|
||||||
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "true"
|
|
||||||
OPENCODE_TEST_HOME: ${{ env.OPENCODE_TEST_HOME }}
|
|
||||||
XDG_DATA_HOME: ${{ env.XDG_DATA_HOME }}
|
|
||||||
XDG_CACHE_HOME: ${{ env.XDG_CACHE_HOME }}
|
|
||||||
XDG_CONFIG_HOME: ${{ env.XDG_CONFIG_HOME }}
|
|
||||||
XDG_STATE_HOME: ${{ env.XDG_STATE_HOME }}
|
|
||||||
OPENCODE_E2E_PROJECT_DIR: ${{ github.workspace }}
|
|
||||||
OPENCODE_E2E_SESSION_TITLE: "E2E Session"
|
|
||||||
OPENCODE_E2E_MESSAGE: "Seeded for UI e2e"
|
|
||||||
OPENCODE_E2E_MODEL: "opencode/gpt-5-nano"
|
|
||||||
|
|
||||||
- name: Run opencode server
|
|
||||||
if: matrix.settings.name != 'windows'
|
|
||||||
working-directory: packages/opencode
|
|
||||||
run: bun dev -- --print-logs --log-level WARN serve --port 4096 --hostname 127.0.0.1 &
|
|
||||||
env:
|
|
||||||
OPENCODE_DISABLE_SHARE: "true"
|
|
||||||
OPENCODE_DISABLE_LSP_DOWNLOAD: "true"
|
|
||||||
OPENCODE_DISABLE_DEFAULT_PLUGINS: "true"
|
|
||||||
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "true"
|
|
||||||
OPENCODE_TEST_HOME: ${{ env.OPENCODE_TEST_HOME }}
|
|
||||||
XDG_DATA_HOME: ${{ env.XDG_DATA_HOME }}
|
|
||||||
XDG_CACHE_HOME: ${{ env.XDG_CACHE_HOME }}
|
|
||||||
XDG_CONFIG_HOME: ${{ env.XDG_CONFIG_HOME }}
|
|
||||||
XDG_STATE_HOME: ${{ env.XDG_STATE_HOME }}
|
|
||||||
OPENCODE_CLIENT: "app"
|
|
||||||
|
|
||||||
- name: Wait for opencode server
|
|
||||||
if: matrix.settings.name != 'windows'
|
|
||||||
run: |
|
|
||||||
for i in {1..120}; do
|
|
||||||
curl -fsS "http://127.0.0.1:4096/global/health" > /dev/null && exit 0
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
exit 1
|
|
||||||
|
|
||||||
- name: run
|
|
||||||
working-directory: ${{ matrix.settings.workdir }}
|
|
||||||
run: ${{ matrix.settings.command }}
|
|
||||||
env:
|
env:
|
||||||
CI: true
|
CI: true
|
||||||
OPENCODE_DISABLE_SHARE: "true"
|
|
||||||
OPENCODE_DISABLE_LSP_DOWNLOAD: "true"
|
|
||||||
OPENCODE_DISABLE_DEFAULT_PLUGINS: "true"
|
|
||||||
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "true"
|
|
||||||
OPENCODE_TEST_HOME: ${{ env.OPENCODE_TEST_HOME }}
|
|
||||||
XDG_DATA_HOME: ${{ env.XDG_DATA_HOME }}
|
|
||||||
XDG_CACHE_HOME: ${{ env.XDG_CACHE_HOME }}
|
|
||||||
XDG_CONFIG_HOME: ${{ env.XDG_CONFIG_HOME }}
|
|
||||||
XDG_STATE_HOME: ${{ env.XDG_STATE_HOME }}
|
|
||||||
PLAYWRIGHT_SERVER_HOST: "127.0.0.1"
|
|
||||||
PLAYWRIGHT_SERVER_PORT: "4096"
|
|
||||||
VITE_OPENCODE_SERVER_HOST: "127.0.0.1"
|
|
||||||
VITE_OPENCODE_SERVER_PORT: "4096"
|
|
||||||
OPENCODE_CLIENT: "app"
|
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
|
|
||||||
- name: Upload Playwright artifacts
|
- name: Upload Playwright artifacts
|
||||||
@@ -136,3 +78,18 @@ jobs:
|
|||||||
path: |
|
path: |
|
||||||
packages/app/e2e/test-results
|
packages/app/e2e/test-results
|
||||||
packages/app/e2e/playwright-report
|
packages/app/e2e/playwright-report
|
||||||
|
|
||||||
|
required:
|
||||||
|
name: test (linux)
|
||||||
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
|
needs:
|
||||||
|
- unit
|
||||||
|
- e2e
|
||||||
|
if: always()
|
||||||
|
steps:
|
||||||
|
- name: Verify upstream test jobs passed
|
||||||
|
run: |
|
||||||
|
echo "unit=${{ needs.unit.result }}"
|
||||||
|
echo "e2e=${{ needs.e2e.result }}"
|
||||||
|
test "${{ needs.unit.result }}" = "success"
|
||||||
|
test "${{ needs.e2e.result }}" = "success"
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
name: typecheck
|
name: typecheck
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
push:
|
||||||
|
branches: [dev]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [dev]
|
branches: [dev]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
name: vouch-check-issue
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check if issue author is denounced
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const author = context.payload.issue.user.login;
|
||||||
|
const issueNumber = context.payload.issue.number;
|
||||||
|
|
||||||
|
// Skip bots
|
||||||
|
if (author.endsWith('[bot]')) {
|
||||||
|
core.info(`Skipping bot: ${author}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the VOUCHED.td file via API (no checkout needed)
|
||||||
|
let content;
|
||||||
|
try {
|
||||||
|
const response = await github.rest.repos.getContent({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
path: '.github/VOUCHED.td',
|
||||||
|
});
|
||||||
|
content = Buffer.from(response.data.content, 'base64').toString('utf-8');
|
||||||
|
} catch (error) {
|
||||||
|
if (error.status === 404) {
|
||||||
|
core.info('No .github/VOUCHED.td file found, skipping check.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the .td file for denounced users
|
||||||
|
const denounced = new Map();
|
||||||
|
for (const line of content.split('\n')) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||||
|
if (!trimmed.startsWith('-')) continue;
|
||||||
|
|
||||||
|
const rest = trimmed.slice(1).trim();
|
||||||
|
if (!rest) continue;
|
||||||
|
const spaceIdx = rest.indexOf(' ');
|
||||||
|
const handle = spaceIdx === -1 ? rest : rest.slice(0, spaceIdx);
|
||||||
|
const reason = spaceIdx === -1 ? null : rest.slice(spaceIdx + 1).trim();
|
||||||
|
|
||||||
|
// Handle platform:username or bare username
|
||||||
|
// Only match bare usernames or github: prefix (skip other platforms)
|
||||||
|
const colonIdx = handle.indexOf(':');
|
||||||
|
if (colonIdx !== -1) {
|
||||||
|
const platform = handle.slice(0, colonIdx).toLowerCase();
|
||||||
|
if (platform !== 'github') continue;
|
||||||
|
}
|
||||||
|
const username = colonIdx === -1 ? handle : handle.slice(colonIdx + 1);
|
||||||
|
if (!username) continue;
|
||||||
|
|
||||||
|
denounced.set(username.toLowerCase(), reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the author is denounced
|
||||||
|
const reason = denounced.get(author.toLowerCase());
|
||||||
|
if (reason === undefined) {
|
||||||
|
core.info(`User ${author} is not denounced. Allowing issue.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Author is denounced — close the issue
|
||||||
|
const body = 'This issue has been automatically closed.';
|
||||||
|
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: issueNumber,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
|
||||||
|
await github.rest.issues.update({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: issueNumber,
|
||||||
|
state: 'closed',
|
||||||
|
state_reason: 'not_planned',
|
||||||
|
});
|
||||||
|
|
||||||
|
core.info(`Closed issue #${issueNumber} from denounced user ${author}`);
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
name: vouch-check-pr
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check if PR author is denounced
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const author = context.payload.pull_request.user.login;
|
||||||
|
const prNumber = context.payload.pull_request.number;
|
||||||
|
|
||||||
|
// Skip bots
|
||||||
|
if (author.endsWith('[bot]')) {
|
||||||
|
core.info(`Skipping bot: ${author}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the VOUCHED.td file via API (no checkout needed)
|
||||||
|
let content;
|
||||||
|
try {
|
||||||
|
const response = await github.rest.repos.getContent({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
path: '.github/VOUCHED.td',
|
||||||
|
});
|
||||||
|
content = Buffer.from(response.data.content, 'base64').toString('utf-8');
|
||||||
|
} catch (error) {
|
||||||
|
if (error.status === 404) {
|
||||||
|
core.info('No .github/VOUCHED.td file found, skipping check.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the .td file for denounced users
|
||||||
|
const denounced = new Map();
|
||||||
|
for (const line of content.split('\n')) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||||
|
if (!trimmed.startsWith('-')) continue;
|
||||||
|
|
||||||
|
const rest = trimmed.slice(1).trim();
|
||||||
|
if (!rest) continue;
|
||||||
|
const spaceIdx = rest.indexOf(' ');
|
||||||
|
const handle = spaceIdx === -1 ? rest : rest.slice(0, spaceIdx);
|
||||||
|
const reason = spaceIdx === -1 ? null : rest.slice(spaceIdx + 1).trim();
|
||||||
|
|
||||||
|
// Handle platform:username or bare username
|
||||||
|
// Only match bare usernames or github: prefix (skip other platforms)
|
||||||
|
const colonIdx = handle.indexOf(':');
|
||||||
|
if (colonIdx !== -1) {
|
||||||
|
const platform = handle.slice(0, colonIdx).toLowerCase();
|
||||||
|
if (platform !== 'github') continue;
|
||||||
|
}
|
||||||
|
const username = colonIdx === -1 ? handle : handle.slice(colonIdx + 1);
|
||||||
|
if (!username) continue;
|
||||||
|
|
||||||
|
denounced.set(username.toLowerCase(), reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the author is denounced
|
||||||
|
const reason = denounced.get(author.toLowerCase());
|
||||||
|
if (reason === undefined) {
|
||||||
|
core.info(`User ${author} is not denounced. Allowing PR.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Author is denounced — close the PR
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: prNumber,
|
||||||
|
body: 'This pull request has been automatically closed.',
|
||||||
|
});
|
||||||
|
|
||||||
|
await github.rest.pulls.update({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
pull_number: prNumber,
|
||||||
|
state: 'closed',
|
||||||
|
});
|
||||||
|
|
||||||
|
core.info(`Closed PR #${prNumber} from denounced user ${author}`);
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
name: vouch-manage-by-issue
|
||||||
|
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types: [created]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: vouch-manage
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
issues: write
|
||||||
|
pull-requests: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
manage:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup git committer
|
||||||
|
id: committer
|
||||||
|
uses: ./.github/actions/setup-git-committer
|
||||||
|
with:
|
||||||
|
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
|
||||||
|
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
|
||||||
|
|
||||||
|
- uses: mitchellh/vouch/action/manage-by-issue@main
|
||||||
|
with:
|
||||||
|
issue-id: ${{ github.event.issue.number }}
|
||||||
|
comment-id: ${{ github.event.comment.id }}
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
|
||||||
@@ -5,6 +5,7 @@ node_modules
|
|||||||
.env
|
.env
|
||||||
.idea
|
.idea
|
||||||
.vscode
|
.vscode
|
||||||
|
.codex
|
||||||
*~
|
*~
|
||||||
playground
|
playground
|
||||||
tmp
|
tmp
|
||||||
|
|||||||
@@ -0,0 +1,883 @@
|
|||||||
|
---
|
||||||
|
description: Translate content for a specified locale while preserving technical terms
|
||||||
|
mode: subagent
|
||||||
|
model: opencode/gemini-3-pro
|
||||||
|
---
|
||||||
|
|
||||||
|
You are a professional translator and localization specialist.
|
||||||
|
|
||||||
|
Translate the user's content into the requested target locale (language + region, e.g. fr-FR, de-DE).
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
|
||||||
|
- Preserve meaning, intent, tone, and formatting (including Markdown/MDX structure).
|
||||||
|
- Preserve all technical terms and artifacts exactly: product/company names, API names, identifiers, code, commands/flags, file paths, URLs, versions, error messages, config keys/values, and anything inside inline code or code blocks.
|
||||||
|
- Also preserve every term listed in the Do-Not-Translate glossary below.
|
||||||
|
- Do not modify fenced code blocks.
|
||||||
|
- Output ONLY the translation (no commentary).
|
||||||
|
|
||||||
|
If the target locale is missing, ask the user to provide it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Do-Not-Translate Terms (OpenCode Docs)
|
||||||
|
|
||||||
|
Generated from: `packages/web/src/content/docs/*.mdx` (default English docs)
|
||||||
|
Generated on: 2026-02-10
|
||||||
|
|
||||||
|
Use this as a translation QA checklist / glossary. Preserve listed terms exactly (spelling, casing, punctuation).
|
||||||
|
|
||||||
|
General rules (verbatim, even if not listed below):
|
||||||
|
|
||||||
|
- Anything inside inline code (single backticks) or fenced code blocks (triple backticks)
|
||||||
|
- MDX/JS code in docs: `import ... from "..."`, component tags, identifiers
|
||||||
|
- CLI commands, flags, config keys/values, file paths, URLs/domains, and env vars
|
||||||
|
|
||||||
|
## Proper nouns and product names
|
||||||
|
|
||||||
|
Additional (not reliably captured via link text):
|
||||||
|
|
||||||
|
```text
|
||||||
|
Astro
|
||||||
|
Bun
|
||||||
|
Chocolatey
|
||||||
|
Cursor
|
||||||
|
Docker
|
||||||
|
Git
|
||||||
|
GitHub Actions
|
||||||
|
GitLab CI
|
||||||
|
GNOME Terminal
|
||||||
|
Homebrew
|
||||||
|
Mise
|
||||||
|
Neovim
|
||||||
|
Node.js
|
||||||
|
npm
|
||||||
|
Obsidian
|
||||||
|
opencode
|
||||||
|
opencode-ai
|
||||||
|
Paru
|
||||||
|
pnpm
|
||||||
|
ripgrep
|
||||||
|
Scoop
|
||||||
|
SST
|
||||||
|
Starlight
|
||||||
|
Visual Studio Code
|
||||||
|
VS Code
|
||||||
|
VSCodium
|
||||||
|
Windsurf
|
||||||
|
Windows Terminal
|
||||||
|
Yarn
|
||||||
|
Zellij
|
||||||
|
Zed
|
||||||
|
anomalyco
|
||||||
|
```
|
||||||
|
|
||||||
|
Extracted from link labels in the English docs (review and prune as desired):
|
||||||
|
|
||||||
|
```text
|
||||||
|
@openspoon/subtask2
|
||||||
|
302.AI console
|
||||||
|
ACP progress report
|
||||||
|
Agent Client Protocol
|
||||||
|
Agent Skills
|
||||||
|
Agentic
|
||||||
|
AGENTS.md
|
||||||
|
AI SDK
|
||||||
|
Alacritty
|
||||||
|
Anthropic
|
||||||
|
Anthropic's Data Policies
|
||||||
|
Atom One
|
||||||
|
Avante.nvim
|
||||||
|
Ayu
|
||||||
|
Azure AI Foundry
|
||||||
|
Azure portal
|
||||||
|
Baseten
|
||||||
|
built-in GITHUB_TOKEN
|
||||||
|
Bun.$
|
||||||
|
Catppuccin
|
||||||
|
Cerebras console
|
||||||
|
ChatGPT Plus or Pro
|
||||||
|
Cloudflare dashboard
|
||||||
|
CodeCompanion.nvim
|
||||||
|
CodeNomad
|
||||||
|
Configuring Adapters: Environment Variables
|
||||||
|
Context7 MCP server
|
||||||
|
Cortecs console
|
||||||
|
Deep Infra dashboard
|
||||||
|
DeepSeek console
|
||||||
|
Duo Agent Platform
|
||||||
|
Everforest
|
||||||
|
Fireworks AI console
|
||||||
|
Firmware dashboard
|
||||||
|
Ghostty
|
||||||
|
GitLab CLI agents docs
|
||||||
|
GitLab docs
|
||||||
|
GitLab User Settings > Access Tokens
|
||||||
|
Granular Rules (Object Syntax)
|
||||||
|
Grep by Vercel
|
||||||
|
Groq console
|
||||||
|
Gruvbox
|
||||||
|
Helicone
|
||||||
|
Helicone documentation
|
||||||
|
Helicone Header Directory
|
||||||
|
Helicone's Model Directory
|
||||||
|
Hugging Face Inference Providers
|
||||||
|
Hugging Face settings
|
||||||
|
install WSL
|
||||||
|
IO.NET console
|
||||||
|
JetBrains IDE
|
||||||
|
Kanagawa
|
||||||
|
Kitty
|
||||||
|
MiniMax API Console
|
||||||
|
Models.dev
|
||||||
|
Moonshot AI console
|
||||||
|
Nebius Token Factory console
|
||||||
|
Nord
|
||||||
|
OAuth
|
||||||
|
Ollama integration docs
|
||||||
|
OpenAI's Data Policies
|
||||||
|
OpenChamber
|
||||||
|
OpenCode
|
||||||
|
OpenCode config
|
||||||
|
OpenCode Config
|
||||||
|
OpenCode TUI with the opencode theme
|
||||||
|
OpenCode Web - Active Session
|
||||||
|
OpenCode Web - New Session
|
||||||
|
OpenCode Web - See Servers
|
||||||
|
OpenCode Zen
|
||||||
|
OpenCode-Obsidian
|
||||||
|
OpenRouter dashboard
|
||||||
|
OpenWork
|
||||||
|
OVHcloud panel
|
||||||
|
Pro+ subscription
|
||||||
|
SAP BTP Cockpit
|
||||||
|
Scaleway Console IAM settings
|
||||||
|
Scaleway Generative APIs
|
||||||
|
SDK documentation
|
||||||
|
Sentry MCP server
|
||||||
|
shell API
|
||||||
|
Together AI console
|
||||||
|
Tokyonight
|
||||||
|
Unified Billing
|
||||||
|
Venice AI console
|
||||||
|
Vercel dashboard
|
||||||
|
WezTerm
|
||||||
|
Windows Subsystem for Linux (WSL)
|
||||||
|
WSL
|
||||||
|
WSL (Windows Subsystem for Linux)
|
||||||
|
WSL extension
|
||||||
|
xAI console
|
||||||
|
Z.AI API console
|
||||||
|
Zed
|
||||||
|
ZenMux dashboard
|
||||||
|
Zod
|
||||||
|
```
|
||||||
|
|
||||||
|
## Acronyms and initialisms
|
||||||
|
|
||||||
|
```text
|
||||||
|
ACP
|
||||||
|
AGENTS
|
||||||
|
AI
|
||||||
|
AI21
|
||||||
|
ANSI
|
||||||
|
API
|
||||||
|
AST
|
||||||
|
AWS
|
||||||
|
BTP
|
||||||
|
CD
|
||||||
|
CDN
|
||||||
|
CI
|
||||||
|
CLI
|
||||||
|
CMD
|
||||||
|
CORS
|
||||||
|
DEBUG
|
||||||
|
EKS
|
||||||
|
ERROR
|
||||||
|
FAQ
|
||||||
|
GLM
|
||||||
|
GNOME
|
||||||
|
GPT
|
||||||
|
HTML
|
||||||
|
HTTP
|
||||||
|
HTTPS
|
||||||
|
IAM
|
||||||
|
ID
|
||||||
|
IDE
|
||||||
|
INFO
|
||||||
|
IO
|
||||||
|
IP
|
||||||
|
IRSA
|
||||||
|
JS
|
||||||
|
JSON
|
||||||
|
JSONC
|
||||||
|
K2
|
||||||
|
LLM
|
||||||
|
LM
|
||||||
|
LSP
|
||||||
|
M2
|
||||||
|
MCP
|
||||||
|
MR
|
||||||
|
NET
|
||||||
|
NPM
|
||||||
|
NTLM
|
||||||
|
OIDC
|
||||||
|
OS
|
||||||
|
PAT
|
||||||
|
PATH
|
||||||
|
PHP
|
||||||
|
PR
|
||||||
|
PTY
|
||||||
|
README
|
||||||
|
RFC
|
||||||
|
RPC
|
||||||
|
SAP
|
||||||
|
SDK
|
||||||
|
SKILL
|
||||||
|
SSE
|
||||||
|
SSO
|
||||||
|
TS
|
||||||
|
TTY
|
||||||
|
TUI
|
||||||
|
UI
|
||||||
|
URL
|
||||||
|
US
|
||||||
|
UX
|
||||||
|
VCS
|
||||||
|
VPC
|
||||||
|
VPN
|
||||||
|
VS
|
||||||
|
WARN
|
||||||
|
WSL
|
||||||
|
X11
|
||||||
|
YAML
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code identifiers used in prose (CamelCase, mixedCase)
|
||||||
|
|
||||||
|
```text
|
||||||
|
apiKey
|
||||||
|
AppleScript
|
||||||
|
AssistantMessage
|
||||||
|
baseURL
|
||||||
|
BurntSushi
|
||||||
|
ChatGPT
|
||||||
|
ClangFormat
|
||||||
|
CodeCompanion
|
||||||
|
CodeNomad
|
||||||
|
DeepSeek
|
||||||
|
DefaultV2
|
||||||
|
FileContent
|
||||||
|
FileDiff
|
||||||
|
FileNode
|
||||||
|
fineGrained
|
||||||
|
FormatterStatus
|
||||||
|
GitHub
|
||||||
|
GitLab
|
||||||
|
iTerm2
|
||||||
|
JavaScript
|
||||||
|
JetBrains
|
||||||
|
macOS
|
||||||
|
mDNS
|
||||||
|
MiniMax
|
||||||
|
NeuralNomadsAI
|
||||||
|
NickvanDyke
|
||||||
|
NoeFabris
|
||||||
|
OpenAI
|
||||||
|
OpenAPI
|
||||||
|
OpenChamber
|
||||||
|
OpenCode
|
||||||
|
OpenRouter
|
||||||
|
OpenTUI
|
||||||
|
OpenWork
|
||||||
|
ownUserPermissions
|
||||||
|
PowerShell
|
||||||
|
ProviderAuthAuthorization
|
||||||
|
ProviderAuthMethod
|
||||||
|
ProviderInitError
|
||||||
|
SessionStatus
|
||||||
|
TabItem
|
||||||
|
tokenType
|
||||||
|
ToolIDs
|
||||||
|
ToolList
|
||||||
|
TypeScript
|
||||||
|
typesUrl
|
||||||
|
UserMessage
|
||||||
|
VcsInfo
|
||||||
|
WebView2
|
||||||
|
WezTerm
|
||||||
|
xAI
|
||||||
|
ZenMux
|
||||||
|
```
|
||||||
|
|
||||||
|
## OpenCode CLI commands (as shown in docs)
|
||||||
|
|
||||||
|
```text
|
||||||
|
opencode
|
||||||
|
opencode [project]
|
||||||
|
opencode /path/to/project
|
||||||
|
opencode acp
|
||||||
|
opencode agent [command]
|
||||||
|
opencode agent create
|
||||||
|
opencode agent list
|
||||||
|
opencode attach [url]
|
||||||
|
opencode attach http://10.20.30.40:4096
|
||||||
|
opencode attach http://localhost:4096
|
||||||
|
opencode auth [command]
|
||||||
|
opencode auth list
|
||||||
|
opencode auth login
|
||||||
|
opencode auth logout
|
||||||
|
opencode auth ls
|
||||||
|
opencode export [sessionID]
|
||||||
|
opencode github [command]
|
||||||
|
opencode github install
|
||||||
|
opencode github run
|
||||||
|
opencode import <file>
|
||||||
|
opencode import https://opncd.ai/s/abc123
|
||||||
|
opencode import session.json
|
||||||
|
opencode mcp [command]
|
||||||
|
opencode mcp add
|
||||||
|
opencode mcp auth [name]
|
||||||
|
opencode mcp auth list
|
||||||
|
opencode mcp auth ls
|
||||||
|
opencode mcp auth my-oauth-server
|
||||||
|
opencode mcp auth sentry
|
||||||
|
opencode mcp debug <name>
|
||||||
|
opencode mcp debug my-oauth-server
|
||||||
|
opencode mcp list
|
||||||
|
opencode mcp logout [name]
|
||||||
|
opencode mcp logout my-oauth-server
|
||||||
|
opencode mcp ls
|
||||||
|
opencode models --refresh
|
||||||
|
opencode models [provider]
|
||||||
|
opencode models anthropic
|
||||||
|
opencode run [message..]
|
||||||
|
opencode run Explain the use of context in Go
|
||||||
|
opencode serve
|
||||||
|
opencode serve --cors http://localhost:5173 --cors https://app.example.com
|
||||||
|
opencode serve --hostname 0.0.0.0 --port 4096
|
||||||
|
opencode serve [--port <number>] [--hostname <string>] [--cors <origin>]
|
||||||
|
opencode session [command]
|
||||||
|
opencode session list
|
||||||
|
opencode stats
|
||||||
|
opencode uninstall
|
||||||
|
opencode upgrade
|
||||||
|
opencode upgrade [target]
|
||||||
|
opencode upgrade v0.1.48
|
||||||
|
opencode web
|
||||||
|
opencode web --cors https://example.com
|
||||||
|
opencode web --hostname 0.0.0.0
|
||||||
|
opencode web --mdns
|
||||||
|
opencode web --mdns --mdns-domain myproject.local
|
||||||
|
opencode web --port 4096
|
||||||
|
opencode web --port 4096 --hostname 0.0.0.0
|
||||||
|
opencode.server.close()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Slash commands and routes
|
||||||
|
|
||||||
|
```text
|
||||||
|
/agent
|
||||||
|
/auth/:id
|
||||||
|
/clear
|
||||||
|
/command
|
||||||
|
/config
|
||||||
|
/config/providers
|
||||||
|
/connect
|
||||||
|
/continue
|
||||||
|
/doc
|
||||||
|
/editor
|
||||||
|
/event
|
||||||
|
/experimental/tool?provider=<p>&model=<m>
|
||||||
|
/experimental/tool/ids
|
||||||
|
/export
|
||||||
|
/file?path=<path>
|
||||||
|
/file/content?path=<p>
|
||||||
|
/file/status
|
||||||
|
/find?pattern=<pat>
|
||||||
|
/find/file
|
||||||
|
/find/file?query=<q>
|
||||||
|
/find/symbol?query=<q>
|
||||||
|
/formatter
|
||||||
|
/global/event
|
||||||
|
/global/health
|
||||||
|
/help
|
||||||
|
/init
|
||||||
|
/instance/dispose
|
||||||
|
/log
|
||||||
|
/lsp
|
||||||
|
/mcp
|
||||||
|
/mnt/
|
||||||
|
/mnt/c/
|
||||||
|
/mnt/d/
|
||||||
|
/models
|
||||||
|
/oc
|
||||||
|
/opencode
|
||||||
|
/path
|
||||||
|
/project
|
||||||
|
/project/current
|
||||||
|
/provider
|
||||||
|
/provider/{id}/oauth/authorize
|
||||||
|
/provider/{id}/oauth/callback
|
||||||
|
/provider/auth
|
||||||
|
/q
|
||||||
|
/quit
|
||||||
|
/redo
|
||||||
|
/resume
|
||||||
|
/session
|
||||||
|
/session/:id
|
||||||
|
/session/:id/abort
|
||||||
|
/session/:id/children
|
||||||
|
/session/:id/command
|
||||||
|
/session/:id/diff
|
||||||
|
/session/:id/fork
|
||||||
|
/session/:id/init
|
||||||
|
/session/:id/message
|
||||||
|
/session/:id/message/:messageID
|
||||||
|
/session/:id/permissions/:permissionID
|
||||||
|
/session/:id/prompt_async
|
||||||
|
/session/:id/revert
|
||||||
|
/session/:id/share
|
||||||
|
/session/:id/shell
|
||||||
|
/session/:id/summarize
|
||||||
|
/session/:id/todo
|
||||||
|
/session/:id/unrevert
|
||||||
|
/session/status
|
||||||
|
/share
|
||||||
|
/summarize
|
||||||
|
/theme
|
||||||
|
/tui
|
||||||
|
/tui/append-prompt
|
||||||
|
/tui/clear-prompt
|
||||||
|
/tui/control/next
|
||||||
|
/tui/control/response
|
||||||
|
/tui/execute-command
|
||||||
|
/tui/open-help
|
||||||
|
/tui/open-models
|
||||||
|
/tui/open-sessions
|
||||||
|
/tui/open-themes
|
||||||
|
/tui/show-toast
|
||||||
|
/tui/submit-prompt
|
||||||
|
/undo
|
||||||
|
/Users/username
|
||||||
|
/Users/username/projects/*
|
||||||
|
/vcs
|
||||||
|
```
|
||||||
|
|
||||||
|
## CLI flags and short options
|
||||||
|
|
||||||
|
```text
|
||||||
|
--agent
|
||||||
|
--attach
|
||||||
|
--command
|
||||||
|
--continue
|
||||||
|
--cors
|
||||||
|
--cwd
|
||||||
|
--days
|
||||||
|
--dir
|
||||||
|
--dry-run
|
||||||
|
--event
|
||||||
|
--file
|
||||||
|
--force
|
||||||
|
--fork
|
||||||
|
--format
|
||||||
|
--help
|
||||||
|
--hostname
|
||||||
|
--hostname 0.0.0.0
|
||||||
|
--keep-config
|
||||||
|
--keep-data
|
||||||
|
--log-level
|
||||||
|
--max-count
|
||||||
|
--mdns
|
||||||
|
--mdns-domain
|
||||||
|
--method
|
||||||
|
--model
|
||||||
|
--models
|
||||||
|
--port
|
||||||
|
--print-logs
|
||||||
|
--project
|
||||||
|
--prompt
|
||||||
|
--refresh
|
||||||
|
--session
|
||||||
|
--share
|
||||||
|
--title
|
||||||
|
--token
|
||||||
|
--tools
|
||||||
|
--verbose
|
||||||
|
--version
|
||||||
|
--wait
|
||||||
|
|
||||||
|
-c
|
||||||
|
-d
|
||||||
|
-f
|
||||||
|
-h
|
||||||
|
-m
|
||||||
|
-n
|
||||||
|
-s
|
||||||
|
-v
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment variables
|
||||||
|
|
||||||
|
```text
|
||||||
|
AI_API_URL
|
||||||
|
AI_FLOW_CONTEXT
|
||||||
|
AI_FLOW_EVENT
|
||||||
|
AI_FLOW_INPUT
|
||||||
|
AICORE_DEPLOYMENT_ID
|
||||||
|
AICORE_RESOURCE_GROUP
|
||||||
|
AICORE_SERVICE_KEY
|
||||||
|
ANTHROPIC_API_KEY
|
||||||
|
AWS_ACCESS_KEY_ID
|
||||||
|
AWS_BEARER_TOKEN_BEDROCK
|
||||||
|
AWS_PROFILE
|
||||||
|
AWS_REGION
|
||||||
|
AWS_ROLE_ARN
|
||||||
|
AWS_SECRET_ACCESS_KEY
|
||||||
|
AWS_WEB_IDENTITY_TOKEN_FILE
|
||||||
|
AZURE_COGNITIVE_SERVICES_RESOURCE_NAME
|
||||||
|
AZURE_RESOURCE_NAME
|
||||||
|
CI_PROJECT_DIR
|
||||||
|
CI_SERVER_FQDN
|
||||||
|
CI_WORKLOAD_REF
|
||||||
|
CLOUDFLARE_ACCOUNT_ID
|
||||||
|
CLOUDFLARE_API_TOKEN
|
||||||
|
CLOUDFLARE_GATEWAY_ID
|
||||||
|
CONTEXT7_API_KEY
|
||||||
|
GITHUB_TOKEN
|
||||||
|
GITLAB_AI_GATEWAY_URL
|
||||||
|
GITLAB_HOST
|
||||||
|
GITLAB_INSTANCE_URL
|
||||||
|
GITLAB_OAUTH_CLIENT_ID
|
||||||
|
GITLAB_TOKEN
|
||||||
|
GITLAB_TOKEN_OPENCODE
|
||||||
|
GOOGLE_APPLICATION_CREDENTIALS
|
||||||
|
GOOGLE_CLOUD_PROJECT
|
||||||
|
HTTP_PROXY
|
||||||
|
HTTPS_PROXY
|
||||||
|
K2_
|
||||||
|
MY_API_KEY
|
||||||
|
MY_ENV_VAR
|
||||||
|
MY_MCP_CLIENT_ID
|
||||||
|
MY_MCP_CLIENT_SECRET
|
||||||
|
NO_PROXY
|
||||||
|
NODE_ENV
|
||||||
|
NODE_EXTRA_CA_CERTS
|
||||||
|
NPM_AUTH_TOKEN
|
||||||
|
OC_ALLOW_WAYLAND
|
||||||
|
OPENCODE_API_KEY
|
||||||
|
OPENCODE_AUTH_JSON
|
||||||
|
OPENCODE_AUTO_SHARE
|
||||||
|
OPENCODE_CLIENT
|
||||||
|
OPENCODE_CONFIG
|
||||||
|
OPENCODE_CONFIG_CONTENT
|
||||||
|
OPENCODE_CONFIG_DIR
|
||||||
|
OPENCODE_DISABLE_AUTOCOMPACT
|
||||||
|
OPENCODE_DISABLE_AUTOUPDATE
|
||||||
|
OPENCODE_DISABLE_CLAUDE_CODE
|
||||||
|
OPENCODE_DISABLE_CLAUDE_CODE_PROMPT
|
||||||
|
OPENCODE_DISABLE_CLAUDE_CODE_SKILLS
|
||||||
|
OPENCODE_DISABLE_DEFAULT_PLUGINS
|
||||||
|
OPENCODE_DISABLE_FILETIME_CHECK
|
||||||
|
OPENCODE_DISABLE_LSP_DOWNLOAD
|
||||||
|
OPENCODE_DISABLE_MODELS_FETCH
|
||||||
|
OPENCODE_DISABLE_PRUNE
|
||||||
|
OPENCODE_DISABLE_TERMINAL_TITLE
|
||||||
|
OPENCODE_ENABLE_EXA
|
||||||
|
OPENCODE_ENABLE_EXPERIMENTAL_MODELS
|
||||||
|
OPENCODE_EXPERIMENTAL
|
||||||
|
OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS
|
||||||
|
OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT
|
||||||
|
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER
|
||||||
|
OPENCODE_EXPERIMENTAL_EXA
|
||||||
|
OPENCODE_EXPERIMENTAL_FILEWATCHER
|
||||||
|
OPENCODE_EXPERIMENTAL_ICON_DISCOVERY
|
||||||
|
OPENCODE_EXPERIMENTAL_LSP_TOOL
|
||||||
|
OPENCODE_EXPERIMENTAL_LSP_TY
|
||||||
|
OPENCODE_EXPERIMENTAL_MARKDOWN
|
||||||
|
OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX
|
||||||
|
OPENCODE_EXPERIMENTAL_OXFMT
|
||||||
|
OPENCODE_EXPERIMENTAL_PLAN_MODE
|
||||||
|
OPENCODE_FAKE_VCS
|
||||||
|
OPENCODE_GIT_BASH_PATH
|
||||||
|
OPENCODE_MODEL
|
||||||
|
OPENCODE_MODELS_URL
|
||||||
|
OPENCODE_PERMISSION
|
||||||
|
OPENCODE_PORT
|
||||||
|
OPENCODE_SERVER_PASSWORD
|
||||||
|
OPENCODE_SERVER_USERNAME
|
||||||
|
PROJECT_ROOT
|
||||||
|
RESOURCE_NAME
|
||||||
|
RUST_LOG
|
||||||
|
VARIABLE_NAME
|
||||||
|
VERTEX_LOCATION
|
||||||
|
XDG_CONFIG_HOME
|
||||||
|
```
|
||||||
|
|
||||||
|
## Package/module identifiers
|
||||||
|
|
||||||
|
```text
|
||||||
|
../../../config.mjs
|
||||||
|
@astrojs/starlight/components
|
||||||
|
@opencode-ai/plugin
|
||||||
|
@opencode-ai/sdk
|
||||||
|
path
|
||||||
|
shescape
|
||||||
|
zod
|
||||||
|
|
||||||
|
@
|
||||||
|
@ai-sdk/anthropic
|
||||||
|
@ai-sdk/cerebras
|
||||||
|
@ai-sdk/google
|
||||||
|
@ai-sdk/openai
|
||||||
|
@ai-sdk/openai-compatible
|
||||||
|
@File#L37-42
|
||||||
|
@modelcontextprotocol/server-everything
|
||||||
|
@opencode
|
||||||
|
```
|
||||||
|
|
||||||
|
## GitHub owner/repo slugs referenced in docs
|
||||||
|
|
||||||
|
```text
|
||||||
|
24601/opencode-zellij-namer
|
||||||
|
angristan/opencode-wakatime
|
||||||
|
anomalyco/opencode
|
||||||
|
apps/opencode-agent
|
||||||
|
athal7/opencode-devcontainers
|
||||||
|
awesome-opencode/awesome-opencode
|
||||||
|
backnotprop/plannotator
|
||||||
|
ben-vargas/ai-sdk-provider-opencode-sdk
|
||||||
|
btriapitsyn/openchamber
|
||||||
|
BurntSushi/ripgrep
|
||||||
|
Cluster444/agentic
|
||||||
|
code-yeongyu/oh-my-opencode
|
||||||
|
darrenhinde/opencode-agents
|
||||||
|
different-ai/opencode-scheduler
|
||||||
|
different-ai/openwork
|
||||||
|
features/copilot
|
||||||
|
folke/tokyonight.nvim
|
||||||
|
franlol/opencode-md-table-formatter
|
||||||
|
ggml-org/llama.cpp
|
||||||
|
ghoulr/opencode-websearch-cited.git
|
||||||
|
H2Shami/opencode-helicone-session
|
||||||
|
hosenur/portal
|
||||||
|
jamesmurdza/daytona
|
||||||
|
jenslys/opencode-gemini-auth
|
||||||
|
JRedeker/opencode-morph-fast-apply
|
||||||
|
JRedeker/opencode-shell-strategy
|
||||||
|
kdcokenny/ocx
|
||||||
|
kdcokenny/opencode-background-agents
|
||||||
|
kdcokenny/opencode-notify
|
||||||
|
kdcokenny/opencode-workspace
|
||||||
|
kdcokenny/opencode-worktree
|
||||||
|
login/device
|
||||||
|
mohak34/opencode-notifier
|
||||||
|
morhetz/gruvbox
|
||||||
|
mtymek/opencode-obsidian
|
||||||
|
NeuralNomadsAI/CodeNomad
|
||||||
|
nick-vi/opencode-type-inject
|
||||||
|
NickvanDyke/opencode.nvim
|
||||||
|
NoeFabris/opencode-antigravity-auth
|
||||||
|
nordtheme/nord
|
||||||
|
numman-ali/opencode-openai-codex-auth
|
||||||
|
olimorris/codecompanion.nvim
|
||||||
|
panta82/opencode-notificator
|
||||||
|
rebelot/kanagawa.nvim
|
||||||
|
remorses/kimaki
|
||||||
|
sainnhe/everforest
|
||||||
|
shekohex/opencode-google-antigravity-auth
|
||||||
|
shekohex/opencode-pty.git
|
||||||
|
spoons-and-mirrors/subtask2
|
||||||
|
sudo-tee/opencode.nvim
|
||||||
|
supermemoryai/opencode-supermemory
|
||||||
|
Tarquinen/opencode-dynamic-context-pruning
|
||||||
|
Th3Whit3Wolf/one-nvim
|
||||||
|
upstash/context7
|
||||||
|
vtemian/micode
|
||||||
|
vtemian/octto
|
||||||
|
yetone/avante.nvim
|
||||||
|
zenobi-us/opencode-plugin-template
|
||||||
|
zenobi-us/opencode-skillful
|
||||||
|
```
|
||||||
|
|
||||||
|
## Paths, filenames, globs, and URLs
|
||||||
|
|
||||||
|
```text
|
||||||
|
./.opencode/themes/*.json
|
||||||
|
./<project-slug>/storage/
|
||||||
|
./config/#custom-directory
|
||||||
|
./global/storage/
|
||||||
|
.agents/skills/*/SKILL.md
|
||||||
|
.agents/skills/<name>/SKILL.md
|
||||||
|
.clang-format
|
||||||
|
.claude
|
||||||
|
.claude/skills
|
||||||
|
.claude/skills/*/SKILL.md
|
||||||
|
.claude/skills/<name>/SKILL.md
|
||||||
|
.env
|
||||||
|
.github/workflows/opencode.yml
|
||||||
|
.gitignore
|
||||||
|
.gitlab-ci.yml
|
||||||
|
.ignore
|
||||||
|
.NET SDK
|
||||||
|
.npmrc
|
||||||
|
.ocamlformat
|
||||||
|
.opencode
|
||||||
|
.opencode/
|
||||||
|
.opencode/agents/
|
||||||
|
.opencode/commands/
|
||||||
|
.opencode/commands/test.md
|
||||||
|
.opencode/modes/
|
||||||
|
.opencode/plans/*.md
|
||||||
|
.opencode/plugins/
|
||||||
|
.opencode/skills/<name>/SKILL.md
|
||||||
|
.opencode/skills/git-release/SKILL.md
|
||||||
|
.opencode/tools/
|
||||||
|
.well-known/opencode
|
||||||
|
{ type: "raw" \| "patch", content: string }
|
||||||
|
{file:path/to/file}
|
||||||
|
**/*.js
|
||||||
|
%USERPROFILE%/intelephense/license.txt
|
||||||
|
%USERPROFILE%\.cache\opencode
|
||||||
|
%USERPROFILE%\.config\opencode\opencode.jsonc
|
||||||
|
%USERPROFILE%\.config\opencode\plugins
|
||||||
|
%USERPROFILE%\.local\share\opencode
|
||||||
|
%USERPROFILE%\.local\share\opencode\log
|
||||||
|
<project-root>/.opencode/themes/*.json
|
||||||
|
<providerId>/<modelId>
|
||||||
|
<your-project>/.opencode/plugins/
|
||||||
|
~
|
||||||
|
~/...
|
||||||
|
~/.agents/skills/*/SKILL.md
|
||||||
|
~/.agents/skills/<name>/SKILL.md
|
||||||
|
~/.aws/credentials
|
||||||
|
~/.bashrc
|
||||||
|
~/.cache/opencode
|
||||||
|
~/.cache/opencode/node_modules/
|
||||||
|
~/.claude/CLAUDE.md
|
||||||
|
~/.claude/skills/
|
||||||
|
~/.claude/skills/*/SKILL.md
|
||||||
|
~/.claude/skills/<name>/SKILL.md
|
||||||
|
~/.config/opencode
|
||||||
|
~/.config/opencode/AGENTS.md
|
||||||
|
~/.config/opencode/agents/
|
||||||
|
~/.config/opencode/commands/
|
||||||
|
~/.config/opencode/modes/
|
||||||
|
~/.config/opencode/opencode.json
|
||||||
|
~/.config/opencode/opencode.jsonc
|
||||||
|
~/.config/opencode/plugins/
|
||||||
|
~/.config/opencode/skills/*/SKILL.md
|
||||||
|
~/.config/opencode/skills/<name>/SKILL.md
|
||||||
|
~/.config/opencode/themes/*.json
|
||||||
|
~/.config/opencode/tools/
|
||||||
|
~/.config/zed/settings.json
|
||||||
|
~/.local/share
|
||||||
|
~/.local/share/opencode/
|
||||||
|
~/.local/share/opencode/auth.json
|
||||||
|
~/.local/share/opencode/log/
|
||||||
|
~/.local/share/opencode/mcp-auth.json
|
||||||
|
~/.local/share/opencode/opencode.jsonc
|
||||||
|
~/.npmrc
|
||||||
|
~/.zshrc
|
||||||
|
~/code/
|
||||||
|
~/Library/Application Support
|
||||||
|
~/projects/*
|
||||||
|
~/projects/personal/
|
||||||
|
${config.github}/blob/dev/packages/sdk/js/src/gen/types.gen.ts
|
||||||
|
$HOME/intelephense/license.txt
|
||||||
|
$HOME/projects/*
|
||||||
|
$XDG_CONFIG_HOME/opencode/themes/*.json
|
||||||
|
agent/
|
||||||
|
agents/
|
||||||
|
build/
|
||||||
|
commands/
|
||||||
|
dist/
|
||||||
|
http://<wsl-ip>:4096
|
||||||
|
http://127.0.0.1:8080/callback
|
||||||
|
http://localhost:<port>
|
||||||
|
http://localhost:4096
|
||||||
|
http://localhost:4096/doc
|
||||||
|
https://app.example.com
|
||||||
|
https://AZURE_COGNITIVE_SERVICES_RESOURCE_NAME.cognitiveservices.azure.com/
|
||||||
|
https://opencode.ai/zen/v1/chat/completions
|
||||||
|
https://opencode.ai/zen/v1/messages
|
||||||
|
https://opencode.ai/zen/v1/models/gemini-3-flash
|
||||||
|
https://opencode.ai/zen/v1/models/gemini-3-pro
|
||||||
|
https://opencode.ai/zen/v1/responses
|
||||||
|
https://RESOURCE_NAME.openai.azure.com/
|
||||||
|
laravel/pint
|
||||||
|
log/
|
||||||
|
model: "anthropic/claude-sonnet-4-5"
|
||||||
|
modes/
|
||||||
|
node_modules/
|
||||||
|
openai/gpt-4.1
|
||||||
|
opencode.ai/config.json
|
||||||
|
opencode/<model-id>
|
||||||
|
opencode/gpt-5.1-codex
|
||||||
|
opencode/gpt-5.2-codex
|
||||||
|
opencode/kimi-k2
|
||||||
|
openrouter/google/gemini-2.5-flash
|
||||||
|
opncd.ai/s/<share-id>
|
||||||
|
packages/*/AGENTS.md
|
||||||
|
plugins/
|
||||||
|
project/
|
||||||
|
provider_id/model_id
|
||||||
|
provider/model
|
||||||
|
provider/model-id
|
||||||
|
rm -rf ~/.cache/opencode
|
||||||
|
skills/
|
||||||
|
skills/*/SKILL.md
|
||||||
|
src/**/*.ts
|
||||||
|
themes/
|
||||||
|
tools/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Keybind strings
|
||||||
|
|
||||||
|
```text
|
||||||
|
alt+b
|
||||||
|
Alt+Ctrl+K
|
||||||
|
alt+d
|
||||||
|
alt+f
|
||||||
|
Cmd+Esc
|
||||||
|
Cmd+Option+K
|
||||||
|
Cmd+Shift+Esc
|
||||||
|
Cmd+Shift+G
|
||||||
|
Cmd+Shift+P
|
||||||
|
ctrl+a
|
||||||
|
ctrl+b
|
||||||
|
ctrl+d
|
||||||
|
ctrl+e
|
||||||
|
Ctrl+Esc
|
||||||
|
ctrl+f
|
||||||
|
ctrl+g
|
||||||
|
ctrl+k
|
||||||
|
Ctrl+Shift+Esc
|
||||||
|
Ctrl+Shift+P
|
||||||
|
ctrl+t
|
||||||
|
ctrl+u
|
||||||
|
ctrl+w
|
||||||
|
ctrl+x
|
||||||
|
DELETE
|
||||||
|
Shift+Enter
|
||||||
|
WIN+R
|
||||||
|
```
|
||||||
|
|
||||||
|
## Model ID strings referenced
|
||||||
|
|
||||||
|
```text
|
||||||
|
{env:OPENCODE_MODEL}
|
||||||
|
anthropic/claude-3-5-sonnet-20241022
|
||||||
|
anthropic/claude-haiku-4-20250514
|
||||||
|
anthropic/claude-haiku-4-5
|
||||||
|
anthropic/claude-sonnet-4-20250514
|
||||||
|
anthropic/claude-sonnet-4-5
|
||||||
|
gitlab/duo-chat-haiku-4-5
|
||||||
|
lmstudio/google/gemma-3n-e4b
|
||||||
|
openai/gpt-4.1
|
||||||
|
openai/gpt-5
|
||||||
|
opencode/gpt-5.1-codex
|
||||||
|
opencode/gpt-5.2-codex
|
||||||
|
opencode/kimi-k2
|
||||||
|
openrouter/google/gemini-2.5-flash
|
||||||
|
```
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
description: git commit and push
|
description: git commit and push
|
||||||
model: opencode/glm-4.7
|
model: opencode/kimi-k2.5
|
||||||
subtask: true
|
subtask: true
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://opencode.ai/config.json",
|
"$schema": "https://opencode.ai/config.json",
|
||||||
// "plugin": ["opencode-openai-codex-auth"],
|
|
||||||
// "enterprise": {
|
// "enterprise": {
|
||||||
// "url": "https://enterprise.dev.opencode.ai",
|
// "url": "https://enterprise.dev.opencode.ai",
|
||||||
// },
|
// },
|
||||||
@@ -9,12 +8,7 @@
|
|||||||
"options": {},
|
"options": {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"mcp": {
|
"mcp": {},
|
||||||
"context7": {
|
|
||||||
"type": "remote",
|
|
||||||
"url": "https://mcp.context7.com/mcp",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"tools": {
|
"tools": {
|
||||||
"github-triage": false,
|
"github-triage": false,
|
||||||
"github-pr-search": false,
|
"github-pr-search": false,
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ description: Use this when you are working on file operations like reading, writ
|
|||||||
- Decode tool stderr with `Bun.readableStreamToText`.
|
- Decode tool stderr with `Bun.readableStreamToText`.
|
||||||
- For large writes, use `Bun.write(Bun.file(path), text)`.
|
- For large writes, use `Bun.write(Bun.file(path), text)`.
|
||||||
|
|
||||||
|
NOTE: Bun.file(...).exists() will return `false` if the value is a directory.
|
||||||
|
Use Filesystem.exists(...) instead if path can be file or directory
|
||||||
|
|
||||||
## Quick checklist
|
## Quick checklist
|
||||||
|
|
||||||
- Use Bun APIs first.
|
- Use Bun APIs first.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
Use this tool to assign and/or label a Github issue.
|
Use this tool to assign and/or label a GitHub issue.
|
||||||
|
|
||||||
You can assign the following users:
|
You can assign the following users:
|
||||||
- thdxr
|
- thdxr
|
||||||
|
|||||||
+2
-1
@@ -1 +1,2 @@
|
|||||||
sst-env.d.ts
|
sst-env.d.ts
|
||||||
|
packages/desktop/src/bindings.ts
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
github-policies:
|
||||||
|
runners:
|
||||||
|
allowed_groups:
|
||||||
|
- "blacksmith runners 01kbd5v56sg8tz7rea39b7ygpt"
|
||||||
|
build:
|
||||||
|
disallow_reruns: false
|
||||||
|
branch_rulesets:
|
||||||
@@ -1,82 +1,112 @@
|
|||||||
- To regenerate the JavaScript SDK, run `./packages/sdk/js/script/build.ts`.
|
- To regenerate the JavaScript SDK, run `./packages/sdk/js/script/build.ts`.
|
||||||
- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE.
|
- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE.
|
||||||
- The default branch in this repo is `dev`.
|
- The default branch in this repo is `dev`.
|
||||||
|
- Local `main` ref may not exist; use `dev` or `origin/dev` for diffs.
|
||||||
- Prefer automation: execute requested actions without confirmation unless blocked by missing info or safety/irreversibility.
|
- Prefer automation: execute requested actions without confirmation unless blocked by missing info or safety/irreversibility.
|
||||||
|
|
||||||
## Style Guide
|
## Style Guide
|
||||||
|
|
||||||
|
### General Principles
|
||||||
|
|
||||||
- Keep things in one function unless composable or reusable
|
- Keep things in one function unless composable or reusable
|
||||||
- Avoid unnecessary destructuring. Instead of `const { a, b } = obj`, use `obj.a` and `obj.b` to preserve context
|
|
||||||
- Avoid `try`/`catch` where possible
|
- Avoid `try`/`catch` where possible
|
||||||
- Avoid using the `any` type
|
- Avoid using the `any` type
|
||||||
- Prefer single word variable names where possible
|
- Prefer single word variable names where possible
|
||||||
- Use Bun APIs when possible, like `Bun.file()`
|
- Use Bun APIs when possible, like `Bun.file()`
|
||||||
- Rely on type inference when possible; avoid explicit type annotations or interfaces unless necessary for exports or clarity
|
- Rely on type inference when possible; avoid explicit type annotations or interfaces unless necessary for exports or clarity
|
||||||
|
- Prefer functional array methods (flatMap, filter, map) over for loops; use type guards on filter to maintain type inference downstream
|
||||||
|
|
||||||
### Avoid let statements
|
### Naming
|
||||||
|
|
||||||
We don't like `let` statements, especially combined with if/else statements.
|
Prefer single word names for variables and functions. Only use multiple words if necessary.
|
||||||
Prefer `const`.
|
|
||||||
|
|
||||||
Good:
|
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
const foo = condition ? 1 : 2
|
// Good
|
||||||
|
const foo = 1
|
||||||
|
function journal(dir: string) {}
|
||||||
|
|
||||||
|
// Bad
|
||||||
|
const fooBar = 1
|
||||||
|
function prepareJournal(dir: string) {}
|
||||||
```
|
```
|
||||||
|
|
||||||
Bad:
|
Reduce total variable count by inlining when a value is only used once.
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
let foo
|
// Good
|
||||||
|
const journal = await Bun.file(path.join(dir, "journal.json")).json()
|
||||||
|
|
||||||
|
// Bad
|
||||||
|
const journalPath = path.join(dir, "journal.json")
|
||||||
|
const journal = await Bun.file(journalPath).json()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Destructuring
|
||||||
|
|
||||||
|
Avoid unnecessary destructuring. Use dot notation to preserve context.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Good
|
||||||
|
obj.a
|
||||||
|
obj.b
|
||||||
|
|
||||||
|
// Bad
|
||||||
|
const { a, b } = obj
|
||||||
|
```
|
||||||
|
|
||||||
|
### Variables
|
||||||
|
|
||||||
|
Prefer `const` over `let`. Use ternaries or early returns instead of reassignment.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Good
|
||||||
|
const foo = condition ? 1 : 2
|
||||||
|
|
||||||
|
// Bad
|
||||||
|
let foo
|
||||||
if (condition) foo = 1
|
if (condition) foo = 1
|
||||||
else foo = 2
|
else foo = 2
|
||||||
```
|
```
|
||||||
|
|
||||||
### Avoid else statements
|
### Control Flow
|
||||||
|
|
||||||
Prefer early returns or using an `iife` to avoid else statements.
|
Avoid `else` statements. Prefer early returns.
|
||||||
|
|
||||||
Good:
|
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
|
// Good
|
||||||
function foo() {
|
function foo() {
|
||||||
if (condition) return 1
|
if (condition) return 1
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
```
|
|
||||||
|
|
||||||
Bad:
|
// Bad
|
||||||
|
|
||||||
```ts
|
|
||||||
function foo() {
|
function foo() {
|
||||||
if (condition) return 1
|
if (condition) return 1
|
||||||
else return 2
|
else return 2
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Prefer single word naming
|
### Schema Definitions (Drizzle)
|
||||||
|
|
||||||
Try your best to find a single word name for your variables, functions, etc.
|
Use snake_case for field names so column names don't need to be redefined as strings.
|
||||||
Only use multiple words if you cannot.
|
|
||||||
|
|
||||||
Good:
|
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
const foo = 1
|
// Good
|
||||||
const bar = 2
|
const table = sqliteTable("session", {
|
||||||
const baz = 3
|
id: text().primaryKey(),
|
||||||
```
|
project_id: text().notNull(),
|
||||||
|
created_at: integer().notNull(),
|
||||||
|
})
|
||||||
|
|
||||||
Bad:
|
// Bad
|
||||||
|
const table = sqliteTable("session", {
|
||||||
```ts
|
id: text("id").primaryKey(),
|
||||||
const fooBar = 1
|
projectID: text("project_id").notNull(),
|
||||||
const barBaz = 2
|
createdAt: integer("created_at").notNull(),
|
||||||
const bazFoo = 3
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
You MUST avoid using `mocks` as much as possible.
|
- Avoid mocks as much as possible
|
||||||
Tests MUST test actual implementation, do not duplicate logic into a test.
|
- Test actual implementation, do not duplicate logic into tests
|
||||||
|
|||||||
@@ -258,3 +258,49 @@ These are not strictly enforced, they are just general guidelines:
|
|||||||
## Feature Requests
|
## Feature Requests
|
||||||
|
|
||||||
For net-new functionality, start with a design conversation. Open an issue describing the problem, your proposed approach (optional), and why it belongs in OpenCode. The core team will help decide whether it should move forward; please wait for that approval instead of opening a feature PR directly.
|
For net-new functionality, start with a design conversation. Open an issue describing the problem, your proposed approach (optional), and why it belongs in OpenCode. The core team will help decide whether it should move forward; please wait for that approval instead of opening a feature PR directly.
|
||||||
|
|
||||||
|
## Trust & Vouch System
|
||||||
|
|
||||||
|
This project uses [vouch](https://github.com/mitchellh/vouch) to manage contributor trust. The vouch list is maintained in [`.github/VOUCHED.td`](.github/VOUCHED.td).
|
||||||
|
|
||||||
|
### How it works
|
||||||
|
|
||||||
|
- **Vouched users** are explicitly trusted contributors.
|
||||||
|
- **Denounced users** are explicitly blocked. Issues and pull requests from denounced users are automatically closed. If you have been denounced, you can request to be unvouched by reaching out to a maintainer on [Discord](https://opencode.ai/discord)
|
||||||
|
- **Everyone else** can participate normally — you don't need to be vouched to open issues or PRs.
|
||||||
|
|
||||||
|
### For maintainers
|
||||||
|
|
||||||
|
Collaborators with write access can manage the vouch list by commenting on any issue:
|
||||||
|
|
||||||
|
- `vouch` — vouch for the issue author
|
||||||
|
- `vouch @username` — vouch for a specific user
|
||||||
|
- `denounce` — denounce the issue author
|
||||||
|
- `denounce @username` — denounce a specific user
|
||||||
|
- `denounce @username <reason>` — denounce with a reason
|
||||||
|
- `unvouch` / `unvouch @username` — remove someone from the list
|
||||||
|
|
||||||
|
Changes are committed automatically to `.github/VOUCHED.td`.
|
||||||
|
|
||||||
|
### Denouncement policy
|
||||||
|
|
||||||
|
Denouncement is reserved for users who repeatedly submit low-quality AI-generated contributions, spam, or otherwise act in bad faith. It is not used for disagreements or honest mistakes.
|
||||||
|
|
||||||
|
## Issue Requirements
|
||||||
|
|
||||||
|
All issues **must** use one of our issue templates:
|
||||||
|
|
||||||
|
- **Bug report** — for reporting bugs (requires a description)
|
||||||
|
- **Feature request** — for suggesting enhancements (requires verification checkbox and description)
|
||||||
|
- **Question** — for asking questions (requires the question)
|
||||||
|
|
||||||
|
Blank issues are not allowed. When a new issue is opened, an automated check verifies that it follows a template and meets our contributing guidelines. If an issue doesn't meet the requirements, you'll receive a comment explaining what needs to be fixed and have **2 hours** to edit the issue. After that, it will be automatically closed.
|
||||||
|
|
||||||
|
Issues may be flagged for:
|
||||||
|
|
||||||
|
- Not using a template
|
||||||
|
- Required fields left empty or filled with placeholder text
|
||||||
|
- AI-generated walls of text
|
||||||
|
- Missing meaningful content
|
||||||
|
|
||||||
|
If you believe your issue was incorrectly flagged, let a maintainer know.
|
||||||
|
|||||||
+3
-1
@@ -29,7 +29,9 @@
|
|||||||
<a href="README.ru.md">Русский</a> |
|
<a href="README.ru.md">Русский</a> |
|
||||||
<a href="README.ar.md">العربية</a> |
|
<a href="README.ar.md">العربية</a> |
|
||||||
<a href="README.no.md">Norsk</a> |
|
<a href="README.no.md">Norsk</a> |
|
||||||
<a href="README.br.md">Português (Brasil)</a>
|
<a href="README.br.md">Português (Brasil)</a> |
|
||||||
|
<a href="README.th.md">ไทย</a> |
|
||||||
|
<a href="README.tr.md">Türkçe</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://opencode.ai)
|
[](https://opencode.ai)
|
||||||
|
|||||||
+3
-1
@@ -29,7 +29,9 @@
|
|||||||
<a href="README.ru.md">Русский</a> |
|
<a href="README.ru.md">Русский</a> |
|
||||||
<a href="README.ar.md">العربية</a> |
|
<a href="README.ar.md">العربية</a> |
|
||||||
<a href="README.no.md">Norsk</a> |
|
<a href="README.no.md">Norsk</a> |
|
||||||
<a href="README.br.md">Português (Brasil)</a>
|
<a href="README.br.md">Português (Brasil)</a> |
|
||||||
|
<a href="README.th.md">ไทย</a> |
|
||||||
|
<a href="README.tr.md">Türkçe</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://opencode.ai)
|
[](https://opencode.ai)
|
||||||
|
|||||||
+136
@@ -0,0 +1,136 @@
|
|||||||
|
<p align="center">
|
||||||
|
<a href="https://opencode.ai">
|
||||||
|
<picture>
|
||||||
|
<source srcset="packages/console/app/src/asset/logo-ornate-dark.svg" media="(prefers-color-scheme: dark)">
|
||||||
|
<source srcset="packages/console/app/src/asset/logo-ornate-light.svg" media="(prefers-color-scheme: light)">
|
||||||
|
<img src="packages/console/app/src/asset/logo-ornate-light.svg" alt="OpenCode logo">
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p align="center">OpenCode je open source AI agent za programiranje.</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://opencode.ai/discord"><img alt="Discord" src="https://img.shields.io/discord/1391832426048651334?style=flat-square&label=discord" /></a>
|
||||||
|
<a href="https://www.npmjs.com/package/opencode-ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a>
|
||||||
|
<a href="https://github.com/anomalyco/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/anomalyco/opencode/publish.yml?style=flat-square&branch=dev" /></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="README.md">English</a> |
|
||||||
|
<a href="README.zh.md">简体中文</a> |
|
||||||
|
<a href="README.zht.md">繁體中文</a> |
|
||||||
|
<a href="README.ko.md">한국어</a> |
|
||||||
|
<a href="README.de.md">Deutsch</a> |
|
||||||
|
<a href="README.es.md">Español</a> |
|
||||||
|
<a href="README.fr.md">Français</a> |
|
||||||
|
<a href="README.it.md">Italiano</a> |
|
||||||
|
<a href="README.da.md">Dansk</a> |
|
||||||
|
<a href="README.ja.md">日本語</a> |
|
||||||
|
<a href="README.pl.md">Polski</a> |
|
||||||
|
<a href="README.ru.md">Русский</a> |
|
||||||
|
<a href="README.bs.md">Bosanski</a> |
|
||||||
|
<a href="README.ar.md">العربية</a> |
|
||||||
|
<a href="README.no.md">Norsk</a> |
|
||||||
|
<a href="README.br.md">Português (Brasil)</a> |
|
||||||
|
<a href="README.th.md">ไทย</a> |
|
||||||
|
<a href="README.tr.md">Türkçe</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
[](https://opencode.ai)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Instalacija
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# YOLO
|
||||||
|
curl -fsSL https://opencode.ai/install | bash
|
||||||
|
|
||||||
|
# Package manageri
|
||||||
|
npm i -g opencode-ai@latest # ili bun/pnpm/yarn
|
||||||
|
scoop install opencode # Windows
|
||||||
|
choco install opencode # Windows
|
||||||
|
brew install anomalyco/tap/opencode # macOS i Linux (preporučeno, uvijek ažurno)
|
||||||
|
brew install opencode # macOS i Linux (zvanična brew formula, rjeđe se ažurira)
|
||||||
|
paru -S opencode-bin # Arch Linux
|
||||||
|
mise use -g opencode # Bilo koji OS
|
||||||
|
nix run nixpkgs#opencode # ili github:anomalyco/opencode za najnoviji dev branch
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> Ukloni verzije starije od 0.1.x prije instalacije.
|
||||||
|
|
||||||
|
### Desktop aplikacija (BETA)
|
||||||
|
|
||||||
|
OpenCode je dostupan i kao desktop aplikacija. Preuzmi je direktno sa [stranice izdanja](https://github.com/anomalyco/opencode/releases) ili sa [opencode.ai/download](https://opencode.ai/download).
|
||||||
|
|
||||||
|
| Platforma | Preuzimanje |
|
||||||
|
| --------------------- | ------------------------------------- |
|
||||||
|
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
|
||||||
|
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
|
||||||
|
| Windows | `opencode-desktop-windows-x64.exe` |
|
||||||
|
| Linux | `.deb`, `.rpm`, ili AppImage |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# macOS (Homebrew)
|
||||||
|
brew install --cask opencode-desktop
|
||||||
|
# Windows (Scoop)
|
||||||
|
scoop bucket add extras; scoop install extras/opencode-desktop
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Instalacijski direktorij
|
||||||
|
|
||||||
|
Instalacijska skripta koristi sljedeći redoslijed prioriteta za putanju instalacije:
|
||||||
|
|
||||||
|
1. `$OPENCODE_INSTALL_DIR` - Prilagođeni instalacijski direktorij
|
||||||
|
2. `$XDG_BIN_DIR` - Putanja usklađena sa XDG Base Directory specifikacijom
|
||||||
|
3. `$HOME/bin` - Standardni korisnički bin direktorij (ako postoji ili se može kreirati)
|
||||||
|
4. `$HOME/.opencode/bin` - Podrazumijevana rezervna lokacija
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Primjeri
|
||||||
|
OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash
|
||||||
|
XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
### Agenti
|
||||||
|
|
||||||
|
OpenCode uključuje dva ugrađena agenta između kojih možeš prebacivati tasterom `Tab`.
|
||||||
|
|
||||||
|
- **build** - Podrazumijevani agent sa punim pristupom za razvoj
|
||||||
|
- **plan** - Agent samo za čitanje za analizu i istraživanje koda
|
||||||
|
- Podrazumijevano zabranjuje izmjene datoteka
|
||||||
|
- Traži dozvolu prije pokretanja bash komandi
|
||||||
|
- Idealan za istraživanje nepoznatih codebase-ova ili planiranje izmjena
|
||||||
|
|
||||||
|
Uključen je i **general** pod-agent za složene pretrage i višekoračne zadatke.
|
||||||
|
Koristi se interno i može se pozvati pomoću `@general` u porukama.
|
||||||
|
|
||||||
|
Saznaj više o [agentima](https://opencode.ai/docs/agents).
|
||||||
|
|
||||||
|
### Dokumentacija
|
||||||
|
|
||||||
|
Za više informacija o konfiguraciji OpenCode-a, [**pogledaj dokumentaciju**](https://opencode.ai/docs).
|
||||||
|
|
||||||
|
### Doprinosi
|
||||||
|
|
||||||
|
Ako želiš doprinositi OpenCode-u, pročitaj [upute za doprinošenje](./CONTRIBUTING.md) prije slanja pull requesta.
|
||||||
|
|
||||||
|
### Gradnja na OpenCode-u
|
||||||
|
|
||||||
|
Ako radiš na projektu koji je povezan s OpenCode-om i koristi "opencode" kao dio naziva, npr. "opencode-dashboard" ili "opencode-mobile", dodaj napomenu u svoj README da projekat nije napravio OpenCode tim i da nije povezan s nama.
|
||||||
|
|
||||||
|
### FAQ
|
||||||
|
|
||||||
|
#### Po čemu se razlikuje od Claude Code-a?
|
||||||
|
|
||||||
|
Po mogućnostima je vrlo sličan Claude Code-u. Ključne razlike su:
|
||||||
|
|
||||||
|
- 100% open source
|
||||||
|
- Nije vezan za jednog provajdera. Iako preporučujemo modele koje nudimo kroz [OpenCode Zen](https://opencode.ai/zen), OpenCode možeš koristiti s Claude, OpenAI, Google ili čak lokalnim modelima. Kako modeli napreduju, razlike među njima će se smanjivati, a cijene padati, zato je nezavisnost od provajdera važna.
|
||||||
|
- LSP podrška odmah po instalaciji
|
||||||
|
- Fokus na TUI. OpenCode grade neovim korisnici i kreatori [terminal.shop](https://terminal.shop); pomjeraćemo granice onoga što je moguće u terminalu.
|
||||||
|
- Klijent/server arhitektura. To, recimo, omogućava da OpenCode radi na tvom računaru dok ga daljinski koristiš iz mobilne aplikacije, što znači da je TUI frontend samo jedan od mogućih klijenata.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Pridruži se našoj zajednici** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode)
|
||||||
+3
-1
@@ -29,7 +29,9 @@
|
|||||||
<a href="README.ru.md">Русский</a> |
|
<a href="README.ru.md">Русский</a> |
|
||||||
<a href="README.ar.md">العربية</a> |
|
<a href="README.ar.md">العربية</a> |
|
||||||
<a href="README.no.md">Norsk</a> |
|
<a href="README.no.md">Norsk</a> |
|
||||||
<a href="README.br.md">Português (Brasil)</a>
|
<a href="README.br.md">Português (Brasil)</a> |
|
||||||
|
<a href="README.th.md">ไทย</a> |
|
||||||
|
<a href="README.tr.md">Türkçe</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://opencode.ai)
|
[](https://opencode.ai)
|
||||||
|
|||||||
+3
-1
@@ -29,7 +29,9 @@
|
|||||||
<a href="README.ru.md">Русский</a> |
|
<a href="README.ru.md">Русский</a> |
|
||||||
<a href="README.ar.md">العربية</a> |
|
<a href="README.ar.md">العربية</a> |
|
||||||
<a href="README.no.md">Norsk</a> |
|
<a href="README.no.md">Norsk</a> |
|
||||||
<a href="README.br.md">Português (Brasil)</a>
|
<a href="README.br.md">Português (Brasil)</a> |
|
||||||
|
<a href="README.th.md">ไทย</a> |
|
||||||
|
<a href="README.tr.md">Türkçe</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://opencode.ai)
|
[](https://opencode.ai)
|
||||||
|
|||||||
+3
-1
@@ -29,7 +29,9 @@
|
|||||||
<a href="README.ru.md">Русский</a> |
|
<a href="README.ru.md">Русский</a> |
|
||||||
<a href="README.ar.md">العربية</a> |
|
<a href="README.ar.md">العربية</a> |
|
||||||
<a href="README.no.md">Norsk</a> |
|
<a href="README.no.md">Norsk</a> |
|
||||||
<a href="README.br.md">Português (Brasil)</a>
|
<a href="README.br.md">Português (Brasil)</a> |
|
||||||
|
<a href="README.th.md">ไทย</a> |
|
||||||
|
<a href="README.tr.md">Türkçe</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://opencode.ai)
|
[](https://opencode.ai)
|
||||||
|
|||||||
+3
-1
@@ -29,7 +29,9 @@
|
|||||||
<a href="README.ru.md">Русский</a> |
|
<a href="README.ru.md">Русский</a> |
|
||||||
<a href="README.ar.md">العربية</a> |
|
<a href="README.ar.md">العربية</a> |
|
||||||
<a href="README.no.md">Norsk</a> |
|
<a href="README.no.md">Norsk</a> |
|
||||||
<a href="README.br.md">Português (Brasil)</a>
|
<a href="README.br.md">Português (Brasil)</a> |
|
||||||
|
<a href="README.th.md">ไทย</a> |
|
||||||
|
<a href="README.tr.md">Türkçe</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://opencode.ai)
|
[](https://opencode.ai)
|
||||||
|
|||||||
+3
-1
@@ -29,7 +29,9 @@
|
|||||||
<a href="README.ru.md">Русский</a> |
|
<a href="README.ru.md">Русский</a> |
|
||||||
<a href="README.ar.md">العربية</a> |
|
<a href="README.ar.md">العربية</a> |
|
||||||
<a href="README.no.md">Norsk</a> |
|
<a href="README.no.md">Norsk</a> |
|
||||||
<a href="README.br.md">Português (Brasil)</a>
|
<a href="README.br.md">Português (Brasil)</a> |
|
||||||
|
<a href="README.th.md">ไทย</a> |
|
||||||
|
<a href="README.tr.md">Türkçe</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://opencode.ai)
|
[](https://opencode.ai)
|
||||||
|
|||||||
+3
-1
@@ -29,7 +29,9 @@
|
|||||||
<a href="README.ru.md">Русский</a> |
|
<a href="README.ru.md">Русский</a> |
|
||||||
<a href="README.ar.md">العربية</a> |
|
<a href="README.ar.md">العربية</a> |
|
||||||
<a href="README.no.md">Norsk</a> |
|
<a href="README.no.md">Norsk</a> |
|
||||||
<a href="README.br.md">Português (Brasil)</a>
|
<a href="README.br.md">Português (Brasil)</a> |
|
||||||
|
<a href="README.th.md">ไทย</a> |
|
||||||
|
<a href="README.tr.md">Türkçe</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://opencode.ai)
|
[](https://opencode.ai)
|
||||||
|
|||||||
+3
-1
@@ -29,7 +29,9 @@
|
|||||||
<a href="README.ru.md">Русский</a> |
|
<a href="README.ru.md">Русский</a> |
|
||||||
<a href="README.ar.md">العربية</a> |
|
<a href="README.ar.md">العربية</a> |
|
||||||
<a href="README.no.md">Norsk</a> |
|
<a href="README.no.md">Norsk</a> |
|
||||||
<a href="README.br.md">Português (Brasil)</a>
|
<a href="README.br.md">Português (Brasil)</a> |
|
||||||
|
<a href="README.th.md">ไทย</a> |
|
||||||
|
<a href="README.tr.md">Türkçe</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://opencode.ai)
|
[](https://opencode.ai)
|
||||||
|
|||||||
@@ -27,10 +27,12 @@
|
|||||||
<a href="README.ja.md">日本語</a> |
|
<a href="README.ja.md">日本語</a> |
|
||||||
<a href="README.pl.md">Polski</a> |
|
<a href="README.pl.md">Polski</a> |
|
||||||
<a href="README.ru.md">Русский</a> |
|
<a href="README.ru.md">Русский</a> |
|
||||||
|
<a href="README.bs.md">Bosanski</a> |
|
||||||
<a href="README.ar.md">العربية</a> |
|
<a href="README.ar.md">العربية</a> |
|
||||||
<a href="README.no.md">Norsk</a> |
|
<a href="README.no.md">Norsk</a> |
|
||||||
<a href="README.br.md">Português (Brasil)</a> |
|
<a href="README.br.md">Português (Brasil)</a> |
|
||||||
<a href="README.th.md">ไทย</a>
|
<a href="README.th.md">ไทย</a> |
|
||||||
|
<a href="README.tr.md">Türkçe</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://opencode.ai)
|
[](https://opencode.ai)
|
||||||
@@ -81,7 +83,7 @@ The install script respects the following priority order for the installation pa
|
|||||||
|
|
||||||
1. `$OPENCODE_INSTALL_DIR` - Custom installation directory
|
1. `$OPENCODE_INSTALL_DIR` - Custom installation directory
|
||||||
2. `$XDG_BIN_DIR` - XDG Base Directory Specification compliant path
|
2. `$XDG_BIN_DIR` - XDG Base Directory Specification compliant path
|
||||||
3. `$HOME/bin` - Standard user binary directory (if exists or can be created)
|
3. `$HOME/bin` - Standard user binary directory (if it exists or can be created)
|
||||||
4. `$HOME/.opencode/bin` - Default fallback
|
4. `$HOME/.opencode/bin` - Default fallback
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -94,20 +96,20 @@ XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash
|
|||||||
|
|
||||||
OpenCode includes two built-in agents you can switch between with the `Tab` key.
|
OpenCode includes two built-in agents you can switch between with the `Tab` key.
|
||||||
|
|
||||||
- **build** - Default, full access agent for development work
|
- **build** - Default, full-access agent for development work
|
||||||
- **plan** - Read-only agent for analysis and code exploration
|
- **plan** - Read-only agent for analysis and code exploration
|
||||||
- Denies file edits by default
|
- Denies file edits by default
|
||||||
- Asks permission before running bash commands
|
- Asks permission before running bash commands
|
||||||
- Ideal for exploring unfamiliar codebases or planning changes
|
- Ideal for exploring unfamiliar codebases or planning changes
|
||||||
|
|
||||||
Also, included is a **general** subagent for complex searches and multistep tasks.
|
Also included is a **general** subagent for complex searches and multistep tasks.
|
||||||
This is used internally and can be invoked using `@general` in messages.
|
This is used internally and can be invoked using `@general` in messages.
|
||||||
|
|
||||||
Learn more about [agents](https://opencode.ai/docs/agents).
|
Learn more about [agents](https://opencode.ai/docs/agents).
|
||||||
|
|
||||||
### Documentation
|
### Documentation
|
||||||
|
|
||||||
For more info on how to configure OpenCode [**head over to our docs**](https://opencode.ai/docs).
|
For more info on how to configure OpenCode, [**head over to our docs**](https://opencode.ai/docs).
|
||||||
|
|
||||||
### Contributing
|
### Contributing
|
||||||
|
|
||||||
@@ -115,7 +117,7 @@ If you're interested in contributing to OpenCode, please read our [contributing
|
|||||||
|
|
||||||
### Building on OpenCode
|
### Building on OpenCode
|
||||||
|
|
||||||
If you are working on a project that's related to OpenCode and is using "opencode" as a part of its name; for example, "opencode-dashboard" or "opencode-mobile", please add a note to your README to clarify that it is not built by the OpenCode team and is not affiliated with us in any way.
|
If you are working on a project that's related to OpenCode and is using "opencode" as part of its name, for example "opencode-dashboard" or "opencode-mobile", please add a note to your README to clarify that it is not built by the OpenCode team and is not affiliated with us in any way.
|
||||||
|
|
||||||
### FAQ
|
### FAQ
|
||||||
|
|
||||||
@@ -124,10 +126,10 @@ If you are working on a project that's related to OpenCode and is using "opencod
|
|||||||
It's very similar to Claude Code in terms of capability. Here are the key differences:
|
It's very similar to Claude Code in terms of capability. Here are the key differences:
|
||||||
|
|
||||||
- 100% open source
|
- 100% open source
|
||||||
- Not coupled to any provider. Although we recommend the models we provide through [OpenCode Zen](https://opencode.ai/zen); OpenCode can be used with Claude, OpenAI, Google or even local models. As models evolve the gaps between them will close and pricing will drop so being provider-agnostic is important.
|
- Not coupled to any provider. Although we recommend the models we provide through [OpenCode Zen](https://opencode.ai/zen), OpenCode can be used with Claude, OpenAI, Google, or even local models. As models evolve, the gaps between them will close and pricing will drop, so being provider-agnostic is important.
|
||||||
- Out of the box LSP support
|
- Out-of-the-box LSP support
|
||||||
- A focus on TUI. OpenCode is built by neovim users and the creators of [terminal.shop](https://terminal.shop); we are going to push the limits of what's possible in the terminal.
|
- A focus on TUI. OpenCode is built by neovim users and the creators of [terminal.shop](https://terminal.shop); we are going to push the limits of what's possible in the terminal.
|
||||||
- A client/server architecture. This for example can allow OpenCode to run on your computer, while you can drive it remotely from a mobile app. Meaning that the TUI frontend is just one of the possible clients.
|
- A client/server architecture. This, for example, can allow OpenCode to run on your computer while you drive it remotely from a mobile app, meaning that the TUI frontend is just one of the possible clients.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -29,7 +29,9 @@
|
|||||||
<a href="README.ru.md">Русский</a> |
|
<a href="README.ru.md">Русский</a> |
|
||||||
<a href="README.ar.md">العربية</a> |
|
<a href="README.ar.md">العربية</a> |
|
||||||
<a href="README.no.md">Norsk</a> |
|
<a href="README.no.md">Norsk</a> |
|
||||||
<a href="README.br.md">Português (Brasil)</a>
|
<a href="README.br.md">Português (Brasil)</a> |
|
||||||
|
<a href="README.th.md">ไทย</a> |
|
||||||
|
<a href="README.tr.md">Türkçe</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://opencode.ai)
|
[](https://opencode.ai)
|
||||||
|
|||||||
+3
-1
@@ -29,7 +29,9 @@
|
|||||||
<a href="README.ru.md">Русский</a> |
|
<a href="README.ru.md">Русский</a> |
|
||||||
<a href="README.ar.md">العربية</a> |
|
<a href="README.ar.md">العربية</a> |
|
||||||
<a href="README.no.md">Norsk</a> |
|
<a href="README.no.md">Norsk</a> |
|
||||||
<a href="README.br.md">Português (Brasil)</a>
|
<a href="README.br.md">Português (Brasil)</a> |
|
||||||
|
<a href="README.th.md">ไทย</a> |
|
||||||
|
<a href="README.tr.md">Türkçe</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://opencode.ai)
|
[](https://opencode.ai)
|
||||||
|
|||||||
+3
-1
@@ -29,7 +29,9 @@
|
|||||||
<a href="README.ru.md">Русский</a> |
|
<a href="README.ru.md">Русский</a> |
|
||||||
<a href="README.ar.md">العربية</a> |
|
<a href="README.ar.md">العربية</a> |
|
||||||
<a href="README.no.md">Norsk</a> |
|
<a href="README.no.md">Norsk</a> |
|
||||||
<a href="README.br.md">Português (Brasil)</a>
|
<a href="README.br.md">Português (Brasil)</a> |
|
||||||
|
<a href="README.th.md">ไทย</a> |
|
||||||
|
<a href="README.tr.md">Türkçe</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://opencode.ai)
|
[](https://opencode.ai)
|
||||||
|
|||||||
+2
-1
@@ -30,7 +30,8 @@
|
|||||||
<a href="README.ar.md">العربية</a> |
|
<a href="README.ar.md">العربية</a> |
|
||||||
<a href="README.no.md">Norsk</a> |
|
<a href="README.no.md">Norsk</a> |
|
||||||
<a href="README.br.md">Português (Brasil)</a> |
|
<a href="README.br.md">Português (Brasil)</a> |
|
||||||
<a href="README.th.md">ไทย</a>
|
<a href="README.th.md">ไทย</a> |
|
||||||
|
<a href="README.tr.md">Türkçe</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://opencode.ai)
|
[](https://opencode.ai)
|
||||||
|
|||||||
+135
@@ -0,0 +1,135 @@
|
|||||||
|
<p align="center">
|
||||||
|
<a href="https://opencode.ai">
|
||||||
|
<picture>
|
||||||
|
<source srcset="packages/console/app/src/asset/logo-ornate-dark.svg" media="(prefers-color-scheme: dark)">
|
||||||
|
<source srcset="packages/console/app/src/asset/logo-ornate-light.svg" media="(prefers-color-scheme: light)">
|
||||||
|
<img src="packages/console/app/src/asset/logo-ornate-light.svg" alt="OpenCode logo">
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p align="center">Açık kaynaklı yapay zeka kodlama asistanı.</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://opencode.ai/discord"><img alt="Discord" src="https://img.shields.io/discord/1391832426048651334?style=flat-square&label=discord" /></a>
|
||||||
|
<a href="https://www.npmjs.com/package/opencode-ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a>
|
||||||
|
<a href="https://github.com/anomalyco/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/anomalyco/opencode/publish.yml?style=flat-square&branch=dev" /></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="README.md">English</a> |
|
||||||
|
<a href="README.zh.md">简体中文</a> |
|
||||||
|
<a href="README.zht.md">繁體中文</a> |
|
||||||
|
<a href="README.ko.md">한국어</a> |
|
||||||
|
<a href="README.de.md">Deutsch</a> |
|
||||||
|
<a href="README.es.md">Español</a> |
|
||||||
|
<a href="README.fr.md">Français</a> |
|
||||||
|
<a href="README.it.md">Italiano</a> |
|
||||||
|
<a href="README.da.md">Dansk</a> |
|
||||||
|
<a href="README.ja.md">日本語</a> |
|
||||||
|
<a href="README.pl.md">Polski</a> |
|
||||||
|
<a href="README.ru.md">Русский</a> |
|
||||||
|
<a href="README.ar.md">العربية</a> |
|
||||||
|
<a href="README.no.md">Norsk</a> |
|
||||||
|
<a href="README.br.md">Português (Brasil)</a> |
|
||||||
|
<a href="README.th.md">ไทย</a> |
|
||||||
|
<a href="README.tr.md">Türkçe</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
[](https://opencode.ai)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Kurulum
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# YOLO
|
||||||
|
curl -fsSL https://opencode.ai/install | bash
|
||||||
|
|
||||||
|
# Paket yöneticileri
|
||||||
|
npm i -g opencode-ai@latest # veya bun/pnpm/yarn
|
||||||
|
scoop install opencode # Windows
|
||||||
|
choco install opencode # Windows
|
||||||
|
brew install anomalyco/tap/opencode # macOS ve Linux (önerilir, her zaman güncel)
|
||||||
|
brew install opencode # macOS ve Linux (resmi brew formülü, daha az güncellenir)
|
||||||
|
paru -S opencode-bin # Arch Linux
|
||||||
|
mise use -g opencode # Tüm işletim sistemleri
|
||||||
|
nix run nixpkgs#opencode # veya en güncel geliştirme dalı için github:anomalyco/opencode
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> Kurulumdan önce 0.1.x'ten eski sürümleri kaldırın.
|
||||||
|
|
||||||
|
### Masaüstü Uygulaması (BETA)
|
||||||
|
|
||||||
|
OpenCode ayrıca masaüstü uygulaması olarak da mevcuttur. Doğrudan [sürüm sayfasından](https://github.com/anomalyco/opencode/releases) veya [opencode.ai/download](https://opencode.ai/download) adresinden indirebilirsiniz.
|
||||||
|
|
||||||
|
| Platform | İndirme |
|
||||||
|
| --------------------- | ------------------------------------- |
|
||||||
|
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
|
||||||
|
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
|
||||||
|
| Windows | `opencode-desktop-windows-x64.exe` |
|
||||||
|
| Linux | `.deb`, `.rpm` veya AppImage |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# macOS (Homebrew)
|
||||||
|
brew install --cask opencode-desktop
|
||||||
|
# Windows (Scoop)
|
||||||
|
scoop bucket add extras; scoop install extras/opencode-desktop
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Kurulum Dizini (Installation Directory)
|
||||||
|
|
||||||
|
Kurulum betiği (install script), kurulum yolu (installation path) için aşağıdaki öncelik sırasını takip eder:
|
||||||
|
|
||||||
|
1. `$OPENCODE_INSTALL_DIR` - Özel kurulum dizini
|
||||||
|
2. `$XDG_BIN_DIR` - XDG Base Directory Specification uyumlu yol
|
||||||
|
3. `$HOME/bin` - Standart kullanıcı binary dizini (varsa veya oluşturulabiliyorsa)
|
||||||
|
4. `$HOME/.opencode/bin` - Varsayılan yedek konum
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Örnekler
|
||||||
|
OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash
|
||||||
|
XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ajanlar
|
||||||
|
|
||||||
|
OpenCode, `Tab` tuşuyla aralarında geçiş yapabileceğiniz iki yerleşik (built-in) ajan içerir.
|
||||||
|
|
||||||
|
- **build** - Varsayılan, geliştirme çalışmaları için tam erişimli ajan
|
||||||
|
- **plan** - Analiz ve kod keşfi için salt okunur ajan
|
||||||
|
- Varsayılan olarak dosya düzenlemelerini reddeder
|
||||||
|
- Bash komutlarını çalıştırmadan önce izin ister
|
||||||
|
- Tanımadığınız kod tabanlarını keşfetmek veya değişiklikleri planlamak için ideal
|
||||||
|
|
||||||
|
Ayrıca, karmaşık aramalar ve çok adımlı görevler için bir **genel** alt ajan bulunmaktadır.
|
||||||
|
Bu dahili olarak kullanılır ve mesajlarda `@general` ile çağrılabilir.
|
||||||
|
|
||||||
|
[Ajanlar](https://opencode.ai/docs/agents) hakkında daha fazla bilgi edinin.
|
||||||
|
|
||||||
|
### Dokümantasyon
|
||||||
|
|
||||||
|
OpenCode'u nasıl yapılandıracağınız hakkında daha fazla bilgi için [**dokümantasyonumuza göz atın**](https://opencode.ai/docs).
|
||||||
|
|
||||||
|
### Katkıda Bulunma
|
||||||
|
|
||||||
|
OpenCode'a katkıda bulunmak istiyorsanız, lütfen bir pull request göndermeden önce [katkıda bulunma dokümanlarımızı](./CONTRIBUTING.md) okuyun.
|
||||||
|
|
||||||
|
### OpenCode Üzerine Geliştirme
|
||||||
|
|
||||||
|
OpenCode ile ilgili bir proje üzerinde çalışıyorsanız ve projenizin adının bir parçası olarak "opencode" kullanıyorsanız (örneğin, "opencode-dashboard" veya "opencode-mobile"), lütfen README dosyanıza projenin OpenCode ekibi tarafından geliştirilmediğini ve bizimle hiçbir şekilde bağlantılı olmadığını belirten bir not ekleyin.
|
||||||
|
|
||||||
|
### SSS
|
||||||
|
|
||||||
|
#### Bu Claude Code'dan nasıl farklı?
|
||||||
|
|
||||||
|
Yetenekler açısından Claude Code'a çok benzer. İşte temel farklar:
|
||||||
|
|
||||||
|
- %100 açık kaynak
|
||||||
|
- Herhangi bir sağlayıcıya bağlı değil. [OpenCode Zen](https://opencode.ai/zen) üzerinden sunduğumuz modelleri önermekle birlikte; OpenCode, Claude, OpenAI, Google veya hatta yerel modellerle kullanılabilir. Modeller geliştikçe aralarındaki farklar kapanacak ve fiyatlar düşecek, bu nedenle sağlayıcıdan bağımsız olmak önemlidir.
|
||||||
|
- Kurulum gerektirmeyen hazır LSP desteği
|
||||||
|
- TUI odaklı yaklaşım. OpenCode, neovim kullanıcıları ve [terminal.shop](https://terminal.shop)'un geliştiricileri tarafından geliştirilmektedir; terminalde olabileceklerin sınırlarını zorlayacağız.
|
||||||
|
- İstemci/sunucu (client/server) mimarisi. Bu, örneğin OpenCode'un bilgisayarınızda çalışması ve siz onu bir mobil uygulamadan uzaktan yönetmenizi sağlar. TUI arayüzü olası istemcilerden sadece biridir.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Topluluğumuza katılın** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode)
|
||||||
+3
-1
@@ -29,7 +29,9 @@
|
|||||||
<a href="README.ru.md">Русский</a> |
|
<a href="README.ru.md">Русский</a> |
|
||||||
<a href="README.ar.md">العربية</a> |
|
<a href="README.ar.md">العربية</a> |
|
||||||
<a href="README.no.md">Norsk</a> |
|
<a href="README.no.md">Norsk</a> |
|
||||||
<a href="README.br.md">Português (Brasil)</a>
|
<a href="README.br.md">Português (Brasil)</a> |
|
||||||
|
<a href="README.th.md">ไทย</a> |
|
||||||
|
<a href="README.tr.md">Türkçe</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://opencode.ai)
|
[](https://opencode.ai)
|
||||||
|
|||||||
+4
-2
@@ -29,7 +29,9 @@
|
|||||||
<a href="README.ru.md">Русский</a> |
|
<a href="README.ru.md">Русский</a> |
|
||||||
<a href="README.ar.md">العربية</a> |
|
<a href="README.ar.md">العربية</a> |
|
||||||
<a href="README.no.md">Norsk</a> |
|
<a href="README.no.md">Norsk</a> |
|
||||||
<a href="README.br.md">Português (Brasil)</a>
|
<a href="README.br.md">Português (Brasil)</a> |
|
||||||
|
<a href="README.th.md">ไทย</a> |
|
||||||
|
<a href="README.tr.md">Türkçe</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://opencode.ai)
|
[](https://opencode.ai)
|
||||||
@@ -124,7 +126,7 @@ OpenCode 內建了兩種 Agent,您可以使用 `Tab` 鍵快速切換。
|
|||||||
- 100% 開源。
|
- 100% 開源。
|
||||||
- 不綁定特定的服務提供商。雖然我們推薦使用透過 [OpenCode Zen](https://opencode.ai/zen) 提供的模型,但 OpenCode 也可搭配 Claude, OpenAI, Google 甚至本地模型使用。隨著模型不斷演進,彼此間的差距會縮小且價格會下降,因此具備「不限廠商 (provider-agnostic)」的特性至關重要。
|
- 不綁定特定的服務提供商。雖然我們推薦使用透過 [OpenCode Zen](https://opencode.ai/zen) 提供的模型,但 OpenCode 也可搭配 Claude, OpenAI, Google 甚至本地模型使用。隨著模型不斷演進,彼此間的差距會縮小且價格會下降,因此具備「不限廠商 (provider-agnostic)」的特性至關重要。
|
||||||
- 內建 LSP (語言伺服器協定) 支援。
|
- 內建 LSP (語言伺服器協定) 支援。
|
||||||
- 專注於終端機介面 (TUI)。OpenCode 由 Neovim 愛好者與 [terminal.shop](https://terminal.shop) 的創作者打造;我們將不斷挑戰終端機介面的極限。
|
- 專注於終端機介面 (TUI)。OpenCode 由 Neovim 愛好者與 [terminal.shop](https://terminal.shop) 的創作者打造。我們將不斷挑戰終端機介面的極限。
|
||||||
- 客戶端/伺服器架構 (Client/Server Architecture)。這讓 OpenCode 能夠在您的電腦上運行的同時,由行動裝置進行遠端操控。這意味著 TUI 前端只是眾多可能的客戶端之一。
|
- 客戶端/伺服器架構 (Client/Server Architecture)。這讓 OpenCode 能夠在您的電腦上運行的同時,由行動裝置進行遠端操控。這意味著 TUI 前端只是眾多可能的客戶端之一。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
},
|
},
|
||||||
"packages/app": {
|
"packages/app": {
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.1.48",
|
"version": "1.1.60",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
"@thisbeyond/solid-dnd": "0.7.5",
|
"@thisbeyond/solid-dnd": "0.7.5",
|
||||||
"diff": "catalog:",
|
"diff": "catalog:",
|
||||||
"fuzzysort": "catalog:",
|
"fuzzysort": "catalog:",
|
||||||
"ghostty-web": "0.3.0",
|
"ghostty-web": "0.4.0",
|
||||||
"luxon": "catalog:",
|
"luxon": "catalog:",
|
||||||
"marked": "catalog:",
|
"marked": "catalog:",
|
||||||
"marked-shiki": "catalog:",
|
"marked-shiki": "catalog:",
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/app": {
|
"packages/console/app": {
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.1.48",
|
"version": "1.1.60",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/vite-plugin": "1.15.2",
|
"@cloudflare/vite-plugin": "1.15.2",
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
@@ -107,7 +107,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/core": {
|
"packages/console/core": {
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.1.48",
|
"version": "1.1.60",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-sts": "3.782.0",
|
"@aws-sdk/client-sts": "3.782.0",
|
||||||
"@jsx-email/render": "1.1.1",
|
"@jsx-email/render": "1.1.1",
|
||||||
@@ -134,7 +134,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/function": {
|
"packages/console/function": {
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.1.48",
|
"version": "1.1.60",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "2.0.0",
|
"@ai-sdk/anthropic": "2.0.0",
|
||||||
"@ai-sdk/openai": "2.0.2",
|
"@ai-sdk/openai": "2.0.2",
|
||||||
@@ -158,7 +158,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/mail": {
|
"packages/console/mail": {
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.1.48",
|
"version": "1.1.60",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
@@ -182,13 +182,15 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop": {
|
"packages/desktop": {
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.1.48",
|
"version": "1.1.60",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/app": "workspace:*",
|
"@opencode-ai/app": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
"@solid-primitives/i18n": "2.2.1",
|
"@solid-primitives/i18n": "2.2.1",
|
||||||
"@solid-primitives/storage": "catalog:",
|
"@solid-primitives/storage": "catalog:",
|
||||||
|
"@solidjs/meta": "catalog:",
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
|
"@tauri-apps/plugin-clipboard-manager": "~2",
|
||||||
"@tauri-apps/plugin-deep-link": "~2",
|
"@tauri-apps/plugin-deep-link": "~2",
|
||||||
"@tauri-apps/plugin-dialog": "~2",
|
"@tauri-apps/plugin-dialog": "~2",
|
||||||
"@tauri-apps/plugin-http": "~2",
|
"@tauri-apps/plugin-http": "~2",
|
||||||
@@ -213,7 +215,7 @@
|
|||||||
},
|
},
|
||||||
"packages/enterprise": {
|
"packages/enterprise": {
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.1.48",
|
"version": "1.1.60",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
@@ -242,7 +244,7 @@
|
|||||||
},
|
},
|
||||||
"packages/function": {
|
"packages/function": {
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.1.48",
|
"version": "1.1.60",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-app": "8.0.1",
|
"@octokit/auth-app": "8.0.1",
|
||||||
"@octokit/rest": "catalog:",
|
"@octokit/rest": "catalog:",
|
||||||
@@ -258,14 +260,14 @@
|
|||||||
},
|
},
|
||||||
"packages/opencode": {
|
"packages/opencode": {
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"version": "1.1.48",
|
"version": "1.1.60",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "1.11.1",
|
"@actions/core": "1.11.1",
|
||||||
"@actions/github": "6.0.1",
|
"@actions/github": "6.0.1",
|
||||||
"@agentclientprotocol/sdk": "0.13.0",
|
"@agentclientprotocol/sdk": "0.14.1",
|
||||||
"@ai-sdk/amazon-bedrock": "3.0.74",
|
"@ai-sdk/amazon-bedrock": "3.0.74",
|
||||||
"@ai-sdk/anthropic": "2.0.58",
|
"@ai-sdk/anthropic": "2.0.58",
|
||||||
"@ai-sdk/azure": "2.0.91",
|
"@ai-sdk/azure": "2.0.91",
|
||||||
@@ -284,9 +286,10 @@
|
|||||||
"@ai-sdk/provider-utils": "3.0.20",
|
"@ai-sdk/provider-utils": "3.0.20",
|
||||||
"@ai-sdk/togetherai": "1.0.34",
|
"@ai-sdk/togetherai": "1.0.34",
|
||||||
"@ai-sdk/vercel": "1.0.33",
|
"@ai-sdk/vercel": "1.0.33",
|
||||||
"@ai-sdk/xai": "2.0.56",
|
"@ai-sdk/xai": "2.0.51",
|
||||||
"@clack/prompts": "1.0.0-alpha.1",
|
"@clack/prompts": "1.0.0-alpha.1",
|
||||||
"@gitlab/gitlab-ai-provider": "3.3.1",
|
"@gitlab/gitlab-ai-provider": "3.5.0",
|
||||||
|
"@gitlab/opencode-gitlab-auth": "1.3.2",
|
||||||
"@hono/standard-validator": "0.1.5",
|
"@hono/standard-validator": "0.1.5",
|
||||||
"@hono/zod-validator": "catalog:",
|
"@hono/zod-validator": "catalog:",
|
||||||
"@modelcontextprotocol/sdk": "1.25.2",
|
"@modelcontextprotocol/sdk": "1.25.2",
|
||||||
@@ -298,8 +301,8 @@
|
|||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
"@openrouter/ai-sdk-provider": "1.5.4",
|
"@openrouter/ai-sdk-provider": "1.5.4",
|
||||||
"@opentui/core": "0.1.75",
|
"@opentui/core": "0.1.79",
|
||||||
"@opentui/solid": "0.1.75",
|
"@opentui/solid": "0.1.79",
|
||||||
"@parcel/watcher": "2.5.1",
|
"@parcel/watcher": "2.5.1",
|
||||||
"@pierre/diffs": "catalog:",
|
"@pierre/diffs": "catalog:",
|
||||||
"@solid-primitives/event-bus": "1.1.2",
|
"@solid-primitives/event-bus": "1.1.2",
|
||||||
@@ -307,8 +310,9 @@
|
|||||||
"@standard-schema/spec": "1.0.0",
|
"@standard-schema/spec": "1.0.0",
|
||||||
"@zip.js/zip.js": "2.7.62",
|
"@zip.js/zip.js": "2.7.62",
|
||||||
"ai": "catalog:",
|
"ai": "catalog:",
|
||||||
|
"ai-gateway-provider": "2.3.1",
|
||||||
"bonjour-service": "1.3.0",
|
"bonjour-service": "1.3.0",
|
||||||
"bun-pty": "0.4.4",
|
"bun-pty": "0.4.8",
|
||||||
"chokidar": "4.0.3",
|
"chokidar": "4.0.3",
|
||||||
"clipboardy": "4.0.0",
|
"clipboardy": "4.0.0",
|
||||||
"decimal.js": "10.5.0",
|
"decimal.js": "10.5.0",
|
||||||
@@ -362,7 +366,7 @@
|
|||||||
},
|
},
|
||||||
"packages/plugin": {
|
"packages/plugin": {
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.1.48",
|
"version": "1.1.60",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
@@ -382,7 +386,7 @@
|
|||||||
},
|
},
|
||||||
"packages/sdk/js": {
|
"packages/sdk/js": {
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.1.48",
|
"version": "1.1.60",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@hey-api/openapi-ts": "0.90.10",
|
"@hey-api/openapi-ts": "0.90.10",
|
||||||
"@tsconfig/node22": "catalog:",
|
"@tsconfig/node22": "catalog:",
|
||||||
@@ -393,7 +397,7 @@
|
|||||||
},
|
},
|
||||||
"packages/slack": {
|
"packages/slack": {
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.1.48",
|
"version": "1.1.60",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@slack/bolt": "^3.17.1",
|
"@slack/bolt": "^3.17.1",
|
||||||
@@ -406,7 +410,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.1.48",
|
"version": "1.1.60",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -448,7 +452,7 @@
|
|||||||
},
|
},
|
||||||
"packages/util": {
|
"packages/util": {
|
||||||
"name": "@opencode-ai/util",
|
"name": "@opencode-ai/util",
|
||||||
"version": "1.1.48",
|
"version": "1.1.60",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -459,14 +463,14 @@
|
|||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"version": "1.1.48",
|
"version": "1.1.60",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/cloudflare": "12.6.3",
|
"@astrojs/cloudflare": "12.6.3",
|
||||||
"@astrojs/markdown-remark": "6.3.1",
|
"@astrojs/markdown-remark": "6.3.1",
|
||||||
"@astrojs/solid-js": "5.1.0",
|
"@astrojs/solid-js": "5.1.0",
|
||||||
"@astrojs/starlight": "0.34.3",
|
"@astrojs/starlight": "0.34.3",
|
||||||
"@fontsource/ibm-plex-mono": "5.2.5",
|
"@fontsource/ibm-plex-mono": "5.2.5",
|
||||||
"@shikijs/transformers": "3.4.2",
|
"@shikijs/transformers": "3.20.0",
|
||||||
"@types/luxon": "catalog:",
|
"@types/luxon": "catalog:",
|
||||||
"ai": "catalog:",
|
"ai": "catalog:",
|
||||||
"astro": "5.7.13",
|
"astro": "5.7.13",
|
||||||
@@ -481,8 +485,10 @@
|
|||||||
"shiki": "catalog:",
|
"shiki": "catalog:",
|
||||||
"solid-js": "catalog:",
|
"solid-js": "catalog:",
|
||||||
"toolbeam-docs-theme": "0.4.8",
|
"toolbeam-docs-theme": "0.4.8",
|
||||||
|
"vscode-languageserver-types": "3.17.5",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@astrojs/check": "0.9.6",
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
"opencode": "workspace:*",
|
"opencode": "workspace:*",
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
@@ -495,7 +501,7 @@
|
|||||||
"tree-sitter-bash",
|
"tree-sitter-bash",
|
||||||
],
|
],
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"ghostty-web@0.3.0": "patches/ghostty-web@0.3.0.patch",
|
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"@types/bun": "catalog:",
|
"@types/bun": "catalog:",
|
||||||
@@ -516,7 +522,7 @@
|
|||||||
"@tailwindcss/vite": "4.1.11",
|
"@tailwindcss/vite": "4.1.11",
|
||||||
"@tsconfig/bun": "1.0.9",
|
"@tsconfig/bun": "1.0.9",
|
||||||
"@tsconfig/node22": "22.0.2",
|
"@tsconfig/node22": "22.0.2",
|
||||||
"@types/bun": "1.3.5",
|
"@types/bun": "1.3.9",
|
||||||
"@types/luxon": "3.7.1",
|
"@types/luxon": "3.7.1",
|
||||||
"@types/node": "22.13.9",
|
"@types/node": "22.13.9",
|
||||||
"@types/semver": "7.7.1",
|
"@types/semver": "7.7.1",
|
||||||
@@ -557,7 +563,7 @@
|
|||||||
|
|
||||||
"@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="],
|
"@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="],
|
||||||
|
|
||||||
"@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.13.0", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Z6/Fp4cXLbYdMXr5AK752JM5qG2VKb6ShM0Ql6FimBSckMmLyK54OA20UhPYoH4C37FSFwUTARuwQOwQUToYrw=="],
|
"@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.14.1", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-b6r3PS3Nly+Wyw9U+0nOr47bV8tfS476EgyEMhoKvJCZLbgqoDFN7DJwkxL88RR0aiOqOYV1ZnESHqb+RmdH8w=="],
|
||||||
|
|
||||||
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@3.0.74", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.58", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-q83HE3FBb/HPIvjXsehrHOgCuGHPorSMFt6BYnzIYZy8gNnSqV1OWX4oXVsCAuYPPMtYW/KMK35hmoIFV8QKoQ=="],
|
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@3.0.74", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.58", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-q83HE3FBb/HPIvjXsehrHOgCuGHPorSMFt6BYnzIYZy8gNnSqV1OWX4oXVsCAuYPPMtYW/KMK35hmoIFV8QKoQ=="],
|
||||||
|
|
||||||
@@ -569,8 +575,16 @@
|
|||||||
|
|
||||||
"@ai-sdk/cohere": ["@ai-sdk/cohere@2.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-yJ9kP5cEDJwo8qpITq5TQFD8YNfNtW+HbyvWwrKMbFzmiMvIZuk95HIaFXE7PCTuZsqMA05yYu+qX/vQ3rNKjA=="],
|
"@ai-sdk/cohere": ["@ai-sdk/cohere@2.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-yJ9kP5cEDJwo8qpITq5TQFD8YNfNtW+HbyvWwrKMbFzmiMvIZuk95HIaFXE7PCTuZsqMA05yYu+qX/vQ3rNKjA=="],
|
||||||
|
|
||||||
|
"@ai-sdk/deepgram": ["@ai-sdk/deepgram@1.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-lqmINr+1Jy2yGXxnQB6IrC2xMtUY5uK96pyKfqTj1kLlXGatKnJfXF7WTkOGgQrFqIYqpjDz+sPVR3n0KUEUtA=="],
|
||||||
|
|
||||||
"@ai-sdk/deepinfra": ["@ai-sdk/deepinfra@1.0.33", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.32", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-hn2y8Q+2iZgGNVJyzPsH8EECECryFMVmxBJrBvBWoi8xcJPRyt0fZP5dOSLyGg3q0oxmPS9M0Eq0NNlKot/bYQ=="],
|
"@ai-sdk/deepinfra": ["@ai-sdk/deepinfra@1.0.33", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.32", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-hn2y8Q+2iZgGNVJyzPsH8EECECryFMVmxBJrBvBWoi8xcJPRyt0fZP5dOSLyGg3q0oxmPS9M0Eq0NNlKot/bYQ=="],
|
||||||
|
|
||||||
|
"@ai-sdk/deepseek": ["@ai-sdk/deepseek@1.0.33", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-NiKjvqXI/96e/7SjZGgQH141PBqggsF7fNbjGTv4RgVWayMXp9mj0Ou2NjAUGwwxJwj/qseY0gXiDCYaHWFBkw=="],
|
||||||
|
|
||||||
|
"@ai-sdk/elevenlabs": ["@ai-sdk/elevenlabs@1.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-4d5EKu0OW7Gf5WFpGo4ixn0iWEwA+GpteqUjEznWGmi7qdLE5zdkbRik5B1HrDDiw5P90yO51xBex/Fp50JcVA=="],
|
||||||
|
|
||||||
|
"@ai-sdk/fireworks": ["@ai-sdk/fireworks@1.0.33", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.32", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-WWOz5Kj+5fVe94h7WeReqjUOVtAquDE2kM575FUc8CsVxH2tRfA5cLa8nu3bknSezsKt3i67YM6mvCRxiXCkWA=="],
|
||||||
|
|
||||||
"@ai-sdk/gateway": ["@ai-sdk/gateway@2.0.30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-5Nrkj8B4MzkkOfjjA+Cs5pamkbkK4lI11bx80QV7TFcen/hWA8wEC+UVzwuM5H2zpekoNMjvl6GonHnR62XIZw=="],
|
"@ai-sdk/gateway": ["@ai-sdk/gateway@2.0.30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-5Nrkj8B4MzkkOfjjA+Cs5pamkbkK4lI11bx80QV7TFcen/hWA8wEC+UVzwuM5H2zpekoNMjvl6GonHnR62XIZw=="],
|
||||||
|
|
||||||
"@ai-sdk/google": ["@ai-sdk/google@2.0.52", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2XUnGi3f7TV4ujoAhA+Fg3idUoG/+Y2xjCRg70a1/m0DH1KSQqYaCboJ1C19y6ZHGdf5KNT20eJdswP6TvrY2g=="],
|
"@ai-sdk/google": ["@ai-sdk/google@2.0.52", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2XUnGi3f7TV4ujoAhA+Fg3idUoG/+Y2xjCRg70a1/m0DH1KSQqYaCboJ1C19y6ZHGdf5KNT20eJdswP6TvrY2g=="],
|
||||||
@@ -595,7 +609,7 @@
|
|||||||
|
|
||||||
"@ai-sdk/vercel": ["@ai-sdk/vercel@1.0.33", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.32", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Qwjm+HdwKasu7L9bDUryBMGKDMscIEzMUkjw/33uGdJpktzyNW13YaNIObOZ2HkskqDMIQJSd4Ao2BBT8fEYLw=="],
|
"@ai-sdk/vercel": ["@ai-sdk/vercel@1.0.33", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.32", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Qwjm+HdwKasu7L9bDUryBMGKDMscIEzMUkjw/33uGdJpktzyNW13YaNIObOZ2HkskqDMIQJSd4Ao2BBT8fEYLw=="],
|
||||||
|
|
||||||
"@ai-sdk/xai": ["@ai-sdk/xai@2.0.56", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.32", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-FGlqwWc3tAYqDHE8r8hQGQLcMiPUwgz90oU2QygUH930OWtCLapFkSu114DgVaIN/qoM1DUX+inv0Ee74Fgp5g=="],
|
"@ai-sdk/xai": ["@ai-sdk/xai@2.0.51", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.30", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-AI3le03qiegkZvn9hpnpDwez49lOvQLj4QUBT8H41SMbrdTYOxn3ktTwrsSu90cNDdzKGMvoH0u2GHju1EdnCg=="],
|
||||||
|
|
||||||
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
|
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
|
||||||
|
|
||||||
@@ -605,12 +619,16 @@
|
|||||||
|
|
||||||
"@anycable/core": ["@anycable/core@0.9.2", "", { "dependencies": { "nanoevents": "^7.0.1" } }, "sha512-x5ZXDcW/N4cxWl93CnbHs/u7qq4793jS2kNPWm+duPrXlrva+ml2ZGT7X9tuOBKzyIHf60zWCdIK7TUgMPAwXA=="],
|
"@anycable/core": ["@anycable/core@0.9.2", "", { "dependencies": { "nanoevents": "^7.0.1" } }, "sha512-x5ZXDcW/N4cxWl93CnbHs/u7qq4793jS2kNPWm+duPrXlrva+ml2ZGT7X9tuOBKzyIHf60zWCdIK7TUgMPAwXA=="],
|
||||||
|
|
||||||
|
"@astrojs/check": ["@astrojs/check@0.9.6", "", { "dependencies": { "@astrojs/language-server": "^2.16.1", "chokidar": "^4.0.1", "kleur": "^4.1.5", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": "^5.0.0" }, "bin": { "astro-check": "bin/astro-check.js" } }, "sha512-jlaEu5SxvSgmfGIFfNgcn5/f+29H61NJzEMfAZ82Xopr4XBchXB1GVlcJsE+elUlsYSbXlptZLX+JMG3b/wZEA=="],
|
||||||
|
|
||||||
"@astrojs/cloudflare": ["@astrojs/cloudflare@12.6.3", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.1", "@astrojs/underscore-redirects": "1.0.0", "@cloudflare/workers-types": "^4.20250507.0", "tinyglobby": "^0.2.13", "vite": "^6.3.5", "wrangler": "^4.14.1" }, "peerDependencies": { "astro": "^5.0.0" } }, "sha512-xhJptF5tU2k5eo70nIMyL1Udma0CqmUEnGSlGyFflLqSY82CRQI6nWZ/xZt0ZvmXuErUjIx0YYQNfZsz5CNjLQ=="],
|
"@astrojs/cloudflare": ["@astrojs/cloudflare@12.6.3", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.1", "@astrojs/underscore-redirects": "1.0.0", "@cloudflare/workers-types": "^4.20250507.0", "tinyglobby": "^0.2.13", "vite": "^6.3.5", "wrangler": "^4.14.1" }, "peerDependencies": { "astro": "^5.0.0" } }, "sha512-xhJptF5tU2k5eo70nIMyL1Udma0CqmUEnGSlGyFflLqSY82CRQI6nWZ/xZt0ZvmXuErUjIx0YYQNfZsz5CNjLQ=="],
|
||||||
|
|
||||||
"@astrojs/compiler": ["@astrojs/compiler@2.13.0", "", {}, "sha512-mqVORhUJViA28fwHYaWmsXSzLO9osbdZ5ImUfxBarqsYdMlPbqAqGJCxsNzvppp1BEzc1mJNjOVvQqeDN8Vspw=="],
|
"@astrojs/compiler": ["@astrojs/compiler@2.13.0", "", {}, "sha512-mqVORhUJViA28fwHYaWmsXSzLO9osbdZ5ImUfxBarqsYdMlPbqAqGJCxsNzvppp1BEzc1mJNjOVvQqeDN8Vspw=="],
|
||||||
|
|
||||||
"@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.7.1", "", {}, "sha512-7dwEVigz9vUWDw3nRwLQ/yH/xYovlUA0ZD86xoeKEBmkz9O6iELG1yri67PgAPW6VLL/xInA4t7H0CK6VmtkKQ=="],
|
"@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.7.1", "", {}, "sha512-7dwEVigz9vUWDw3nRwLQ/yH/xYovlUA0ZD86xoeKEBmkz9O6iELG1yri67PgAPW6VLL/xInA4t7H0CK6VmtkKQ=="],
|
||||||
|
|
||||||
|
"@astrojs/language-server": ["@astrojs/language-server@2.16.3", "", { "dependencies": { "@astrojs/compiler": "^2.13.0", "@astrojs/yaml2ts": "^0.2.2", "@jridgewell/sourcemap-codec": "^1.5.5", "@volar/kit": "~2.4.27", "@volar/language-core": "~2.4.27", "@volar/language-server": "~2.4.27", "@volar/language-service": "~2.4.27", "muggle-string": "^0.4.1", "tinyglobby": "^0.2.15", "volar-service-css": "0.0.68", "volar-service-emmet": "0.0.68", "volar-service-html": "0.0.68", "volar-service-prettier": "0.0.68", "volar-service-typescript": "0.0.68", "volar-service-typescript-twoslash-queries": "0.0.68", "volar-service-yaml": "0.0.68", "vscode-html-languageservice": "^5.6.1", "vscode-uri": "^3.1.0" }, "peerDependencies": { "prettier": "^3.0.0", "prettier-plugin-astro": ">=0.11.0" }, "optionalPeers": ["prettier", "prettier-plugin-astro"], "bin": { "astro-ls": "bin/nodeServer.js" } }, "sha512-yO5K7RYCMXUfeDlnU6UnmtnoXzpuQc0yhlaCNZ67k1C/MiwwwvMZz+LGa+H35c49w5QBfvtr4w4Zcf5PcH8uYA=="],
|
||||||
|
|
||||||
"@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.1", "", { "dependencies": { "@astrojs/internal-helpers": "0.6.1", "@astrojs/prism": "3.2.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.1.0", "js-yaml": "^4.1.0", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.1", "remark-smartypants": "^3.0.2", "shiki": "^3.0.0", "smol-toml": "^1.3.1", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1", "vfile": "^6.0.3" } }, "sha512-c5F5gGrkczUaTVgmMW9g1YMJGzOtRvjjhw6IfGuxarM6ct09MpwysP10US729dy07gg8y+ofVifezvP3BNsWZg=="],
|
"@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.1", "", { "dependencies": { "@astrojs/internal-helpers": "0.6.1", "@astrojs/prism": "3.2.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.1.0", "js-yaml": "^4.1.0", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.1", "remark-smartypants": "^3.0.2", "shiki": "^3.0.0", "smol-toml": "^1.3.1", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1", "vfile": "^6.0.3" } }, "sha512-c5F5gGrkczUaTVgmMW9g1YMJGzOtRvjjhw6IfGuxarM6ct09MpwysP10US729dy07gg8y+ofVifezvP3BNsWZg=="],
|
||||||
|
|
||||||
"@astrojs/mdx": ["@astrojs/mdx@4.3.13", "", { "dependencies": { "@astrojs/markdown-remark": "6.3.10", "@mdx-js/mdx": "^3.1.1", "acorn": "^8.15.0", "es-module-lexer": "^1.7.0", "estree-util-visit": "^2.0.0", "hast-util-to-html": "^9.0.5", "piccolore": "^0.1.3", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "remark-smartypants": "^3.0.2", "source-map": "^0.7.6", "unist-util-visit": "^5.0.0", "vfile": "^6.0.3" }, "peerDependencies": { "astro": "^5.0.0" } }, "sha512-IHDHVKz0JfKBy3//52JSiyWv089b7GVSChIXLrlUOoTLWowG3wr2/8hkaEgEyd/vysvNQvGk+QhysXpJW5ve6Q=="],
|
"@astrojs/mdx": ["@astrojs/mdx@4.3.13", "", { "dependencies": { "@astrojs/markdown-remark": "6.3.10", "@mdx-js/mdx": "^3.1.1", "acorn": "^8.15.0", "es-module-lexer": "^1.7.0", "estree-util-visit": "^2.0.0", "hast-util-to-html": "^9.0.5", "piccolore": "^0.1.3", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "remark-smartypants": "^3.0.2", "source-map": "^0.7.6", "unist-util-visit": "^5.0.0", "vfile": "^6.0.3" }, "peerDependencies": { "astro": "^5.0.0" } }, "sha512-IHDHVKz0JfKBy3//52JSiyWv089b7GVSChIXLrlUOoTLWowG3wr2/8hkaEgEyd/vysvNQvGk+QhysXpJW5ve6Q=="],
|
||||||
@@ -627,6 +645,8 @@
|
|||||||
|
|
||||||
"@astrojs/underscore-redirects": ["@astrojs/underscore-redirects@1.0.0", "", {}, "sha512-qZxHwVnmb5FXuvRsaIGaqWgnftjCuMY+GSbaVZdBmE4j8AfgPqKPxYp8SUERyJcjpKCEmO4wD6ybuGH8A2kVRQ=="],
|
"@astrojs/underscore-redirects": ["@astrojs/underscore-redirects@1.0.0", "", {}, "sha512-qZxHwVnmb5FXuvRsaIGaqWgnftjCuMY+GSbaVZdBmE4j8AfgPqKPxYp8SUERyJcjpKCEmO4wD6ybuGH8A2kVRQ=="],
|
||||||
|
|
||||||
|
"@astrojs/yaml2ts": ["@astrojs/yaml2ts@0.2.2", "", { "dependencies": { "yaml": "^2.5.0" } }, "sha512-GOfvSr5Nqy2z5XiwqTouBBpy5FyI6DEe+/g/Mk5am9SjILN1S5fOEvYK0GuWHg98yS/dobP4m8qyqw/URW35fQ=="],
|
||||||
|
|
||||||
"@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="],
|
"@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="],
|
||||||
|
|
||||||
"@aws-crypto/crc32c": ["@aws-crypto/crc32c@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag=="],
|
"@aws-crypto/crc32c": ["@aws-crypto/crc32c@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag=="],
|
||||||
@@ -837,6 +857,20 @@
|
|||||||
|
|
||||||
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="],
|
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="],
|
||||||
|
|
||||||
|
"@emmetio/abbreviation": ["@emmetio/abbreviation@2.3.3", "", { "dependencies": { "@emmetio/scanner": "^1.0.4" } }, "sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA=="],
|
||||||
|
|
||||||
|
"@emmetio/css-abbreviation": ["@emmetio/css-abbreviation@2.1.8", "", { "dependencies": { "@emmetio/scanner": "^1.0.4" } }, "sha512-s9yjhJ6saOO/uk1V74eifykk2CBYi01STTK3WlXWGOepyKa23ymJ053+DNQjpFcy1ingpaO7AxCcwLvHFY9tuw=="],
|
||||||
|
|
||||||
|
"@emmetio/css-parser": ["@emmetio/css-parser@0.4.1", "", { "dependencies": { "@emmetio/stream-reader": "^2.2.0", "@emmetio/stream-reader-utils": "^0.1.0" } }, "sha512-2bC6m0MV/voF4CTZiAbG5MWKbq5EBmDPKu9Sb7s7nVcEzNQlrZP6mFFFlIaISM8X6514H9shWMme1fCm8cWAfQ=="],
|
||||||
|
|
||||||
|
"@emmetio/html-matcher": ["@emmetio/html-matcher@1.3.0", "", { "dependencies": { "@emmetio/scanner": "^1.0.0" } }, "sha512-NTbsvppE5eVyBMuyGfVu2CRrLvo7J4YHb6t9sBFLyY03WYhXET37qA4zOYUjBWFCRHO7pS1B9khERtY0f5JXPQ=="],
|
||||||
|
|
||||||
|
"@emmetio/scanner": ["@emmetio/scanner@1.0.4", "", {}, "sha512-IqRuJtQff7YHHBk4G8YZ45uB9BaAGcwQeVzgj/zj8/UdOhtQpEIupUhSk8dys6spFIWVZVeK20CzGEnqR5SbqA=="],
|
||||||
|
|
||||||
|
"@emmetio/stream-reader": ["@emmetio/stream-reader@2.2.0", "", {}, "sha512-fXVXEyFA5Yv3M3n8sUGT7+fvecGrZP4k6FnWWMSZVQf69kAq0LLpaBQLGcPR30m3zMmKYhECP4k/ZkzvhEW5kw=="],
|
||||||
|
|
||||||
|
"@emmetio/stream-reader-utils": ["@emmetio/stream-reader-utils@0.1.0", "", {}, "sha512-ZsZ2I9Vzso3Ho/pjZFsmmZ++FWeEd/txqybHTm4OgaZzdS8V9V/YYWQwg5TC38Z7uLWUV1vavpLLbjJtKubR1A=="],
|
||||||
|
|
||||||
"@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
|
"@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
|
||||||
|
|
||||||
"@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
|
"@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
|
||||||
@@ -911,8 +945,22 @@
|
|||||||
|
|
||||||
"@expressive-code/plugin-text-markers": ["@expressive-code/plugin-text-markers@0.41.6", "", { "dependencies": { "@expressive-code/core": "^0.41.6" } }, "sha512-PBFa1wGyYzRExMDzBmAWC6/kdfG1oLn4pLpBeTfIRrALPjcGA/59HP3e7q9J0Smk4pC7U+lWkA2LHR8FYV8U7Q=="],
|
"@expressive-code/plugin-text-markers": ["@expressive-code/plugin-text-markers@0.41.6", "", { "dependencies": { "@expressive-code/core": "^0.41.6" } }, "sha512-PBFa1wGyYzRExMDzBmAWC6/kdfG1oLn4pLpBeTfIRrALPjcGA/59HP3e7q9J0Smk4pC7U+lWkA2LHR8FYV8U7Q=="],
|
||||||
|
|
||||||
|
"@fastify/ajv-compiler": ["@fastify/ajv-compiler@4.0.5", "", { "dependencies": { "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "fast-uri": "^3.0.0" } }, "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A=="],
|
||||||
|
|
||||||
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
|
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
|
||||||
|
|
||||||
|
"@fastify/error": ["@fastify/error@4.2.0", "", {}, "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ=="],
|
||||||
|
|
||||||
|
"@fastify/fast-json-stringify-compiler": ["@fastify/fast-json-stringify-compiler@5.0.3", "", { "dependencies": { "fast-json-stringify": "^6.0.0" } }, "sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ=="],
|
||||||
|
|
||||||
|
"@fastify/forwarded": ["@fastify/forwarded@3.0.1", "", {}, "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw=="],
|
||||||
|
|
||||||
|
"@fastify/merge-json-schemas": ["@fastify/merge-json-schemas@0.2.1", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A=="],
|
||||||
|
|
||||||
|
"@fastify/proxy-addr": ["@fastify/proxy-addr@5.1.0", "", { "dependencies": { "@fastify/forwarded": "^3.0.0", "ipaddr.js": "^2.1.0" } }, "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw=="],
|
||||||
|
|
||||||
|
"@fastify/rate-limit": ["@fastify/rate-limit@10.3.0", "", { "dependencies": { "@lukeed/ms": "^2.0.2", "fastify-plugin": "^5.0.0", "toad-cache": "^3.7.0" } }, "sha512-eIGkG9XKQs0nyynatApA3EVrojHOuq4l6fhB4eeCk4PIOeadvOJz9/4w3vGI44Go17uaXOWEcPkaD8kuKm7g6Q=="],
|
||||||
|
|
||||||
"@floating-ui/core": ["@floating-ui/core@1.7.4", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg=="],
|
"@floating-ui/core": ["@floating-ui/core@1.7.4", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg=="],
|
||||||
|
|
||||||
"@floating-ui/dom": ["@floating-ui/dom@1.7.5", "", { "dependencies": { "@floating-ui/core": "^1.7.4", "@floating-ui/utils": "^0.2.10" } }, "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg=="],
|
"@floating-ui/dom": ["@floating-ui/dom@1.7.5", "", { "dependencies": { "@floating-ui/core": "^1.7.4", "@floating-ui/utils": "^0.2.10" } }, "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg=="],
|
||||||
@@ -925,7 +973,9 @@
|
|||||||
|
|
||||||
"@fontsource/inter": ["@fontsource/inter@5.2.8", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="],
|
"@fontsource/inter": ["@fontsource/inter@5.2.8", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="],
|
||||||
|
|
||||||
"@gitlab/gitlab-ai-provider": ["@gitlab/gitlab-ai-provider@3.3.1", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-J4/LfVcxOKbR2gfoBWRKp1BpWppprC2Cz/Ff5E0B/0lS341CDtZwzkgWvHfkM/XU6q83JRs059dS0cR8VOODOQ=="],
|
"@gitlab/gitlab-ai-provider": ["@gitlab/gitlab-ai-provider@3.5.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-OoAwCz4fOci3h/2l+PRHMclclh3IaFq8w1es2wvBJ8ca7vtglKsBYT7dvmYpsXlu7pg9mopbjcexvmVCQEUTAQ=="],
|
||||||
|
|
||||||
|
"@gitlab/opencode-gitlab-auth": ["@gitlab/opencode-gitlab-auth@1.3.2", "", { "dependencies": { "@fastify/rate-limit": "^10.2.0", "@opencode-ai/plugin": "*", "fastify": "^5.2.0", "open": "^10.0.0" } }, "sha512-pvGrC+aDVLY8bRCC/fZaG/Qihvt2r4by5xbTo5JTSz9O7yIcR6xG2d9Wkuu4bcXFz674z2C+i5bUk+J/RSdBpg=="],
|
||||||
|
|
||||||
"@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="],
|
"@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="],
|
||||||
|
|
||||||
@@ -1119,6 +1169,8 @@
|
|||||||
|
|
||||||
"@leichtgewicht/ip-codec": ["@leichtgewicht/ip-codec@2.0.5", "", {}, "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw=="],
|
"@leichtgewicht/ip-codec": ["@leichtgewicht/ip-codec@2.0.5", "", {}, "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw=="],
|
||||||
|
|
||||||
|
"@lukeed/ms": ["@lukeed/ms@2.0.2", "", {}, "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA=="],
|
||||||
|
|
||||||
"@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="],
|
"@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="],
|
||||||
|
|
||||||
"@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="],
|
"@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="],
|
||||||
@@ -1227,21 +1279,21 @@
|
|||||||
|
|
||||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||||
|
|
||||||
"@opentui/core": ["@opentui/core@0.1.75", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.75", "@opentui/core-darwin-x64": "0.1.75", "@opentui/core-linux-arm64": "0.1.75", "@opentui/core-linux-x64": "0.1.75", "@opentui/core-win32-arm64": "0.1.75", "@opentui/core-win32-x64": "0.1.75", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-8ARRZxSG+BXkJmEVtM2DQ4se7DAF1ZCKD07d+AklgTr2mxCzmdxxPbOwRzboSQ6FM7qGuTVPVbV4O2W9DpUmoA=="],
|
"@opentui/core": ["@opentui/core@0.1.79", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.79", "@opentui/core-darwin-x64": "0.1.79", "@opentui/core-linux-arm64": "0.1.79", "@opentui/core-linux-x64": "0.1.79", "@opentui/core-win32-arm64": "0.1.79", "@opentui/core-win32-x64": "0.1.79", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-job/t09w8A/aHb/WuaVbimu5fIffyN+PCuVO5cYhXEg/NkOkC/WdFi80B8bwncR/DBPyLAh6oJ3EG86grOVo5g=="],
|
||||||
|
|
||||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.75", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gGaGZjkFpqcXJk6321JzhRl66pM2VxBlI470L8W4DQUW4S6iDT1R9L7awSzGB4Cn9toUl7DTV8BemaXZYXV4SA=="],
|
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.79", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kgsGniV+DM5G1P3GideyJhvfnthNKcVCAm2mPTIr9InQ3L0gS/Feh7zgwOS/jxDvdlQbOWGKMk2Z3JApeC1MLw=="],
|
||||||
|
|
||||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.75", "", { "os": "darwin", "cpu": "x64" }, "sha512-tPlvqQI0whZ76amHydpJs5kN+QeWAIcFbI8RAtlAo9baj2EbxTDC+JGwgb9Fnt0/YQx831humbtaNDhV2Jt1bw=="],
|
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.79", "", { "os": "darwin", "cpu": "x64" }, "sha512-OpyAmFqAAKQ2CeFmf/oLWcNksmP6Ryx/3R5dbKXThOudMCeQvfvInJTRbc2jTn9VFpf+Qj4BgHkJg1h90tf/EA=="],
|
||||||
|
|
||||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.75", "", { "os": "linux", "cpu": "arm64" }, "sha512-nVxIQ4Hqf84uBergDpWiVzU6pzpjy6tqBHRQpySxZ2flkJ/U6/aMEizVrQ1jcgIdxZtvqWDETZhzxhG0yDx+cw=="],
|
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.79", "", { "os": "linux", "cpu": "arm64" }, "sha512-DCa5YaknS4bWhFt8TMEGH+qmTinyzuY8hoZbO4crtWXAxofPP7Pas76Cwxlvis/PyLffA+pPxAl1l5sUZpsvqw=="],
|
||||||
|
|
||||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.75", "", { "os": "linux", "cpu": "x64" }, "sha512-1CnApef4kxA+ORyLfbuCLgZfEjp4wr3HjFnt7FAfOb73kIZH82cb7JYixeqRyy9eOcKfKqxLmBYy3o8IDkc4Rg=="],
|
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.79", "", { "os": "linux", "cpu": "x64" }, "sha512-V6xjvFfHh3NGvsuuDae1KHPRZXHMEE8XL0A/GM6v4I4OCC23kDmkK60Vn6OptQwAzwwbz0X0IX+Ut/GQU9qGgA=="],
|
||||||
|
|
||||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.75", "", { "os": "win32", "cpu": "arm64" }, "sha512-j0UB95nmkYGNzmOrs6GqaddO1S90R0YC6IhbKnbKBdjchFPNVLz9JpexAs6MBDXPZwdKAywMxtwG2h3aTJtxng=="],
|
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.79", "", { "os": "win32", "cpu": "arm64" }, "sha512-sPRKnVzOdT5szI59tte7pxwwkYA+07EQN+6miFAvkFuiLmRUngONUD8HVjL7nCnxcPFqxaU4Rvl1y40ST86g8g=="],
|
||||||
|
|
||||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.75", "", { "os": "win32", "cpu": "x64" }, "sha512-ESpVZVGewe3JkB2TwrG3VRbkxT909iPdtvgNT7xTCIYH2VB4jqZomJfvERPTE0tvqAZJm19mHECzJFI8asSJgQ=="],
|
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.79", "", { "os": "win32", "cpu": "x64" }, "sha512-vmQcFTvKf9fqajnDtgU6/uAsiTGwx8//khqHVBmiTEXUsiT792Ki9l8sgNughbuldqG5iZOiF6IaAWU1H67UpA=="],
|
||||||
|
|
||||||
"@opentui/solid": ["@opentui/solid@0.1.75", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.75", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-WjKsZIfrm29znfRlcD9w3uUn/+uvoy2MmeoDwTvg1YOa0OjCTCmjZ43L9imp0m9S4HmVU8ma6o2bR4COzcyDdg=="],
|
"@opentui/solid": ["@opentui/solid@0.1.79", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.79", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-c5+0jexKxb8GwRDDkQ/U6isZZqClAzHccXmYiLYmSnqdoQQp2lIGHLartL+K8lfIQrsKClzP2ZHumN6nexRfRg=="],
|
||||||
|
|
||||||
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
||||||
|
|
||||||
@@ -1359,6 +1411,8 @@
|
|||||||
|
|
||||||
"@pierre/diffs": ["@pierre/diffs@1.0.2", "", { "dependencies": { "@shikijs/core": "^3.0.0", "@shikijs/engine-javascript": "3.19.0", "@shikijs/transformers": "3.19.0", "diff": "8.0.2", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "3.19.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-RkFSDD5X/U+8QjyilPViYGJfmJNWXR17zTL8zw48+DcVC1Ujbh6I1edyuRnFfgRzpft05x2DSCkz2cjoIAxPvQ=="],
|
"@pierre/diffs": ["@pierre/diffs@1.0.2", "", { "dependencies": { "@shikijs/core": "^3.0.0", "@shikijs/engine-javascript": "3.19.0", "@shikijs/transformers": "3.19.0", "diff": "8.0.2", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "3.19.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-RkFSDD5X/U+8QjyilPViYGJfmJNWXR17zTL8zw48+DcVC1Ujbh6I1edyuRnFfgRzpft05x2DSCkz2cjoIAxPvQ=="],
|
||||||
|
|
||||||
|
"@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="],
|
||||||
|
|
||||||
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
|
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
|
||||||
|
|
||||||
"@planetscale/database": ["@planetscale/database@1.19.0", "", {}, "sha512-Tv4jcFUFAFjOWrGSio49H6R2ijALv0ZzVBfJKIdm+kl9X046Fh4LLawrF9OMsglVbK6ukqMJsUCeucGAFTBcMA=="],
|
"@planetscale/database": ["@planetscale/database@1.19.0", "", {}, "sha512-Tv4jcFUFAFjOWrGSio49H6R2ijALv0ZzVBfJKIdm+kl9X046Fh4LLawrF9OMsglVbK6ukqMJsUCeucGAFTBcMA=="],
|
||||||
@@ -1753,6 +1807,8 @@
|
|||||||
|
|
||||||
"@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.9.6", "", { "os": "win32", "cpu": "x64" }, "sha512-ldWuWSSkWbKOPjQMJoYVj9wLHcOniv7diyI5UAJ4XsBdtaFB0pKHQsqw/ItUma0VXGC7vB4E9fZjivmxur60aw=="],
|
"@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.9.6", "", { "os": "win32", "cpu": "x64" }, "sha512-ldWuWSSkWbKOPjQMJoYVj9wLHcOniv7diyI5UAJ4XsBdtaFB0pKHQsqw/ItUma0VXGC7vB4E9fZjivmxur60aw=="],
|
||||||
|
|
||||||
|
"@tauri-apps/plugin-clipboard-manager": ["@tauri-apps/plugin-clipboard-manager@2.3.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-CUlb5Hqi2oZbcZf4VUyUH53XWPPdtpw43EUpCza5HWZJwxEoDowFzNUDt1tRUXA8Uq+XPn17Ysfptip33sG4eQ=="],
|
||||||
|
|
||||||
"@tauri-apps/plugin-deep-link": ["@tauri-apps/plugin-deep-link@2.4.6", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-UUOSt0U5juK20uhO2MoHZX/IPblkrhUh+VPtIeu3RwtzI0R9Em3Auzfg/PwcZ9Pv8mLne3cQ4p9CFXD6WxqCZA=="],
|
"@tauri-apps/plugin-deep-link": ["@tauri-apps/plugin-deep-link@2.4.6", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-UUOSt0U5juK20uhO2MoHZX/IPblkrhUh+VPtIeu3RwtzI0R9Em3Auzfg/PwcZ9Pv8mLne3cQ4p9CFXD6WxqCZA=="],
|
||||||
|
|
||||||
"@tauri-apps/plugin-dialog": ["@tauri-apps/plugin-dialog@2.6.0", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg=="],
|
"@tauri-apps/plugin-dialog": ["@tauri-apps/plugin-dialog@2.6.0", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg=="],
|
||||||
@@ -1797,7 +1853,7 @@
|
|||||||
|
|
||||||
"@types/braces": ["@types/braces@3.0.5", "", {}, "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w=="],
|
"@types/braces": ["@types/braces@3.0.5", "", {}, "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w=="],
|
||||||
|
|
||||||
"@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="],
|
"@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
|
||||||
|
|
||||||
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
|
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
|
||||||
|
|
||||||
@@ -1927,6 +1983,22 @@
|
|||||||
|
|
||||||
"@vitest/utils": ["@vitest/utils@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "tinyrainbow": "^3.0.3" } }, "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA=="],
|
"@vitest/utils": ["@vitest/utils@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "tinyrainbow": "^3.0.3" } }, "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA=="],
|
||||||
|
|
||||||
|
"@volar/kit": ["@volar/kit@2.4.28", "", { "dependencies": { "@volar/language-service": "2.4.28", "@volar/typescript": "2.4.28", "typesafe-path": "^0.2.2", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "typescript": "*" } }, "sha512-cKX4vK9dtZvDRaAzeoUdaAJEew6IdxHNCRrdp5Kvcl6zZOqb6jTOfk3kXkIkG3T7oTFXguEMt5+9ptyqYR84Pg=="],
|
||||||
|
|
||||||
|
"@volar/language-core": ["@volar/language-core@2.4.28", "", { "dependencies": { "@volar/source-map": "2.4.28" } }, "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ=="],
|
||||||
|
|
||||||
|
"@volar/language-server": ["@volar/language-server@2.4.28", "", { "dependencies": { "@volar/language-core": "2.4.28", "@volar/language-service": "2.4.28", "@volar/typescript": "2.4.28", "path-browserify": "^1.0.1", "request-light": "^0.7.0", "vscode-languageserver": "^9.0.1", "vscode-languageserver-protocol": "^3.17.5", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" } }, "sha512-NqcLnE5gERKuS4PUFwlhMxf6vqYo7hXtbMFbViXcbVkbZ905AIVWhnSo0ZNBC2V127H1/2zP7RvVOVnyITFfBw=="],
|
||||||
|
|
||||||
|
"@volar/language-service": ["@volar/language-service@2.4.28", "", { "dependencies": { "@volar/language-core": "2.4.28", "vscode-languageserver-protocol": "^3.17.5", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" } }, "sha512-Rh/wYCZJrI5vCwMk9xyw/Z+MsWxlJY1rmMZPsxUoJKfzIRjS/NF1NmnuEcrMbEVGja00aVpCsInJfixQTMdvLw=="],
|
||||||
|
|
||||||
|
"@volar/source-map": ["@volar/source-map@2.4.28", "", {}, "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ=="],
|
||||||
|
|
||||||
|
"@volar/typescript": ["@volar/typescript@2.4.28", "", { "dependencies": { "@volar/language-core": "2.4.28", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw=="],
|
||||||
|
|
||||||
|
"@vscode/emmet-helper": ["@vscode/emmet-helper@2.11.0", "", { "dependencies": { "emmet": "^2.4.3", "jsonc-parser": "^2.3.0", "vscode-languageserver-textdocument": "^1.0.1", "vscode-languageserver-types": "^3.15.1", "vscode-uri": "^3.0.8" } }, "sha512-QLxjQR3imPZPQltfbWRnHU6JecWTF1QSWhx3GAKQpslx7y3Dp6sIIXhKjiUJ/BR9FX8PVthjr9PD6pNwOJfAzw=="],
|
||||||
|
|
||||||
|
"@vscode/l10n": ["@vscode/l10n@0.0.18", "", {}, "sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ=="],
|
||||||
|
|
||||||
"@webgpu/types": ["@webgpu/types@0.1.54", "", {}, "sha512-81oaalC8LFrXjhsczomEQ0u3jG+TqE6V9QHLA8GNZq/Rnot0KDugu3LhSYSlie8tSdooAN1Hov05asrUUp9qgg=="],
|
"@webgpu/types": ["@webgpu/types@0.1.54", "", {}, "sha512-81oaalC8LFrXjhsczomEQ0u3jG+TqE6V9QHLA8GNZq/Rnot0KDugu3LhSYSlie8tSdooAN1Hov05asrUUp9qgg=="],
|
||||||
|
|
||||||
"@zip.js/zip.js": ["@zip.js/zip.js@2.7.62", "", {}, "sha512-OaLvZ8j4gCkLn048ypkZu29KX30r8/OfFF2w4Jo5WXFr+J04J+lzJ5TKZBVgFXhlvSkqNFQdfnY1Q8TMTCyBVA=="],
|
"@zip.js/zip.js": ["@zip.js/zip.js@2.7.62", "", {}, "sha512-OaLvZ8j4gCkLn048ypkZu29KX30r8/OfFF2w4Jo5WXFr+J04J+lzJ5TKZBVgFXhlvSkqNFQdfnY1Q8TMTCyBVA=="],
|
||||||
@@ -1935,6 +2007,8 @@
|
|||||||
|
|
||||||
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
|
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
|
||||||
|
|
||||||
|
"abstract-logging": ["abstract-logging@2.0.1", "", {}, "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA=="],
|
||||||
|
|
||||||
"accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
|
"accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
|
||||||
|
|
||||||
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||||
@@ -1949,8 +2023,12 @@
|
|||||||
|
|
||||||
"ai": ["ai@5.0.124", "", { "dependencies": { "@ai-sdk/gateway": "2.0.30", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Li6Jw9F9qsvFJXZPBfxj38ddP2iURCnMs96f9Q3OeQzrDVcl1hvtwSEAuxA/qmfh6SDV2ERqFUOFzigvr0697g=="],
|
"ai": ["ai@5.0.124", "", { "dependencies": { "@ai-sdk/gateway": "2.0.30", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Li6Jw9F9qsvFJXZPBfxj38ddP2iURCnMs96f9Q3OeQzrDVcl1hvtwSEAuxA/qmfh6SDV2ERqFUOFzigvr0697g=="],
|
||||||
|
|
||||||
|
"ai-gateway-provider": ["ai-gateway-provider@2.3.1", "", { "dependencies": { "@ai-sdk/provider": "^2.0.0", "@ai-sdk/provider-utils": "^3.0.19", "ai": "^5.0.116" }, "optionalDependencies": { "@ai-sdk/amazon-bedrock": "^3.0.71", "@ai-sdk/anthropic": "^2.0.56", "@ai-sdk/azure": "^2.0.90", "@ai-sdk/cerebras": "^1.0.33", "@ai-sdk/cohere": "^2.0.21", "@ai-sdk/deepgram": "^1.0.21", "@ai-sdk/deepseek": "^1.0.32", "@ai-sdk/elevenlabs": "^1.0.21", "@ai-sdk/fireworks": "^1.0.30", "@ai-sdk/google": "^2.0.51", "@ai-sdk/google-vertex": "3.0.90", "@ai-sdk/groq": "^2.0.33", "@ai-sdk/mistral": "^2.0.26", "@ai-sdk/openai": "^2.0.88", "@ai-sdk/perplexity": "^2.0.22", "@ai-sdk/xai": "^2.0.42", "@openrouter/ai-sdk-provider": "^1.5.3" }, "peerDependencies": { "@ai-sdk/openai-compatible": "^1.0.29" } }, "sha512-PqI6TVNEDNwr7kOhy7XUGnA8XJB1SpeA9aLqGjr0CyWkKgH+y+ofPm8MZGZ74DOwVejDF+POZq0Qs9jKEKUeYg=="],
|
||||||
|
|
||||||
"ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
|
"ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
|
||||||
|
|
||||||
|
"ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="],
|
||||||
|
|
||||||
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
|
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
|
||||||
|
|
||||||
"ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="],
|
"ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="],
|
||||||
@@ -2009,10 +2087,14 @@
|
|||||||
|
|
||||||
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||||
|
|
||||||
|
"atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="],
|
||||||
|
|
||||||
"autoprefixer": ["autoprefixer@10.4.23", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001760", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA=="],
|
"autoprefixer": ["autoprefixer@10.4.23", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001760", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA=="],
|
||||||
|
|
||||||
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
|
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
|
||||||
|
|
||||||
|
"avvio": ["avvio@9.1.0", "", { "dependencies": { "@fastify/error": "^4.0.0", "fastq": "^1.17.1" } }, "sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw=="],
|
||||||
|
|
||||||
"await-to-js": ["await-to-js@3.0.0", "", {}, "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g=="],
|
"await-to-js": ["await-to-js@3.0.0", "", {}, "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g=="],
|
||||||
|
|
||||||
"aws-sdk": ["aws-sdk@2.1692.0", "", { "dependencies": { "buffer": "4.9.2", "events": "1.1.1", "ieee754": "1.1.13", "jmespath": "0.16.0", "querystring": "0.2.0", "sax": "1.2.1", "url": "0.10.3", "util": "^0.12.4", "uuid": "8.0.0", "xml2js": "0.6.2" } }, "sha512-x511uiJ/57FIsbgUe5csJ13k3uzu25uWQE+XqfBis/sB0SFoiElJWXRkgEAUh0U6n40eT3ay5Ue4oPkRMu1LYw=="],
|
"aws-sdk": ["aws-sdk@2.1692.0", "", { "dependencies": { "buffer": "4.9.2", "events": "1.1.1", "ieee754": "1.1.13", "jmespath": "0.16.0", "querystring": "0.2.0", "sax": "1.2.1", "url": "0.10.3", "util": "^0.12.4", "uuid": "8.0.0", "xml2js": "0.6.2" } }, "sha512-x511uiJ/57FIsbgUe5csJ13k3uzu25uWQE+XqfBis/sB0SFoiElJWXRkgEAUh0U6n40eT3ay5Ue4oPkRMu1LYw=="],
|
||||||
@@ -2097,9 +2179,9 @@
|
|||||||
|
|
||||||
"bun-ffi-structs": ["bun-ffi-structs@0.1.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-Lh1oQAYHDcnesJauieA4UNkWGXY9hYck7OA5IaRwE3Bp6K2F2pJSNYqq+hIy7P3uOvo3km3oxS8304g5gDMl/w=="],
|
"bun-ffi-structs": ["bun-ffi-structs@0.1.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-Lh1oQAYHDcnesJauieA4UNkWGXY9hYck7OA5IaRwE3Bp6K2F2pJSNYqq+hIy7P3uOvo3km3oxS8304g5gDMl/w=="],
|
||||||
|
|
||||||
"bun-pty": ["bun-pty@0.4.4", "", {}, "sha512-WK4G6uWsZgu1v4hKIlw6G1q2AOf8Rbga2Yr7RnxArVjjyb+mtVa/CFc9GOJf+OYSJSH8k7LonAtQOVeNAddRyg=="],
|
"bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="],
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="],
|
"bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
|
||||||
|
|
||||||
"bun-webgpu": ["bun-webgpu@0.1.4", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.4", "bun-webgpu-darwin-x64": "^0.1.4", "bun-webgpu-linux-x64": "^0.1.4", "bun-webgpu-win32-x64": "^0.1.4" } }, "sha512-Kw+HoXl1PMWJTh9wvh63SSRofTA8vYBFCw0XEP1V1fFdQEDhI8Sgf73sdndE/oDpN/7CMx0Yv/q8FCvO39ROMQ=="],
|
"bun-webgpu": ["bun-webgpu@0.1.4", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.4", "bun-webgpu-darwin-x64": "^0.1.4", "bun-webgpu-linux-x64": "^0.1.4", "bun-webgpu-win32-x64": "^0.1.4" } }, "sha512-Kw+HoXl1PMWJTh9wvh63SSRofTA8vYBFCw0XEP1V1fFdQEDhI8Sgf73sdndE/oDpN/7CMx0Yv/q8FCvO39ROMQ=="],
|
||||||
|
|
||||||
@@ -2349,6 +2431,8 @@
|
|||||||
|
|
||||||
"electron-to-chromium": ["electron-to-chromium@1.5.282", "", {}, "sha512-FCPkJtpst28UmFzd903iU7PdeVTfY0KAeJy+Lk0GLZRwgwYHn/irRcaCbQQOmr5Vytc/7rcavsYLvTM8RiHYhQ=="],
|
"electron-to-chromium": ["electron-to-chromium@1.5.282", "", {}, "sha512-FCPkJtpst28UmFzd903iU7PdeVTfY0KAeJy+Lk0GLZRwgwYHn/irRcaCbQQOmr5Vytc/7rcavsYLvTM8RiHYhQ=="],
|
||||||
|
|
||||||
|
"emmet": ["emmet@2.4.11", "", { "dependencies": { "@emmetio/abbreviation": "^2.3.3", "@emmetio/css-abbreviation": "^2.1.8" } }, "sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ=="],
|
||||||
|
|
||||||
"emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
|
"emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
|
||||||
|
|
||||||
"emoji-regex-xs": ["emoji-regex-xs@1.0.0", "", {}, "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg=="],
|
"emoji-regex-xs": ["emoji-regex-xs@1.0.0", "", {}, "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg=="],
|
||||||
@@ -2455,16 +2539,26 @@
|
|||||||
|
|
||||||
"fast-content-type-parse": ["fast-content-type-parse@3.0.0", "", {}, "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg=="],
|
"fast-content-type-parse": ["fast-content-type-parse@3.0.0", "", {}, "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg=="],
|
||||||
|
|
||||||
|
"fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
|
||||||
|
|
||||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||||
|
|
||||||
"fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="],
|
"fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="],
|
||||||
|
|
||||||
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
||||||
|
|
||||||
|
"fast-json-stringify": ["fast-json-stringify@6.2.0", "", { "dependencies": { "@fastify/merge-json-schemas": "^0.2.0", "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "fast-uri": "^3.0.0", "json-schema-ref-resolver": "^3.0.0", "rfdc": "^1.2.0" } }, "sha512-Eaf/KNIDwHkzfyeQFNfLXJnQ7cl1XQI3+zRqmPlvtkMigbXnAcasTrvJQmquBSxKfFGeRA6PFog8t+hFmpDoWw=="],
|
||||||
|
|
||||||
|
"fast-querystring": ["fast-querystring@1.1.2", "", { "dependencies": { "fast-decode-uri-component": "^1.0.1" } }, "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg=="],
|
||||||
|
|
||||||
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
|
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
|
||||||
|
|
||||||
"fast-xml-parser": ["fast-xml-parser@4.4.1", "", { "dependencies": { "strnum": "^1.0.5" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw=="],
|
"fast-xml-parser": ["fast-xml-parser@4.4.1", "", { "dependencies": { "strnum": "^1.0.5" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw=="],
|
||||||
|
|
||||||
|
"fastify": ["fastify@5.7.4", "", { "dependencies": { "@fastify/ajv-compiler": "^4.0.5", "@fastify/error": "^4.0.0", "@fastify/fast-json-stringify-compiler": "^5.0.0", "@fastify/proxy-addr": "^5.0.0", "abstract-logging": "^2.0.1", "avvio": "^9.0.0", "fast-json-stringify": "^6.0.0", "find-my-way": "^9.0.0", "light-my-request": "^6.0.0", "pino": "^10.1.0", "process-warning": "^5.0.0", "rfdc": "^1.3.1", "secure-json-parse": "^4.0.0", "semver": "^7.6.0", "toad-cache": "^3.7.0" } }, "sha512-e6l5NsRdaEP8rdD8VR0ErJASeyaRbzXYpmkrpr2SuvuMq6Si3lvsaVy5C+7gLanEkvjpMDzBXWE5HPeb/hgTxA=="],
|
||||||
|
|
||||||
|
"fastify-plugin": ["fastify-plugin@5.1.0", "", {}, "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw=="],
|
||||||
|
|
||||||
"fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
|
"fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
|
||||||
|
|
||||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||||
@@ -2479,6 +2573,8 @@
|
|||||||
|
|
||||||
"find-babel-config": ["find-babel-config@2.1.2", "", { "dependencies": { "json5": "^2.2.3" } }, "sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg=="],
|
"find-babel-config": ["find-babel-config@2.1.2", "", { "dependencies": { "json5": "^2.2.3" } }, "sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg=="],
|
||||||
|
|
||||||
|
"find-my-way": ["find-my-way@9.4.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-querystring": "^1.0.0", "safe-regex2": "^5.0.0" } }, "sha512-5Ye4vHsypZRYtS01ob/iwHzGRUDELlsoCftI/OZFhcLs1M0tkGPcXldE80TAZC5yYuJMBPJQQ43UHlqbJWiX2w=="],
|
||||||
|
|
||||||
"find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
|
"find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
|
||||||
|
|
||||||
"finity": ["finity@0.5.4", "", {}, "sha512-3l+5/1tuw616Lgb0QBimxfdd2TqaDGpfCBpfX6EqtFmqUV3FtQnVEX4Aa62DagYEqnsTIjZcTfbq9msDbXYgyA=="],
|
"finity": ["finity@0.5.4", "", {}, "sha512-3l+5/1tuw616Lgb0QBimxfdd2TqaDGpfCBpfX6EqtFmqUV3FtQnVEX4Aa62DagYEqnsTIjZcTfbq9msDbXYgyA=="],
|
||||||
@@ -2555,7 +2651,7 @@
|
|||||||
|
|
||||||
"get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="],
|
"get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="],
|
||||||
|
|
||||||
"ghostty-web": ["ghostty-web@0.3.0", "", {}, "sha512-SAdSHWYF20GMZUB0n8kh1N6Z4ljMnuUqT8iTB2n5FAPswEV10MejEpLlhW/769GL5+BQa1NYwEg9y/XCckV5+A=="],
|
"ghostty-web": ["ghostty-web@0.4.0", "", {}, "sha512-0puDBik2qapbD/QQBW9o5ZHfXnZBqZWx/ctBiVtKZ6ZLds4NYb+wZuw1cRLXZk9zYovIQ908z3rvFhexAvc5Hg=="],
|
||||||
|
|
||||||
"gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="],
|
"gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="],
|
||||||
|
|
||||||
@@ -2845,6 +2941,8 @@
|
|||||||
|
|
||||||
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
|
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
|
||||||
|
|
||||||
|
"json-schema-ref-resolver": ["json-schema-ref-resolver@3.0.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A=="],
|
||||||
|
|
||||||
"json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="],
|
"json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="],
|
||||||
|
|
||||||
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||||
@@ -2881,6 +2979,8 @@
|
|||||||
|
|
||||||
"leac": ["leac@0.6.0", "", {}, "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg=="],
|
"leac": ["leac@0.6.0", "", {}, "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg=="],
|
||||||
|
|
||||||
|
"light-my-request": ["light-my-request@6.6.0", "", { "dependencies": { "cookie": "^1.0.1", "process-warning": "^4.0.0", "set-cookie-parser": "^2.6.0" } }, "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A=="],
|
||||||
|
|
||||||
"lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="],
|
"lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="],
|
||||||
|
|
||||||
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="],
|
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="],
|
||||||
@@ -3111,6 +3211,8 @@
|
|||||||
|
|
||||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
|
"muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="],
|
||||||
|
|
||||||
"multicast-dns": ["multicast-dns@7.2.5", "", { "dependencies": { "dns-packet": "^5.2.2", "thunky": "^1.0.2" }, "bin": { "multicast-dns": "cli.js" } }, "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg=="],
|
"multicast-dns": ["multicast-dns@7.2.5", "", { "dependencies": { "dns-packet": "^5.2.2", "thunky": "^1.0.2" }, "bin": { "multicast-dns": "cli.js" } }, "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg=="],
|
||||||
|
|
||||||
"mustache": ["mustache@4.2.0", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="],
|
"mustache": ["mustache@4.2.0", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="],
|
||||||
@@ -3183,6 +3285,8 @@
|
|||||||
|
|
||||||
"omggif": ["omggif@1.0.10", "", {}, "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw=="],
|
"omggif": ["omggif@1.0.10", "", {}, "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw=="],
|
||||||
|
|
||||||
|
"on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="],
|
||||||
|
|
||||||
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
||||||
|
|
||||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||||
@@ -3261,6 +3365,8 @@
|
|||||||
|
|
||||||
"pascal-case": ["pascal-case@3.1.2", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g=="],
|
"pascal-case": ["pascal-case@3.1.2", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g=="],
|
||||||
|
|
||||||
|
"path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
|
||||||
|
|
||||||
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
||||||
|
|
||||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||||
@@ -3289,6 +3395,12 @@
|
|||||||
|
|
||||||
"pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="],
|
"pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="],
|
||||||
|
|
||||||
|
"pino": ["pino@10.3.0", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^3.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^4.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-0GNPNzHXBKw6U/InGe79A3Crzyk9bcSyObF9/Gfo9DLEf5qj5RF50RSjsu0W1rZ6ZqRGdzDFCRBQvi9/rSGPtA=="],
|
||||||
|
|
||||||
|
"pino-abstract-transport": ["pino-abstract-transport@3.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg=="],
|
||||||
|
|
||||||
|
"pino-std-serializers": ["pino-std-serializers@7.1.0", "", {}, "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw=="],
|
||||||
|
|
||||||
"pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="],
|
"pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="],
|
||||||
|
|
||||||
"pixelmatch": ["pixelmatch@5.3.0", "", { "dependencies": { "pngjs": "^6.0.0" }, "bin": { "pixelmatch": "bin/pixelmatch" } }, "sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q=="],
|
"pixelmatch": ["pixelmatch@5.3.0", "", { "dependencies": { "pngjs": "^6.0.0" }, "bin": { "pixelmatch": "bin/pixelmatch" } }, "sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q=="],
|
||||||
@@ -3341,6 +3453,8 @@
|
|||||||
|
|
||||||
"process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
|
"process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
|
||||||
|
|
||||||
|
"process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="],
|
||||||
|
|
||||||
"promise.allsettled": ["promise.allsettled@1.0.7", "", { "dependencies": { "array.prototype.map": "^1.0.5", "call-bind": "^1.0.2", "define-properties": "^1.2.0", "es-abstract": "^1.22.1", "get-intrinsic": "^1.2.1", "iterate-value": "^1.0.2" } }, "sha512-hezvKvQQmsFkOdrZfYxUxkyxl8mgFQeT259Ajj9PXdbg9VzBCWrItOev72JyWxkCD5VSSqAeHmlN3tWx4DlmsA=="],
|
"promise.allsettled": ["promise.allsettled@1.0.7", "", { "dependencies": { "array.prototype.map": "^1.0.5", "call-bind": "^1.0.2", "define-properties": "^1.2.0", "es-abstract": "^1.22.1", "get-intrinsic": "^1.2.1", "iterate-value": "^1.0.2" } }, "sha512-hezvKvQQmsFkOdrZfYxUxkyxl8mgFQeT259Ajj9PXdbg9VzBCWrItOev72JyWxkCD5VSSqAeHmlN3tWx4DlmsA=="],
|
||||||
|
|
||||||
"prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="],
|
"prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="],
|
||||||
@@ -3363,6 +3477,8 @@
|
|||||||
|
|
||||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||||
|
|
||||||
|
"quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="],
|
||||||
|
|
||||||
"radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="],
|
"radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="],
|
||||||
|
|
||||||
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
||||||
@@ -3397,6 +3513,8 @@
|
|||||||
|
|
||||||
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||||
|
|
||||||
|
"real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="],
|
||||||
|
|
||||||
"recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="],
|
"recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="],
|
||||||
|
|
||||||
"recma-jsx": ["recma-jsx@1.0.1", "", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" }, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w=="],
|
"recma-jsx": ["recma-jsx@1.0.1", "", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" }, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w=="],
|
||||||
@@ -3449,6 +3567,10 @@
|
|||||||
|
|
||||||
"remeda": ["remeda@2.26.0", "", { "dependencies": { "type-fest": "^4.41.0" } }, "sha512-lmNNwtaC6Co4m0WTTNoZ/JlpjEqAjPZO0+czC9YVRQUpkbS4x8Hmh+Mn9HPfJfiXqUQ5IXXgSXSOB2pBKAytdA=="],
|
"remeda": ["remeda@2.26.0", "", { "dependencies": { "type-fest": "^4.41.0" } }, "sha512-lmNNwtaC6Co4m0WTTNoZ/JlpjEqAjPZO0+czC9YVRQUpkbS4x8Hmh+Mn9HPfJfiXqUQ5IXXgSXSOB2pBKAytdA=="],
|
||||||
|
|
||||||
|
"request-light": ["request-light@0.7.0", "", {}, "sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q=="],
|
||||||
|
|
||||||
|
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
||||||
|
|
||||||
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||||
|
|
||||||
"reselect": ["reselect@4.1.8", "", {}, "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ=="],
|
"reselect": ["reselect@4.1.8", "", {}, "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ=="],
|
||||||
@@ -3463,6 +3585,8 @@
|
|||||||
|
|
||||||
"restructure": ["restructure@3.0.2", "", {}, "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw=="],
|
"restructure": ["restructure@3.0.2", "", {}, "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw=="],
|
||||||
|
|
||||||
|
"ret": ["ret@0.5.0", "", {}, "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw=="],
|
||||||
|
|
||||||
"retext": ["retext@9.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "retext-latin": "^4.0.0", "retext-stringify": "^4.0.0", "unified": "^11.0.0" } }, "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA=="],
|
"retext": ["retext@9.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "retext-latin": "^4.0.0", "retext-stringify": "^4.0.0", "unified": "^11.0.0" } }, "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA=="],
|
||||||
|
|
||||||
"retext-latin": ["retext-latin@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "parse-latin": "^7.0.0", "unified": "^11.0.0" } }, "sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA=="],
|
"retext-latin": ["retext-latin@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "parse-latin": "^7.0.0", "unified": "^11.0.0" } }, "sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA=="],
|
||||||
@@ -3475,6 +3599,8 @@
|
|||||||
|
|
||||||
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
||||||
|
|
||||||
|
"rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
|
||||||
|
|
||||||
"rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="],
|
"rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="],
|
||||||
|
|
||||||
"rollup": ["rollup@4.57.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.0", "@rollup/rollup-android-arm64": "4.57.0", "@rollup/rollup-darwin-arm64": "4.57.0", "@rollup/rollup-darwin-x64": "4.57.0", "@rollup/rollup-freebsd-arm64": "4.57.0", "@rollup/rollup-freebsd-x64": "4.57.0", "@rollup/rollup-linux-arm-gnueabihf": "4.57.0", "@rollup/rollup-linux-arm-musleabihf": "4.57.0", "@rollup/rollup-linux-arm64-gnu": "4.57.0", "@rollup/rollup-linux-arm64-musl": "4.57.0", "@rollup/rollup-linux-loong64-gnu": "4.57.0", "@rollup/rollup-linux-loong64-musl": "4.57.0", "@rollup/rollup-linux-ppc64-gnu": "4.57.0", "@rollup/rollup-linux-ppc64-musl": "4.57.0", "@rollup/rollup-linux-riscv64-gnu": "4.57.0", "@rollup/rollup-linux-riscv64-musl": "4.57.0", "@rollup/rollup-linux-s390x-gnu": "4.57.0", "@rollup/rollup-linux-x64-gnu": "4.57.0", "@rollup/rollup-linux-x64-musl": "4.57.0", "@rollup/rollup-openbsd-x64": "4.57.0", "@rollup/rollup-openharmony-arm64": "4.57.0", "@rollup/rollup-win32-arm64-msvc": "4.57.0", "@rollup/rollup-win32-ia32-msvc": "4.57.0", "@rollup/rollup-win32-x64-gnu": "4.57.0", "@rollup/rollup-win32-x64-msvc": "4.57.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA=="],
|
"rollup": ["rollup@4.57.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.0", "@rollup/rollup-android-arm64": "4.57.0", "@rollup/rollup-darwin-arm64": "4.57.0", "@rollup/rollup-darwin-x64": "4.57.0", "@rollup/rollup-freebsd-arm64": "4.57.0", "@rollup/rollup-freebsd-x64": "4.57.0", "@rollup/rollup-linux-arm-gnueabihf": "4.57.0", "@rollup/rollup-linux-arm-musleabihf": "4.57.0", "@rollup/rollup-linux-arm64-gnu": "4.57.0", "@rollup/rollup-linux-arm64-musl": "4.57.0", "@rollup/rollup-linux-loong64-gnu": "4.57.0", "@rollup/rollup-linux-loong64-musl": "4.57.0", "@rollup/rollup-linux-ppc64-gnu": "4.57.0", "@rollup/rollup-linux-ppc64-musl": "4.57.0", "@rollup/rollup-linux-riscv64-gnu": "4.57.0", "@rollup/rollup-linux-riscv64-musl": "4.57.0", "@rollup/rollup-linux-s390x-gnu": "4.57.0", "@rollup/rollup-linux-x64-gnu": "4.57.0", "@rollup/rollup-linux-x64-musl": "4.57.0", "@rollup/rollup-openbsd-x64": "4.57.0", "@rollup/rollup-openharmony-arm64": "4.57.0", "@rollup/rollup-win32-arm64-msvc": "4.57.0", "@rollup/rollup-win32-ia32-msvc": "4.57.0", "@rollup/rollup-win32-x64-gnu": "4.57.0", "@rollup/rollup-win32-x64-msvc": "4.57.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA=="],
|
||||||
@@ -3497,6 +3623,10 @@
|
|||||||
|
|
||||||
"safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
|
"safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
|
||||||
|
|
||||||
|
"safe-regex2": ["safe-regex2@5.0.0", "", { "dependencies": { "ret": "~0.5.0" } }, "sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw=="],
|
||||||
|
|
||||||
|
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
|
||||||
|
|
||||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||||
|
|
||||||
"sax": ["sax@1.2.1", "", {}, "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA=="],
|
"sax": ["sax@1.2.1", "", {}, "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA=="],
|
||||||
@@ -3505,6 +3635,8 @@
|
|||||||
|
|
||||||
"section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="],
|
"section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="],
|
||||||
|
|
||||||
|
"secure-json-parse": ["secure-json-parse@4.1.0", "", {}, "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA=="],
|
||||||
|
|
||||||
"selderee": ["selderee@0.11.0", "", { "dependencies": { "parseley": "^0.12.0" } }, "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA=="],
|
"selderee": ["selderee@0.11.0", "", { "dependencies": { "parseley": "^0.12.0" } }, "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA=="],
|
||||||
|
|
||||||
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||||
@@ -3519,6 +3651,8 @@
|
|||||||
|
|
||||||
"serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="],
|
"serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="],
|
||||||
|
|
||||||
|
"set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
|
||||||
|
|
||||||
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
|
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
|
||||||
|
|
||||||
"set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="],
|
"set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="],
|
||||||
@@ -3581,6 +3715,8 @@
|
|||||||
|
|
||||||
"solid-use": ["solid-use@0.9.1", "", { "peerDependencies": { "solid-js": "^1.7" } }, "sha512-UwvXDVPlrrbj/9ewG9ys5uL2IO4jSiwys2KPzK4zsnAcmEl7iDafZWW1Mo4BSEWOmQCGK6IvpmGHo1aou8iOFw=="],
|
"solid-use": ["solid-use@0.9.1", "", { "peerDependencies": { "solid-js": "^1.7" } }, "sha512-UwvXDVPlrrbj/9ewG9ys5uL2IO4jSiwys2KPzK4zsnAcmEl7iDafZWW1Mo4BSEWOmQCGK6IvpmGHo1aou8iOFw=="],
|
||||||
|
|
||||||
|
"sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="],
|
||||||
|
|
||||||
"source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
|
"source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
|
||||||
|
|
||||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
@@ -3589,6 +3725,8 @@
|
|||||||
|
|
||||||
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
|
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
|
||||||
|
|
||||||
|
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
|
||||||
|
|
||||||
"sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
|
"sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
|
||||||
|
|
||||||
"sqlstring": ["sqlstring@2.3.3", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="],
|
"sqlstring": ["sqlstring@2.3.3", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="],
|
||||||
@@ -3691,6 +3829,8 @@
|
|||||||
|
|
||||||
"thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="],
|
"thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="],
|
||||||
|
|
||||||
|
"thread-stream": ["thread-stream@4.0.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA=="],
|
||||||
|
|
||||||
"three": ["three@0.177.0", "", {}, "sha512-EiXv5/qWAaGI+Vz2A+JfavwYCMdGjxVsrn3oBwllUoqYeaBO75J63ZfyaQKoiLrqNHoTlUc6PFgMXnS0kI45zg=="],
|
"three": ["three@0.177.0", "", {}, "sha512-EiXv5/qWAaGI+Vz2A+JfavwYCMdGjxVsrn3oBwllUoqYeaBO75J63ZfyaQKoiLrqNHoTlUc6PFgMXnS0kI45zg=="],
|
||||||
|
|
||||||
"thunky": ["thunky@1.1.0", "", {}, "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA=="],
|
"thunky": ["thunky@1.1.0", "", {}, "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA=="],
|
||||||
@@ -3773,8 +3913,12 @@
|
|||||||
|
|
||||||
"typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="],
|
"typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="],
|
||||||
|
|
||||||
|
"typesafe-path": ["typesafe-path@0.2.2", "", {}, "sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA=="],
|
||||||
|
|
||||||
"typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="],
|
"typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="],
|
||||||
|
|
||||||
|
"typescript-auto-import-cache": ["typescript-auto-import-cache@0.3.6", "", { "dependencies": { "semver": "^7.3.8" } }, "sha512-RpuHXrknHdVdK7wv/8ug3Fr0WNsNi5l5aB8MYYuXhq2UH5lnEB1htJ1smhtD5VeCsGr2p8mUDtd83LCQDFVgjQ=="],
|
||||||
|
|
||||||
"ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="],
|
"ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="],
|
||||||
|
|
||||||
"ulid": ["ulid@3.0.1", "", { "bin": { "ulid": "dist/cli.js" } }, "sha512-dPJyqPzx8preQhqq24bBG1YNkvigm87K8kVEHCD+ruZg24t6IFEFv00xMWfxcC4djmFtiTLdFuADn4+DOz6R7Q=="],
|
"ulid": ["ulid@3.0.1", "", { "bin": { "ulid": "dist/cli.js" } }, "sha512-dPJyqPzx8preQhqq24bBG1YNkvigm87K8kVEHCD+ruZg24t6IFEFv00xMWfxcC4djmFtiTLdFuADn4+DOz6R7Q=="],
|
||||||
@@ -3871,10 +4015,40 @@
|
|||||||
|
|
||||||
"vitest": ["vitest@4.0.18", "", { "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", "@vitest/pretty-format": "4.0.18", "@vitest/runner": "4.0.18", "@vitest/snapshot": "4.0.18", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.18", "@vitest/browser-preview": "4.0.18", "@vitest/browser-webdriverio": "4.0.18", "@vitest/ui": "4.0.18", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ=="],
|
"vitest": ["vitest@4.0.18", "", { "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", "@vitest/pretty-format": "4.0.18", "@vitest/runner": "4.0.18", "@vitest/snapshot": "4.0.18", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.18", "@vitest/browser-preview": "4.0.18", "@vitest/browser-webdriverio": "4.0.18", "@vitest/ui": "4.0.18", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ=="],
|
||||||
|
|
||||||
|
"volar-service-css": ["volar-service-css@0.0.68", "", { "dependencies": { "vscode-css-languageservice": "^6.3.0", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-lJSMh6f3QzZ1tdLOZOzovLX0xzAadPhx8EKwraDLPxBndLCYfoTvnNuiFFV8FARrpAlW5C0WkH+TstPaCxr00Q=="],
|
||||||
|
|
||||||
|
"volar-service-emmet": ["volar-service-emmet@0.0.68", "", { "dependencies": { "@emmetio/css-parser": "^0.4.1", "@emmetio/html-matcher": "^1.3.0", "@vscode/emmet-helper": "^2.9.3", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-nHvixrRQ83EzkQ4G/jFxu9Y4eSsXS/X2cltEPDM+K9qZmIv+Ey1w0tg1+6caSe8TU5Hgw4oSTwNMf/6cQb3LzQ=="],
|
||||||
|
|
||||||
|
"volar-service-html": ["volar-service-html@0.0.68", "", { "dependencies": { "vscode-html-languageservice": "^5.3.0", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-fru9gsLJxy33xAltXOh4TEdi312HP80hpuKhpYQD4O5hDnkNPEBdcQkpB+gcX0oK0VxRv1UOzcGQEUzWCVHLfA=="],
|
||||||
|
|
||||||
|
"volar-service-prettier": ["volar-service-prettier@0.0.68", "", { "dependencies": { "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0", "prettier": "^2.2 || ^3.0" }, "optionalPeers": ["@volar/language-service", "prettier"] }, "sha512-grUmWHkHlebMOd6V8vXs2eNQUw/bJGJMjekh/EPf/p2ZNTK0Uyz7hoBRngcvGfJHMsSXZH8w/dZTForIW/4ihw=="],
|
||||||
|
|
||||||
|
"volar-service-typescript": ["volar-service-typescript@0.0.68", "", { "dependencies": { "path-browserify": "^1.0.1", "semver": "^7.6.2", "typescript-auto-import-cache": "^0.3.5", "vscode-languageserver-textdocument": "^1.0.11", "vscode-nls": "^5.2.0", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-z7B/7CnJ0+TWWFp/gh2r5/QwMObHNDiQiv4C9pTBNI2Wxuwymd4bjEORzrJ/hJ5Yd5+OzeYK+nFCKevoGEEeKw=="],
|
||||||
|
|
||||||
|
"volar-service-typescript-twoslash-queries": ["volar-service-typescript-twoslash-queries@0.0.68", "", { "dependencies": { "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-NugzXcM0iwuZFLCJg47vI93su5YhTIweQuLmZxvz5ZPTaman16JCvmDZexx2rd5T/75SNuvvZmrTOTNYUsfe5w=="],
|
||||||
|
|
||||||
|
"volar-service-yaml": ["volar-service-yaml@0.0.68", "", { "dependencies": { "vscode-uri": "^3.0.8", "yaml-language-server": "~1.19.2" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-84XgE02LV0OvTcwfqhcSwVg4of3MLNUWPMArO6Aj8YXqyEVnPu8xTEMY2btKSq37mVAPuaEVASI4e3ptObmqcA=="],
|
||||||
|
|
||||||
|
"vscode-css-languageservice": ["vscode-css-languageservice@6.3.9", "", { "dependencies": { "@vscode/l10n": "^0.0.18", "vscode-languageserver-textdocument": "^1.0.12", "vscode-languageserver-types": "3.17.5", "vscode-uri": "^3.1.0" } }, "sha512-1tLWfp+TDM5ZuVWht3jmaY5y7O6aZmpeXLoHl5bv1QtRsRKt4xYGRMmdJa5Pqx/FTkgRbsna9R+Gn2xE+evVuA=="],
|
||||||
|
|
||||||
|
"vscode-html-languageservice": ["vscode-html-languageservice@5.6.1", "", { "dependencies": { "@vscode/l10n": "^0.0.18", "vscode-languageserver-textdocument": "^1.0.12", "vscode-languageserver-types": "^3.17.5", "vscode-uri": "^3.1.0" } }, "sha512-5Mrqy5CLfFZUgkyhNZLA1Ye5g12Cb/v6VM7SxUzZUaRKWMDz4md+y26PrfRTSU0/eQAl3XpO9m2og+GGtDMuaA=="],
|
||||||
|
|
||||||
|
"vscode-json-languageservice": ["vscode-json-languageservice@4.1.8", "", { "dependencies": { "jsonc-parser": "^3.0.0", "vscode-languageserver-textdocument": "^1.0.1", "vscode-languageserver-types": "^3.16.0", "vscode-nls": "^5.0.0", "vscode-uri": "^3.0.2" } }, "sha512-0vSpg6Xd9hfV+eZAaYN63xVVMOTmJ4GgHxXnkLCh+9RsQBkWKIghzLhW2B9ebfG+LQQg8uLtsQ2aUKjTgE+QOg=="],
|
||||||
|
|
||||||
"vscode-jsonrpc": ["vscode-jsonrpc@8.2.1", "", {}, "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ=="],
|
"vscode-jsonrpc": ["vscode-jsonrpc@8.2.1", "", {}, "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ=="],
|
||||||
|
|
||||||
|
"vscode-languageserver": ["vscode-languageserver@9.0.1", "", { "dependencies": { "vscode-languageserver-protocol": "3.17.5" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g=="],
|
||||||
|
|
||||||
|
"vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.17.5", "", { "dependencies": { "vscode-jsonrpc": "8.2.0", "vscode-languageserver-types": "3.17.5" } }, "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg=="],
|
||||||
|
|
||||||
|
"vscode-languageserver-textdocument": ["vscode-languageserver-textdocument@1.0.12", "", {}, "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="],
|
||||||
|
|
||||||
"vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="],
|
"vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="],
|
||||||
|
|
||||||
|
"vscode-nls": ["vscode-nls@5.2.0", "", {}, "sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng=="],
|
||||||
|
|
||||||
|
"vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="],
|
||||||
|
|
||||||
"web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
|
"web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
|
||||||
|
|
||||||
"web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="],
|
"web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="],
|
||||||
@@ -3935,6 +4109,8 @@
|
|||||||
|
|
||||||
"yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="],
|
"yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="],
|
||||||
|
|
||||||
|
"yaml-language-server": ["yaml-language-server@1.19.2", "", { "dependencies": { "@vscode/l10n": "^0.0.18", "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "lodash": "4.17.21", "prettier": "^3.5.0", "request-light": "^0.5.7", "vscode-json-languageservice": "4.1.8", "vscode-languageserver": "^9.0.0", "vscode-languageserver-textdocument": "^1.0.1", "vscode-languageserver-types": "^3.16.0", "vscode-uri": "^3.0.2", "yaml": "2.7.1" }, "bin": { "yaml-language-server": "bin/yaml-language-server" } }, "sha512-9F3myNmJzUN/679jycdMxqtydPSDRAarSj3wPiF7pchEPnO9Dg07Oc+gIYLqXR4L+g+FSEVXXv2+mr54StLFOg=="],
|
||||||
|
|
||||||
"yargs": ["yargs@18.0.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="],
|
"yargs": ["yargs@18.0.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="],
|
||||||
|
|
||||||
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
||||||
@@ -3989,6 +4165,8 @@
|
|||||||
|
|
||||||
"@ai-sdk/deepinfra/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="],
|
"@ai-sdk/deepinfra/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="],
|
||||||
|
|
||||||
|
"@ai-sdk/fireworks/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="],
|
||||||
|
|
||||||
"@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.58", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CkNW5L1Arv8gPtPlEmKd+yf/SG9ucJf0XQdpMG8OiYEtEMc2smuCA+tyCp8zI7IBVg/FE7nUfFHntQFaOjRwJQ=="],
|
"@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.58", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CkNW5L1Arv8gPtPlEmKd+yf/SG9ucJf0XQdpMG8OiYEtEMc2smuCA+tyCp8zI7IBVg/FE7nUfFHntQFaOjRwJQ=="],
|
||||||
|
|
||||||
"@ai-sdk/openai/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="],
|
"@ai-sdk/openai/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="],
|
||||||
@@ -4003,7 +4181,9 @@
|
|||||||
|
|
||||||
"@ai-sdk/vercel/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="],
|
"@ai-sdk/vercel/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="],
|
||||||
|
|
||||||
"@ai-sdk/xai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="],
|
"@ai-sdk/xai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-thubwhRtv9uicAxSWwNpinM7hiL/0CkhL/ymPaHuKvI494J7HIzn8KQZQ2ymRz284WTIZnI7VMyyejxW4RMM6w=="],
|
||||||
|
|
||||||
|
"@astrojs/check/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
|
||||||
|
|
||||||
"@astrojs/cloudflare/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="],
|
"@astrojs/cloudflare/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="],
|
||||||
|
|
||||||
@@ -4071,6 +4251,8 @@
|
|||||||
|
|
||||||
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
|
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
|
||||||
|
|
||||||
|
"@fastify/proxy-addr/ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="],
|
||||||
|
|
||||||
"@gitlab/gitlab-ai-provider/openai": ["openai@6.17.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-NHRpPEUPzAvFOAFs9+9pC6+HCw/iWsYsKCMPXH5Kw7BpMxqd8g/A07/1o7Gx2TWtCnzevVRyKMRFqyiHyAlqcA=="],
|
"@gitlab/gitlab-ai-provider/openai": ["openai@6.17.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-NHRpPEUPzAvFOAFs9+9pC6+HCw/iWsYsKCMPXH5Kw7BpMxqd8g/A07/1o7Gx2TWtCnzevVRyKMRFqyiHyAlqcA=="],
|
||||||
|
|
||||||
"@gitlab/gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
"@gitlab/gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||||
@@ -4197,7 +4379,7 @@
|
|||||||
|
|
||||||
"@opencode-ai/desktop/typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="],
|
"@opencode-ai/desktop/typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="],
|
||||||
|
|
||||||
"@opencode-ai/web/@shikijs/transformers": ["@shikijs/transformers@3.4.2", "", { "dependencies": { "@shikijs/core": "3.4.2", "@shikijs/types": "3.4.2" } }, "sha512-I5baLVi/ynLEOZoWSAMlACHNnG+yw5HDmse0oe+GW6U1u+ULdEB3UHiVWaHoJSSONV7tlcVxuaMy74sREDkSvg=="],
|
"@opencode-ai/web/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="],
|
||||||
|
|
||||||
"@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
|
"@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
|
||||||
|
|
||||||
@@ -4271,8 +4453,20 @@
|
|||||||
|
|
||||||
"@tanstack/server-functions-plugin/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
"@tanstack/server-functions-plugin/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
|
"@vscode/emmet-helper/jsonc-parser": ["jsonc-parser@2.3.1", "", {}, "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg=="],
|
||||||
|
|
||||||
"accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
"accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||||
|
|
||||||
|
"ai-gateway-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.58", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CkNW5L1Arv8gPtPlEmKd+yf/SG9ucJf0XQdpMG8OiYEtEMc2smuCA+tyCp8zI7IBVg/FE7nUfFHntQFaOjRwJQ=="],
|
||||||
|
|
||||||
|
"ai-gateway-provider/@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.90", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.56", "@ai-sdk/google": "2.0.46", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-C9MLe1KZGg1ZbupV2osygHtL5qngyCDA6ATatunyfTbIe8TXKG8HGni/3O6ifbnI5qxTidIn150Ox7eIFZVMYg=="],
|
||||||
|
|
||||||
|
"ai-gateway-provider/@ai-sdk/openai": ["@ai-sdk/openai@2.0.89", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-4+qWkBCbL9HPKbgrUO/F2uXZ8GqrYxHa8SWEYIzxEJ9zvWw3ISr3t1/27O1i8MGSym+PzEyHBT48EV4LAwWaEw=="],
|
||||||
|
|
||||||
|
"ai-gateway-provider/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="],
|
||||||
|
|
||||||
|
"ai-gateway-provider/@ai-sdk/xai": ["@ai-sdk/xai@2.0.56", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.32", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-FGlqwWc3tAYqDHE8r8hQGQLcMiPUwgz90oU2QygUH930OWtCLapFkSu114DgVaIN/qoM1DUX+inv0Ee74Fgp5g=="],
|
||||||
|
|
||||||
"ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
"ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||||
|
|
||||||
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||||
@@ -4361,6 +4555,8 @@
|
|||||||
|
|
||||||
"lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
"lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
||||||
|
|
||||||
|
"light-my-request/process-warning": ["process-warning@4.0.1", "", {}, "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q=="],
|
||||||
|
|
||||||
"lightningcss/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
"lightningcss/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||||
|
|
||||||
"md-to-react-email/marked": ["marked@7.0.4", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ=="],
|
"md-to-react-email/marked": ["marked@7.0.4", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ=="],
|
||||||
@@ -4485,6 +4681,8 @@
|
|||||||
|
|
||||||
"vitest/why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
|
"vitest/why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
|
||||||
|
|
||||||
|
"vscode-languageserver-protocol/vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="],
|
||||||
|
|
||||||
"which-builtin-type/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
"which-builtin-type/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
||||||
|
|
||||||
"wrangler/esbuild": ["esbuild@0.25.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q=="],
|
"wrangler/esbuild": ["esbuild@0.25.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q=="],
|
||||||
@@ -4495,6 +4693,12 @@
|
|||||||
|
|
||||||
"wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
"wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
|
"yaml-language-server/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
||||||
|
|
||||||
|
"yaml-language-server/request-light": ["request-light@0.5.8", "", {}, "sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg=="],
|
||||||
|
|
||||||
|
"yaml-language-server/yaml": ["yaml@2.7.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ=="],
|
||||||
|
|
||||||
"yargs/yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="],
|
"yargs/yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="],
|
||||||
|
|
||||||
"zod-to-json-schema/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
"zod-to-json-schema/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||||
@@ -4515,6 +4719,10 @@
|
|||||||
|
|
||||||
"@ai-sdk/openai/@ai-sdk/provider-utils/zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="],
|
"@ai-sdk/openai/@ai-sdk/provider-utils/zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="],
|
||||||
|
|
||||||
|
"@astrojs/check/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
||||||
|
|
||||||
|
"@astrojs/check/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||||
|
|
||||||
"@astrojs/mdx/@astrojs/markdown-remark/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.7.5", "", {}, "sha512-vreGnYSSKhAjFJCWAwe/CNhONvoc5lokxtRoZims+0wa3KbHBdPHSSthJsKxPd8d/aic6lWKpRTYGY/hsgK6EA=="],
|
"@astrojs/mdx/@astrojs/markdown-remark/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.7.5", "", {}, "sha512-vreGnYSSKhAjFJCWAwe/CNhONvoc5lokxtRoZims+0wa3KbHBdPHSSthJsKxPd8d/aic6lWKpRTYGY/hsgK6EA=="],
|
||||||
|
|
||||||
"@astrojs/mdx/@astrojs/markdown-remark/@astrojs/prism": ["@astrojs/prism@3.3.0", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ=="],
|
"@astrojs/mdx/@astrojs/markdown-remark/@astrojs/prism": ["@astrojs/prism@3.3.0", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ=="],
|
||||||
@@ -4759,9 +4967,9 @@
|
|||||||
|
|
||||||
"@opencode-ai/desktop/@actions/artifact/@actions/http-client": ["@actions/http-client@2.2.3", "", { "dependencies": { "tunnel": "^0.0.6", "undici": "^5.25.4" } }, "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA=="],
|
"@opencode-ai/desktop/@actions/artifact/@actions/http-client": ["@actions/http-client@2.2.3", "", { "dependencies": { "tunnel": "^0.0.6", "undici": "^5.25.4" } }, "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA=="],
|
||||||
|
|
||||||
"@opencode-ai/web/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.4.2", "", { "dependencies": { "@shikijs/types": "3.4.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-AG8vnSi1W2pbgR2B911EfGqtLE9c4hQBYkv/x7Z+Kt0VxhgQKcW7UNDVYsu9YxwV6u+OJrvdJrMq6DNWoBjihQ=="],
|
"@opencode-ai/web/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="],
|
||||||
|
|
||||||
"@opencode-ai/web/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.4.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-zHC1l7L+eQlDXLnxvM9R91Efh2V4+rN3oMVS2swCBssbj2U/FBwybD1eeLaq8yl/iwT+zih8iUbTBCgGZOYlVg=="],
|
"@opencode-ai/web/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="],
|
||||||
|
|
||||||
"@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
"@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
|
|
||||||
@@ -4805,6 +5013,14 @@
|
|||||||
|
|
||||||
"accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
"accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||||
|
|
||||||
|
"ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.56", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XHJKu0Yvfu9SPzRfsAFESa+9T7f2YJY6TxykKMfRsAwpeWAiX/Gbx5J5uM15AzYC3Rw8tVP3oH+j7jEivENirQ=="],
|
||||||
|
|
||||||
|
"ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/google": ["@ai-sdk/google@2.0.46", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8PK6u4sGE/kXebd7ZkTp+0aya4kNqzoqpS5m7cHY2NfTK6fhPc6GNvE+MZIZIoHQTp5ed86wGBdeBPpFaaUtyg=="],
|
||||||
|
|
||||||
|
"ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="],
|
||||||
|
|
||||||
|
"ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
|
||||||
|
|
||||||
"ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
"ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
|
|
||||||
"ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
"ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
@@ -4997,6 +5213,14 @@
|
|||||||
|
|
||||||
"@actions/github/@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="],
|
"@actions/github/@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="],
|
||||||
|
|
||||||
|
"@astrojs/check/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
|
"@astrojs/check/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||||
|
|
||||||
|
"@astrojs/check/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
|
|
||||||
|
"@astrojs/check/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
"@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
|
"@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
|
||||||
|
|
||||||
"@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
|
"@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
|
||||||
@@ -5137,6 +5361,10 @@
|
|||||||
|
|
||||||
"tw-to-css/tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
"tw-to-css/tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
||||||
|
|
||||||
|
"@astrojs/check/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|
||||||
|
"@astrojs/check/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|
||||||
"@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.782.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.775.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", "@aws-sdk/middleware-user-agent": "3.782.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.782.0", "@aws-sdk/util-user-agent-browser": "3.775.0", "@aws-sdk/util-user-agent-node": "3.782.0", "@smithy/config-resolver": "^4.1.0", "@smithy/core": "^3.2.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", "@smithy/middleware-endpoint": "^4.1.0", "@smithy/middleware-retry": "^4.1.0", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.8", "@smithy/util-defaults-mode-node": "^4.0.8", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-QOYC8q7luzHFXrP0xYAqBctoPkynjfV0r9dqntFu4/IWMTyC1vlo1UTxFAjIPyclYw92XJyEkVCVg9v/nQnsUA=="],
|
"@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.782.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.775.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", "@aws-sdk/middleware-user-agent": "3.782.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.782.0", "@aws-sdk/util-user-agent-browser": "3.775.0", "@aws-sdk/util-user-agent-node": "3.782.0", "@smithy/config-resolver": "^4.1.0", "@smithy/core": "^3.2.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", "@smithy/middleware-endpoint": "^4.1.0", "@smithy/middleware-retry": "^4.1.0", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.8", "@smithy/util-defaults-mode-node": "^4.0.8", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-QOYC8q7luzHFXrP0xYAqBctoPkynjfV0r9dqntFu4/IWMTyC1vlo1UTxFAjIPyclYw92XJyEkVCVg9v/nQnsUA=="],
|
||||||
|
|
||||||
"@jsx-email/cli/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
"@jsx-email/cli/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||||
|
|||||||
Generated
+3
-3
@@ -2,11 +2,11 @@
|
|||||||
"nodes": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1768393167,
|
"lastModified": 1770073757,
|
||||||
"narHash": "sha256-n2063BRjHde6DqAz2zavhOOiLUwA3qXt7jQYHyETjX8=",
|
"narHash": "sha256-Vy+G+F+3E/Tl+GMNgiHl9Pah2DgShmIUBJXmbiQPHbI=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "2f594d5af95d4fdac67fba60376ec11e482041cb",
|
"rev": "47472570b1e607482890801aeaf29bfb749884f6",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -30,6 +30,26 @@
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
overlays = {
|
||||||
|
default =
|
||||||
|
final: _prev:
|
||||||
|
let
|
||||||
|
node_modules = final.callPackage ./nix/node_modules.nix {
|
||||||
|
inherit rev;
|
||||||
|
};
|
||||||
|
opencode = final.callPackage ./nix/opencode.nix {
|
||||||
|
inherit node_modules;
|
||||||
|
};
|
||||||
|
desktop = final.callPackage ./nix/desktop.nix {
|
||||||
|
inherit opencode;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
inherit opencode;
|
||||||
|
opencode-desktop = desktop;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
packages = forEachSystem (
|
packages = forEachSystem (
|
||||||
pkgs:
|
pkgs:
|
||||||
let
|
let
|
||||||
@@ -42,28 +62,15 @@
|
|||||||
desktop = pkgs.callPackage ./nix/desktop.nix {
|
desktop = pkgs.callPackage ./nix/desktop.nix {
|
||||||
inherit opencode;
|
inherit opencode;
|
||||||
};
|
};
|
||||||
# nixpkgs cpu naming to bun cpu naming
|
|
||||||
cpuMap = { x86_64 = "x64"; aarch64 = "arm64"; };
|
|
||||||
# matrix of node_modules builds - these will always fail due to fakeHash usage
|
|
||||||
# but allow computation of the correct hash from any build machine for any cpu/os
|
|
||||||
# see the update-nix-hashes workflow for usage
|
|
||||||
moduleUpdaters = pkgs.lib.listToAttrs (
|
|
||||||
pkgs.lib.concatMap (cpu:
|
|
||||||
map (os: {
|
|
||||||
name = "${cpu}-${os}_node_modules";
|
|
||||||
value = node_modules.override {
|
|
||||||
bunCpu = cpuMap.${cpu};
|
|
||||||
bunOs = os;
|
|
||||||
hash = pkgs.lib.fakeHash;
|
|
||||||
};
|
|
||||||
}) [ "linux" "darwin" ]
|
|
||||||
) [ "x86_64" "aarch64" ]
|
|
||||||
);
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
default = opencode;
|
default = opencode;
|
||||||
inherit opencode desktop;
|
inherit opencode desktop;
|
||||||
} // moduleUpdaters
|
# Updater derivation with fakeHash - build fails and reveals correct hash
|
||||||
|
node_modules_updater = node_modules.override {
|
||||||
|
hash = pkgs.lib.fakeHash;
|
||||||
|
};
|
||||||
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -275,7 +275,7 @@ async function assertOpencodeConnected() {
|
|||||||
body: {
|
body: {
|
||||||
service: "github-workflow",
|
service: "github-workflow",
|
||||||
level: "info",
|
level: "info",
|
||||||
message: "Prepare to react to Github Workflow event",
|
message: "Prepare to react to GitHub Workflow event",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
connected = true
|
connected = true
|
||||||
|
|||||||
+15
-9
@@ -135,6 +135,16 @@ const ZEN_MODELS = [
|
|||||||
new sst.Secret("ZEN_MODELS8"),
|
new sst.Secret("ZEN_MODELS8"),
|
||||||
new sst.Secret("ZEN_MODELS9"),
|
new sst.Secret("ZEN_MODELS9"),
|
||||||
new sst.Secret("ZEN_MODELS10"),
|
new sst.Secret("ZEN_MODELS10"),
|
||||||
|
new sst.Secret("ZEN_MODELS11"),
|
||||||
|
new sst.Secret("ZEN_MODELS12"),
|
||||||
|
new sst.Secret("ZEN_MODELS13"),
|
||||||
|
new sst.Secret("ZEN_MODELS14"),
|
||||||
|
new sst.Secret("ZEN_MODELS15"),
|
||||||
|
new sst.Secret("ZEN_MODELS16"),
|
||||||
|
new sst.Secret("ZEN_MODELS17"),
|
||||||
|
new sst.Secret("ZEN_MODELS18"),
|
||||||
|
new sst.Secret("ZEN_MODELS19"),
|
||||||
|
new sst.Secret("ZEN_MODELS20"),
|
||||||
]
|
]
|
||||||
const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
|
const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
|
||||||
const STRIPE_PUBLISHABLE_KEY = new sst.Secret("STRIPE_PUBLISHABLE_KEY")
|
const STRIPE_PUBLISHABLE_KEY = new sst.Secret("STRIPE_PUBLISHABLE_KEY")
|
||||||
@@ -156,14 +166,10 @@ const bucketNew = new sst.cloudflare.Bucket("ZenDataNew")
|
|||||||
const AWS_SES_ACCESS_KEY_ID = new sst.Secret("AWS_SES_ACCESS_KEY_ID")
|
const AWS_SES_ACCESS_KEY_ID = new sst.Secret("AWS_SES_ACCESS_KEY_ID")
|
||||||
const AWS_SES_SECRET_ACCESS_KEY = new sst.Secret("AWS_SES_SECRET_ACCESS_KEY")
|
const AWS_SES_SECRET_ACCESS_KEY = new sst.Secret("AWS_SES_SECRET_ACCESS_KEY")
|
||||||
|
|
||||||
let logProcessor
|
const logProcessor = new sst.cloudflare.Worker("LogProcessor", {
|
||||||
if ($app.stage === "production" || $app.stage === "frank") {
|
handler: "packages/console/function/src/log-processor.ts",
|
||||||
const HONEYCOMB_API_KEY = new sst.Secret("HONEYCOMB_API_KEY")
|
link: [new sst.Secret("HONEYCOMB_API_KEY")],
|
||||||
logProcessor = new sst.cloudflare.Worker("LogProcessor", {
|
})
|
||||||
handler: "packages/console/function/src/log-processor.ts",
|
|
||||||
link: [HONEYCOMB_API_KEY],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
new sst.cloudflare.x.SolidStart("Console", {
|
new sst.cloudflare.x.SolidStart("Console", {
|
||||||
domain,
|
domain,
|
||||||
@@ -201,7 +207,7 @@ new sst.cloudflare.x.SolidStart("Console", {
|
|||||||
transform: {
|
transform: {
|
||||||
worker: {
|
worker: {
|
||||||
placement: { mode: "smart" },
|
placement: { mode: "smart" },
|
||||||
tailConsumers: logProcessor ? [{ service: logProcessor.nodes.worker.scriptName }] : [],
|
tailConsumers: [{ service: logProcessor.nodes.worker.scriptName }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"keep": {
|
|
||||||
"days": true,
|
|
||||||
"amount": 14
|
|
||||||
},
|
|
||||||
"auditLog": "/home/thdxr/dev/projects/sst/opencode/logs/.2c5480b3b2480f80fa29b850af461dce619c0b2f-audit.json",
|
|
||||||
"files": [
|
|
||||||
{
|
|
||||||
"date": 1759827172859,
|
|
||||||
"name": "/home/thdxr/dev/projects/sst/opencode/logs/mcp-puppeteer-2025-10-07.log",
|
|
||||||
"hash": "a3d98b26edd793411b968a0d24cfeee8332138e282023c3b83ec169d55c67f16"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"hashType": "sha256"
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:52.879"}
|
|
||||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:52.880"}
|
|
||||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:56.191"}
|
|
||||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:56.192"}
|
|
||||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:59.267"}
|
|
||||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:59.268"}
|
|
||||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:20.276"}
|
|
||||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:20.277"}
|
|
||||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:30.838"}
|
|
||||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:30.839"}
|
|
||||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:42.452"}
|
|
||||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:42.452"}
|
|
||||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:46.499"}
|
|
||||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:46.500"}
|
|
||||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:54:02.295"}
|
|
||||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:54:02.295"}
|
|
||||||
{"arguments":{"url":"https://google.com"},"level":"debug","message":"Tool call received","service":"mcp-puppeteer","timestamp":"2025-10-07 04:54:37.150","tool":"puppeteer_navigate"}
|
|
||||||
{"0":"n","1":"p","2":"x","level":"info","message":"Launching browser with config:","service":"mcp-puppeteer","timestamp":"2025-10-07 04:54:37.150"}
|
|
||||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:55:08.488"}
|
|
||||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:55:08.489"}
|
|
||||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:11.815"}
|
|
||||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:11.816"}
|
|
||||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:21.934"}
|
|
||||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:21.935"}
|
|
||||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:32.544"}
|
|
||||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:32.544"}
|
|
||||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:41.154"}
|
|
||||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:41.155"}
|
|
||||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:55.426"}
|
|
||||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:55.427"}
|
|
||||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:15.715"}
|
|
||||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:15.716"}
|
|
||||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:25.063"}
|
|
||||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:25.064"}
|
|
||||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:48.567"}
|
|
||||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:48.568"}
|
|
||||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:25:08.937"}
|
|
||||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:25:08.938"}
|
|
||||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 22:38:37.120"}
|
|
||||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 22:38:37.121"}
|
|
||||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 22:38:52.490"}
|
|
||||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 22:38:52.491"}
|
|
||||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 22:39:25.524"}
|
|
||||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 22:39:25.525"}
|
|
||||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 22:40:57.126"}
|
|
||||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 22:40:57.127"}
|
|
||||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 22:42:24.175"}
|
|
||||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 22:42:24.176"}
|
|
||||||
+3
-3
@@ -45,8 +45,7 @@ rustPlatform.buildRustPackage (finalAttrs: {
|
|||||||
rustc
|
rustc
|
||||||
jq
|
jq
|
||||||
makeWrapper
|
makeWrapper
|
||||||
]
|
] ++ lib.optionals stdenv.hostPlatform.isLinux [ wrapGAppsHook4 ];
|
||||||
++ lib.optionals stdenv.hostPlatform.isLinux [ wrapGAppsHook4 ];
|
|
||||||
|
|
||||||
buildInputs = lib.optionals stdenv.isLinux [
|
buildInputs = lib.optionals stdenv.isLinux [
|
||||||
dbus
|
dbus
|
||||||
@@ -61,6 +60,7 @@ rustPlatform.buildRustPackage (finalAttrs: {
|
|||||||
gst_all_1.gstreamer
|
gst_all_1.gstreamer
|
||||||
gst_all_1.gst-plugins-base
|
gst_all_1.gst-plugins-base
|
||||||
gst_all_1.gst-plugins-good
|
gst_all_1.gst-plugins-good
|
||||||
|
gst_all_1.gst-plugins-bad
|
||||||
];
|
];
|
||||||
|
|
||||||
strictDeps = true;
|
strictDeps = true;
|
||||||
@@ -97,4 +97,4 @@ rustPlatform.buildRustPackage (finalAttrs: {
|
|||||||
mainProgram = "opencode-desktop";
|
mainProgram = "opencode-desktop";
|
||||||
inherit (opencode.meta) platforms;
|
inherit (opencode.meta) platforms;
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
|||||||
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"nodeModules": {
|
"nodeModules": {
|
||||||
"x86_64-linux": "sha256-3wRTDLo5FZoUc2Bwm1aAJZ4dNsekX8XoY6TwTmohgYo=",
|
"x86_64-linux": "sha256-pp2gb4nxiIT3VltB6Xli2wZPH32JfnMsI+BbihyU1+E=",
|
||||||
"aarch64-linux": "sha256-CKiuc6c52UV9cLEtccYEYS4QN0jYzNJv1fHSayqbHKo=",
|
"aarch64-linux": "sha256-hJwxhBICZz/pbIxQsF/sIpZTlFIgLpcAyF44O8wxMdU=",
|
||||||
"aarch64-darwin": "sha256-pWfXomWTDvG8WpWmUCwNXdbSHw6hPlqoT0Q/XuNceMc=",
|
"aarch64-darwin": "sha256-DPONXP52XOg/ApdSnLp32a+K5XCOnDGhbTUto2Rme0g=",
|
||||||
"x86_64-darwin": "sha256-Dmg4+cUq2r6vZB2ta9tLpNAWqcl11ZCu4ZpieegRFrY="
|
"x86_64-darwin": "sha256-KX1h5LRJSgthpbOPqWlbM/sPf8cvQrdRJvxtrz/FzBQ="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-11
@@ -2,8 +2,6 @@
|
|||||||
lib,
|
lib,
|
||||||
stdenvNoCC,
|
stdenvNoCC,
|
||||||
bun,
|
bun,
|
||||||
bunCpu ? if stdenvNoCC.hostPlatform.isAarch64 then "arm64" else "x64",
|
|
||||||
bunOs ? if stdenvNoCC.hostPlatform.isLinux then "linux" else "darwin",
|
|
||||||
rev ? "dirty",
|
rev ? "dirty",
|
||||||
hash ?
|
hash ?
|
||||||
(lib.pipe ./hashes.json [
|
(lib.pipe ./hashes.json [
|
||||||
@@ -16,6 +14,9 @@ let
|
|||||||
builtins.readFile
|
builtins.readFile
|
||||||
builtins.fromJSON
|
builtins.fromJSON
|
||||||
];
|
];
|
||||||
|
platform = stdenvNoCC.hostPlatform;
|
||||||
|
bunCpu = if platform.isAarch64 then "arm64" else "x64";
|
||||||
|
bunOs = if platform.isLinux then "linux" else "darwin";
|
||||||
in
|
in
|
||||||
stdenvNoCC.mkDerivation {
|
stdenvNoCC.mkDerivation {
|
||||||
pname = "opencode-node_modules";
|
pname = "opencode-node_modules";
|
||||||
@@ -29,7 +30,7 @@ stdenvNoCC.mkDerivation {
|
|||||||
../bun.lock
|
../bun.lock
|
||||||
../package.json
|
../package.json
|
||||||
../patches
|
../patches
|
||||||
../install
|
../install # required by desktop build (cli.rs include_str!)
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -39,23 +40,22 @@ stdenvNoCC.mkDerivation {
|
|||||||
"SOCKS_SERVER"
|
"SOCKS_SERVER"
|
||||||
];
|
];
|
||||||
|
|
||||||
nativeBuildInputs = [
|
nativeBuildInputs = [ bun ];
|
||||||
bun
|
|
||||||
];
|
|
||||||
|
|
||||||
dontConfigure = true;
|
dontConfigure = true;
|
||||||
|
|
||||||
buildPhase = ''
|
buildPhase = ''
|
||||||
runHook preBuild
|
runHook preBuild
|
||||||
export HOME=$(mktemp -d)
|
|
||||||
export BUN_INSTALL_CACHE_DIR=$(mktemp -d)
|
export BUN_INSTALL_CACHE_DIR=$(mktemp -d)
|
||||||
bun install \
|
bun install \
|
||||||
--cpu="${bunCpu}" \
|
--cpu="${bunCpu}" \
|
||||||
--os="${bunOs}" \
|
--os="${bunOs}" \
|
||||||
|
--filter '!./' \
|
||||||
|
--filter './packages/opencode' \
|
||||||
|
--filter './packages/desktop' \
|
||||||
--frozen-lockfile \
|
--frozen-lockfile \
|
||||||
--ignore-scripts \
|
--ignore-scripts \
|
||||||
--no-progress \
|
--no-progress
|
||||||
--linker=isolated
|
|
||||||
bun --bun ${./scripts/canonicalize-node-modules.ts}
|
bun --bun ${./scripts/canonicalize-node-modules.ts}
|
||||||
bun --bun ${./scripts/normalize-bun-binaries.ts}
|
bun --bun ${./scripts/normalize-bun-binaries.ts}
|
||||||
runHook postBuild
|
runHook postBuild
|
||||||
@@ -63,10 +63,8 @@ stdenvNoCC.mkDerivation {
|
|||||||
|
|
||||||
installPhase = ''
|
installPhase = ''
|
||||||
runHook preInstall
|
runHook preInstall
|
||||||
|
|
||||||
mkdir -p $out
|
mkdir -p $out
|
||||||
find . -type d -name node_modules -exec cp -R --parents {} $out \;
|
find . -type d -name node_modules -exec cp -R --parents {} $out \;
|
||||||
|
|
||||||
runHook postInstall
|
runHook postInstall
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -34,6 +34,7 @@ stdenvNoCC.mkDerivation (finalAttrs: {
|
|||||||
'';
|
'';
|
||||||
|
|
||||||
env.MODELS_DEV_API_JSON = "${models-dev}/dist/_api.json";
|
env.MODELS_DEV_API_JSON = "${models-dev}/dist/_api.json";
|
||||||
|
env.OPENCODE_DISABLE_MODELS_FETCH = true;
|
||||||
env.OPENCODE_VERSION = finalAttrs.version;
|
env.OPENCODE_VERSION = finalAttrs.version;
|
||||||
env.OPENCODE_CHANNEL = "local";
|
env.OPENCODE_CHANNEL = "local";
|
||||||
|
|
||||||
@@ -79,7 +80,7 @@ stdenvNoCC.mkDerivation (finalAttrs: {
|
|||||||
writableTmpDirAsHomeHook
|
writableTmpDirAsHomeHook
|
||||||
];
|
];
|
||||||
doInstallCheck = true;
|
doInstallCheck = true;
|
||||||
versionCheckKeepEnvironment = [ "HOME" ];
|
versionCheckKeepEnvironment = [ "HOME" "OPENCODE_DISABLE_MODELS_FETCH" ];
|
||||||
versionCheckProgramArg = "--version";
|
versionCheckProgramArg = "--version";
|
||||||
|
|
||||||
passthru = {
|
passthru = {
|
||||||
|
|||||||
@@ -1,27 +1,32 @@
|
|||||||
import { lstat, mkdir, readdir, rm, symlink } from "fs/promises"
|
import { lstat, mkdir, readdir, rm, symlink } from "fs/promises"
|
||||||
import { join, relative } from "path"
|
import { join, relative } from "path"
|
||||||
|
|
||||||
type SemverLike = {
|
|
||||||
valid: (value: string) => string | null
|
|
||||||
rcompare: (left: string, right: string) => number
|
|
||||||
}
|
|
||||||
|
|
||||||
type Entry = {
|
type Entry = {
|
||||||
dir: string
|
dir: string
|
||||||
version: string
|
version: string
|
||||||
label: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function isDirectory(path: string) {
|
||||||
|
try {
|
||||||
|
const info = await lstat(path)
|
||||||
|
return info.isDirectory()
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValidSemver = (v: string) => Bun.semver.satisfies(v, "x.x.x")
|
||||||
|
|
||||||
const root = process.cwd()
|
const root = process.cwd()
|
||||||
const bunRoot = join(root, "node_modules/.bun")
|
const bunRoot = join(root, "node_modules/.bun")
|
||||||
const linkRoot = join(bunRoot, "node_modules")
|
const linkRoot = join(bunRoot, "node_modules")
|
||||||
const directories = (await readdir(bunRoot)).sort()
|
const directories = (await readdir(bunRoot)).sort()
|
||||||
|
|
||||||
const versions = new Map<string, Entry[]>()
|
const versions = new Map<string, Entry[]>()
|
||||||
|
|
||||||
for (const entry of directories) {
|
for (const entry of directories) {
|
||||||
const full = join(bunRoot, entry)
|
const full = join(bunRoot, entry)
|
||||||
const info = await lstat(full)
|
if (!(await isDirectory(full))) {
|
||||||
if (!info.isDirectory()) {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const parsed = parseEntry(entry)
|
const parsed = parseEntry(entry)
|
||||||
@@ -29,37 +34,23 @@ for (const entry of directories) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const list = versions.get(parsed.name) ?? []
|
const list = versions.get(parsed.name) ?? []
|
||||||
list.push({ dir: full, version: parsed.version, label: entry })
|
list.push({ dir: full, version: parsed.version })
|
||||||
versions.set(parsed.name, list)
|
versions.set(parsed.name, list)
|
||||||
}
|
}
|
||||||
|
|
||||||
const semverModule = (await import(join(bunRoot, "node_modules/semver"))) as
|
|
||||||
| SemverLike
|
|
||||||
| {
|
|
||||||
default: SemverLike
|
|
||||||
}
|
|
||||||
const semver = "default" in semverModule ? semverModule.default : semverModule
|
|
||||||
const selections = new Map<string, Entry>()
|
const selections = new Map<string, Entry>()
|
||||||
|
|
||||||
for (const [slug, list] of versions) {
|
for (const [slug, list] of versions) {
|
||||||
list.sort((a, b) => {
|
list.sort((a, b) => {
|
||||||
const left = semver.valid(a.version)
|
const aValid = isValidSemver(a.version)
|
||||||
const right = semver.valid(b.version)
|
const bValid = isValidSemver(b.version)
|
||||||
if (left && right) {
|
if (aValid && bValid) return -Bun.semver.order(a.version, b.version)
|
||||||
const delta = semver.rcompare(left, right)
|
if (aValid) return -1
|
||||||
if (delta !== 0) {
|
if (bValid) return 1
|
||||||
return delta
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (left && !right) {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
if (!left && right) {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
return b.version.localeCompare(a.version)
|
return b.version.localeCompare(a.version)
|
||||||
})
|
})
|
||||||
selections.set(slug, list[0])
|
const first = list[0]
|
||||||
|
if (first) selections.set(slug, first)
|
||||||
}
|
}
|
||||||
|
|
||||||
await rm(linkRoot, { recursive: true, force: true })
|
await rm(linkRoot, { recursive: true, force: true })
|
||||||
@@ -77,10 +68,7 @@ for (const [slug, entry] of Array.from(selections.entries()).sort((a, b) => a[0]
|
|||||||
await mkdir(parent, { recursive: true })
|
await mkdir(parent, { recursive: true })
|
||||||
const linkPath = join(parent, leaf)
|
const linkPath = join(parent, leaf)
|
||||||
const desired = join(entry.dir, "node_modules", slug)
|
const desired = join(entry.dir, "node_modules", slug)
|
||||||
const exists = await lstat(desired)
|
if (!(await isDirectory(desired))) {
|
||||||
.then((info) => info.isDirectory())
|
|
||||||
.catch(() => false)
|
|
||||||
if (!exists) {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const relativeTarget = relative(parent, desired)
|
const relativeTarget = relative(parent, desired)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ type PackageManifest = {
|
|||||||
|
|
||||||
const root = process.cwd()
|
const root = process.cwd()
|
||||||
const bunRoot = join(root, "node_modules/.bun")
|
const bunRoot = join(root, "node_modules/.bun")
|
||||||
const bunEntries = (await safeReadDir(bunRoot)).sort()
|
const bunEntries = (await readdir(bunRoot)).sort()
|
||||||
let rewritten = 0
|
let rewritten = 0
|
||||||
|
|
||||||
for (const entry of bunEntries) {
|
for (const entry of bunEntries) {
|
||||||
@@ -45,11 +45,11 @@ for (const entry of bunEntries) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[normalize-bun-binaries] rewrote ${rewritten} links`)
|
console.log(`[normalize-bun-binaries] rebuilt ${rewritten} links`)
|
||||||
|
|
||||||
async function collectPackages(modulesRoot: string) {
|
async function collectPackages(modulesRoot: string) {
|
||||||
const found: string[] = []
|
const found: string[] = []
|
||||||
const topLevel = (await safeReadDir(modulesRoot)).sort()
|
const topLevel = (await readdir(modulesRoot)).sort()
|
||||||
for (const name of topLevel) {
|
for (const name of topLevel) {
|
||||||
if (name === ".bin" || name === ".bun") {
|
if (name === ".bin" || name === ".bun") {
|
||||||
continue
|
continue
|
||||||
@@ -59,7 +59,7 @@ async function collectPackages(modulesRoot: string) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (name.startsWith("@")) {
|
if (name.startsWith("@")) {
|
||||||
const scoped = (await safeReadDir(full)).sort()
|
const scoped = (await readdir(full)).sort()
|
||||||
for (const child of scoped) {
|
for (const child of scoped) {
|
||||||
const scopedDir = join(full, child)
|
const scopedDir = join(full, child)
|
||||||
if (await isDirectory(scopedDir)) {
|
if (await isDirectory(scopedDir)) {
|
||||||
@@ -121,14 +121,6 @@ async function isDirectory(path: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function safeReadDir(path: string) {
|
|
||||||
try {
|
|
||||||
return await readdir(path)
|
|
||||||
} catch {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeBinName(name: string) {
|
function normalizeBinName(name: string) {
|
||||||
const slash = name.lastIndexOf("/")
|
const slash = name.lastIndexOf("/")
|
||||||
if (slash >= 0) {
|
if (slash >= 0) {
|
||||||
|
|||||||
+5
-3
@@ -4,9 +4,11 @@
|
|||||||
"description": "AI-powered development tool",
|
"description": "AI-powered development tool",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "bun@1.3.5",
|
"packageManager": "bun@1.3.9",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
||||||
|
"dev:desktop": "bun --cwd packages/desktop tauri dev",
|
||||||
|
"dev:web": "bun --cwd packages/app dev",
|
||||||
"typecheck": "bun turbo typecheck",
|
"typecheck": "bun turbo typecheck",
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"random": "echo 'Random script'",
|
"random": "echo 'Random script'",
|
||||||
@@ -21,7 +23,7 @@
|
|||||||
"packages/slack"
|
"packages/slack"
|
||||||
],
|
],
|
||||||
"catalog": {
|
"catalog": {
|
||||||
"@types/bun": "1.3.5",
|
"@types/bun": "1.3.9",
|
||||||
"@octokit/rest": "22.0.0",
|
"@octokit/rest": "22.0.0",
|
||||||
"@hono/zod-validator": "0.4.2",
|
"@hono/zod-validator": "0.4.2",
|
||||||
"ulid": "3.0.1",
|
"ulid": "3.0.1",
|
||||||
@@ -99,6 +101,6 @@
|
|||||||
"@types/node": "catalog:"
|
"@types/node": "catalog:"
|
||||||
},
|
},
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"ghostty-web@0.3.0": "patches/ghostty-web@0.3.0.patch"
|
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
[test]
|
[test]
|
||||||
|
root = "./src"
|
||||||
preload = ["./happydom.ts"]
|
preload = ["./happydom.ts"]
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
# E2E Testing Guide
|
||||||
|
|
||||||
|
## Build/Lint/Test Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all e2e tests
|
||||||
|
bun test:e2e
|
||||||
|
|
||||||
|
# Run specific test file
|
||||||
|
bun test:e2e -- app/home.spec.ts
|
||||||
|
|
||||||
|
# Run single test by title
|
||||||
|
bun test:e2e -- -g "home renders and shows core entrypoints"
|
||||||
|
|
||||||
|
# Run tests with UI mode (for debugging)
|
||||||
|
bun test:e2e:ui
|
||||||
|
|
||||||
|
# Run tests locally with full server setup
|
||||||
|
bun test:e2e:local
|
||||||
|
|
||||||
|
# View test report
|
||||||
|
bun test:e2e:report
|
||||||
|
|
||||||
|
# Typecheck
|
||||||
|
bun typecheck
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Structure
|
||||||
|
|
||||||
|
All tests live in `packages/app/e2e/`:
|
||||||
|
|
||||||
|
```
|
||||||
|
e2e/
|
||||||
|
├── fixtures.ts # Test fixtures (test, expect, gotoSession, sdk)
|
||||||
|
├── actions.ts # Reusable action helpers
|
||||||
|
├── selectors.ts # DOM selectors
|
||||||
|
├── utils.ts # Utilities (serverUrl, modKey, path helpers)
|
||||||
|
└── [feature]/
|
||||||
|
└── *.spec.ts # Test files
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Patterns
|
||||||
|
|
||||||
|
### Basic Test Structure
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { test, expect } from "../fixtures"
|
||||||
|
import { promptSelector } from "../selectors"
|
||||||
|
import { withSession } from "../actions"
|
||||||
|
|
||||||
|
test("test description", async ({ page, sdk, gotoSession }) => {
|
||||||
|
await gotoSession() // or gotoSession(sessionID)
|
||||||
|
|
||||||
|
// Your test code
|
||||||
|
await expect(page.locator(promptSelector)).toBeVisible()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Fixtures
|
||||||
|
|
||||||
|
- `page` - Playwright page
|
||||||
|
- `sdk` - OpenCode SDK client for API calls
|
||||||
|
- `gotoSession(sessionID?)` - Navigate to session
|
||||||
|
|
||||||
|
### Helper Functions
|
||||||
|
|
||||||
|
**Actions** (`actions.ts`):
|
||||||
|
|
||||||
|
- `openPalette(page)` - Open command palette
|
||||||
|
- `openSettings(page)` - Open settings dialog
|
||||||
|
- `closeDialog(page, dialog)` - Close any dialog
|
||||||
|
- `openSidebar(page)` / `closeSidebar(page)` - Toggle sidebar
|
||||||
|
- `withSession(sdk, title, callback)` - Create temp session
|
||||||
|
- `clickListItem(container, filter)` - Click list item by key/text
|
||||||
|
|
||||||
|
**Selectors** (`selectors.ts`):
|
||||||
|
|
||||||
|
- `promptSelector` - Prompt input
|
||||||
|
- `terminalSelector` - Terminal panel
|
||||||
|
- `sessionItemSelector(id)` - Session in sidebar
|
||||||
|
- `listItemSelector` - Generic list items
|
||||||
|
|
||||||
|
**Utils** (`utils.ts`):
|
||||||
|
|
||||||
|
- `modKey` - Meta (Mac) or Control (Linux/Win)
|
||||||
|
- `serverUrl` - Backend server URL
|
||||||
|
- `sessionPath(dir, id?)` - Build session URL
|
||||||
|
|
||||||
|
## Code Style Guidelines
|
||||||
|
|
||||||
|
### Imports
|
||||||
|
|
||||||
|
Always import from `../fixtures`, not `@playwright/test`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ Good
|
||||||
|
import { test, expect } from "../fixtures"
|
||||||
|
|
||||||
|
// ❌ Bad
|
||||||
|
import { test, expect } from "@playwright/test"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Naming Conventions
|
||||||
|
|
||||||
|
- Test files: `feature-name.spec.ts`
|
||||||
|
- Test names: lowercase, descriptive: `"sidebar can be toggled"`
|
||||||
|
- Variables: camelCase
|
||||||
|
- Constants: SCREAMING_SNAKE_CASE
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
Tests should clean up after themselves:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
test("test with cleanup", async ({ page, sdk, gotoSession }) => {
|
||||||
|
await withSession(sdk, "test session", async (session) => {
|
||||||
|
await gotoSession(session.id)
|
||||||
|
// Test code...
|
||||||
|
}) // Auto-deletes session
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Timeouts
|
||||||
|
|
||||||
|
Default: 60s per test, 10s per assertion. Override when needed:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
test.setTimeout(120_000) // For long LLM operations
|
||||||
|
test("slow test", async () => {
|
||||||
|
await expect.poll(() => check(), { timeout: 90_000 }).toBe(true)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Selectors
|
||||||
|
|
||||||
|
Use `data-component`, `data-action`, or semantic roles:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ Good
|
||||||
|
await page.locator('[data-component="prompt-input"]').click()
|
||||||
|
await page.getByRole("button", { name: "Open settings" }).click()
|
||||||
|
|
||||||
|
// ❌ Bad
|
||||||
|
await page.locator(".css-class-name").click()
|
||||||
|
await page.locator("#id-name").click()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Keyboard Shortcuts
|
||||||
|
|
||||||
|
Use `modKey` for cross-platform compatibility:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { modKey } from "../utils"
|
||||||
|
|
||||||
|
await page.keyboard.press(`${modKey}+B`) // Toggle sidebar
|
||||||
|
await page.keyboard.press(`${modKey}+Comma`) // Open settings
|
||||||
|
```
|
||||||
|
|
||||||
|
## Writing New Tests
|
||||||
|
|
||||||
|
1. Choose appropriate folder or create new one
|
||||||
|
2. Import from `../fixtures`
|
||||||
|
3. Use helper functions from `../actions` and `../selectors`
|
||||||
|
4. Clean up any created resources
|
||||||
|
5. Use specific selectors (avoid CSS classes)
|
||||||
|
6. Test one feature per test file
|
||||||
|
|
||||||
|
## Local Development
|
||||||
|
|
||||||
|
For UI debugging, use:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun test:e2e:ui
|
||||||
|
```
|
||||||
|
|
||||||
|
This opens Playwright's interactive UI for step-through debugging.
|
||||||
+156
-6
@@ -8,16 +8,25 @@ import {
|
|||||||
sessionItemSelector,
|
sessionItemSelector,
|
||||||
dropdownMenuTriggerSelector,
|
dropdownMenuTriggerSelector,
|
||||||
dropdownMenuContentSelector,
|
dropdownMenuContentSelector,
|
||||||
|
projectMenuTriggerSelector,
|
||||||
|
projectWorkspacesToggleSelector,
|
||||||
titlebarRightSelector,
|
titlebarRightSelector,
|
||||||
popoverBodySelector,
|
popoverBodySelector,
|
||||||
listItemSelector,
|
listItemSelector,
|
||||||
listItemKeySelector,
|
listItemKeySelector,
|
||||||
listItemKeyStartsWithSelector,
|
listItemKeyStartsWithSelector,
|
||||||
|
workspaceItemSelector,
|
||||||
|
workspaceMenuTriggerSelector,
|
||||||
} from "./selectors"
|
} from "./selectors"
|
||||||
import type { createSdk } from "./utils"
|
import type { createSdk } from "./utils"
|
||||||
|
|
||||||
export async function defocus(page: Page) {
|
export async function defocus(page: Page) {
|
||||||
await page.mouse.click(5, 5)
|
await page
|
||||||
|
.evaluate(() => {
|
||||||
|
const el = document.activeElement
|
||||||
|
if (el instanceof HTMLElement) el.blur()
|
||||||
|
})
|
||||||
|
.catch(() => undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function openPalette(page: Page) {
|
export async function openPalette(page: Page) {
|
||||||
@@ -64,14 +73,50 @@ export async function toggleSidebar(page: Page) {
|
|||||||
|
|
||||||
export async function openSidebar(page: Page) {
|
export async function openSidebar(page: Page) {
|
||||||
if (!(await isSidebarClosed(page))) return
|
if (!(await isSidebarClosed(page))) return
|
||||||
|
|
||||||
|
const button = page.getByRole("button", { name: /toggle sidebar/i }).first()
|
||||||
|
const visible = await button
|
||||||
|
.isVisible()
|
||||||
|
.then((x) => x)
|
||||||
|
.catch(() => false)
|
||||||
|
|
||||||
|
if (visible) await button.click()
|
||||||
|
if (!visible) await toggleSidebar(page)
|
||||||
|
|
||||||
|
const main = page.locator("main")
|
||||||
|
const opened = await expect(main)
|
||||||
|
.not.toHaveClass(/xl:border-l/, { timeout: 1500 })
|
||||||
|
.then(() => true)
|
||||||
|
.catch(() => false)
|
||||||
|
|
||||||
|
if (opened) return
|
||||||
|
|
||||||
await toggleSidebar(page)
|
await toggleSidebar(page)
|
||||||
await expect(page.locator("main")).not.toHaveClass(/xl:border-l/)
|
await expect(main).not.toHaveClass(/xl:border-l/)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function closeSidebar(page: Page) {
|
export async function closeSidebar(page: Page) {
|
||||||
if (await isSidebarClosed(page)) return
|
if (await isSidebarClosed(page)) return
|
||||||
|
|
||||||
|
const button = page.getByRole("button", { name: /toggle sidebar/i }).first()
|
||||||
|
const visible = await button
|
||||||
|
.isVisible()
|
||||||
|
.then((x) => x)
|
||||||
|
.catch(() => false)
|
||||||
|
|
||||||
|
if (visible) await button.click()
|
||||||
|
if (!visible) await toggleSidebar(page)
|
||||||
|
|
||||||
|
const main = page.locator("main")
|
||||||
|
const closed = await expect(main)
|
||||||
|
.toHaveClass(/xl:border-l/, { timeout: 1500 })
|
||||||
|
.then(() => true)
|
||||||
|
.catch(() => false)
|
||||||
|
|
||||||
|
if (closed) return
|
||||||
|
|
||||||
await toggleSidebar(page)
|
await toggleSidebar(page)
|
||||||
await expect(page.locator("main")).toHaveClass(/xl:border-l/)
|
await expect(main).toHaveClass(/xl:border-l/)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function openSettings(page: Page) {
|
export async function openSettings(page: Page) {
|
||||||
@@ -178,13 +223,30 @@ export async function hoverSessionItem(page: Page, sessionID: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function openSessionMoreMenu(page: Page, sessionID: string) {
|
export async function openSessionMoreMenu(page: Page, sessionID: string) {
|
||||||
const sessionEl = await hoverSessionItem(page, sessionID)
|
await expect(page).toHaveURL(new RegExp(`/session/${sessionID}(?:[/?#]|$)`))
|
||||||
|
|
||||||
const menuTrigger = sessionEl.locator(dropdownMenuTriggerSelector).first()
|
const scroller = page.locator(".session-scroller").first()
|
||||||
|
await expect(scroller).toBeVisible()
|
||||||
|
await expect(scroller.getByRole("heading", { level: 1 }).first()).toBeVisible({ timeout: 30_000 })
|
||||||
|
|
||||||
|
const menu = page
|
||||||
|
.locator(dropdownMenuContentSelector)
|
||||||
|
.filter({ has: page.getByRole("menuitem", { name: /rename/i }) })
|
||||||
|
.filter({ has: page.getByRole("menuitem", { name: /archive/i }) })
|
||||||
|
.filter({ has: page.getByRole("menuitem", { name: /delete/i }) })
|
||||||
|
.first()
|
||||||
|
|
||||||
|
const opened = await menu
|
||||||
|
.isVisible()
|
||||||
|
.then((x) => x)
|
||||||
|
.catch(() => false)
|
||||||
|
|
||||||
|
if (opened) return menu
|
||||||
|
|
||||||
|
const menuTrigger = scroller.getByRole("button", { name: /more options/i }).first()
|
||||||
await expect(menuTrigger).toBeVisible()
|
await expect(menuTrigger).toBeVisible()
|
||||||
await menuTrigger.click()
|
await menuTrigger.click()
|
||||||
|
|
||||||
const menu = page.locator(dropdownMenuContentSelector).first()
|
|
||||||
await expect(menu).toBeVisible()
|
await expect(menu).toBeVisible()
|
||||||
return menu
|
return menu
|
||||||
}
|
}
|
||||||
@@ -269,3 +331,91 @@ export async function withSession<T>(
|
|||||||
await sdk.session.delete({ sessionID: session.id }).catch(() => undefined)
|
await sdk.session.delete({ sessionID: session.id }).catch(() => undefined)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function openStatusPopover(page: Page) {
|
||||||
|
await defocus(page)
|
||||||
|
|
||||||
|
const rightSection = page.locator(titlebarRightSelector)
|
||||||
|
const trigger = rightSection.getByRole("button", { name: /status/i }).first()
|
||||||
|
|
||||||
|
const popoverBody = page.locator(popoverBodySelector).filter({ has: page.locator('[data-component="tabs"]') })
|
||||||
|
|
||||||
|
const opened = await popoverBody
|
||||||
|
.isVisible()
|
||||||
|
.then((x) => x)
|
||||||
|
.catch(() => false)
|
||||||
|
|
||||||
|
if (!opened) {
|
||||||
|
await expect(trigger).toBeVisible()
|
||||||
|
await trigger.click()
|
||||||
|
await expect(popoverBody).toBeVisible()
|
||||||
|
}
|
||||||
|
|
||||||
|
return { rightSection, popoverBody }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function openProjectMenu(page: Page, projectSlug: string) {
|
||||||
|
const trigger = page.locator(projectMenuTriggerSelector(projectSlug)).first()
|
||||||
|
await expect(trigger).toHaveCount(1)
|
||||||
|
|
||||||
|
await trigger.focus()
|
||||||
|
await page.keyboard.press("Enter")
|
||||||
|
|
||||||
|
const menu = page.locator(dropdownMenuContentSelector).first()
|
||||||
|
const opened = await menu
|
||||||
|
.waitFor({ state: "visible", timeout: 1500 })
|
||||||
|
.then(() => true)
|
||||||
|
.catch(() => false)
|
||||||
|
|
||||||
|
if (opened) {
|
||||||
|
const viewport = page.viewportSize()
|
||||||
|
const x = viewport ? Math.max(viewport.width - 5, 0) : 1200
|
||||||
|
const y = viewport ? Math.max(viewport.height - 5, 0) : 800
|
||||||
|
await page.mouse.move(x, y)
|
||||||
|
return menu
|
||||||
|
}
|
||||||
|
|
||||||
|
await trigger.click({ force: true })
|
||||||
|
|
||||||
|
await expect(menu).toBeVisible()
|
||||||
|
|
||||||
|
const viewport = page.viewportSize()
|
||||||
|
const x = viewport ? Math.max(viewport.width - 5, 0) : 1200
|
||||||
|
const y = viewport ? Math.max(viewport.height - 5, 0) : 800
|
||||||
|
await page.mouse.move(x, y)
|
||||||
|
return menu
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setWorkspacesEnabled(page: Page, projectSlug: string, enabled: boolean) {
|
||||||
|
const current = await page
|
||||||
|
.getByRole("button", { name: "New workspace" })
|
||||||
|
.first()
|
||||||
|
.isVisible()
|
||||||
|
.then((x) => x)
|
||||||
|
.catch(() => false)
|
||||||
|
|
||||||
|
if (current === enabled) return
|
||||||
|
|
||||||
|
await openProjectMenu(page, projectSlug)
|
||||||
|
|
||||||
|
const toggle = page.locator(projectWorkspacesToggleSelector(projectSlug)).first()
|
||||||
|
await expect(toggle).toBeVisible()
|
||||||
|
await toggle.click({ force: true })
|
||||||
|
|
||||||
|
const expected = enabled ? "New workspace" : "New session"
|
||||||
|
await expect(page.getByRole("button", { name: expected }).first()).toBeVisible()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function openWorkspaceMenu(page: Page, workspaceSlug: string) {
|
||||||
|
const item = page.locator(workspaceItemSelector(workspaceSlug)).first()
|
||||||
|
await expect(item).toBeVisible()
|
||||||
|
await item.hover()
|
||||||
|
|
||||||
|
const trigger = page.locator(workspaceMenuTriggerSelector(workspaceSlug)).first()
|
||||||
|
await expect(trigger).toBeVisible()
|
||||||
|
await trigger.click({ force: true })
|
||||||
|
|
||||||
|
const menu = page.locator(dropdownMenuContentSelector).first()
|
||||||
|
await expect(menu).toBeVisible()
|
||||||
|
return menu
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { test, expect } from "../fixtures"
|
import { test, expect } from "../fixtures"
|
||||||
import { openSidebar, withSession } from "../actions"
|
import { defocus, openSidebar, withSession } from "../actions"
|
||||||
import { promptSelector } from "../selectors"
|
import { promptSelector } from "../selectors"
|
||||||
|
import { modKey } from "../utils"
|
||||||
|
|
||||||
test("titlebar back/forward navigates between sessions", async ({ page, slug, sdk, gotoSession }) => {
|
test("titlebar back/forward navigates between sessions", async ({ page, slug, sdk, gotoSession }) => {
|
||||||
await page.setViewportSize({ width: 1400, height: 800 })
|
await page.setViewportSize({ width: 1400, height: 800 })
|
||||||
@@ -40,3 +41,84 @@ test("titlebar back/forward navigates between sessions", async ({ page, slug, sd
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("titlebar forward is cleared after branching history from sidebar", async ({ page, slug, sdk, gotoSession }) => {
|
||||||
|
await page.setViewportSize({ width: 1400, height: 800 })
|
||||||
|
|
||||||
|
const stamp = Date.now()
|
||||||
|
|
||||||
|
await withSession(sdk, `e2e titlebar history a ${stamp}`, async (a) => {
|
||||||
|
await withSession(sdk, `e2e titlebar history b ${stamp}`, async (b) => {
|
||||||
|
await withSession(sdk, `e2e titlebar history c ${stamp}`, async (c) => {
|
||||||
|
await gotoSession(a.id)
|
||||||
|
|
||||||
|
await openSidebar(page)
|
||||||
|
|
||||||
|
const second = page.locator(`[data-session-id="${b.id}"] a`).first()
|
||||||
|
await expect(second).toBeVisible()
|
||||||
|
await second.scrollIntoViewIfNeeded()
|
||||||
|
await second.click()
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(new RegExp(`/${slug}/session/${b.id}(?:\\?|#|$)`))
|
||||||
|
await expect(page.locator(promptSelector)).toBeVisible()
|
||||||
|
|
||||||
|
const back = page.getByRole("button", { name: "Back" })
|
||||||
|
const forward = page.getByRole("button", { name: "Forward" })
|
||||||
|
|
||||||
|
await expect(back).toBeVisible()
|
||||||
|
await expect(back).toBeEnabled()
|
||||||
|
await back.click()
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(new RegExp(`/${slug}/session/${a.id}(?:\\?|#|$)`))
|
||||||
|
await expect(page.locator(promptSelector)).toBeVisible()
|
||||||
|
|
||||||
|
await openSidebar(page)
|
||||||
|
|
||||||
|
const third = page.locator(`[data-session-id="${c.id}"] a`).first()
|
||||||
|
await expect(third).toBeVisible()
|
||||||
|
await third.scrollIntoViewIfNeeded()
|
||||||
|
await third.click()
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(new RegExp(`/${slug}/session/${c.id}(?:\\?|#|$)`))
|
||||||
|
await expect(page.locator(promptSelector)).toBeVisible()
|
||||||
|
|
||||||
|
await expect(forward).toBeVisible()
|
||||||
|
await expect(forward).toBeDisabled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("keyboard shortcuts navigate titlebar history", async ({ page, slug, sdk, gotoSession }) => {
|
||||||
|
await page.setViewportSize({ width: 1400, height: 800 })
|
||||||
|
|
||||||
|
const stamp = Date.now()
|
||||||
|
|
||||||
|
await withSession(sdk, `e2e titlebar shortcuts 1 ${stamp}`, async (one) => {
|
||||||
|
await withSession(sdk, `e2e titlebar shortcuts 2 ${stamp}`, async (two) => {
|
||||||
|
await gotoSession(one.id)
|
||||||
|
|
||||||
|
await openSidebar(page)
|
||||||
|
|
||||||
|
const link = page.locator(`[data-session-id="${two.id}"] a`).first()
|
||||||
|
await expect(link).toBeVisible()
|
||||||
|
await link.scrollIntoViewIfNeeded()
|
||||||
|
await link.click()
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`))
|
||||||
|
await expect(page.locator(promptSelector)).toBeVisible()
|
||||||
|
|
||||||
|
await defocus(page)
|
||||||
|
await page.keyboard.press(`${modKey}+[`)
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(new RegExp(`/${slug}/session/${one.id}(?:\\?|#|$)`))
|
||||||
|
await expect(page.locator(promptSelector)).toBeVisible()
|
||||||
|
|
||||||
|
await defocus(page)
|
||||||
|
await page.keyboard.press(`${modKey}+]`)
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`))
|
||||||
|
await expect(page.locator(promptSelector)).toBeVisible()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -1,37 +1,49 @@
|
|||||||
import { test, expect } from "../fixtures"
|
import { test, expect } from "../fixtures"
|
||||||
|
|
||||||
test.skip("file tree can expand folders and open a file", async ({ page, gotoSession }) => {
|
test("file tree can expand folders and open a file", async ({ page, gotoSession }) => {
|
||||||
await gotoSession()
|
await gotoSession()
|
||||||
|
|
||||||
const toggle = page.getByRole("button", { name: "Toggle file tree" })
|
const toggle = page.getByRole("button", { name: "Toggle file tree" })
|
||||||
const treeTabs = page.locator('[data-component="tabs"][data-variant="pill"][data-scope="filetree"]')
|
const panel = page.locator("#file-tree-panel")
|
||||||
|
const treeTabs = panel.locator('[data-component="tabs"][data-variant="pill"][data-scope="filetree"]')
|
||||||
|
|
||||||
|
await expect(toggle).toBeVisible()
|
||||||
if ((await toggle.getAttribute("aria-expanded")) !== "true") await toggle.click()
|
if ((await toggle.getAttribute("aria-expanded")) !== "true") await toggle.click()
|
||||||
|
await expect(toggle).toHaveAttribute("aria-expanded", "true")
|
||||||
|
await expect(panel).toBeVisible()
|
||||||
await expect(treeTabs).toBeVisible()
|
await expect(treeTabs).toBeVisible()
|
||||||
|
|
||||||
await treeTabs.locator('[data-slot="tabs-trigger"]').nth(1).click()
|
const allTab = treeTabs.getByRole("tab", { name: /^all files$/i })
|
||||||
|
await expect(allTab).toBeVisible()
|
||||||
|
await allTab.click()
|
||||||
|
await expect(allTab).toHaveAttribute("aria-selected", "true")
|
||||||
|
|
||||||
const node = (name: string) => treeTabs.getByRole("button", { name, exact: true })
|
const tree = treeTabs.locator('[data-slot="tabs-content"]:not([hidden])')
|
||||||
|
await expect(tree).toBeVisible()
|
||||||
|
|
||||||
await expect(node("packages")).toBeVisible()
|
const expand = async (name: string) => {
|
||||||
await node("packages").click()
|
const folder = tree.getByRole("button", { name, exact: true }).first()
|
||||||
|
await expect(folder).toBeVisible()
|
||||||
|
await expect(folder).toHaveAttribute("aria-expanded", /true|false/)
|
||||||
|
if ((await folder.getAttribute("aria-expanded")) === "false") await folder.click()
|
||||||
|
await expect(folder).toHaveAttribute("aria-expanded", "true")
|
||||||
|
}
|
||||||
|
|
||||||
await expect(node("app")).toBeVisible()
|
await expand("packages")
|
||||||
await node("app").click()
|
await expand("app")
|
||||||
|
await expand("src")
|
||||||
|
await expand("components")
|
||||||
|
|
||||||
await expect(node("src")).toBeVisible()
|
const file = tree.getByRole("button", { name: "file-tree.tsx", exact: true }).first()
|
||||||
await node("src").click()
|
await expect(file).toBeVisible()
|
||||||
|
await file.click()
|
||||||
await expect(node("components")).toBeVisible()
|
|
||||||
await node("components").click()
|
|
||||||
|
|
||||||
await expect(node("file-tree.tsx")).toBeVisible()
|
|
||||||
await node("file-tree.tsx").click()
|
|
||||||
|
|
||||||
const tab = page.getByRole("tab", { name: "file-tree.tsx" })
|
const tab = page.getByRole("tab", { name: "file-tree.tsx" })
|
||||||
await expect(tab).toBeVisible()
|
await expect(tab).toBeVisible()
|
||||||
await tab.click()
|
await tab.click()
|
||||||
|
await expect(tab).toHaveAttribute("aria-selected", "true")
|
||||||
|
|
||||||
const code = page.locator('[data-component="code"]').first()
|
const code = page.locator('[data-component="code"]').first()
|
||||||
await expect(code.getByText("export default function FileTree")).toBeVisible()
|
await expect(code).toBeVisible()
|
||||||
|
await expect(code).toContainText("export default function FileTree")
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,11 +1,21 @@
|
|||||||
import { test as base, expect } from "@playwright/test"
|
import { test as base, expect, type Page } from "@playwright/test"
|
||||||
import { seedProjects } from "./actions"
|
import { cleanupTestProject, createTestProject, seedProjects } from "./actions"
|
||||||
import { promptSelector } from "./selectors"
|
import { promptSelector } from "./selectors"
|
||||||
import { createSdk, dirSlug, getWorktree, sessionPath } from "./utils"
|
import { createSdk, dirSlug, getWorktree, sessionPath } from "./utils"
|
||||||
|
|
||||||
|
export const settingsKey = "settings.v3"
|
||||||
|
|
||||||
type TestFixtures = {
|
type TestFixtures = {
|
||||||
sdk: ReturnType<typeof createSdk>
|
sdk: ReturnType<typeof createSdk>
|
||||||
gotoSession: (sessionID?: string) => Promise<void>
|
gotoSession: (sessionID?: string) => Promise<void>
|
||||||
|
withProject: <T>(
|
||||||
|
callback: (project: {
|
||||||
|
directory: string
|
||||||
|
slug: string
|
||||||
|
gotoSession: (sessionID?: string) => Promise<void>
|
||||||
|
}) => Promise<T>,
|
||||||
|
options?: { extra?: string[] },
|
||||||
|
) => Promise<T>
|
||||||
}
|
}
|
||||||
|
|
||||||
type WorkerFixtures = {
|
type WorkerFixtures = {
|
||||||
@@ -31,17 +41,7 @@ export const test = base.extend<TestFixtures, WorkerFixtures>({
|
|||||||
await use(createSdk(directory))
|
await use(createSdk(directory))
|
||||||
},
|
},
|
||||||
gotoSession: async ({ page, directory }, use) => {
|
gotoSession: async ({ page, directory }, use) => {
|
||||||
await seedProjects(page, { directory })
|
await seedStorage(page, { directory })
|
||||||
await page.addInitScript(() => {
|
|
||||||
localStorage.setItem(
|
|
||||||
"opencode.global.dat:model",
|
|
||||||
JSON.stringify({
|
|
||||||
recent: [{ providerID: "opencode", modelID: "big-pickle" }],
|
|
||||||
user: [],
|
|
||||||
variant: {},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const gotoSession = async (sessionID?: string) => {
|
const gotoSession = async (sessionID?: string) => {
|
||||||
await page.goto(sessionPath(directory, sessionID))
|
await page.goto(sessionPath(directory, sessionID))
|
||||||
@@ -49,6 +49,39 @@ export const test = base.extend<TestFixtures, WorkerFixtures>({
|
|||||||
}
|
}
|
||||||
await use(gotoSession)
|
await use(gotoSession)
|
||||||
},
|
},
|
||||||
|
withProject: async ({ page }, use) => {
|
||||||
|
await use(async (callback, options) => {
|
||||||
|
const directory = await createTestProject()
|
||||||
|
const slug = dirSlug(directory)
|
||||||
|
await seedStorage(page, { directory, extra: options?.extra })
|
||||||
|
|
||||||
|
const gotoSession = async (sessionID?: string) => {
|
||||||
|
await page.goto(sessionPath(directory, sessionID))
|
||||||
|
await expect(page.locator(promptSelector)).toBeVisible()
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await gotoSession()
|
||||||
|
return await callback({ directory, slug, gotoSession })
|
||||||
|
} finally {
|
||||||
|
await cleanupTestProject(directory)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
async function seedStorage(page: Page, input: { directory: string; extra?: string[] }) {
|
||||||
|
await seedProjects(page, input)
|
||||||
|
await page.addInitScript(() => {
|
||||||
|
localStorage.setItem(
|
||||||
|
"opencode.global.dat:model",
|
||||||
|
JSON.stringify({
|
||||||
|
recent: [{ providerID: "opencode", modelID: "big-pickle" }],
|
||||||
|
user: [],
|
||||||
|
variant: {},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export { expect }
|
export { expect }
|
||||||
|
|||||||
@@ -1,52 +1,53 @@
|
|||||||
import { test, expect } from "../fixtures"
|
import { test, expect } from "../fixtures"
|
||||||
import { openSidebar } from "../actions"
|
import { openSidebar } from "../actions"
|
||||||
|
|
||||||
test("dialog edit project updates name and startup script", async ({ page, gotoSession }) => {
|
test("dialog edit project updates name and startup script", async ({ page, withProject }) => {
|
||||||
await gotoSession()
|
|
||||||
await page.setViewportSize({ width: 1400, height: 800 })
|
await page.setViewportSize({ width: 1400, height: 800 })
|
||||||
|
|
||||||
await openSidebar(page)
|
await withProject(async () => {
|
||||||
|
await openSidebar(page)
|
||||||
|
|
||||||
|
const open = async () => {
|
||||||
|
const header = page.locator(".group\\/project").first()
|
||||||
|
await header.hover()
|
||||||
|
const trigger = header.getByRole("button", { name: "More options" }).first()
|
||||||
|
await expect(trigger).toBeVisible()
|
||||||
|
await trigger.click({ force: true })
|
||||||
|
|
||||||
|
const menu = page.locator('[data-component="dropdown-menu-content"]').first()
|
||||||
|
await expect(menu).toBeVisible()
|
||||||
|
|
||||||
|
const editItem = menu.getByRole("menuitem", { name: "Edit" }).first()
|
||||||
|
await expect(editItem).toBeVisible()
|
||||||
|
await editItem.click({ force: true })
|
||||||
|
|
||||||
|
const dialog = page.getByRole("dialog")
|
||||||
|
await expect(dialog).toBeVisible()
|
||||||
|
await expect(dialog.getByRole("heading", { level: 2 })).toHaveText("Edit project")
|
||||||
|
return dialog
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = `e2e project ${Date.now()}`
|
||||||
|
const startup = `echo e2e_${Date.now()}`
|
||||||
|
|
||||||
|
const dialog = await open()
|
||||||
|
|
||||||
|
const nameInput = dialog.getByLabel("Name")
|
||||||
|
await nameInput.fill(name)
|
||||||
|
|
||||||
|
const startupInput = dialog.getByLabel("Workspace startup script")
|
||||||
|
await startupInput.fill(startup)
|
||||||
|
|
||||||
|
await dialog.getByRole("button", { name: "Save" }).click()
|
||||||
|
await expect(dialog).toHaveCount(0)
|
||||||
|
|
||||||
const open = async () => {
|
|
||||||
const header = page.locator(".group\\/project").first()
|
const header = page.locator(".group\\/project").first()
|
||||||
await header.hover()
|
await expect(header).toContainText(name)
|
||||||
const trigger = header.getByRole("button", { name: "More options" }).first()
|
|
||||||
await expect(trigger).toBeVisible()
|
|
||||||
await trigger.click({ force: true })
|
|
||||||
|
|
||||||
const menu = page.locator('[data-component="dropdown-menu-content"]').first()
|
const reopened = await open()
|
||||||
await expect(menu).toBeVisible()
|
await expect(reopened.getByLabel("Name")).toHaveValue(name)
|
||||||
|
await expect(reopened.getByLabel("Workspace startup script")).toHaveValue(startup)
|
||||||
const editItem = menu.getByRole("menuitem", { name: "Edit" }).first()
|
await reopened.getByRole("button", { name: "Cancel" }).click()
|
||||||
await expect(editItem).toBeVisible()
|
await expect(reopened).toHaveCount(0)
|
||||||
await editItem.click({ force: true })
|
})
|
||||||
|
|
||||||
const dialog = page.getByRole("dialog")
|
|
||||||
await expect(dialog).toBeVisible()
|
|
||||||
await expect(dialog.getByRole("heading", { level: 2 })).toHaveText("Edit project")
|
|
||||||
return dialog
|
|
||||||
}
|
|
||||||
|
|
||||||
const name = `e2e project ${Date.now()}`
|
|
||||||
const startup = `echo e2e_${Date.now()}`
|
|
||||||
|
|
||||||
const dialog = await open()
|
|
||||||
|
|
||||||
const nameInput = dialog.getByLabel("Name")
|
|
||||||
await nameInput.fill(name)
|
|
||||||
|
|
||||||
const startupInput = dialog.getByLabel("Workspace startup script")
|
|
||||||
await startupInput.fill(startup)
|
|
||||||
|
|
||||||
await dialog.getByRole("button", { name: "Save" }).click()
|
|
||||||
await expect(dialog).toHaveCount(0)
|
|
||||||
|
|
||||||
const header = page.locator(".group\\/project").first()
|
|
||||||
await expect(header).toContainText(name)
|
|
||||||
|
|
||||||
const reopened = await open()
|
|
||||||
await expect(reopened.getByLabel("Name")).toHaveValue(name)
|
|
||||||
await expect(reopened.getByLabel("Workspace startup script")).toHaveValue(startup)
|
|
||||||
await reopened.getByRole("button", { name: "Cancel" }).click()
|
|
||||||
await expect(reopened).toHaveCount(0)
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,69 +1,71 @@
|
|||||||
import { test, expect } from "../fixtures"
|
import { test, expect } from "../fixtures"
|
||||||
import { createTestProject, seedProjects, cleanupTestProject, openSidebar, clickMenuItem } from "../actions"
|
import { createTestProject, cleanupTestProject, openSidebar, clickMenuItem, openProjectMenu } from "../actions"
|
||||||
import { projectCloseHoverSelector, projectCloseMenuSelector, projectSwitchSelector } from "../selectors"
|
import { projectCloseHoverSelector, projectSwitchSelector } from "../selectors"
|
||||||
import { dirSlug } from "../utils"
|
import { dirSlug } from "../utils"
|
||||||
|
|
||||||
test("can close a project via hover card close button", async ({ page, directory, gotoSession }) => {
|
test("can close a project via hover card close button", async ({ page, withProject }) => {
|
||||||
await page.setViewportSize({ width: 1400, height: 800 })
|
await page.setViewportSize({ width: 1400, height: 800 })
|
||||||
|
|
||||||
const other = await createTestProject()
|
const other = await createTestProject()
|
||||||
const otherSlug = dirSlug(other)
|
const otherSlug = dirSlug(other)
|
||||||
await seedProjects(page, { directory, extra: [other] })
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await gotoSession()
|
await withProject(
|
||||||
|
async () => {
|
||||||
|
await openSidebar(page)
|
||||||
|
|
||||||
await openSidebar(page)
|
const otherButton = page.locator(projectSwitchSelector(otherSlug)).first()
|
||||||
|
await expect(otherButton).toBeVisible()
|
||||||
|
await otherButton.hover()
|
||||||
|
|
||||||
const otherButton = page.locator(projectSwitchSelector(otherSlug)).first()
|
const close = page.locator(projectCloseHoverSelector(otherSlug)).first()
|
||||||
await expect(otherButton).toBeVisible()
|
await expect(close).toBeVisible()
|
||||||
await otherButton.hover()
|
await close.click()
|
||||||
|
|
||||||
const close = page.locator(projectCloseHoverSelector(otherSlug)).first()
|
await expect(otherButton).toHaveCount(0)
|
||||||
await expect(close).toBeVisible()
|
},
|
||||||
await close.click()
|
{ extra: [other] },
|
||||||
|
)
|
||||||
await expect(otherButton).toHaveCount(0)
|
|
||||||
} finally {
|
} finally {
|
||||||
await cleanupTestProject(other)
|
await cleanupTestProject(other)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
test("can close a project via project header more options menu", async ({ page, directory, gotoSession }) => {
|
test("closing active project navigates to another open project", async ({ page, withProject }) => {
|
||||||
await page.setViewportSize({ width: 1400, height: 800 })
|
await page.setViewportSize({ width: 1400, height: 800 })
|
||||||
|
|
||||||
const other = await createTestProject()
|
const other = await createTestProject()
|
||||||
const otherName = other.split("/").pop() ?? other
|
|
||||||
const otherSlug = dirSlug(other)
|
const otherSlug = dirSlug(other)
|
||||||
await seedProjects(page, { directory, extra: [other] })
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await gotoSession()
|
await withProject(
|
||||||
|
async ({ slug }) => {
|
||||||
|
await openSidebar(page)
|
||||||
|
|
||||||
await openSidebar(page)
|
const otherButton = page.locator(projectSwitchSelector(otherSlug)).first()
|
||||||
|
await expect(otherButton).toBeVisible()
|
||||||
|
await otherButton.click()
|
||||||
|
|
||||||
const otherButton = page.locator(projectSwitchSelector(otherSlug)).first()
|
await expect(page).toHaveURL(new RegExp(`/${otherSlug}/session`))
|
||||||
await expect(otherButton).toBeVisible()
|
|
||||||
await otherButton.click()
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(new RegExp(`/${otherSlug}/session`))
|
const menu = await openProjectMenu(page, otherSlug)
|
||||||
|
|
||||||
const header = page
|
await clickMenuItem(menu, /^Close$/i, { force: true })
|
||||||
.locator(".group\\/project")
|
|
||||||
.filter({ has: page.locator(`[data-action="project-menu"][data-project="${otherSlug}"]`) })
|
|
||||||
.first()
|
|
||||||
await expect(header).toContainText(otherName)
|
|
||||||
|
|
||||||
const trigger = header.locator(`[data-action="project-menu"][data-project="${otherSlug}"]`).first()
|
await expect
|
||||||
await expect(trigger).toHaveCount(1)
|
.poll(() => {
|
||||||
await trigger.focus()
|
const pathname = new URL(page.url()).pathname
|
||||||
await page.keyboard.press("Enter")
|
if (new RegExp(`^/${slug}/session(?:/[^/]+)?/?$`).test(pathname)) return "project"
|
||||||
|
if (pathname === "/") return "home"
|
||||||
|
return ""
|
||||||
|
})
|
||||||
|
.toMatch(/^(project|home)$/)
|
||||||
|
|
||||||
const menu = page.locator('[data-component="dropdown-menu-content"]').first()
|
await expect(page).not.toHaveURL(new RegExp(`/${otherSlug}/session(?:[/?#]|$)`))
|
||||||
await expect(menu).toBeVisible({ timeout: 10_000 })
|
await expect(otherButton).toHaveCount(0)
|
||||||
|
},
|
||||||
await clickMenuItem(menu, /^Close$/i, { force: true })
|
{ extra: [other] },
|
||||||
await expect(otherButton).toHaveCount(0)
|
)
|
||||||
} finally {
|
} finally {
|
||||||
await cleanupTestProject(other)
|
await cleanupTestProject(other)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,34 @@
|
|||||||
import { test, expect } from "../fixtures"
|
import { test, expect } from "../fixtures"
|
||||||
import { defocus, createTestProject, seedProjects, cleanupTestProject } from "../actions"
|
import { defocus, createTestProject, cleanupTestProject } from "../actions"
|
||||||
import { projectSwitchSelector } from "../selectors"
|
import { projectSwitchSelector } from "../selectors"
|
||||||
import { dirSlug } from "../utils"
|
import { dirSlug } from "../utils"
|
||||||
|
|
||||||
test("can switch between projects from sidebar", async ({ page, directory, gotoSession }) => {
|
test("can switch between projects from sidebar", async ({ page, withProject }) => {
|
||||||
await page.setViewportSize({ width: 1400, height: 800 })
|
await page.setViewportSize({ width: 1400, height: 800 })
|
||||||
|
|
||||||
const other = await createTestProject()
|
const other = await createTestProject()
|
||||||
const otherSlug = dirSlug(other)
|
const otherSlug = dirSlug(other)
|
||||||
|
|
||||||
await seedProjects(page, { directory, extra: [other] })
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await gotoSession()
|
await withProject(
|
||||||
|
async ({ directory }) => {
|
||||||
|
await defocus(page)
|
||||||
|
|
||||||
await defocus(page)
|
const currentSlug = dirSlug(directory)
|
||||||
|
const otherButton = page.locator(projectSwitchSelector(otherSlug)).first()
|
||||||
|
await expect(otherButton).toBeVisible()
|
||||||
|
await otherButton.click()
|
||||||
|
|
||||||
const currentSlug = dirSlug(directory)
|
await expect(page).toHaveURL(new RegExp(`/${otherSlug}/session`))
|
||||||
const otherButton = page.locator(projectSwitchSelector(otherSlug)).first()
|
|
||||||
await expect(otherButton).toBeVisible()
|
|
||||||
await otherButton.click()
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(new RegExp(`/${otherSlug}/session`))
|
const currentButton = page.locator(projectSwitchSelector(currentSlug)).first()
|
||||||
|
await expect(currentButton).toBeVisible()
|
||||||
|
await currentButton.click()
|
||||||
|
|
||||||
const currentButton = page.locator(projectSwitchSelector(currentSlug)).first()
|
await expect(page).toHaveURL(new RegExp(`/${currentSlug}/session`))
|
||||||
await expect(currentButton).toBeVisible()
|
},
|
||||||
await currentButton.click()
|
{ extra: [other] },
|
||||||
|
)
|
||||||
await expect(page).toHaveURL(new RegExp(`/${currentSlug}/session`))
|
|
||||||
} finally {
|
} finally {
|
||||||
await cleanupTestProject(other)
|
await cleanupTestProject(other)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import { base64Decode } from "@opencode-ai/util/encode"
|
||||||
|
import type { Page } from "@playwright/test"
|
||||||
|
import { test, expect } from "../fixtures"
|
||||||
|
import { cleanupTestProject, openSidebar, sessionIDFromUrl, setWorkspacesEnabled } from "../actions"
|
||||||
|
import { promptSelector, workspaceItemSelector, workspaceNewSessionSelector } from "../selectors"
|
||||||
|
import { createSdk } from "../utils"
|
||||||
|
|
||||||
|
function slugFromUrl(url: string) {
|
||||||
|
return /\/([^/]+)\/session(?:\/|$)/.exec(url)?.[1] ?? ""
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitWorkspaceReady(page: Page, slug: string) {
|
||||||
|
await openSidebar(page)
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const item = page.locator(workspaceItemSelector(slug)).first()
|
||||||
|
try {
|
||||||
|
await item.hover({ timeout: 500 })
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ timeout: 60_000 },
|
||||||
|
)
|
||||||
|
.toBe(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createWorkspace(page: Page, root: string, seen: string[]) {
|
||||||
|
await openSidebar(page)
|
||||||
|
await page.getByRole("button", { name: "New workspace" }).first().click()
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
() => {
|
||||||
|
const slug = slugFromUrl(page.url())
|
||||||
|
if (!slug) return ""
|
||||||
|
if (slug === root) return ""
|
||||||
|
if (seen.includes(slug)) return ""
|
||||||
|
return slug
|
||||||
|
},
|
||||||
|
{ timeout: 45_000 },
|
||||||
|
)
|
||||||
|
.not.toBe("")
|
||||||
|
|
||||||
|
const slug = slugFromUrl(page.url())
|
||||||
|
const directory = base64Decode(slug)
|
||||||
|
if (!directory) throw new Error(`Failed to decode workspace slug: ${slug}`)
|
||||||
|
return { slug, directory }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openWorkspaceNewSession(page: Page, slug: string) {
|
||||||
|
await waitWorkspaceReady(page, slug)
|
||||||
|
|
||||||
|
const item = page.locator(workspaceItemSelector(slug)).first()
|
||||||
|
await item.hover()
|
||||||
|
|
||||||
|
const button = page.locator(workspaceNewSessionSelector(slug)).first()
|
||||||
|
await expect(button).toBeVisible()
|
||||||
|
await button.click({ force: true })
|
||||||
|
|
||||||
|
await expect.poll(() => slugFromUrl(page.url())).toBe(slug)
|
||||||
|
await expect(page).toHaveURL(new RegExp(`/${slug}/session(?:[/?#]|$)`))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createSessionFromWorkspace(page: Page, slug: string, text: string) {
|
||||||
|
await openWorkspaceNewSession(page, slug)
|
||||||
|
|
||||||
|
const prompt = page.locator(promptSelector)
|
||||||
|
await expect(prompt).toBeVisible()
|
||||||
|
await prompt.click()
|
||||||
|
await page.keyboard.type(text)
|
||||||
|
await page.keyboard.press("Enter")
|
||||||
|
|
||||||
|
await expect.poll(() => slugFromUrl(page.url())).toBe(slug)
|
||||||
|
await expect(page).toHaveURL(new RegExp(`/${slug}/session/[^/?#]+`), { timeout: 30_000 })
|
||||||
|
|
||||||
|
const sessionID = sessionIDFromUrl(page.url())
|
||||||
|
if (!sessionID) throw new Error(`Failed to parse session id from url: ${page.url()}`)
|
||||||
|
return sessionID
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sessionDirectory(directory: string, sessionID: string) {
|
||||||
|
const info = await createSdk(directory)
|
||||||
|
.session.get({ sessionID })
|
||||||
|
.then((x) => x.data)
|
||||||
|
.catch(() => undefined)
|
||||||
|
if (!info) return ""
|
||||||
|
return info.directory
|
||||||
|
}
|
||||||
|
|
||||||
|
test("new sessions from sidebar workspace actions stay in selected workspace", async ({ page, withProject }) => {
|
||||||
|
await page.setViewportSize({ width: 1400, height: 800 })
|
||||||
|
|
||||||
|
await withProject(async ({ directory, slug: root }) => {
|
||||||
|
const workspaces = [] as { slug: string; directory: string }[]
|
||||||
|
const sessions = [] as string[]
|
||||||
|
|
||||||
|
try {
|
||||||
|
await openSidebar(page)
|
||||||
|
await setWorkspacesEnabled(page, root, true)
|
||||||
|
|
||||||
|
const first = await createWorkspace(page, root, [])
|
||||||
|
workspaces.push(first)
|
||||||
|
await waitWorkspaceReady(page, first.slug)
|
||||||
|
|
||||||
|
const second = await createWorkspace(page, root, [first.slug])
|
||||||
|
workspaces.push(second)
|
||||||
|
await waitWorkspaceReady(page, second.slug)
|
||||||
|
|
||||||
|
const firstSession = await createSessionFromWorkspace(page, first.slug, `workspace one ${Date.now()}`)
|
||||||
|
sessions.push(firstSession)
|
||||||
|
|
||||||
|
const secondSession = await createSessionFromWorkspace(page, second.slug, `workspace two ${Date.now()}`)
|
||||||
|
sessions.push(secondSession)
|
||||||
|
|
||||||
|
const thirdSession = await createSessionFromWorkspace(page, first.slug, `workspace one again ${Date.now()}`)
|
||||||
|
sessions.push(thirdSession)
|
||||||
|
|
||||||
|
await expect.poll(() => sessionDirectory(first.directory, firstSession)).toBe(first.directory)
|
||||||
|
await expect.poll(() => sessionDirectory(second.directory, secondSession)).toBe(second.directory)
|
||||||
|
await expect.poll(() => sessionDirectory(first.directory, thirdSession)).toBe(first.directory)
|
||||||
|
} finally {
|
||||||
|
const dirs = [directory, ...workspaces.map((workspace) => workspace.directory)]
|
||||||
|
await Promise.all(
|
||||||
|
sessions.map((sessionID) =>
|
||||||
|
Promise.all(
|
||||||
|
dirs.map((dir) =>
|
||||||
|
createSdk(dir)
|
||||||
|
.session.delete({ sessionID })
|
||||||
|
.catch(() => undefined),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
await Promise.all(workspaces.map((workspace) => cleanupTestProject(workspace.directory)))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,375 @@
|
|||||||
|
import { base64Decode } from "@opencode-ai/util/encode"
|
||||||
|
import fs from "node:fs/promises"
|
||||||
|
import os from "node:os"
|
||||||
|
import path from "node:path"
|
||||||
|
import type { Page } from "@playwright/test"
|
||||||
|
|
||||||
|
import { test, expect } from "../fixtures"
|
||||||
|
|
||||||
|
test.describe.configure({ mode: "serial" })
|
||||||
|
import {
|
||||||
|
cleanupTestProject,
|
||||||
|
clickMenuItem,
|
||||||
|
confirmDialog,
|
||||||
|
openProjectMenu,
|
||||||
|
openSidebar,
|
||||||
|
openWorkspaceMenu,
|
||||||
|
setWorkspacesEnabled,
|
||||||
|
} from "../actions"
|
||||||
|
import {
|
||||||
|
inlineInputSelector,
|
||||||
|
projectSwitchSelector,
|
||||||
|
projectWorkspacesToggleSelector,
|
||||||
|
workspaceItemSelector,
|
||||||
|
} from "../selectors"
|
||||||
|
import { dirSlug } from "../utils"
|
||||||
|
|
||||||
|
function slugFromUrl(url: string) {
|
||||||
|
return /\/([^/]+)\/session(?:\/|$)/.exec(url)?.[1] ?? ""
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setupWorkspaceTest(page: Page, project: { slug: string }) {
|
||||||
|
const rootSlug = project.slug
|
||||||
|
await openSidebar(page)
|
||||||
|
|
||||||
|
await setWorkspacesEnabled(page, rootSlug, true)
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "New workspace" }).first().click()
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
() => {
|
||||||
|
const slug = slugFromUrl(page.url())
|
||||||
|
return slug.length > 0 && slug !== rootSlug
|
||||||
|
},
|
||||||
|
{ timeout: 45_000 },
|
||||||
|
)
|
||||||
|
.toBe(true)
|
||||||
|
|
||||||
|
const slug = slugFromUrl(page.url())
|
||||||
|
const dir = base64Decode(slug)
|
||||||
|
|
||||||
|
await openSidebar(page)
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const item = page.locator(workspaceItemSelector(slug)).first()
|
||||||
|
try {
|
||||||
|
await item.hover({ timeout: 500 })
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ timeout: 60_000 },
|
||||||
|
)
|
||||||
|
.toBe(true)
|
||||||
|
|
||||||
|
return { rootSlug, slug, directory: dir }
|
||||||
|
}
|
||||||
|
|
||||||
|
test("can enable and disable workspaces from project menu", async ({ page, withProject }) => {
|
||||||
|
await page.setViewportSize({ width: 1400, height: 800 })
|
||||||
|
|
||||||
|
await withProject(async ({ slug }) => {
|
||||||
|
await openSidebar(page)
|
||||||
|
|
||||||
|
await expect(page.getByRole("button", { name: "New session" }).first()).toBeVisible()
|
||||||
|
await expect(page.getByRole("button", { name: "New workspace" })).toHaveCount(0)
|
||||||
|
|
||||||
|
await setWorkspacesEnabled(page, slug, true)
|
||||||
|
await expect(page.getByRole("button", { name: "New workspace" }).first()).toBeVisible()
|
||||||
|
await expect(page.locator(workspaceItemSelector(slug)).first()).toBeVisible()
|
||||||
|
|
||||||
|
await setWorkspacesEnabled(page, slug, false)
|
||||||
|
await expect(page.getByRole("button", { name: "New session" }).first()).toBeVisible()
|
||||||
|
await expect(page.locator(workspaceItemSelector(slug))).toHaveCount(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("can create a workspace", async ({ page, withProject }) => {
|
||||||
|
await page.setViewportSize({ width: 1400, height: 800 })
|
||||||
|
|
||||||
|
await withProject(async ({ slug }) => {
|
||||||
|
await openSidebar(page)
|
||||||
|
await setWorkspacesEnabled(page, slug, true)
|
||||||
|
|
||||||
|
await expect(page.getByRole("button", { name: "New workspace" }).first()).toBeVisible()
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "New workspace" }).first().click()
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
() => {
|
||||||
|
const currentSlug = slugFromUrl(page.url())
|
||||||
|
return currentSlug.length > 0 && currentSlug !== slug
|
||||||
|
},
|
||||||
|
{ timeout: 45_000 },
|
||||||
|
)
|
||||||
|
.toBe(true)
|
||||||
|
|
||||||
|
const workspaceSlug = slugFromUrl(page.url())
|
||||||
|
const workspaceDir = base64Decode(workspaceSlug)
|
||||||
|
|
||||||
|
await openSidebar(page)
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const item = page.locator(workspaceItemSelector(workspaceSlug)).first()
|
||||||
|
try {
|
||||||
|
await item.hover({ timeout: 500 })
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ timeout: 60_000 },
|
||||||
|
)
|
||||||
|
.toBe(true)
|
||||||
|
|
||||||
|
await expect(page.locator(workspaceItemSelector(workspaceSlug)).first()).toBeVisible()
|
||||||
|
|
||||||
|
await cleanupTestProject(workspaceDir)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("non-git projects keep workspace mode disabled", async ({ page, withProject }) => {
|
||||||
|
await page.setViewportSize({ width: 1400, height: 800 })
|
||||||
|
|
||||||
|
const nonGit = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-e2e-project-nongit-"))
|
||||||
|
const nonGitSlug = dirSlug(nonGit)
|
||||||
|
|
||||||
|
await fs.writeFile(path.join(nonGit, "README.md"), "# e2e nongit\n")
|
||||||
|
|
||||||
|
try {
|
||||||
|
await withProject(
|
||||||
|
async () => {
|
||||||
|
await openSidebar(page)
|
||||||
|
|
||||||
|
const nonGitButton = page.locator(projectSwitchSelector(nonGitSlug)).first()
|
||||||
|
await expect(nonGitButton).toBeVisible()
|
||||||
|
await nonGitButton.click()
|
||||||
|
await expect(page).toHaveURL(new RegExp(`/${nonGitSlug}/session`))
|
||||||
|
|
||||||
|
const menu = await openProjectMenu(page, nonGitSlug)
|
||||||
|
const toggle = menu.locator(projectWorkspacesToggleSelector(nonGitSlug)).first()
|
||||||
|
|
||||||
|
await expect(toggle).toBeVisible()
|
||||||
|
await expect(toggle).toBeDisabled()
|
||||||
|
|
||||||
|
await expect(menu.getByRole("menuitem", { name: "New workspace" })).toHaveCount(0)
|
||||||
|
await expect(page.getByRole("button", { name: "New workspace" })).toHaveCount(0)
|
||||||
|
},
|
||||||
|
{ extra: [nonGit] },
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
await cleanupTestProject(nonGit)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test("can rename a workspace", async ({ page, withProject }) => {
|
||||||
|
await page.setViewportSize({ width: 1400, height: 800 })
|
||||||
|
|
||||||
|
await withProject(async (project) => {
|
||||||
|
const { slug } = await setupWorkspaceTest(page, project)
|
||||||
|
|
||||||
|
const rename = `e2e workspace ${Date.now()}`
|
||||||
|
const menu = await openWorkspaceMenu(page, slug)
|
||||||
|
await clickMenuItem(menu, /^Rename$/i, { force: true })
|
||||||
|
|
||||||
|
await expect(menu).toHaveCount(0)
|
||||||
|
|
||||||
|
const item = page.locator(workspaceItemSelector(slug)).first()
|
||||||
|
await expect(item).toBeVisible()
|
||||||
|
const input = item.locator(inlineInputSelector).first()
|
||||||
|
await expect(input).toBeVisible()
|
||||||
|
await input.fill(rename)
|
||||||
|
await input.press("Enter")
|
||||||
|
await expect(item).toContainText(rename)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("can reset a workspace", async ({ page, sdk, withProject }) => {
|
||||||
|
await page.setViewportSize({ width: 1400, height: 800 })
|
||||||
|
|
||||||
|
await withProject(async (project) => {
|
||||||
|
const { slug, directory: createdDir } = await setupWorkspaceTest(page, project)
|
||||||
|
|
||||||
|
const readme = path.join(createdDir, "README.md")
|
||||||
|
const extra = path.join(createdDir, `e2e_reset_${Date.now()}.txt`)
|
||||||
|
const original = await fs.readFile(readme, "utf8")
|
||||||
|
const dirty = `${original.trimEnd()}\n\nchange_${Date.now()}\n`
|
||||||
|
await fs.writeFile(readme, dirty, "utf8")
|
||||||
|
await fs.writeFile(extra, `created_${Date.now()}\n`, "utf8")
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(async () => {
|
||||||
|
return await fs
|
||||||
|
.stat(extra)
|
||||||
|
.then(() => true)
|
||||||
|
.catch(() => false)
|
||||||
|
})
|
||||||
|
.toBe(true)
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(async () => {
|
||||||
|
const files = await sdk.file
|
||||||
|
.status({ directory: createdDir })
|
||||||
|
.then((r) => r.data ?? [])
|
||||||
|
.catch(() => [])
|
||||||
|
return files.length
|
||||||
|
})
|
||||||
|
.toBeGreaterThan(0)
|
||||||
|
|
||||||
|
const menu = await openWorkspaceMenu(page, slug)
|
||||||
|
await clickMenuItem(menu, /^Reset$/i, { force: true })
|
||||||
|
await confirmDialog(page, /^Reset workspace$/i)
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const files = await sdk.file
|
||||||
|
.status({ directory: createdDir })
|
||||||
|
.then((r) => r.data ?? [])
|
||||||
|
.catch(() => [])
|
||||||
|
return files.length
|
||||||
|
},
|
||||||
|
{ timeout: 60_000 },
|
||||||
|
)
|
||||||
|
.toBe(0)
|
||||||
|
|
||||||
|
await expect.poll(() => fs.readFile(readme, "utf8"), { timeout: 60_000 }).toBe(original)
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(async () => {
|
||||||
|
return await fs
|
||||||
|
.stat(extra)
|
||||||
|
.then(() => true)
|
||||||
|
.catch(() => false)
|
||||||
|
})
|
||||||
|
.toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("can delete a workspace", async ({ page, withProject }) => {
|
||||||
|
await page.setViewportSize({ width: 1400, height: 800 })
|
||||||
|
|
||||||
|
await withProject(async (project) => {
|
||||||
|
const { rootSlug, slug } = await setupWorkspaceTest(page, project)
|
||||||
|
|
||||||
|
const menu = await openWorkspaceMenu(page, slug)
|
||||||
|
await clickMenuItem(menu, /^Delete$/i, { force: true })
|
||||||
|
await confirmDialog(page, /^Delete workspace$/i)
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(new RegExp(`/${rootSlug}/session`))
|
||||||
|
await expect(page.locator(workspaceItemSelector(slug))).toHaveCount(0)
|
||||||
|
await expect(page.locator(workspaceItemSelector(rootSlug)).first()).toBeVisible()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("can reorder workspaces by drag and drop", async ({ page, withProject }) => {
|
||||||
|
await page.setViewportSize({ width: 1400, height: 800 })
|
||||||
|
await withProject(async ({ slug: rootSlug }) => {
|
||||||
|
const workspaces = [] as { directory: string; slug: string }[]
|
||||||
|
|
||||||
|
const listSlugs = async () => {
|
||||||
|
const nodes = page.locator('[data-component="sidebar-nav-desktop"] [data-component="workspace-item"]')
|
||||||
|
const slugs = await nodes.evaluateAll((els) => {
|
||||||
|
return els.map((el) => el.getAttribute("data-workspace") ?? "").filter((x) => x.length > 0)
|
||||||
|
})
|
||||||
|
return slugs
|
||||||
|
}
|
||||||
|
|
||||||
|
const waitReady = async (slug: string) => {
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const item = page.locator(workspaceItemSelector(slug)).first()
|
||||||
|
try {
|
||||||
|
await item.hover({ timeout: 500 })
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ timeout: 60_000 },
|
||||||
|
)
|
||||||
|
.toBe(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const drag = async (from: string, to: string) => {
|
||||||
|
const src = page.locator(workspaceItemSelector(from)).first()
|
||||||
|
const dst = page.locator(workspaceItemSelector(to)).first()
|
||||||
|
|
||||||
|
await src.scrollIntoViewIfNeeded()
|
||||||
|
await dst.scrollIntoViewIfNeeded()
|
||||||
|
|
||||||
|
const a = await src.boundingBox()
|
||||||
|
const b = await dst.boundingBox()
|
||||||
|
if (!a || !b) throw new Error("Failed to resolve workspace drag bounds")
|
||||||
|
|
||||||
|
await page.mouse.move(a.x + a.width / 2, a.y + a.height / 2)
|
||||||
|
await page.mouse.down()
|
||||||
|
await page.mouse.move(b.x + b.width / 2, b.y + b.height / 2, { steps: 12 })
|
||||||
|
await page.mouse.up()
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await openSidebar(page)
|
||||||
|
|
||||||
|
await setWorkspacesEnabled(page, rootSlug, true)
|
||||||
|
|
||||||
|
for (const _ of [0, 1]) {
|
||||||
|
const prev = slugFromUrl(page.url())
|
||||||
|
await page.getByRole("button", { name: "New workspace" }).first().click()
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
() => {
|
||||||
|
const slug = slugFromUrl(page.url())
|
||||||
|
return slug.length > 0 && slug !== rootSlug && slug !== prev
|
||||||
|
},
|
||||||
|
{ timeout: 45_000 },
|
||||||
|
)
|
||||||
|
.toBe(true)
|
||||||
|
|
||||||
|
const slug = slugFromUrl(page.url())
|
||||||
|
const dir = base64Decode(slug)
|
||||||
|
workspaces.push({ slug, directory: dir })
|
||||||
|
|
||||||
|
await openSidebar(page)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (workspaces.length !== 2) throw new Error("Expected two created workspaces")
|
||||||
|
|
||||||
|
const a = workspaces[0].slug
|
||||||
|
const b = workspaces[1].slug
|
||||||
|
|
||||||
|
await waitReady(a)
|
||||||
|
await waitReady(b)
|
||||||
|
|
||||||
|
const list = async () => {
|
||||||
|
const slugs = await listSlugs()
|
||||||
|
return slugs.filter((s) => s !== rootSlug && (s === a || s === b)).slice(0, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(async () => {
|
||||||
|
const slugs = await list()
|
||||||
|
return slugs.length === 2
|
||||||
|
})
|
||||||
|
.toBe(true)
|
||||||
|
|
||||||
|
const before = await list()
|
||||||
|
const from = before[1]
|
||||||
|
const to = before[0]
|
||||||
|
if (!from || !to) throw new Error("Failed to resolve initial workspace order")
|
||||||
|
|
||||||
|
await drag(from, to)
|
||||||
|
|
||||||
|
await expect.poll(async () => await list()).toEqual([from, to])
|
||||||
|
} finally {
|
||||||
|
await Promise.all(workspaces.map((w) => cleanupTestProject(w.directory)))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -3,6 +3,17 @@ export const terminalSelector = '[data-component="terminal"]'
|
|||||||
|
|
||||||
export const modelVariantCycleSelector = '[data-action="model-variant-cycle"]'
|
export const modelVariantCycleSelector = '[data-action="model-variant-cycle"]'
|
||||||
export const settingsLanguageSelectSelector = '[data-action="settings-language"]'
|
export const settingsLanguageSelectSelector = '[data-action="settings-language"]'
|
||||||
|
export const settingsColorSchemeSelector = '[data-action="settings-color-scheme"]'
|
||||||
|
export const settingsThemeSelector = '[data-action="settings-theme"]'
|
||||||
|
export const settingsFontSelector = '[data-action="settings-font"]'
|
||||||
|
export const settingsNotificationsAgentSelector = '[data-action="settings-notifications-agent"]'
|
||||||
|
export const settingsNotificationsPermissionsSelector = '[data-action="settings-notifications-permissions"]'
|
||||||
|
export const settingsNotificationsErrorsSelector = '[data-action="settings-notifications-errors"]'
|
||||||
|
export const settingsSoundsAgentSelector = '[data-action="settings-sounds-agent"]'
|
||||||
|
export const settingsSoundsPermissionsSelector = '[data-action="settings-sounds-permissions"]'
|
||||||
|
export const settingsSoundsErrorsSelector = '[data-action="settings-sounds-errors"]'
|
||||||
|
export const settingsUpdatesStartupSelector = '[data-action="settings-updates-startup"]'
|
||||||
|
export const settingsReleaseNotesSelector = '[data-action="settings-release-notes"]'
|
||||||
|
|
||||||
export const sidebarNavSelector = '[data-component="sidebar-nav-desktop"]'
|
export const sidebarNavSelector = '[data-component="sidebar-nav-desktop"]'
|
||||||
|
|
||||||
@@ -16,6 +27,9 @@ export const projectMenuTriggerSelector = (slug: string) =>
|
|||||||
|
|
||||||
export const projectCloseMenuSelector = (slug: string) => `[data-action="project-close-menu"][data-project="${slug}"]`
|
export const projectCloseMenuSelector = (slug: string) => `[data-action="project-close-menu"][data-project="${slug}"]`
|
||||||
|
|
||||||
|
export const projectWorkspacesToggleSelector = (slug: string) =>
|
||||||
|
`[data-action="project-workspaces-toggle"][data-project="${slug}"]`
|
||||||
|
|
||||||
export const titlebarRightSelector = "#opencode-titlebar-right"
|
export const titlebarRightSelector = "#opencode-titlebar-right"
|
||||||
|
|
||||||
export const popoverBodySelector = '[data-slot="popover-body"]'
|
export const popoverBodySelector = '[data-slot="popover-body"]'
|
||||||
@@ -28,8 +42,19 @@ export const inlineInputSelector = '[data-component="inline-input"]'
|
|||||||
|
|
||||||
export const sessionItemSelector = (sessionID: string) => `${sidebarNavSelector} [data-session-id="${sessionID}"]`
|
export const sessionItemSelector = (sessionID: string) => `${sidebarNavSelector} [data-session-id="${sessionID}"]`
|
||||||
|
|
||||||
|
export const workspaceItemSelector = (slug: string) =>
|
||||||
|
`${sidebarNavSelector} [data-component="workspace-item"][data-workspace="${slug}"]`
|
||||||
|
|
||||||
|
export const workspaceMenuTriggerSelector = (slug: string) =>
|
||||||
|
`${sidebarNavSelector} [data-action="workspace-menu"][data-workspace="${slug}"]`
|
||||||
|
|
||||||
|
export const workspaceNewSessionSelector = (slug: string) =>
|
||||||
|
`${sidebarNavSelector} [data-action="workspace-new-session"][data-workspace="${slug}"]`
|
||||||
|
|
||||||
export const listItemSelector = '[data-slot="list-item"]'
|
export const listItemSelector = '[data-slot="list-item"]'
|
||||||
|
|
||||||
export const listItemKeyStartsWithSelector = (prefix: string) => `${listItemSelector}[data-key^="${prefix}"]`
|
export const listItemKeyStartsWithSelector = (prefix: string) => `${listItemSelector}[data-key^="${prefix}"]`
|
||||||
|
|
||||||
export const listItemKeySelector = (key: string) => `${listItemSelector}[data-key="${key}"]`
|
export const listItemKeySelector = (key: string) => `${listItemSelector}[data-key="${key}"]`
|
||||||
|
|
||||||
|
export const keybindButtonSelector = (id: string) => `[data-keybind-id="${id}"]`
|
||||||
|
|||||||
@@ -0,0 +1,235 @@
|
|||||||
|
import type { Page } from "@playwright/test"
|
||||||
|
import { test, expect } from "../fixtures"
|
||||||
|
import { withSession } from "../actions"
|
||||||
|
import { createSdk, modKey } from "../utils"
|
||||||
|
import { promptSelector } from "../selectors"
|
||||||
|
|
||||||
|
async function seedConversation(input: {
|
||||||
|
page: Page
|
||||||
|
sdk: ReturnType<typeof createSdk>
|
||||||
|
sessionID: string
|
||||||
|
token: string
|
||||||
|
}) {
|
||||||
|
const prompt = input.page.locator(promptSelector)
|
||||||
|
await expect(prompt).toBeVisible()
|
||||||
|
await prompt.click()
|
||||||
|
await input.page.keyboard.type(`Reply with exactly: ${input.token}`)
|
||||||
|
await input.page.keyboard.press("Enter")
|
||||||
|
|
||||||
|
let userMessageID: string | undefined
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const messages = await input.sdk.session
|
||||||
|
.messages({ sessionID: input.sessionID, limit: 50 })
|
||||||
|
.then((r) => r.data ?? [])
|
||||||
|
const users = messages.filter(
|
||||||
|
(m) =>
|
||||||
|
m.info.role === "user" &&
|
||||||
|
m.parts.filter((p) => p.type === "text").some((p) => p.text.includes(input.token)),
|
||||||
|
)
|
||||||
|
if (users.length === 0) return false
|
||||||
|
|
||||||
|
const user = users[users.length - 1]
|
||||||
|
if (!user) return false
|
||||||
|
userMessageID = user.info.id
|
||||||
|
|
||||||
|
const assistantText = messages
|
||||||
|
.filter((m) => m.info.role === "assistant")
|
||||||
|
.flatMap((m) => m.parts)
|
||||||
|
.filter((p) => p.type === "text")
|
||||||
|
.map((p) => p.text)
|
||||||
|
.join("\n")
|
||||||
|
|
||||||
|
return assistantText.includes(input.token)
|
||||||
|
},
|
||||||
|
{ timeout: 90_000 },
|
||||||
|
)
|
||||||
|
.toBe(true)
|
||||||
|
|
||||||
|
if (!userMessageID) throw new Error("Expected a user message id")
|
||||||
|
return { prompt, userMessageID }
|
||||||
|
}
|
||||||
|
|
||||||
|
test("slash undo sets revert and restores prior prompt", async ({ page, withProject }) => {
|
||||||
|
test.setTimeout(120_000)
|
||||||
|
|
||||||
|
const token = `undo_${Date.now()}`
|
||||||
|
|
||||||
|
await withProject(async (project) => {
|
||||||
|
const sdk = createSdk(project.directory)
|
||||||
|
|
||||||
|
await withSession(sdk, `e2e undo ${Date.now()}`, async (session) => {
|
||||||
|
await project.gotoSession(session.id)
|
||||||
|
|
||||||
|
const seeded = await seedConversation({ page, sdk, sessionID: session.id, token })
|
||||||
|
|
||||||
|
await seeded.prompt.click()
|
||||||
|
await page.keyboard.type("/undo")
|
||||||
|
|
||||||
|
const undo = page.locator('[data-slash-id="session.undo"]').first()
|
||||||
|
await expect(undo).toBeVisible()
|
||||||
|
await page.keyboard.press("Enter")
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
|
||||||
|
timeout: 30_000,
|
||||||
|
})
|
||||||
|
.toBe(seeded.userMessageID)
|
||||||
|
|
||||||
|
await expect(seeded.prompt).toContainText(token)
|
||||||
|
await expect(page.locator(`[data-message-id="${seeded.userMessageID}"]`)).toHaveCount(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("slash redo clears revert and restores latest state", async ({ page, withProject }) => {
|
||||||
|
test.setTimeout(120_000)
|
||||||
|
|
||||||
|
const token = `redo_${Date.now()}`
|
||||||
|
|
||||||
|
await withProject(async (project) => {
|
||||||
|
const sdk = createSdk(project.directory)
|
||||||
|
|
||||||
|
await withSession(sdk, `e2e redo ${Date.now()}`, async (session) => {
|
||||||
|
await project.gotoSession(session.id)
|
||||||
|
|
||||||
|
const seeded = await seedConversation({ page, sdk, sessionID: session.id, token })
|
||||||
|
|
||||||
|
await seeded.prompt.click()
|
||||||
|
await page.keyboard.type("/undo")
|
||||||
|
|
||||||
|
const undo = page.locator('[data-slash-id="session.undo"]').first()
|
||||||
|
await expect(undo).toBeVisible()
|
||||||
|
await page.keyboard.press("Enter")
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
|
||||||
|
timeout: 30_000,
|
||||||
|
})
|
||||||
|
.toBe(seeded.userMessageID)
|
||||||
|
|
||||||
|
await seeded.prompt.click()
|
||||||
|
await page.keyboard.press(`${modKey}+A`)
|
||||||
|
await page.keyboard.press("Backspace")
|
||||||
|
await page.keyboard.type("/redo")
|
||||||
|
|
||||||
|
const redo = page.locator('[data-slash-id="session.redo"]').first()
|
||||||
|
await expect(redo).toBeVisible()
|
||||||
|
await page.keyboard.press("Enter")
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
|
||||||
|
timeout: 30_000,
|
||||||
|
})
|
||||||
|
.toBeUndefined()
|
||||||
|
|
||||||
|
await expect(seeded.prompt).not.toContainText(token)
|
||||||
|
await expect(page.locator(`[data-message-id="${seeded.userMessageID}"]`).first()).toBeVisible()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("slash undo/redo traverses multi-step revert stack", async ({ page, withProject }) => {
|
||||||
|
test.setTimeout(120_000)
|
||||||
|
|
||||||
|
const firstToken = `undo_redo_first_${Date.now()}`
|
||||||
|
const secondToken = `undo_redo_second_${Date.now()}`
|
||||||
|
|
||||||
|
await withProject(async (project) => {
|
||||||
|
const sdk = createSdk(project.directory)
|
||||||
|
|
||||||
|
await withSession(sdk, `e2e undo redo stack ${Date.now()}`, async (session) => {
|
||||||
|
await project.gotoSession(session.id)
|
||||||
|
|
||||||
|
const first = await seedConversation({
|
||||||
|
page,
|
||||||
|
sdk,
|
||||||
|
sessionID: session.id,
|
||||||
|
token: firstToken,
|
||||||
|
})
|
||||||
|
const second = await seedConversation({
|
||||||
|
page,
|
||||||
|
sdk,
|
||||||
|
sessionID: session.id,
|
||||||
|
token: secondToken,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(first.userMessageID).not.toBe(second.userMessageID)
|
||||||
|
|
||||||
|
const firstMessage = page.locator(`[data-message-id="${first.userMessageID}"]`)
|
||||||
|
const secondMessage = page.locator(`[data-message-id="${second.userMessageID}"]`)
|
||||||
|
|
||||||
|
await expect(firstMessage.first()).toBeVisible()
|
||||||
|
await expect(secondMessage.first()).toBeVisible()
|
||||||
|
|
||||||
|
await second.prompt.click()
|
||||||
|
await page.keyboard.press(`${modKey}+A`)
|
||||||
|
await page.keyboard.press("Backspace")
|
||||||
|
await page.keyboard.type("/undo")
|
||||||
|
|
||||||
|
const undo = page.locator('[data-slash-id="session.undo"]').first()
|
||||||
|
await expect(undo).toBeVisible()
|
||||||
|
await page.keyboard.press("Enter")
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
|
||||||
|
timeout: 30_000,
|
||||||
|
})
|
||||||
|
.toBe(second.userMessageID)
|
||||||
|
|
||||||
|
await expect(firstMessage.first()).toBeVisible()
|
||||||
|
await expect(secondMessage).toHaveCount(0)
|
||||||
|
|
||||||
|
await second.prompt.click()
|
||||||
|
await page.keyboard.press(`${modKey}+A`)
|
||||||
|
await page.keyboard.press("Backspace")
|
||||||
|
await page.keyboard.type("/undo")
|
||||||
|
await expect(undo).toBeVisible()
|
||||||
|
await page.keyboard.press("Enter")
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
|
||||||
|
timeout: 30_000,
|
||||||
|
})
|
||||||
|
.toBe(first.userMessageID)
|
||||||
|
|
||||||
|
await expect(firstMessage).toHaveCount(0)
|
||||||
|
await expect(secondMessage).toHaveCount(0)
|
||||||
|
|
||||||
|
await second.prompt.click()
|
||||||
|
await page.keyboard.press(`${modKey}+A`)
|
||||||
|
await page.keyboard.press("Backspace")
|
||||||
|
await page.keyboard.type("/redo")
|
||||||
|
|
||||||
|
const redo = page.locator('[data-slash-id="session.redo"]').first()
|
||||||
|
await expect(redo).toBeVisible()
|
||||||
|
await page.keyboard.press("Enter")
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
|
||||||
|
timeout: 30_000,
|
||||||
|
})
|
||||||
|
.toBe(second.userMessageID)
|
||||||
|
|
||||||
|
await expect(firstMessage.first()).toBeVisible()
|
||||||
|
await expect(secondMessage).toHaveCount(0)
|
||||||
|
|
||||||
|
await second.prompt.click()
|
||||||
|
await page.keyboard.press(`${modKey}+A`)
|
||||||
|
await page.keyboard.press("Backspace")
|
||||||
|
await page.keyboard.type("/redo")
|
||||||
|
await expect(redo).toBeVisible()
|
||||||
|
await page.keyboard.press("Enter")
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
|
||||||
|
timeout: 30_000,
|
||||||
|
})
|
||||||
|
.toBeUndefined()
|
||||||
|
|
||||||
|
await expect(firstMessage.first()).toBeVisible()
|
||||||
|
await expect(secondMessage.first()).toBeVisible()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -11,57 +11,98 @@ import { sessionItemSelector, inlineInputSelector } from "../selectors"
|
|||||||
|
|
||||||
const shareDisabled = process.env.OPENCODE_DISABLE_SHARE === "true" || process.env.OPENCODE_DISABLE_SHARE === "1"
|
const shareDisabled = process.env.OPENCODE_DISABLE_SHARE === "true" || process.env.OPENCODE_DISABLE_SHARE === "1"
|
||||||
|
|
||||||
test("sidebar session can be renamed", async ({ page, sdk, gotoSession }) => {
|
type Sdk = Parameters<typeof withSession>[0]
|
||||||
|
|
||||||
|
async function seedMessage(sdk: Sdk, sessionID: string) {
|
||||||
|
await sdk.session.promptAsync({
|
||||||
|
sessionID,
|
||||||
|
noReply: true,
|
||||||
|
parts: [{ type: "text", text: "e2e seed" }],
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const messages = await sdk.session.messages({ sessionID, limit: 1 }).then((r) => r.data ?? [])
|
||||||
|
return messages.length
|
||||||
|
},
|
||||||
|
{ timeout: 30_000 },
|
||||||
|
)
|
||||||
|
.toBeGreaterThan(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
test("session can be renamed via header menu", async ({ page, sdk, gotoSession }) => {
|
||||||
const stamp = Date.now()
|
const stamp = Date.now()
|
||||||
const originalTitle = `e2e rename test ${stamp}`
|
const originalTitle = `e2e rename test ${stamp}`
|
||||||
const newTitle = `e2e renamed ${stamp}`
|
const newTitle = `e2e renamed ${stamp}`
|
||||||
|
|
||||||
await withSession(sdk, originalTitle, async (session) => {
|
await withSession(sdk, originalTitle, async (session) => {
|
||||||
|
await seedMessage(sdk, session.id)
|
||||||
await gotoSession(session.id)
|
await gotoSession(session.id)
|
||||||
await openSidebar(page)
|
|
||||||
|
|
||||||
const menu = await openSessionMoreMenu(page, session.id)
|
const menu = await openSessionMoreMenu(page, session.id)
|
||||||
await clickMenuItem(menu, /rename/i)
|
await clickMenuItem(menu, /rename/i)
|
||||||
|
|
||||||
const input = page.locator(sessionItemSelector(session.id)).locator(inlineInputSelector).first()
|
const input = page.locator(".session-scroller").locator(inlineInputSelector).first()
|
||||||
await expect(input).toBeVisible()
|
await expect(input).toBeVisible()
|
||||||
await input.fill(newTitle)
|
await input.fill(newTitle)
|
||||||
await input.press("Enter")
|
await input.press("Enter")
|
||||||
|
|
||||||
await expect(page.locator(sessionItemSelector(session.id)).locator("a").first()).toContainText(newTitle)
|
await expect(page.getByRole("heading", { level: 1 }).first()).toContainText(newTitle)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("sidebar session can be archived", async ({ page, sdk, gotoSession }) => {
|
test("session can be archived via header menu", async ({ page, sdk, gotoSession }) => {
|
||||||
const stamp = Date.now()
|
const stamp = Date.now()
|
||||||
const title = `e2e archive test ${stamp}`
|
const title = `e2e archive test ${stamp}`
|
||||||
|
|
||||||
await withSession(sdk, title, async (session) => {
|
await withSession(sdk, title, async (session) => {
|
||||||
|
await seedMessage(sdk, session.id)
|
||||||
await gotoSession(session.id)
|
await gotoSession(session.id)
|
||||||
await openSidebar(page)
|
|
||||||
|
|
||||||
const sessionEl = page.locator(sessionItemSelector(session.id))
|
|
||||||
const menu = await openSessionMoreMenu(page, session.id)
|
const menu = await openSessionMoreMenu(page, session.id)
|
||||||
await clickMenuItem(menu, /archive/i)
|
await clickMenuItem(menu, /archive/i)
|
||||||
|
|
||||||
await expect(sessionEl).not.toBeVisible()
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const data = await sdk.session.get({ sessionID: session.id }).then((r) => r.data)
|
||||||
|
return data?.time?.archived
|
||||||
|
},
|
||||||
|
{ timeout: 30_000 },
|
||||||
|
)
|
||||||
|
.not.toBeUndefined()
|
||||||
|
|
||||||
|
await openSidebar(page)
|
||||||
|
await expect(page.locator(sessionItemSelector(session.id))).toHaveCount(0)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("sidebar session can be deleted", async ({ page, sdk, gotoSession }) => {
|
test("session can be deleted via header menu", async ({ page, sdk, gotoSession }) => {
|
||||||
const stamp = Date.now()
|
const stamp = Date.now()
|
||||||
const title = `e2e delete test ${stamp}`
|
const title = `e2e delete test ${stamp}`
|
||||||
|
|
||||||
await withSession(sdk, title, async (session) => {
|
await withSession(sdk, title, async (session) => {
|
||||||
|
await seedMessage(sdk, session.id)
|
||||||
await gotoSession(session.id)
|
await gotoSession(session.id)
|
||||||
await openSidebar(page)
|
|
||||||
|
|
||||||
const sessionEl = page.locator(sessionItemSelector(session.id))
|
|
||||||
const menu = await openSessionMoreMenu(page, session.id)
|
const menu = await openSessionMoreMenu(page, session.id)
|
||||||
await clickMenuItem(menu, /delete/i)
|
await clickMenuItem(menu, /delete/i)
|
||||||
await confirmDialog(page, /delete/i)
|
await confirmDialog(page, /delete/i)
|
||||||
|
|
||||||
await expect(sessionEl).not.toBeVisible()
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const data = await sdk.session
|
||||||
|
.get({ sessionID: session.id })
|
||||||
|
.then((r) => r.data)
|
||||||
|
.catch(() => undefined)
|
||||||
|
return data?.id
|
||||||
|
},
|
||||||
|
{ timeout: 30_000 },
|
||||||
|
)
|
||||||
|
.toBeUndefined()
|
||||||
|
|
||||||
|
await openSidebar(page)
|
||||||
|
await expect(page.locator(sessionItemSelector(session.id))).toHaveCount(0)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -72,6 +113,7 @@ test("session can be shared and unshared via header button", async ({ page, sdk,
|
|||||||
const title = `e2e share test ${stamp}`
|
const title = `e2e share test ${stamp}`
|
||||||
|
|
||||||
await withSession(sdk, title, async (session) => {
|
await withSession(sdk, title, async (session) => {
|
||||||
|
await seedMessage(sdk, session.id)
|
||||||
await gotoSession(session.id)
|
await gotoSession(session.id)
|
||||||
|
|
||||||
const { rightSection, popoverBody } = await openSharePopover(page)
|
const { rightSection, popoverBody } = await openSharePopover(page)
|
||||||
|
|||||||
@@ -0,0 +1,392 @@
|
|||||||
|
import { test, expect } from "../fixtures"
|
||||||
|
import { openSettings, closeDialog, withSession } from "../actions"
|
||||||
|
import { keybindButtonSelector, terminalSelector } from "../selectors"
|
||||||
|
import { modKey } from "../utils"
|
||||||
|
|
||||||
|
test("changing sidebar toggle keybind works", async ({ page, gotoSession }) => {
|
||||||
|
await gotoSession()
|
||||||
|
|
||||||
|
const dialog = await openSettings(page)
|
||||||
|
await dialog.getByRole("tab", { name: "Shortcuts" }).click()
|
||||||
|
|
||||||
|
const keybindButton = dialog.locator(keybindButtonSelector("sidebar.toggle")).first()
|
||||||
|
await expect(keybindButton).toBeVisible()
|
||||||
|
|
||||||
|
const initialKeybind = await keybindButton.textContent()
|
||||||
|
expect(initialKeybind).toContain("B")
|
||||||
|
|
||||||
|
await keybindButton.click()
|
||||||
|
await expect(keybindButton).toHaveText(/press/i)
|
||||||
|
|
||||||
|
await page.keyboard.press(`${modKey}+Shift+KeyH`)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
const newKeybind = await keybindButton.textContent()
|
||||||
|
expect(newKeybind).toContain("H")
|
||||||
|
|
||||||
|
const stored = await page.evaluate(() => {
|
||||||
|
const raw = localStorage.getItem("settings.v3")
|
||||||
|
return raw ? JSON.parse(raw) : null
|
||||||
|
})
|
||||||
|
expect(stored?.keybinds?.["sidebar.toggle"]).toBe("mod+shift+h")
|
||||||
|
|
||||||
|
await closeDialog(page, dialog)
|
||||||
|
|
||||||
|
const main = page.locator("main")
|
||||||
|
const initialClasses = (await main.getAttribute("class")) ?? ""
|
||||||
|
const initiallyClosed = initialClasses.includes("xl:border-l")
|
||||||
|
|
||||||
|
await page.keyboard.press(`${modKey}+Shift+H`)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
const afterToggleClasses = (await main.getAttribute("class")) ?? ""
|
||||||
|
const afterToggleClosed = afterToggleClasses.includes("xl:border-l")
|
||||||
|
expect(afterToggleClosed).toBe(!initiallyClosed)
|
||||||
|
|
||||||
|
await page.keyboard.press(`${modKey}+Shift+H`)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
const finalClasses = (await main.getAttribute("class")) ?? ""
|
||||||
|
const finalClosed = finalClasses.includes("xl:border-l")
|
||||||
|
expect(finalClosed).toBe(initiallyClosed)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("sidebar toggle keybind guards against shortcut conflicts", async ({ page, gotoSession }) => {
|
||||||
|
await gotoSession()
|
||||||
|
|
||||||
|
const dialog = await openSettings(page)
|
||||||
|
await dialog.getByRole("tab", { name: "Shortcuts" }).click()
|
||||||
|
|
||||||
|
const keybindButton = dialog.locator(keybindButtonSelector("sidebar.toggle"))
|
||||||
|
await expect(keybindButton).toBeVisible()
|
||||||
|
|
||||||
|
const initialKeybind = await keybindButton.textContent()
|
||||||
|
expect(initialKeybind).toContain("B")
|
||||||
|
|
||||||
|
await keybindButton.click()
|
||||||
|
await expect(keybindButton).toHaveText(/press/i)
|
||||||
|
|
||||||
|
await page.keyboard.press(`${modKey}+Shift+KeyP`)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
const toast = page.locator('[data-component="toast"]').last()
|
||||||
|
await expect(toast).toBeVisible()
|
||||||
|
await expect(toast).toContainText(/already/i)
|
||||||
|
|
||||||
|
await keybindButton.click()
|
||||||
|
await expect(keybindButton).toContainText("B")
|
||||||
|
|
||||||
|
const stored = await page.evaluate(() => {
|
||||||
|
const raw = localStorage.getItem("settings.v3")
|
||||||
|
return raw ? JSON.parse(raw) : null
|
||||||
|
})
|
||||||
|
expect(stored?.keybinds?.["sidebar.toggle"]).toBeUndefined()
|
||||||
|
|
||||||
|
await closeDialog(page, dialog)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("resetting all keybinds to defaults works", async ({ page, gotoSession }) => {
|
||||||
|
await page.addInitScript(() => {
|
||||||
|
localStorage.setItem("settings.v3", JSON.stringify({ keybinds: { "sidebar.toggle": "mod+shift+x" } }))
|
||||||
|
})
|
||||||
|
|
||||||
|
await gotoSession()
|
||||||
|
|
||||||
|
const dialog = await openSettings(page)
|
||||||
|
await dialog.getByRole("tab", { name: "Shortcuts" }).click()
|
||||||
|
|
||||||
|
const keybindButton = dialog.locator(keybindButtonSelector("sidebar.toggle"))
|
||||||
|
await expect(keybindButton).toBeVisible()
|
||||||
|
|
||||||
|
const customKeybind = await keybindButton.textContent()
|
||||||
|
expect(customKeybind).toContain("X")
|
||||||
|
|
||||||
|
const resetButton = dialog.getByRole("button", { name: "Reset to defaults" })
|
||||||
|
await expect(resetButton).toBeVisible()
|
||||||
|
await expect(resetButton).toBeEnabled()
|
||||||
|
await resetButton.click()
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
const restoredKeybind = await keybindButton.textContent()
|
||||||
|
expect(restoredKeybind).toContain("B")
|
||||||
|
|
||||||
|
const stored = await page.evaluate(() => {
|
||||||
|
const raw = localStorage.getItem("settings.v3")
|
||||||
|
return raw ? JSON.parse(raw) : null
|
||||||
|
})
|
||||||
|
expect(stored?.keybinds?.["sidebar.toggle"]).toBeUndefined()
|
||||||
|
|
||||||
|
await closeDialog(page, dialog)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("clearing a keybind works", async ({ page, gotoSession }) => {
|
||||||
|
await gotoSession()
|
||||||
|
|
||||||
|
const dialog = await openSettings(page)
|
||||||
|
await dialog.getByRole("tab", { name: "Shortcuts" }).click()
|
||||||
|
|
||||||
|
const keybindButton = dialog.locator(keybindButtonSelector("sidebar.toggle"))
|
||||||
|
await expect(keybindButton).toBeVisible()
|
||||||
|
|
||||||
|
const initialKeybind = await keybindButton.textContent()
|
||||||
|
expect(initialKeybind).toContain("B")
|
||||||
|
|
||||||
|
await keybindButton.click()
|
||||||
|
await expect(keybindButton).toHaveText(/press/i)
|
||||||
|
|
||||||
|
await page.keyboard.press("Delete")
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
const clearedKeybind = await keybindButton.textContent()
|
||||||
|
expect(clearedKeybind).toMatch(/unassigned|press/i)
|
||||||
|
|
||||||
|
const stored = await page.evaluate(() => {
|
||||||
|
const raw = localStorage.getItem("settings.v3")
|
||||||
|
return raw ? JSON.parse(raw) : null
|
||||||
|
})
|
||||||
|
expect(stored?.keybinds?.["sidebar.toggle"]).toBe("none")
|
||||||
|
|
||||||
|
await closeDialog(page, dialog)
|
||||||
|
|
||||||
|
await page.keyboard.press(`${modKey}+B`)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
const stillOnSession = page.url().includes("/session")
|
||||||
|
expect(stillOnSession).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("changing settings open keybind works", async ({ page, gotoSession }) => {
|
||||||
|
await gotoSession()
|
||||||
|
|
||||||
|
const dialog = await openSettings(page)
|
||||||
|
await dialog.getByRole("tab", { name: "Shortcuts" }).click()
|
||||||
|
|
||||||
|
const keybindButton = dialog.locator(keybindButtonSelector("settings.open"))
|
||||||
|
await expect(keybindButton).toBeVisible()
|
||||||
|
|
||||||
|
const initialKeybind = await keybindButton.textContent()
|
||||||
|
expect(initialKeybind).toContain(",")
|
||||||
|
|
||||||
|
await keybindButton.click()
|
||||||
|
await expect(keybindButton).toHaveText(/press/i)
|
||||||
|
|
||||||
|
await page.keyboard.press(`${modKey}+Slash`)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
const newKeybind = await keybindButton.textContent()
|
||||||
|
expect(newKeybind).toContain("/")
|
||||||
|
|
||||||
|
const stored = await page.evaluate(() => {
|
||||||
|
const raw = localStorage.getItem("settings.v3")
|
||||||
|
return raw ? JSON.parse(raw) : null
|
||||||
|
})
|
||||||
|
expect(stored?.keybinds?.["settings.open"]).toBe("mod+/")
|
||||||
|
|
||||||
|
await closeDialog(page, dialog)
|
||||||
|
|
||||||
|
const settingsDialog = page.getByRole("dialog")
|
||||||
|
await expect(settingsDialog).toHaveCount(0)
|
||||||
|
|
||||||
|
await page.keyboard.press(`${modKey}+Slash`)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
await expect(settingsDialog).toBeVisible()
|
||||||
|
|
||||||
|
await closeDialog(page, settingsDialog)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("changing new session keybind works", async ({ page, sdk, gotoSession }) => {
|
||||||
|
await withSession(sdk, "test session for keybind", async (session) => {
|
||||||
|
await gotoSession(session.id)
|
||||||
|
|
||||||
|
const initialUrl = page.url()
|
||||||
|
expect(initialUrl).toContain(`/session/${session.id}`)
|
||||||
|
|
||||||
|
const dialog = await openSettings(page)
|
||||||
|
await dialog.getByRole("tab", { name: "Shortcuts" }).click()
|
||||||
|
|
||||||
|
const keybindButton = dialog.locator(keybindButtonSelector("session.new"))
|
||||||
|
await expect(keybindButton).toBeVisible()
|
||||||
|
|
||||||
|
await keybindButton.click()
|
||||||
|
await expect(keybindButton).toHaveText(/press/i)
|
||||||
|
|
||||||
|
await page.keyboard.press(`${modKey}+Shift+KeyN`)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
const newKeybind = await keybindButton.textContent()
|
||||||
|
expect(newKeybind).toContain("N")
|
||||||
|
|
||||||
|
const stored = await page.evaluate(() => {
|
||||||
|
const raw = localStorage.getItem("settings.v3")
|
||||||
|
return raw ? JSON.parse(raw) : null
|
||||||
|
})
|
||||||
|
expect(stored?.keybinds?.["session.new"]).toBe("mod+shift+n")
|
||||||
|
|
||||||
|
await closeDialog(page, dialog)
|
||||||
|
|
||||||
|
await page.keyboard.press(`${modKey}+Shift+N`)
|
||||||
|
await page.waitForTimeout(200)
|
||||||
|
|
||||||
|
const newUrl = page.url()
|
||||||
|
expect(newUrl).toMatch(/\/session\/?$/)
|
||||||
|
expect(newUrl).not.toContain(session.id)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("changing file open keybind works", async ({ page, gotoSession }) => {
|
||||||
|
await gotoSession()
|
||||||
|
|
||||||
|
const dialog = await openSettings(page)
|
||||||
|
await dialog.getByRole("tab", { name: "Shortcuts" }).click()
|
||||||
|
|
||||||
|
const keybindButton = dialog.locator(keybindButtonSelector("file.open"))
|
||||||
|
await expect(keybindButton).toBeVisible()
|
||||||
|
|
||||||
|
const initialKeybind = await keybindButton.textContent()
|
||||||
|
expect(initialKeybind).toContain("P")
|
||||||
|
|
||||||
|
await keybindButton.click()
|
||||||
|
await expect(keybindButton).toHaveText(/press/i)
|
||||||
|
|
||||||
|
await page.keyboard.press(`${modKey}+Shift+KeyF`)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
const newKeybind = await keybindButton.textContent()
|
||||||
|
expect(newKeybind).toContain("F")
|
||||||
|
|
||||||
|
const stored = await page.evaluate(() => {
|
||||||
|
const raw = localStorage.getItem("settings.v3")
|
||||||
|
return raw ? JSON.parse(raw) : null
|
||||||
|
})
|
||||||
|
expect(stored?.keybinds?.["file.open"]).toBe("mod+shift+f")
|
||||||
|
|
||||||
|
await closeDialog(page, dialog)
|
||||||
|
|
||||||
|
const filePickerDialog = page.getByRole("dialog").filter({ has: page.getByPlaceholder(/search files/i) })
|
||||||
|
await expect(filePickerDialog).toHaveCount(0)
|
||||||
|
|
||||||
|
await page.keyboard.press(`${modKey}+Shift+F`)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
await expect(filePickerDialog).toBeVisible()
|
||||||
|
|
||||||
|
await page.keyboard.press("Escape")
|
||||||
|
await expect(filePickerDialog).toHaveCount(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("changing terminal toggle keybind works", async ({ page, gotoSession }) => {
|
||||||
|
await gotoSession()
|
||||||
|
|
||||||
|
const dialog = await openSettings(page)
|
||||||
|
await dialog.getByRole("tab", { name: "Shortcuts" }).click()
|
||||||
|
|
||||||
|
const keybindButton = dialog.locator(keybindButtonSelector("terminal.toggle"))
|
||||||
|
await expect(keybindButton).toBeVisible()
|
||||||
|
|
||||||
|
await keybindButton.click()
|
||||||
|
await expect(keybindButton).toHaveText(/press/i)
|
||||||
|
|
||||||
|
await page.keyboard.press(`${modKey}+KeyY`)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
const newKeybind = await keybindButton.textContent()
|
||||||
|
expect(newKeybind).toContain("Y")
|
||||||
|
|
||||||
|
const stored = await page.evaluate(() => {
|
||||||
|
const raw = localStorage.getItem("settings.v3")
|
||||||
|
return raw ? JSON.parse(raw) : null
|
||||||
|
})
|
||||||
|
expect(stored?.keybinds?.["terminal.toggle"]).toBe("mod+y")
|
||||||
|
|
||||||
|
await closeDialog(page, dialog)
|
||||||
|
|
||||||
|
const terminal = page.locator(terminalSelector)
|
||||||
|
await expect(terminal).not.toBeVisible()
|
||||||
|
|
||||||
|
await page.keyboard.press(`${modKey}+Y`)
|
||||||
|
await expect(terminal).toBeVisible()
|
||||||
|
|
||||||
|
await page.keyboard.press(`${modKey}+Y`)
|
||||||
|
await expect(terminal).not.toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("terminal toggle keybind persists after reload", async ({ page, gotoSession }) => {
|
||||||
|
await gotoSession()
|
||||||
|
|
||||||
|
const dialog = await openSettings(page)
|
||||||
|
await dialog.getByRole("tab", { name: "Shortcuts" }).click()
|
||||||
|
|
||||||
|
const keybindButton = dialog.locator(keybindButtonSelector("terminal.toggle"))
|
||||||
|
await expect(keybindButton).toBeVisible()
|
||||||
|
|
||||||
|
await keybindButton.click()
|
||||||
|
await expect(keybindButton).toHaveText(/press/i)
|
||||||
|
|
||||||
|
await page.keyboard.press(`${modKey}+Shift+KeyY`)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
await expect(keybindButton).toContainText("Y")
|
||||||
|
await closeDialog(page, dialog)
|
||||||
|
|
||||||
|
await page.reload()
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(async () => {
|
||||||
|
return await page.evaluate(() => {
|
||||||
|
const raw = localStorage.getItem("settings.v3")
|
||||||
|
if (!raw) return
|
||||||
|
const parsed = JSON.parse(raw)
|
||||||
|
return parsed?.keybinds?.["terminal.toggle"]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.toBe("mod+shift+y")
|
||||||
|
|
||||||
|
const reloaded = await openSettings(page)
|
||||||
|
await reloaded.getByRole("tab", { name: "Shortcuts" }).click()
|
||||||
|
const reloadedKeybind = reloaded.locator(keybindButtonSelector("terminal.toggle")).first()
|
||||||
|
await expect(reloadedKeybind).toContainText("Y")
|
||||||
|
await closeDialog(page, reloaded)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("changing command palette keybind works", async ({ page, gotoSession }) => {
|
||||||
|
await gotoSession()
|
||||||
|
|
||||||
|
const dialog = await openSettings(page)
|
||||||
|
await dialog.getByRole("tab", { name: "Shortcuts" }).click()
|
||||||
|
|
||||||
|
const keybindButton = dialog.locator(keybindButtonSelector("command.palette"))
|
||||||
|
await expect(keybindButton).toBeVisible()
|
||||||
|
|
||||||
|
const initialKeybind = await keybindButton.textContent()
|
||||||
|
expect(initialKeybind).toContain("P")
|
||||||
|
|
||||||
|
await keybindButton.click()
|
||||||
|
await expect(keybindButton).toHaveText(/press/i)
|
||||||
|
|
||||||
|
await page.keyboard.press(`${modKey}+Shift+KeyK`)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
const newKeybind = await keybindButton.textContent()
|
||||||
|
expect(newKeybind).toContain("K")
|
||||||
|
|
||||||
|
const stored = await page.evaluate(() => {
|
||||||
|
const raw = localStorage.getItem("settings.v3")
|
||||||
|
return raw ? JSON.parse(raw) : null
|
||||||
|
})
|
||||||
|
expect(stored?.keybinds?.["command.palette"]).toBe("mod+shift+k")
|
||||||
|
|
||||||
|
await closeDialog(page, dialog)
|
||||||
|
|
||||||
|
const palette = page.getByRole("dialog").filter({ has: page.getByRole("textbox").first() })
|
||||||
|
await expect(palette).toHaveCount(0)
|
||||||
|
|
||||||
|
await page.keyboard.press(`${modKey}+Shift+K`)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
await expect(palette).toBeVisible()
|
||||||
|
await expect(palette.getByRole("textbox").first()).toBeVisible()
|
||||||
|
|
||||||
|
await page.keyboard.press("Escape")
|
||||||
|
await expect(palette).toHaveCount(0)
|
||||||
|
})
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { test, expect } from "../fixtures"
|
|
||||||
import { settingsLanguageSelectSelector } from "../selectors"
|
|
||||||
import { openSettings } from "../actions"
|
|
||||||
|
|
||||||
test("smoke changing language updates settings labels", async ({ page, gotoSession }) => {
|
|
||||||
await page.addInitScript(() => {
|
|
||||||
localStorage.setItem("opencode.global.dat:language", JSON.stringify({ locale: "en" }))
|
|
||||||
})
|
|
||||||
|
|
||||||
await gotoSession()
|
|
||||||
|
|
||||||
const dialog = await openSettings(page)
|
|
||||||
|
|
||||||
const heading = dialog.getByRole("heading", { level: 2 })
|
|
||||||
await expect(heading).toHaveText("General")
|
|
||||||
|
|
||||||
const select = dialog.locator(settingsLanguageSelectSelector)
|
|
||||||
await expect(select).toBeVisible()
|
|
||||||
await select.locator('[data-slot="select-select-trigger"]').click()
|
|
||||||
|
|
||||||
await page.locator('[data-slot="select-select-item"]').filter({ hasText: "Deutsch" }).click()
|
|
||||||
|
|
||||||
await expect(heading).toHaveText("Allgemein")
|
|
||||||
|
|
||||||
await select.locator('[data-slot="select-select-trigger"]').click()
|
|
||||||
await page.locator('[data-slot="select-select-item"]').filter({ hasText: "English" }).click()
|
|
||||||
await expect(heading).toHaveText("General")
|
|
||||||
})
|
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import { test, expect } from "../fixtures"
|
||||||
|
import { promptSelector } from "../selectors"
|
||||||
|
import { closeDialog, openSettings } from "../actions"
|
||||||
|
|
||||||
|
test("hiding a model removes it from the model picker", async ({ page, gotoSession }) => {
|
||||||
|
await gotoSession()
|
||||||
|
|
||||||
|
await page.locator(promptSelector).click()
|
||||||
|
await page.keyboard.type("/model")
|
||||||
|
|
||||||
|
const command = page.locator('[data-slash-id="model.choose"]')
|
||||||
|
await expect(command).toBeVisible()
|
||||||
|
await command.hover()
|
||||||
|
await page.keyboard.press("Enter")
|
||||||
|
|
||||||
|
const picker = page.getByRole("dialog")
|
||||||
|
await expect(picker).toBeVisible()
|
||||||
|
|
||||||
|
const target = picker.locator('[data-slot="list-item"]').first()
|
||||||
|
await expect(target).toBeVisible()
|
||||||
|
|
||||||
|
const key = await target.getAttribute("data-key")
|
||||||
|
if (!key) throw new Error("Failed to resolve model key from list item")
|
||||||
|
|
||||||
|
const name = (await target.locator("span").first().innerText()).trim()
|
||||||
|
if (!name) throw new Error("Failed to resolve model name from list item")
|
||||||
|
|
||||||
|
await page.keyboard.press("Escape")
|
||||||
|
await expect(picker).toHaveCount(0)
|
||||||
|
|
||||||
|
const settings = await openSettings(page)
|
||||||
|
|
||||||
|
await settings.getByRole("tab", { name: "Models" }).click()
|
||||||
|
const search = settings.getByPlaceholder("Search models")
|
||||||
|
await expect(search).toBeVisible()
|
||||||
|
await search.fill(name)
|
||||||
|
|
||||||
|
const toggle = settings.locator('[data-component="switch"]').filter({ hasText: name }).first()
|
||||||
|
const input = toggle.locator('[data-slot="switch-input"]')
|
||||||
|
await expect(toggle).toBeVisible()
|
||||||
|
await expect(input).toHaveAttribute("aria-checked", "true")
|
||||||
|
await toggle.locator('[data-slot="switch-control"]').click()
|
||||||
|
await expect(input).toHaveAttribute("aria-checked", "false")
|
||||||
|
|
||||||
|
await closeDialog(page, settings)
|
||||||
|
|
||||||
|
await page.locator(promptSelector).click()
|
||||||
|
await page.keyboard.type("/model")
|
||||||
|
await expect(command).toBeVisible()
|
||||||
|
await command.hover()
|
||||||
|
await page.keyboard.press("Enter")
|
||||||
|
|
||||||
|
const pickerAgain = page.getByRole("dialog")
|
||||||
|
await expect(pickerAgain).toBeVisible()
|
||||||
|
await expect(pickerAgain.locator('[data-slot="list-item"]').first()).toBeVisible()
|
||||||
|
|
||||||
|
await expect(pickerAgain.locator(`[data-slot="list-item"][data-key="${key}"]`)).toHaveCount(0)
|
||||||
|
|
||||||
|
await page.keyboard.press("Escape")
|
||||||
|
await expect(pickerAgain).toHaveCount(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("showing a hidden model restores it to the model picker", async ({ page, gotoSession }) => {
|
||||||
|
await gotoSession()
|
||||||
|
|
||||||
|
await page.locator(promptSelector).click()
|
||||||
|
await page.keyboard.type("/model")
|
||||||
|
|
||||||
|
const command = page.locator('[data-slash-id="model.choose"]')
|
||||||
|
await expect(command).toBeVisible()
|
||||||
|
await command.hover()
|
||||||
|
await page.keyboard.press("Enter")
|
||||||
|
|
||||||
|
const picker = page.getByRole("dialog")
|
||||||
|
await expect(picker).toBeVisible()
|
||||||
|
|
||||||
|
const target = picker.locator('[data-slot="list-item"]').first()
|
||||||
|
await expect(target).toBeVisible()
|
||||||
|
|
||||||
|
const key = await target.getAttribute("data-key")
|
||||||
|
if (!key) throw new Error("Failed to resolve model key from list item")
|
||||||
|
|
||||||
|
const name = (await target.locator("span").first().innerText()).trim()
|
||||||
|
if (!name) throw new Error("Failed to resolve model name from list item")
|
||||||
|
|
||||||
|
await page.keyboard.press("Escape")
|
||||||
|
await expect(picker).toHaveCount(0)
|
||||||
|
|
||||||
|
const settings = await openSettings(page)
|
||||||
|
|
||||||
|
await settings.getByRole("tab", { name: "Models" }).click()
|
||||||
|
const search = settings.getByPlaceholder("Search models")
|
||||||
|
await expect(search).toBeVisible()
|
||||||
|
await search.fill(name)
|
||||||
|
|
||||||
|
const toggle = settings.locator('[data-component="switch"]').filter({ hasText: name }).first()
|
||||||
|
const input = toggle.locator('[data-slot="switch-input"]')
|
||||||
|
await expect(toggle).toBeVisible()
|
||||||
|
await expect(input).toHaveAttribute("aria-checked", "true")
|
||||||
|
|
||||||
|
await toggle.locator('[data-slot="switch-control"]').click()
|
||||||
|
await expect(input).toHaveAttribute("aria-checked", "false")
|
||||||
|
|
||||||
|
await toggle.locator('[data-slot="switch-control"]').click()
|
||||||
|
await expect(input).toHaveAttribute("aria-checked", "true")
|
||||||
|
|
||||||
|
await closeDialog(page, settings)
|
||||||
|
|
||||||
|
await page.locator(promptSelector).click()
|
||||||
|
await page.keyboard.type("/model")
|
||||||
|
await expect(command).toBeVisible()
|
||||||
|
await command.hover()
|
||||||
|
await page.keyboard.press("Enter")
|
||||||
|
|
||||||
|
const pickerAgain = page.getByRole("dialog")
|
||||||
|
await expect(pickerAgain).toBeVisible()
|
||||||
|
|
||||||
|
await expect(pickerAgain.locator(`[data-slot="list-item"][data-key="${key}"]`)).toBeVisible()
|
||||||
|
|
||||||
|
await page.keyboard.press("Escape")
|
||||||
|
await expect(pickerAgain).toHaveCount(0)
|
||||||
|
})
|
||||||
@@ -1,30 +1,136 @@
|
|||||||
import { test, expect } from "../fixtures"
|
import { test, expect } from "../fixtures"
|
||||||
import { promptSelector } from "../selectors"
|
import { closeDialog, openSettings } from "../actions"
|
||||||
import { closeDialog, openSettings, clickListItem } from "../actions"
|
|
||||||
|
|
||||||
test("smoke providers settings opens provider selector", async ({ page, gotoSession }) => {
|
test("custom provider form can be filled and validates input", async ({ page, gotoSession }) => {
|
||||||
await gotoSession()
|
await gotoSession()
|
||||||
|
|
||||||
const dialog = await openSettings(page)
|
const settings = await openSettings(page)
|
||||||
|
await settings.getByRole("tab", { name: "Providers" }).click()
|
||||||
|
|
||||||
await dialog.getByRole("tab", { name: "Providers" }).click()
|
const customProviderSection = settings.locator('[data-component="custom-provider-section"]')
|
||||||
await expect(dialog.getByText("Connected providers", { exact: true })).toBeVisible()
|
await expect(customProviderSection).toBeVisible()
|
||||||
await expect(dialog.getByText("Popular providers", { exact: true })).toBeVisible()
|
|
||||||
|
|
||||||
await dialog.getByRole("button", { name: "Show more providers" }).click()
|
const connectButton = customProviderSection.getByRole("button", { name: "Connect" })
|
||||||
|
await connectButton.click()
|
||||||
const providerDialog = page.getByRole("dialog").filter({ has: page.getByPlaceholder("Search providers") })
|
|
||||||
|
|
||||||
|
const providerDialog = page.getByRole("dialog").filter({ has: page.getByText("Custom provider") })
|
||||||
await expect(providerDialog).toBeVisible()
|
await expect(providerDialog).toBeVisible()
|
||||||
await expect(providerDialog.getByPlaceholder("Search providers")).toBeVisible()
|
|
||||||
await expect(providerDialog.locator('[data-slot="list-item"]').first()).toBeVisible()
|
await providerDialog.getByLabel("Provider ID").fill("test-provider")
|
||||||
|
await providerDialog.getByLabel("Display name").fill("Test Provider")
|
||||||
|
await providerDialog.getByLabel("Base URL").fill("http://localhost:9999/fake")
|
||||||
|
await providerDialog.getByLabel("API key").fill("fake-key")
|
||||||
|
|
||||||
|
await providerDialog.getByPlaceholder("model-id").first().fill("test-model")
|
||||||
|
await providerDialog.getByPlaceholder("Display Name").first().fill("Test Model")
|
||||||
|
|
||||||
|
await expect(providerDialog.getByRole("textbox", { name: "Provider ID" })).toHaveValue("test-provider")
|
||||||
|
await expect(providerDialog.getByRole("textbox", { name: "Display name" })).toHaveValue("Test Provider")
|
||||||
|
await expect(providerDialog.getByRole("textbox", { name: "Base URL" })).toHaveValue("http://localhost:9999/fake")
|
||||||
|
await expect(providerDialog.getByRole("textbox", { name: "API key" })).toHaveValue("fake-key")
|
||||||
|
await expect(providerDialog.getByPlaceholder("model-id").first()).toHaveValue("test-model")
|
||||||
|
await expect(providerDialog.getByPlaceholder("Display Name").first()).toHaveValue("Test Model")
|
||||||
|
|
||||||
await page.keyboard.press("Escape")
|
await page.keyboard.press("Escape")
|
||||||
await expect(providerDialog).toHaveCount(0)
|
await expect(providerDialog).toHaveCount(0)
|
||||||
await expect(page.locator(promptSelector)).toBeVisible()
|
|
||||||
|
|
||||||
const stillOpen = await dialog.isVisible().catch(() => false)
|
await closeDialog(page, settings)
|
||||||
if (!stillOpen) return
|
})
|
||||||
|
|
||||||
await closeDialog(page, dialog)
|
test("custom provider form shows validation errors", async ({ page, gotoSession }) => {
|
||||||
|
await gotoSession()
|
||||||
|
|
||||||
|
const settings = await openSettings(page)
|
||||||
|
await settings.getByRole("tab", { name: "Providers" }).click()
|
||||||
|
|
||||||
|
const customProviderSection = settings.locator('[data-component="custom-provider-section"]')
|
||||||
|
await customProviderSection.getByRole("button", { name: "Connect" }).click()
|
||||||
|
|
||||||
|
const providerDialog = page.getByRole("dialog").filter({ has: page.getByText("Custom provider") })
|
||||||
|
await expect(providerDialog).toBeVisible()
|
||||||
|
|
||||||
|
await providerDialog.getByLabel("Provider ID").fill("invalid provider id")
|
||||||
|
await providerDialog.getByLabel("Base URL").fill("not-a-url")
|
||||||
|
|
||||||
|
await providerDialog.getByRole("button", { name: /submit|save/i }).click()
|
||||||
|
|
||||||
|
await expect(providerDialog.locator('[data-slot="input-error"]').filter({ hasText: /lowercase/i })).toBeVisible()
|
||||||
|
await expect(providerDialog.locator('[data-slot="input-error"]').filter({ hasText: /http/i })).toBeVisible()
|
||||||
|
|
||||||
|
await page.keyboard.press("Escape")
|
||||||
|
await expect(providerDialog).toHaveCount(0)
|
||||||
|
|
||||||
|
await closeDialog(page, settings)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("custom provider form can add and remove models", async ({ page, gotoSession }) => {
|
||||||
|
await gotoSession()
|
||||||
|
|
||||||
|
const settings = await openSettings(page)
|
||||||
|
await settings.getByRole("tab", { name: "Providers" }).click()
|
||||||
|
|
||||||
|
const customProviderSection = settings.locator('[data-component="custom-provider-section"]')
|
||||||
|
await customProviderSection.getByRole("button", { name: "Connect" }).click()
|
||||||
|
|
||||||
|
const providerDialog = page.getByRole("dialog").filter({ has: page.getByText("Custom provider") })
|
||||||
|
await expect(providerDialog).toBeVisible()
|
||||||
|
|
||||||
|
await providerDialog.getByLabel("Provider ID").fill("multi-model-test")
|
||||||
|
await providerDialog.getByLabel("Display name").fill("Multi Model Test")
|
||||||
|
await providerDialog.getByLabel("Base URL").fill("http://localhost:9999/multi")
|
||||||
|
|
||||||
|
await providerDialog.getByPlaceholder("model-id").first().fill("model-1")
|
||||||
|
await providerDialog.getByPlaceholder("Display Name").first().fill("Model 1")
|
||||||
|
|
||||||
|
const idInputsBefore = await providerDialog.getByPlaceholder("model-id").count()
|
||||||
|
await providerDialog.getByRole("button", { name: "Add model" }).click()
|
||||||
|
const idInputsAfter = await providerDialog.getByPlaceholder("model-id").count()
|
||||||
|
expect(idInputsAfter).toBe(idInputsBefore + 1)
|
||||||
|
|
||||||
|
await providerDialog.getByPlaceholder("model-id").nth(1).fill("model-2")
|
||||||
|
await providerDialog.getByPlaceholder("Display Name").nth(1).fill("Model 2")
|
||||||
|
|
||||||
|
await expect(providerDialog.getByPlaceholder("model-id").nth(1)).toHaveValue("model-2")
|
||||||
|
await expect(providerDialog.getByPlaceholder("Display Name").nth(1)).toHaveValue("Model 2")
|
||||||
|
|
||||||
|
await page.keyboard.press("Escape")
|
||||||
|
await expect(providerDialog).toHaveCount(0)
|
||||||
|
|
||||||
|
await closeDialog(page, settings)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("custom provider form can add and remove headers", async ({ page, gotoSession }) => {
|
||||||
|
await gotoSession()
|
||||||
|
|
||||||
|
const settings = await openSettings(page)
|
||||||
|
await settings.getByRole("tab", { name: "Providers" }).click()
|
||||||
|
|
||||||
|
const customProviderSection = settings.locator('[data-component="custom-provider-section"]')
|
||||||
|
await customProviderSection.getByRole("button", { name: "Connect" }).click()
|
||||||
|
|
||||||
|
const providerDialog = page.getByRole("dialog").filter({ has: page.getByText("Custom provider") })
|
||||||
|
await expect(providerDialog).toBeVisible()
|
||||||
|
|
||||||
|
await providerDialog.getByLabel("Provider ID").fill("header-test")
|
||||||
|
await providerDialog.getByLabel("Display name").fill("Header Test")
|
||||||
|
await providerDialog.getByLabel("Base URL").fill("http://localhost:9999/headers")
|
||||||
|
|
||||||
|
await providerDialog.getByPlaceholder("model-id").first().fill("model-x")
|
||||||
|
await providerDialog.getByPlaceholder("Display Name").first().fill("Model X")
|
||||||
|
|
||||||
|
const headerInputsBefore = await providerDialog.getByPlaceholder("Header-Name").count()
|
||||||
|
await providerDialog.getByRole("button", { name: "Add header" }).click()
|
||||||
|
const headerInputsAfter = await providerDialog.getByPlaceholder("Header-Name").count()
|
||||||
|
expect(headerInputsAfter).toBe(headerInputsBefore + 1)
|
||||||
|
|
||||||
|
await providerDialog.getByPlaceholder("Header-Name").first().fill("Authorization")
|
||||||
|
await providerDialog.getByPlaceholder("value").first().fill("Bearer token123")
|
||||||
|
|
||||||
|
await expect(providerDialog.getByPlaceholder("Header-Name").first()).toHaveValue("Authorization")
|
||||||
|
await expect(providerDialog.getByPlaceholder("value").first()).toHaveValue("Bearer token123")
|
||||||
|
|
||||||
|
await page.keyboard.press("Escape")
|
||||||
|
await expect(providerDialog).toHaveCount(0)
|
||||||
|
|
||||||
|
await closeDialog(page, settings)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
import { test, expect } from "../fixtures"
|
import { test, expect, settingsKey } from "../fixtures"
|
||||||
import { closeDialog, openSettings } from "../actions"
|
import { closeDialog, openSettings } from "../actions"
|
||||||
|
import {
|
||||||
|
settingsColorSchemeSelector,
|
||||||
|
settingsFontSelector,
|
||||||
|
settingsLanguageSelectSelector,
|
||||||
|
settingsNotificationsAgentSelector,
|
||||||
|
settingsNotificationsErrorsSelector,
|
||||||
|
settingsNotificationsPermissionsSelector,
|
||||||
|
settingsReleaseNotesSelector,
|
||||||
|
settingsSoundsAgentSelector,
|
||||||
|
settingsSoundsErrorsSelector,
|
||||||
|
settingsSoundsPermissionsSelector,
|
||||||
|
settingsThemeSelector,
|
||||||
|
settingsUpdatesStartupSelector,
|
||||||
|
} from "../selectors"
|
||||||
|
|
||||||
test("smoke settings dialog opens, switches tabs, closes", async ({ page, gotoSession }) => {
|
test("smoke settings dialog opens, switches tabs, closes", async ({ page, gotoSession }) => {
|
||||||
await gotoSession()
|
await gotoSession()
|
||||||
@@ -12,3 +26,429 @@ test("smoke settings dialog opens, switches tabs, closes", async ({ page, gotoSe
|
|||||||
|
|
||||||
await closeDialog(page, dialog)
|
await closeDialog(page, dialog)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("changing language updates settings labels", async ({ page, gotoSession }) => {
|
||||||
|
await page.addInitScript(() => {
|
||||||
|
localStorage.setItem("opencode.global.dat:language", JSON.stringify({ locale: "en" }))
|
||||||
|
})
|
||||||
|
|
||||||
|
await gotoSession()
|
||||||
|
|
||||||
|
const dialog = await openSettings(page)
|
||||||
|
|
||||||
|
const heading = dialog.getByRole("heading", { level: 2 })
|
||||||
|
await expect(heading).toHaveText("General")
|
||||||
|
|
||||||
|
const select = dialog.locator(settingsLanguageSelectSelector)
|
||||||
|
await expect(select).toBeVisible()
|
||||||
|
await select.locator('[data-slot="select-select-trigger"]').click()
|
||||||
|
|
||||||
|
await page.locator('[data-slot="select-select-item"]').filter({ hasText: "Deutsch" }).click()
|
||||||
|
|
||||||
|
await expect(heading).toHaveText("Allgemein")
|
||||||
|
|
||||||
|
await select.locator('[data-slot="select-select-trigger"]').click()
|
||||||
|
await page.locator('[data-slot="select-select-item"]').filter({ hasText: "English" }).click()
|
||||||
|
await expect(heading).toHaveText("General")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("changing color scheme persists in localStorage", async ({ page, gotoSession }) => {
|
||||||
|
await gotoSession()
|
||||||
|
|
||||||
|
const dialog = await openSettings(page)
|
||||||
|
const select = dialog.locator(settingsColorSchemeSelector)
|
||||||
|
await expect(select).toBeVisible()
|
||||||
|
|
||||||
|
await select.locator('[data-slot="select-select-trigger"]').click()
|
||||||
|
await page.locator('[data-slot="select-select-item"]').filter({ hasText: "Dark" }).click()
|
||||||
|
|
||||||
|
const colorScheme = await page.evaluate(() => {
|
||||||
|
return document.documentElement.getAttribute("data-color-scheme")
|
||||||
|
})
|
||||||
|
expect(colorScheme).toBe("dark")
|
||||||
|
|
||||||
|
await select.locator('[data-slot="select-select-trigger"]').click()
|
||||||
|
await page.locator('[data-slot="select-select-item"]').filter({ hasText: "Light" }).click()
|
||||||
|
|
||||||
|
const lightColorScheme = await page.evaluate(() => {
|
||||||
|
return document.documentElement.getAttribute("data-color-scheme")
|
||||||
|
})
|
||||||
|
expect(lightColorScheme).toBe("light")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("changing theme persists in localStorage", async ({ page, gotoSession }) => {
|
||||||
|
await gotoSession()
|
||||||
|
|
||||||
|
const dialog = await openSettings(page)
|
||||||
|
const select = dialog.locator(settingsThemeSelector)
|
||||||
|
await expect(select).toBeVisible()
|
||||||
|
|
||||||
|
await select.locator('[data-slot="select-select-trigger"]').click()
|
||||||
|
|
||||||
|
const items = page.locator('[data-slot="select-select-item"]')
|
||||||
|
const count = await items.count()
|
||||||
|
expect(count).toBeGreaterThan(1)
|
||||||
|
|
||||||
|
const firstTheme = await items.nth(1).locator('[data-slot="select-select-item-label"]').textContent()
|
||||||
|
expect(firstTheme).toBeTruthy()
|
||||||
|
|
||||||
|
await items.nth(1).click()
|
||||||
|
|
||||||
|
await page.keyboard.press("Escape")
|
||||||
|
|
||||||
|
const storedThemeId = await page.evaluate(() => {
|
||||||
|
return localStorage.getItem("opencode-theme-id")
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(storedThemeId).not.toBeNull()
|
||||||
|
expect(storedThemeId).not.toBe("oc-1")
|
||||||
|
|
||||||
|
const dataTheme = await page.evaluate(() => {
|
||||||
|
return document.documentElement.getAttribute("data-theme")
|
||||||
|
})
|
||||||
|
expect(dataTheme).toBe(storedThemeId)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("changing font persists in localStorage and updates CSS variable", async ({ page, gotoSession }) => {
|
||||||
|
await gotoSession()
|
||||||
|
|
||||||
|
const dialog = await openSettings(page)
|
||||||
|
const select = dialog.locator(settingsFontSelector)
|
||||||
|
await expect(select).toBeVisible()
|
||||||
|
|
||||||
|
const initialFontFamily = await page.evaluate(() => {
|
||||||
|
return getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono")
|
||||||
|
})
|
||||||
|
expect(initialFontFamily).toContain("IBM Plex Mono")
|
||||||
|
|
||||||
|
await select.locator('[data-slot="select-select-trigger"]').click()
|
||||||
|
|
||||||
|
const items = page.locator('[data-slot="select-select-item"]')
|
||||||
|
await items.nth(2).click()
|
||||||
|
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
const stored = await page.evaluate((key) => {
|
||||||
|
const raw = localStorage.getItem(key)
|
||||||
|
return raw ? JSON.parse(raw) : null
|
||||||
|
}, settingsKey)
|
||||||
|
|
||||||
|
expect(stored?.appearance?.font).not.toBe("ibm-plex-mono")
|
||||||
|
|
||||||
|
const newFontFamily = await page.evaluate(() => {
|
||||||
|
return getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono")
|
||||||
|
})
|
||||||
|
expect(newFontFamily).not.toBe(initialFontFamily)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("color scheme and font rehydrate after reload", async ({ page, gotoSession }) => {
|
||||||
|
await gotoSession()
|
||||||
|
|
||||||
|
const dialog = await openSettings(page)
|
||||||
|
|
||||||
|
const colorSchemeSelect = dialog.locator(settingsColorSchemeSelector)
|
||||||
|
await expect(colorSchemeSelect).toBeVisible()
|
||||||
|
await colorSchemeSelect.locator('[data-slot="select-select-trigger"]').click()
|
||||||
|
await page.locator('[data-slot="select-select-item"]').filter({ hasText: "Dark" }).click()
|
||||||
|
await expect(page.locator("html")).toHaveAttribute("data-color-scheme", "dark")
|
||||||
|
|
||||||
|
const fontSelect = dialog.locator(settingsFontSelector)
|
||||||
|
await expect(fontSelect).toBeVisible()
|
||||||
|
|
||||||
|
const initialFontFamily = await page.evaluate(() => {
|
||||||
|
return getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim()
|
||||||
|
})
|
||||||
|
|
||||||
|
const initialSettings = await page.evaluate((key) => {
|
||||||
|
const raw = localStorage.getItem(key)
|
||||||
|
return raw ? JSON.parse(raw) : null
|
||||||
|
}, settingsKey)
|
||||||
|
|
||||||
|
const currentFont =
|
||||||
|
(await fontSelect.locator('[data-slot="select-select-trigger-value"]').textContent())?.trim() ?? ""
|
||||||
|
await fontSelect.locator('[data-slot="select-select-trigger"]').click()
|
||||||
|
|
||||||
|
const fontItems = page.locator('[data-slot="select-select-item"]')
|
||||||
|
expect(await fontItems.count()).toBeGreaterThan(1)
|
||||||
|
|
||||||
|
if (currentFont) {
|
||||||
|
await fontItems.filter({ hasNotText: currentFont }).first().click()
|
||||||
|
}
|
||||||
|
if (!currentFont) {
|
||||||
|
await fontItems.nth(1).click()
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(async () => {
|
||||||
|
return await page.evaluate((key) => {
|
||||||
|
const raw = localStorage.getItem(key)
|
||||||
|
return raw ? JSON.parse(raw) : null
|
||||||
|
}, settingsKey)
|
||||||
|
})
|
||||||
|
.toMatchObject({
|
||||||
|
appearance: {
|
||||||
|
font: expect.any(String),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const updatedSettings = await page.evaluate((key) => {
|
||||||
|
const raw = localStorage.getItem(key)
|
||||||
|
return raw ? JSON.parse(raw) : null
|
||||||
|
}, settingsKey)
|
||||||
|
|
||||||
|
const updatedFontFamily = await page.evaluate(() => {
|
||||||
|
return getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim()
|
||||||
|
})
|
||||||
|
expect(updatedFontFamily).not.toBe(initialFontFamily)
|
||||||
|
expect(updatedSettings?.appearance?.font).not.toBe(initialSettings?.appearance?.font)
|
||||||
|
|
||||||
|
await closeDialog(page, dialog)
|
||||||
|
await page.reload()
|
||||||
|
|
||||||
|
await expect(page.locator("html")).toHaveAttribute("data-color-scheme", "dark")
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(async () => {
|
||||||
|
return await page.evaluate((key) => {
|
||||||
|
const raw = localStorage.getItem(key)
|
||||||
|
return raw ? JSON.parse(raw) : null
|
||||||
|
}, settingsKey)
|
||||||
|
})
|
||||||
|
.toMatchObject({
|
||||||
|
appearance: {
|
||||||
|
font: updatedSettings?.appearance?.font,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const rehydratedSettings = await page.evaluate((key) => {
|
||||||
|
const raw = localStorage.getItem(key)
|
||||||
|
return raw ? JSON.parse(raw) : null
|
||||||
|
}, settingsKey)
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(async () => {
|
||||||
|
return await page.evaluate(() => {
|
||||||
|
return getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.not.toBe(initialFontFamily)
|
||||||
|
|
||||||
|
const rehydratedFontFamily = await page.evaluate(() => {
|
||||||
|
return getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim()
|
||||||
|
})
|
||||||
|
expect(rehydratedFontFamily).not.toBe(initialFontFamily)
|
||||||
|
expect(rehydratedSettings?.appearance?.font).toBe(updatedSettings?.appearance?.font)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("toggling notification agent switch updates localStorage", async ({ page, gotoSession }) => {
|
||||||
|
await gotoSession()
|
||||||
|
|
||||||
|
const dialog = await openSettings(page)
|
||||||
|
const switchContainer = dialog.locator(settingsNotificationsAgentSelector)
|
||||||
|
await expect(switchContainer).toBeVisible()
|
||||||
|
|
||||||
|
const toggleInput = switchContainer.locator('[data-slot="switch-input"]')
|
||||||
|
const initialState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
|
||||||
|
expect(initialState).toBe(true)
|
||||||
|
|
||||||
|
await switchContainer.locator('[data-slot="switch-control"]').click()
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
const newState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
|
||||||
|
expect(newState).toBe(false)
|
||||||
|
|
||||||
|
const stored = await page.evaluate((key) => {
|
||||||
|
const raw = localStorage.getItem(key)
|
||||||
|
return raw ? JSON.parse(raw) : null
|
||||||
|
}, settingsKey)
|
||||||
|
|
||||||
|
expect(stored?.notifications?.agent).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("toggling notification permissions switch updates localStorage", async ({ page, gotoSession }) => {
|
||||||
|
await gotoSession()
|
||||||
|
|
||||||
|
const dialog = await openSettings(page)
|
||||||
|
const switchContainer = dialog.locator(settingsNotificationsPermissionsSelector)
|
||||||
|
await expect(switchContainer).toBeVisible()
|
||||||
|
|
||||||
|
const toggleInput = switchContainer.locator('[data-slot="switch-input"]')
|
||||||
|
const initialState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
|
||||||
|
expect(initialState).toBe(true)
|
||||||
|
|
||||||
|
await switchContainer.locator('[data-slot="switch-control"]').click()
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
const newState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
|
||||||
|
expect(newState).toBe(false)
|
||||||
|
|
||||||
|
const stored = await page.evaluate((key) => {
|
||||||
|
const raw = localStorage.getItem(key)
|
||||||
|
return raw ? JSON.parse(raw) : null
|
||||||
|
}, settingsKey)
|
||||||
|
|
||||||
|
expect(stored?.notifications?.permissions).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("toggling notification errors switch updates localStorage", async ({ page, gotoSession }) => {
|
||||||
|
await gotoSession()
|
||||||
|
|
||||||
|
const dialog = await openSettings(page)
|
||||||
|
const switchContainer = dialog.locator(settingsNotificationsErrorsSelector)
|
||||||
|
await expect(switchContainer).toBeVisible()
|
||||||
|
|
||||||
|
const toggleInput = switchContainer.locator('[data-slot="switch-input"]')
|
||||||
|
const initialState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
|
||||||
|
expect(initialState).toBe(false)
|
||||||
|
|
||||||
|
await switchContainer.locator('[data-slot="switch-control"]').click()
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
const newState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
|
||||||
|
expect(newState).toBe(true)
|
||||||
|
|
||||||
|
const stored = await page.evaluate((key) => {
|
||||||
|
const raw = localStorage.getItem(key)
|
||||||
|
return raw ? JSON.parse(raw) : null
|
||||||
|
}, settingsKey)
|
||||||
|
|
||||||
|
expect(stored?.notifications?.errors).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("changing sound agent selection persists in localStorage", async ({ page, gotoSession }) => {
|
||||||
|
await gotoSession()
|
||||||
|
|
||||||
|
const dialog = await openSettings(page)
|
||||||
|
const select = dialog.locator(settingsSoundsAgentSelector)
|
||||||
|
await expect(select).toBeVisible()
|
||||||
|
|
||||||
|
await select.locator('[data-slot="select-select-trigger"]').click()
|
||||||
|
|
||||||
|
const items = page.locator('[data-slot="select-select-item"]')
|
||||||
|
await items.nth(2).click()
|
||||||
|
|
||||||
|
const stored = await page.evaluate((key) => {
|
||||||
|
const raw = localStorage.getItem(key)
|
||||||
|
return raw ? JSON.parse(raw) : null
|
||||||
|
}, settingsKey)
|
||||||
|
|
||||||
|
expect(stored?.sounds?.agent).not.toBe("staplebops-01")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("changing permissions and errors sounds updates localStorage", async ({ page, gotoSession }) => {
|
||||||
|
await gotoSession()
|
||||||
|
|
||||||
|
const dialog = await openSettings(page)
|
||||||
|
const permissionsSelect = dialog.locator(settingsSoundsPermissionsSelector)
|
||||||
|
const errorsSelect = dialog.locator(settingsSoundsErrorsSelector)
|
||||||
|
await expect(permissionsSelect).toBeVisible()
|
||||||
|
await expect(errorsSelect).toBeVisible()
|
||||||
|
|
||||||
|
const initial = await page.evaluate((key) => {
|
||||||
|
const raw = localStorage.getItem(key)
|
||||||
|
return raw ? JSON.parse(raw) : null
|
||||||
|
}, settingsKey)
|
||||||
|
|
||||||
|
const permissionsCurrent =
|
||||||
|
(await permissionsSelect.locator('[data-slot="select-select-trigger-value"]').textContent())?.trim() ?? ""
|
||||||
|
await permissionsSelect.locator('[data-slot="select-select-trigger"]').click()
|
||||||
|
const permissionItems = page.locator('[data-slot="select-select-item"]')
|
||||||
|
expect(await permissionItems.count()).toBeGreaterThan(1)
|
||||||
|
if (permissionsCurrent) {
|
||||||
|
await permissionItems.filter({ hasNotText: permissionsCurrent }).first().click()
|
||||||
|
}
|
||||||
|
if (!permissionsCurrent) {
|
||||||
|
await permissionItems.nth(1).click()
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorsCurrent =
|
||||||
|
(await errorsSelect.locator('[data-slot="select-select-trigger-value"]').textContent())?.trim() ?? ""
|
||||||
|
await errorsSelect.locator('[data-slot="select-select-trigger"]').click()
|
||||||
|
const errorItems = page.locator('[data-slot="select-select-item"]')
|
||||||
|
expect(await errorItems.count()).toBeGreaterThan(1)
|
||||||
|
if (errorsCurrent) {
|
||||||
|
await errorItems.filter({ hasNotText: errorsCurrent }).first().click()
|
||||||
|
}
|
||||||
|
if (!errorsCurrent) {
|
||||||
|
await errorItems.nth(1).click()
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(async () => {
|
||||||
|
return await page.evaluate((key) => {
|
||||||
|
const raw = localStorage.getItem(key)
|
||||||
|
return raw ? JSON.parse(raw) : null
|
||||||
|
}, settingsKey)
|
||||||
|
})
|
||||||
|
.toMatchObject({
|
||||||
|
sounds: {
|
||||||
|
permissions: expect.any(String),
|
||||||
|
errors: expect.any(String),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const stored = await page.evaluate((key) => {
|
||||||
|
const raw = localStorage.getItem(key)
|
||||||
|
return raw ? JSON.parse(raw) : null
|
||||||
|
}, settingsKey)
|
||||||
|
|
||||||
|
expect(stored?.sounds?.permissions).not.toBe(initial?.sounds?.permissions)
|
||||||
|
expect(stored?.sounds?.errors).not.toBe(initial?.sounds?.errors)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("toggling updates startup switch updates localStorage", async ({ page, gotoSession }) => {
|
||||||
|
await gotoSession()
|
||||||
|
|
||||||
|
const dialog = await openSettings(page)
|
||||||
|
const switchContainer = dialog.locator(settingsUpdatesStartupSelector)
|
||||||
|
await expect(switchContainer).toBeVisible()
|
||||||
|
|
||||||
|
const toggleInput = switchContainer.locator('[data-slot="switch-input"]')
|
||||||
|
|
||||||
|
const isDisabled = await toggleInput.evaluate((el: HTMLInputElement) => el.disabled)
|
||||||
|
if (isDisabled) {
|
||||||
|
test.skip()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
|
||||||
|
expect(initialState).toBe(true)
|
||||||
|
|
||||||
|
await switchContainer.locator('[data-slot="switch-control"]').click()
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
const newState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
|
||||||
|
expect(newState).toBe(false)
|
||||||
|
|
||||||
|
const stored = await page.evaluate((key) => {
|
||||||
|
const raw = localStorage.getItem(key)
|
||||||
|
return raw ? JSON.parse(raw) : null
|
||||||
|
}, settingsKey)
|
||||||
|
|
||||||
|
expect(stored?.updates?.startup).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("toggling release notes switch updates localStorage", async ({ page, gotoSession }) => {
|
||||||
|
await gotoSession()
|
||||||
|
|
||||||
|
const dialog = await openSettings(page)
|
||||||
|
const switchContainer = dialog.locator(settingsReleaseNotesSelector)
|
||||||
|
await expect(switchContainer).toBeVisible()
|
||||||
|
|
||||||
|
const toggleInput = switchContainer.locator('[data-slot="switch-input"]')
|
||||||
|
const initialState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
|
||||||
|
expect(initialState).toBe(true)
|
||||||
|
|
||||||
|
await switchContainer.locator('[data-slot="switch-control"]').click()
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
const newState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
|
||||||
|
expect(newState).toBe(false)
|
||||||
|
|
||||||
|
const stored = await page.evaluate((key) => {
|
||||||
|
const raw = localStorage.getItem(key)
|
||||||
|
return raw ? JSON.parse(raw) : null
|
||||||
|
}, settingsKey)
|
||||||
|
|
||||||
|
expect(stored?.general?.releaseNotes).toBe(false)
|
||||||
|
})
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { test, expect } from "../fixtures"
|
||||||
|
import { closeSidebar, hoverSessionItem } from "../actions"
|
||||||
|
import { projectSwitchSelector, sessionItemSelector } from "../selectors"
|
||||||
|
|
||||||
|
test("collapsed sidebar popover stays open when archiving a session", async ({ page, slug, sdk, gotoSession }) => {
|
||||||
|
const stamp = Date.now()
|
||||||
|
|
||||||
|
const one = await sdk.session.create({ title: `e2e sidebar popover archive 1 ${stamp}` }).then((r) => r.data)
|
||||||
|
const two = await sdk.session.create({ title: `e2e sidebar popover archive 2 ${stamp}` }).then((r) => r.data)
|
||||||
|
|
||||||
|
if (!one?.id) throw new Error("Session create did not return an id")
|
||||||
|
if (!two?.id) throw new Error("Session create did not return an id")
|
||||||
|
|
||||||
|
try {
|
||||||
|
await gotoSession(one.id)
|
||||||
|
await closeSidebar(page)
|
||||||
|
|
||||||
|
const project = page.locator(projectSwitchSelector(slug)).first()
|
||||||
|
await expect(project).toBeVisible()
|
||||||
|
await project.hover()
|
||||||
|
|
||||||
|
await expect(page.locator(sessionItemSelector(one.id)).first()).toBeVisible()
|
||||||
|
await expect(page.locator(sessionItemSelector(two.id)).first()).toBeVisible()
|
||||||
|
|
||||||
|
const item = await hoverSessionItem(page, one.id)
|
||||||
|
await item
|
||||||
|
.getByRole("button", { name: /archive/i })
|
||||||
|
.first()
|
||||||
|
.click()
|
||||||
|
|
||||||
|
await expect(page.locator(sessionItemSelector(two.id)).first()).toBeVisible()
|
||||||
|
} finally {
|
||||||
|
await sdk.session.delete({ sessionID: one.id }).catch(() => undefined)
|
||||||
|
await sdk.session.delete({ sessionID: two.id }).catch(() => undefined)
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { test, expect } from "../fixtures"
|
import { test, expect } from "../fixtures"
|
||||||
import { openSidebar, toggleSidebar } from "../actions"
|
import { openSidebar, toggleSidebar, withSession } from "../actions"
|
||||||
|
|
||||||
test("sidebar can be collapsed and expanded", async ({ page, gotoSession }) => {
|
test("sidebar can be collapsed and expanded", async ({ page, gotoSession }) => {
|
||||||
await gotoSession()
|
await gotoSession()
|
||||||
@@ -12,3 +12,26 @@ test("sidebar can be collapsed and expanded", async ({ page, gotoSession }) => {
|
|||||||
await toggleSidebar(page)
|
await toggleSidebar(page)
|
||||||
await expect(page.locator("main")).not.toHaveClass(/xl:border-l/)
|
await expect(page.locator("main")).not.toHaveClass(/xl:border-l/)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("sidebar collapsed state persists across navigation and reload", async ({ page, sdk, gotoSession }) => {
|
||||||
|
await withSession(sdk, "sidebar persist session 1", async (session1) => {
|
||||||
|
await withSession(sdk, "sidebar persist session 2", async (session2) => {
|
||||||
|
await gotoSession(session1.id)
|
||||||
|
|
||||||
|
await openSidebar(page)
|
||||||
|
await toggleSidebar(page)
|
||||||
|
await expect(page.locator("main")).toHaveClass(/xl:border-l/)
|
||||||
|
|
||||||
|
await gotoSession(session2.id)
|
||||||
|
await expect(page.locator("main")).toHaveClass(/xl:border-l/)
|
||||||
|
|
||||||
|
await page.reload()
|
||||||
|
await expect(page.locator("main")).toHaveClass(/xl:border-l/)
|
||||||
|
|
||||||
|
const opened = await page.evaluate(
|
||||||
|
() => JSON.parse(localStorage.getItem("opencode.global.dat:layout") ?? "{}").sidebar?.opened,
|
||||||
|
)
|
||||||
|
await expect(opened).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { test, expect } from "../fixtures"
|
||||||
|
import { openStatusPopover } from "../actions"
|
||||||
|
|
||||||
|
test("status popover opens and shows tabs", async ({ page, gotoSession }) => {
|
||||||
|
await gotoSession()
|
||||||
|
|
||||||
|
const { popoverBody } = await openStatusPopover(page)
|
||||||
|
|
||||||
|
await expect(popoverBody.getByRole("tab", { name: /servers/i })).toBeVisible()
|
||||||
|
await expect(popoverBody.getByRole("tab", { name: /mcp/i })).toBeVisible()
|
||||||
|
await expect(popoverBody.getByRole("tab", { name: /lsp/i })).toBeVisible()
|
||||||
|
await expect(popoverBody.getByRole("tab", { name: /plugins/i })).toBeVisible()
|
||||||
|
|
||||||
|
await page.keyboard.press("Escape")
|
||||||
|
await expect(popoverBody).toHaveCount(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("status popover servers tab shows current server", async ({ page, gotoSession }) => {
|
||||||
|
await gotoSession()
|
||||||
|
|
||||||
|
const { popoverBody } = await openStatusPopover(page)
|
||||||
|
|
||||||
|
const serversTab = popoverBody.getByRole("tab", { name: /servers/i })
|
||||||
|
await expect(serversTab).toHaveAttribute("aria-selected", "true")
|
||||||
|
|
||||||
|
const serverList = popoverBody.locator('[role="tabpanel"]').first()
|
||||||
|
await expect(serverList.locator("button").first()).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("status popover can switch to mcp tab", async ({ page, gotoSession }) => {
|
||||||
|
await gotoSession()
|
||||||
|
|
||||||
|
const { popoverBody } = await openStatusPopover(page)
|
||||||
|
|
||||||
|
const mcpTab = popoverBody.getByRole("tab", { name: /mcp/i })
|
||||||
|
await mcpTab.click()
|
||||||
|
|
||||||
|
const ariaSelected = await mcpTab.getAttribute("aria-selected")
|
||||||
|
expect(ariaSelected).toBe("true")
|
||||||
|
|
||||||
|
const mcpContent = popoverBody.locator('[role="tabpanel"]:visible').first()
|
||||||
|
await expect(mcpContent).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("status popover can switch to lsp tab", async ({ page, gotoSession }) => {
|
||||||
|
await gotoSession()
|
||||||
|
|
||||||
|
const { popoverBody } = await openStatusPopover(page)
|
||||||
|
|
||||||
|
const lspTab = popoverBody.getByRole("tab", { name: /lsp/i })
|
||||||
|
await lspTab.click()
|
||||||
|
|
||||||
|
const ariaSelected = await lspTab.getAttribute("aria-selected")
|
||||||
|
expect(ariaSelected).toBe("true")
|
||||||
|
|
||||||
|
const lspContent = popoverBody.locator('[role="tabpanel"]:visible').first()
|
||||||
|
await expect(lspContent).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("status popover can switch to plugins tab", async ({ page, gotoSession }) => {
|
||||||
|
await gotoSession()
|
||||||
|
|
||||||
|
const { popoverBody } = await openStatusPopover(page)
|
||||||
|
|
||||||
|
const pluginsTab = popoverBody.getByRole("tab", { name: /plugins/i })
|
||||||
|
await pluginsTab.click()
|
||||||
|
|
||||||
|
const ariaSelected = await pluginsTab.getAttribute("aria-selected")
|
||||||
|
expect(ariaSelected).toBe("true")
|
||||||
|
|
||||||
|
const pluginsContent = popoverBody.locator('[role="tabpanel"]:visible').first()
|
||||||
|
await expect(pluginsContent).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("status popover closes on escape", async ({ page, gotoSession }) => {
|
||||||
|
await gotoSession()
|
||||||
|
|
||||||
|
const { popoverBody } = await openStatusPopover(page)
|
||||||
|
await expect(popoverBody).toBeVisible()
|
||||||
|
|
||||||
|
await page.keyboard.press("Escape")
|
||||||
|
await expect(popoverBody).toHaveCount(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("status popover closes when clicking outside", async ({ page, gotoSession }) => {
|
||||||
|
await gotoSession()
|
||||||
|
|
||||||
|
const { popoverBody } = await openStatusPopover(page)
|
||||||
|
await expect(popoverBody).toBeVisible()
|
||||||
|
|
||||||
|
await page.getByRole("main").click({ position: { x: 5, y: 5 } })
|
||||||
|
|
||||||
|
await expect(popoverBody).toHaveCount(0)
|
||||||
|
})
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.1.48",
|
"version": "1.1.60",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts",
|
".": "./src/index.ts",
|
||||||
"./vite": "./vite.js"
|
"./vite": "./vite.js",
|
||||||
|
"./index.css": "./src/index.css"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "tsgo -b",
|
"typecheck": "tsgo -b",
|
||||||
@@ -13,7 +14,9 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"serve": "vite preview",
|
"serve": "vite preview",
|
||||||
"test": "playwright test",
|
"test": "bun run test:unit",
|
||||||
|
"test:unit": "bun test --preload ./happydom.ts ./src",
|
||||||
|
"test:unit:watch": "bun test --watch --preload ./happydom.ts ./src",
|
||||||
"test:e2e": "playwright test",
|
"test:e2e": "playwright test",
|
||||||
"test:e2e:local": "bun script/e2e-local.ts",
|
"test:e2e:local": "bun script/e2e-local.ts",
|
||||||
"test:e2e:ui": "playwright test --ui",
|
"test:e2e:ui": "playwright test --ui",
|
||||||
@@ -54,7 +57,7 @@
|
|||||||
"@thisbeyond/solid-dnd": "0.7.5",
|
"@thisbeyond/solid-dnd": "0.7.5",
|
||||||
"diff": "catalog:",
|
"diff": "catalog:",
|
||||||
"fuzzysort": "catalog:",
|
"fuzzysort": "catalog:",
|
||||||
"ghostty-web": "0.3.0",
|
"ghostty-web": "0.4.0",
|
||||||
"luxon": "catalog:",
|
"luxon": "catalog:",
|
||||||
"marked": "catalog:",
|
"marked": "catalog:",
|
||||||
"marked-shiki": "catalog:",
|
"marked-shiki": "catalog:",
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export default defineConfig({
|
|||||||
expect: {
|
expect: {
|
||||||
timeout: 10_000,
|
timeout: 10_000,
|
||||||
},
|
},
|
||||||
fullyParallel: true,
|
fullyParallel: process.env.PLAYWRIGHT_FULLY_PARALLEL === "1",
|
||||||
forbidOnly: !!process.env.CI,
|
forbidOnly: !!process.env.CI,
|
||||||
retries: process.env.CI ? 2 : 0,
|
retries: process.env.CI ? 2 : 0,
|
||||||
reporter: [["html", { outputFolder: "e2e/playwright-report", open: "never" }], ["line"]],
|
reporter: [["html", { outputFolder: "e2e/playwright-report", open: "never" }], ["line"]],
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ const extraArgs = (() => {
|
|||||||
const [serverPort, webPort] = await Promise.all([freePort(), freePort()])
|
const [serverPort, webPort] = await Promise.all([freePort(), freePort()])
|
||||||
|
|
||||||
const sandbox = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-e2e-"))
|
const sandbox = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-e2e-"))
|
||||||
|
const keepSandbox = process.env.OPENCODE_E2E_KEEP_SANDBOX === "1"
|
||||||
|
|
||||||
const serverEnv = {
|
const serverEnv = {
|
||||||
...process.env,
|
...process.env,
|
||||||
@@ -83,58 +84,95 @@ const runnerEnv = {
|
|||||||
PLAYWRIGHT_PORT: String(webPort),
|
PLAYWRIGHT_PORT: String(webPort),
|
||||||
} satisfies Record<string, string>
|
} satisfies Record<string, string>
|
||||||
|
|
||||||
const seed = Bun.spawn(["bun", "script/seed-e2e.ts"], {
|
let seed: ReturnType<typeof Bun.spawn> | undefined
|
||||||
cwd: opencodeDir,
|
let runner: ReturnType<typeof Bun.spawn> | undefined
|
||||||
env: serverEnv,
|
let server: { stop: () => Promise<void> | void } | undefined
|
||||||
stdout: "inherit",
|
let inst: { Instance: { disposeAll: () => Promise<void> | void } } | undefined
|
||||||
stderr: "inherit",
|
let cleaned = false
|
||||||
})
|
|
||||||
|
|
||||||
const seedExit = await seed.exited
|
const cleanup = async () => {
|
||||||
if (seedExit !== 0) {
|
if (cleaned) return
|
||||||
process.exit(seedExit)
|
cleaned = true
|
||||||
|
|
||||||
|
if (seed && seed.exitCode === null) seed.kill("SIGTERM")
|
||||||
|
if (runner && runner.exitCode === null) runner.kill("SIGTERM")
|
||||||
|
|
||||||
|
const jobs = [
|
||||||
|
inst?.Instance.disposeAll(),
|
||||||
|
server?.stop(),
|
||||||
|
keepSandbox ? undefined : fs.rm(sandbox, { recursive: true, force: true }),
|
||||||
|
].filter(Boolean)
|
||||||
|
await Promise.allSettled(jobs)
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.assign(process.env, serverEnv)
|
const shutdown = (code: number, reason: string) => {
|
||||||
process.env.AGENT = "1"
|
process.exitCode = code
|
||||||
process.env.OPENCODE = "1"
|
void cleanup().finally(() => {
|
||||||
|
console.error(`e2e-local shutdown: ${reason}`)
|
||||||
|
process.exit(code)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const log = await import("../../opencode/src/util/log")
|
const reportInternalError = (reason: string, error: unknown) => {
|
||||||
const install = await import("../../opencode/src/installation")
|
console.warn(`e2e-local ignored server error: ${reason}`)
|
||||||
await log.Log.init({
|
console.warn(error)
|
||||||
print: true,
|
}
|
||||||
dev: install.Installation.isLocal(),
|
|
||||||
level: "WARN",
|
process.once("SIGINT", () => shutdown(130, "SIGINT"))
|
||||||
|
process.once("SIGTERM", () => shutdown(143, "SIGTERM"))
|
||||||
|
process.once("SIGHUP", () => shutdown(129, "SIGHUP"))
|
||||||
|
process.once("uncaughtException", (error) => {
|
||||||
|
reportInternalError("uncaughtException", error)
|
||||||
|
})
|
||||||
|
process.once("unhandledRejection", (error) => {
|
||||||
|
reportInternalError("unhandledRejection", error)
|
||||||
})
|
})
|
||||||
|
|
||||||
const servermod = await import("../../opencode/src/server/server")
|
let code = 1
|
||||||
const inst = await import("../../opencode/src/project/instance")
|
|
||||||
const server = servermod.Server.listen({ port: serverPort, hostname: "127.0.0.1" })
|
try {
|
||||||
console.log(`opencode server listening on http://127.0.0.1:${serverPort}`)
|
seed = Bun.spawn(["bun", "script/seed-e2e.ts"], {
|
||||||
|
cwd: opencodeDir,
|
||||||
|
env: serverEnv,
|
||||||
|
stdout: "inherit",
|
||||||
|
stderr: "inherit",
|
||||||
|
})
|
||||||
|
|
||||||
|
const seedExit = await seed.exited
|
||||||
|
if (seedExit !== 0) {
|
||||||
|
code = seedExit
|
||||||
|
} else {
|
||||||
|
Object.assign(process.env, serverEnv)
|
||||||
|
process.env.AGENT = "1"
|
||||||
|
process.env.OPENCODE = "1"
|
||||||
|
|
||||||
|
const log = await import("../../opencode/src/util/log")
|
||||||
|
const install = await import("../../opencode/src/installation")
|
||||||
|
await log.Log.init({
|
||||||
|
print: true,
|
||||||
|
dev: install.Installation.isLocal(),
|
||||||
|
level: "WARN",
|
||||||
|
})
|
||||||
|
|
||||||
|
const servermod = await import("../../opencode/src/server/server")
|
||||||
|
inst = await import("../../opencode/src/project/instance")
|
||||||
|
server = servermod.Server.listen({ port: serverPort, hostname: "127.0.0.1" })
|
||||||
|
console.log(`opencode server listening on http://127.0.0.1:${serverPort}`)
|
||||||
|
|
||||||
const result = await (async () => {
|
|
||||||
try {
|
|
||||||
await waitForHealth(`http://127.0.0.1:${serverPort}/global/health`)
|
await waitForHealth(`http://127.0.0.1:${serverPort}/global/health`)
|
||||||
|
runner = Bun.spawn(["bun", "test:e2e", ...extraArgs], {
|
||||||
const runner = Bun.spawn(["bun", "test:e2e", ...extraArgs], {
|
|
||||||
cwd: appDir,
|
cwd: appDir,
|
||||||
env: runnerEnv,
|
env: runnerEnv,
|
||||||
stdout: "inherit",
|
stdout: "inherit",
|
||||||
stderr: "inherit",
|
stderr: "inherit",
|
||||||
})
|
})
|
||||||
|
code = await runner.exited
|
||||||
return { code: await runner.exited }
|
|
||||||
} catch (error) {
|
|
||||||
return { error }
|
|
||||||
} finally {
|
|
||||||
await inst.Instance.disposeAll()
|
|
||||||
await server.stop()
|
|
||||||
}
|
}
|
||||||
})()
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
if ("error" in result) {
|
code = 1
|
||||||
console.error(result.error)
|
} finally {
|
||||||
process.exit(1)
|
await cleanup()
|
||||||
}
|
}
|
||||||
|
|
||||||
process.exit(result.code)
|
process.exit(code)
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ function writeAndWait(term: Terminal, data: string): Promise<void> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
describe.skip("SerializeAddon", () => {
|
describe("SerializeAddon", () => {
|
||||||
describe("ANSI color preservation", () => {
|
describe("ANSI color preservation", () => {
|
||||||
test("should preserve text attributes (bold, italic, underline)", async () => {
|
test("should preserve text attributes (bold, italic, underline)", async () => {
|
||||||
const { term, addon } = createTerminal()
|
const { term, addon } = createTerminal()
|
||||||
|
|||||||
@@ -56,6 +56,39 @@ interface IBufferCell {
|
|||||||
isDim(): boolean
|
isDim(): boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TerminalBuffers = {
|
||||||
|
active?: IBuffer
|
||||||
|
normal?: IBuffer
|
||||||
|
alternate?: IBuffer
|
||||||
|
}
|
||||||
|
|
||||||
|
const isRecord = (value: unknown): value is Record<string, unknown> => {
|
||||||
|
return typeof value === "object" && value !== null
|
||||||
|
}
|
||||||
|
|
||||||
|
const isBuffer = (value: unknown): value is IBuffer => {
|
||||||
|
if (!isRecord(value)) return false
|
||||||
|
if (typeof value.length !== "number") return false
|
||||||
|
if (typeof value.cursorX !== "number") return false
|
||||||
|
if (typeof value.cursorY !== "number") return false
|
||||||
|
if (typeof value.baseY !== "number") return false
|
||||||
|
if (typeof value.viewportY !== "number") return false
|
||||||
|
if (typeof value.getLine !== "function") return false
|
||||||
|
if (typeof value.getNullCell !== "function") return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTerminalBuffers = (value: ITerminalCore): TerminalBuffers | undefined => {
|
||||||
|
if (!isRecord(value)) return
|
||||||
|
const raw = value.buffer
|
||||||
|
if (!isRecord(raw)) return
|
||||||
|
const active = isBuffer(raw.active) ? raw.active : undefined
|
||||||
|
const normal = isBuffer(raw.normal) ? raw.normal : undefined
|
||||||
|
const alternate = isBuffer(raw.alternate) ? raw.alternate : undefined
|
||||||
|
if (!active && !normal) return
|
||||||
|
return { active, normal, alternate }
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Types
|
// Types
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -241,19 +274,19 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
|||||||
protected _rowEnd(row: number, isLastRow: boolean): void {
|
protected _rowEnd(row: number, isLastRow: boolean): void {
|
||||||
let rowSeparator = ""
|
let rowSeparator = ""
|
||||||
|
|
||||||
if (this._nullCellCount > 0) {
|
const nextLine = isLastRow ? undefined : this._buffer.getLine(row + 1)
|
||||||
|
const wrapped = !!nextLine?.isWrapped
|
||||||
|
|
||||||
|
if (this._nullCellCount > 0 && wrapped) {
|
||||||
this._currentRow += " ".repeat(this._nullCellCount)
|
this._currentRow += " ".repeat(this._nullCellCount)
|
||||||
this._nullCellCount = 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isLastRow) {
|
this._nullCellCount = 0
|
||||||
const nextLine = this._buffer.getLine(row + 1)
|
|
||||||
|
|
||||||
if (!nextLine?.isWrapped) {
|
if (!isLastRow && !wrapped) {
|
||||||
rowSeparator = "\r\n"
|
rowSeparator = "\r\n"
|
||||||
this._lastCursorRow = row + 1
|
this._lastCursorRow = row + 1
|
||||||
this._lastCursorCol = 0
|
this._lastCursorCol = 0
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this._allRows[this._rowIndex] = this._currentRow
|
this._allRows[this._rowIndex] = this._currentRow
|
||||||
@@ -389,7 +422,7 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
|||||||
|
|
||||||
const sgrSeq = this._diffStyle(cell, this._cursorStyle)
|
const sgrSeq = this._diffStyle(cell, this._cursorStyle)
|
||||||
|
|
||||||
const styleChanged = isEmptyCell ? !equalBg(this._cursorStyle, cell) : sgrSeq.length > 0
|
const styleChanged = sgrSeq.length > 0
|
||||||
|
|
||||||
if (styleChanged) {
|
if (styleChanged) {
|
||||||
if (this._nullCellCount > 0) {
|
if (this._nullCellCount > 0) {
|
||||||
@@ -442,12 +475,24 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!excludeFinalCursorPosition) {
|
if (excludeFinalCursorPosition) return content
|
||||||
const absoluteCursorRow = (this._buffer.baseY ?? 0) + this._buffer.cursorY
|
|
||||||
const cursorRow = constrain(absoluteCursorRow - this._firstRow + 1, 1, Number.MAX_SAFE_INTEGER)
|
const absoluteCursorRow = (this._buffer.baseY ?? 0) + this._buffer.cursorY
|
||||||
const cursorCol = this._buffer.cursorX + 1
|
const cursorRow = constrain(absoluteCursorRow - this._firstRow + 1, 1, Number.MAX_SAFE_INTEGER)
|
||||||
content += `\u001b[${cursorRow};${cursorCol}H`
|
const cursorCol = this._buffer.cursorX + 1
|
||||||
}
|
content += `\u001b[${cursorRow};${cursorCol}H`
|
||||||
|
|
||||||
|
const line = this._buffer.getLine(absoluteCursorRow)
|
||||||
|
const cell = line?.getCell(this._buffer.cursorX)
|
||||||
|
const style = (() => {
|
||||||
|
if (!cell) return this._buffer.getNullCell()
|
||||||
|
if (cell.getWidth() !== 0) return cell
|
||||||
|
if (this._buffer.cursorX > 0) return line?.getCell(this._buffer.cursorX - 1) ?? cell
|
||||||
|
return cell
|
||||||
|
})()
|
||||||
|
|
||||||
|
const sgrSeq = this._diffStyle(style, this._cursorStyle)
|
||||||
|
if (sgrSeq.length) content += `\u001b[${sgrSeq.join(";")}m`
|
||||||
|
|
||||||
return content
|
return content
|
||||||
}
|
}
|
||||||
@@ -486,14 +531,13 @@ export class SerializeAddon implements ITerminalAddon {
|
|||||||
throw new Error("Cannot use addon until it has been loaded")
|
throw new Error("Cannot use addon until it has been loaded")
|
||||||
}
|
}
|
||||||
|
|
||||||
const terminal = this._terminal as any
|
const buffer = getTerminalBuffers(this._terminal)
|
||||||
const buffer = terminal.buffer
|
|
||||||
|
|
||||||
if (!buffer) {
|
if (!buffer) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalBuffer = buffer.normal || buffer.active
|
const normalBuffer = buffer.normal ?? buffer.active
|
||||||
const altBuffer = buffer.alternate
|
const altBuffer = buffer.alternate
|
||||||
|
|
||||||
if (!normalBuffer) {
|
if (!normalBuffer) {
|
||||||
@@ -521,14 +565,13 @@ export class SerializeAddon implements ITerminalAddon {
|
|||||||
throw new Error("Cannot use addon until it has been loaded")
|
throw new Error("Cannot use addon until it has been loaded")
|
||||||
}
|
}
|
||||||
|
|
||||||
const terminal = this._terminal as any
|
const buffer = getTerminalBuffers(this._terminal)
|
||||||
const buffer = terminal.buffer
|
|
||||||
|
|
||||||
if (!buffer) {
|
if (!buffer) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeBuffer = buffer.active || buffer.normal
|
const activeBuffer = buffer.active ?? buffer.normal
|
||||||
if (!activeBuffer) {
|
if (!activeBuffer) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import { HighlightsProvider } from "@/context/highlights"
|
|||||||
import Layout from "@/pages/layout"
|
import Layout from "@/pages/layout"
|
||||||
import DirectoryLayout from "@/pages/directory-layout"
|
import DirectoryLayout from "@/pages/directory-layout"
|
||||||
import { ErrorPage } from "./pages/error"
|
import { ErrorPage } from "./pages/error"
|
||||||
import { Suspense } from "solid-js"
|
import { Suspense, JSX } from "solid-js"
|
||||||
|
|
||||||
const Home = lazy(() => import("@/pages/home"))
|
const Home = lazy(() => import("@/pages/home"))
|
||||||
const Session = lazy(() => import("@/pages/session"))
|
const Session = lazy(() => import("@/pages/session"))
|
||||||
@@ -43,7 +43,7 @@ function UiI18nBridge(props: ParentProps) {
|
|||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
__OPENCODE__?: { updaterEnabled?: boolean; serverPassword?: string; deepLinks?: string[] }
|
__OPENCODE__?: { updaterEnabled?: boolean; serverPassword?: string; deepLinks?: string[]; wsl?: boolean }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ function ServerKey(props: ParentProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AppInterface(props: { defaultUrl?: string }) {
|
export function AppInterface(props: { defaultUrl?: string; children?: JSX.Element; isSidecar?: boolean }) {
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
|
|
||||||
const stored = (() => {
|
const stored = (() => {
|
||||||
@@ -106,12 +106,12 @@ export function AppInterface(props: { defaultUrl?: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ServerProvider defaultUrl={defaultServerUrl()}>
|
<ServerProvider defaultUrl={defaultServerUrl()} isSidecar={props.isSidecar}>
|
||||||
<ServerKey>
|
<ServerKey>
|
||||||
<GlobalSDKProvider>
|
<GlobalSDKProvider>
|
||||||
<GlobalSyncProvider>
|
<GlobalSyncProvider>
|
||||||
<Router
|
<Router
|
||||||
root={(props) => (
|
root={(routerProps) => (
|
||||||
<SettingsProvider>
|
<SettingsProvider>
|
||||||
<PermissionProvider>
|
<PermissionProvider>
|
||||||
<LayoutProvider>
|
<LayoutProvider>
|
||||||
@@ -119,7 +119,10 @@ export function AppInterface(props: { defaultUrl?: string }) {
|
|||||||
<ModelsProvider>
|
<ModelsProvider>
|
||||||
<CommandProvider>
|
<CommandProvider>
|
||||||
<HighlightsProvider>
|
<HighlightsProvider>
|
||||||
<Layout>{props.children}</Layout>
|
<Layout>
|
||||||
|
{props.children}
|
||||||
|
{routerProps.children}
|
||||||
|
</Layout>
|
||||||
</HighlightsProvider>
|
</HighlightsProvider>
|
||||||
</CommandProvider>
|
</CommandProvider>
|
||||||
</ModelsProvider>
|
</ModelsProvider>
|
||||||
|
|||||||
@@ -124,16 +124,16 @@ export function DialogCustomProvider(props: Props) {
|
|||||||
const key = apiKey && !env ? apiKey : undefined
|
const key = apiKey && !env ? apiKey : undefined
|
||||||
|
|
||||||
const idError = !providerID
|
const idError = !providerID
|
||||||
? "Provider ID is required"
|
? language.t("provider.custom.error.providerID.required")
|
||||||
: !PROVIDER_ID.test(providerID)
|
: !PROVIDER_ID.test(providerID)
|
||||||
? "Use lowercase letters, numbers, hyphens, or underscores"
|
? language.t("provider.custom.error.providerID.format")
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
const nameError = !name ? "Display name is required" : undefined
|
const nameError = !name ? language.t("provider.custom.error.name.required") : undefined
|
||||||
const urlError = !baseURL
|
const urlError = !baseURL
|
||||||
? "Base URL is required"
|
? language.t("provider.custom.error.baseURL.required")
|
||||||
: !/^https?:\/\//.test(baseURL)
|
: !/^https?:\/\//.test(baseURL)
|
||||||
? "Must start with http:// or https://"
|
? language.t("provider.custom.error.baseURL.format")
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
const disabled = (globalSync.data.config.disabled_providers ?? []).includes(providerID)
|
const disabled = (globalSync.data.config.disabled_providers ?? []).includes(providerID)
|
||||||
@@ -141,21 +141,21 @@ export function DialogCustomProvider(props: Props) {
|
|||||||
const existsError = idError
|
const existsError = idError
|
||||||
? undefined
|
? undefined
|
||||||
: existingProvider && !disabled
|
: existingProvider && !disabled
|
||||||
? "That provider ID already exists"
|
? language.t("provider.custom.error.providerID.exists")
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
const seenModels = new Set<string>()
|
const seenModels = new Set<string>()
|
||||||
const modelErrors = form.models.map((m) => {
|
const modelErrors = form.models.map((m) => {
|
||||||
const id = m.id.trim()
|
const id = m.id.trim()
|
||||||
const modelIdError = !id
|
const modelIdError = !id
|
||||||
? "Required"
|
? language.t("provider.custom.error.required")
|
||||||
: seenModels.has(id)
|
: seenModels.has(id)
|
||||||
? "Duplicate"
|
? language.t("provider.custom.error.duplicate")
|
||||||
: (() => {
|
: (() => {
|
||||||
seenModels.add(id)
|
seenModels.add(id)
|
||||||
return undefined
|
return undefined
|
||||||
})()
|
})()
|
||||||
const modelNameError = !m.name.trim() ? "Required" : undefined
|
const modelNameError = !m.name.trim() ? language.t("provider.custom.error.required") : undefined
|
||||||
return { id: modelIdError, name: modelNameError }
|
return { id: modelIdError, name: modelNameError }
|
||||||
})
|
})
|
||||||
const modelsValid = modelErrors.every((m) => !m.id && !m.name)
|
const modelsValid = modelErrors.every((m) => !m.id && !m.name)
|
||||||
@@ -168,14 +168,14 @@ export function DialogCustomProvider(props: Props) {
|
|||||||
|
|
||||||
if (!key && !value) return {}
|
if (!key && !value) return {}
|
||||||
const keyError = !key
|
const keyError = !key
|
||||||
? "Required"
|
? language.t("provider.custom.error.required")
|
||||||
: seenHeaders.has(key.toLowerCase())
|
: seenHeaders.has(key.toLowerCase())
|
||||||
? "Duplicate"
|
? language.t("provider.custom.error.duplicate")
|
||||||
: (() => {
|
: (() => {
|
||||||
seenHeaders.add(key.toLowerCase())
|
seenHeaders.add(key.toLowerCase())
|
||||||
return undefined
|
return undefined
|
||||||
})()
|
})()
|
||||||
const valueError = !value ? "Required" : undefined
|
const valueError = !value ? language.t("provider.custom.error.required") : undefined
|
||||||
return { key: keyError, value: valueError }
|
return { key: keyError, value: valueError }
|
||||||
})
|
})
|
||||||
const headersValid = headerErrors.every((h) => !h.key && !h.value)
|
const headersValid = headerErrors.every((h) => !h.key && !h.value)
|
||||||
@@ -278,64 +278,64 @@ export function DialogCustomProvider(props: Props) {
|
|||||||
<div class="flex flex-col gap-6 px-2.5 pb-3 overflow-y-auto max-h-[60vh]">
|
<div class="flex flex-col gap-6 px-2.5 pb-3 overflow-y-auto max-h-[60vh]">
|
||||||
<div class="px-2.5 flex gap-4 items-center">
|
<div class="px-2.5 flex gap-4 items-center">
|
||||||
<ProviderIcon id="synthetic" class="size-5 shrink-0 icon-strong-base" />
|
<ProviderIcon id="synthetic" class="size-5 shrink-0 icon-strong-base" />
|
||||||
<div class="text-16-medium text-text-strong">Custom provider</div>
|
<div class="text-16-medium text-text-strong">{language.t("provider.custom.title")}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={save} class="px-2.5 pb-6 flex flex-col gap-6">
|
<form onSubmit={save} class="px-2.5 pb-6 flex flex-col gap-6">
|
||||||
<p class="text-14-regular text-text-base">
|
<p class="text-14-regular text-text-base">
|
||||||
Configure an OpenAI-compatible provider. See the{" "}
|
{language.t("provider.custom.description.prefix")}
|
||||||
<Link href="https://opencode.ai/docs/providers/#custom-provider" tabIndex={-1}>
|
<Link href="https://opencode.ai/docs/providers/#custom-provider" tabIndex={-1}>
|
||||||
provider config docs
|
{language.t("provider.custom.description.link")}
|
||||||
</Link>
|
</Link>
|
||||||
.
|
{language.t("provider.custom.description.suffix")}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<TextField
|
<TextField
|
||||||
autofocus
|
autofocus
|
||||||
label="Provider ID"
|
label={language.t("provider.custom.field.providerID.label")}
|
||||||
placeholder="myprovider"
|
placeholder={language.t("provider.custom.field.providerID.placeholder")}
|
||||||
description="Lowercase letters, numbers, hyphens, or underscores"
|
description={language.t("provider.custom.field.providerID.description")}
|
||||||
value={form.providerID}
|
value={form.providerID}
|
||||||
onChange={setForm.bind(null, "providerID")}
|
onChange={setForm.bind(null, "providerID")}
|
||||||
validationState={errors.providerID ? "invalid" : undefined}
|
validationState={errors.providerID ? "invalid" : undefined}
|
||||||
error={errors.providerID}
|
error={errors.providerID}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
label="Display name"
|
label={language.t("provider.custom.field.name.label")}
|
||||||
placeholder="My AI Provider"
|
placeholder={language.t("provider.custom.field.name.placeholder")}
|
||||||
value={form.name}
|
value={form.name}
|
||||||
onChange={setForm.bind(null, "name")}
|
onChange={setForm.bind(null, "name")}
|
||||||
validationState={errors.name ? "invalid" : undefined}
|
validationState={errors.name ? "invalid" : undefined}
|
||||||
error={errors.name}
|
error={errors.name}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
label="Base URL"
|
label={language.t("provider.custom.field.baseURL.label")}
|
||||||
placeholder="https://api.myprovider.com/v1"
|
placeholder={language.t("provider.custom.field.baseURL.placeholder")}
|
||||||
value={form.baseURL}
|
value={form.baseURL}
|
||||||
onChange={setForm.bind(null, "baseURL")}
|
onChange={setForm.bind(null, "baseURL")}
|
||||||
validationState={errors.baseURL ? "invalid" : undefined}
|
validationState={errors.baseURL ? "invalid" : undefined}
|
||||||
error={errors.baseURL}
|
error={errors.baseURL}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
label="API key"
|
label={language.t("provider.custom.field.apiKey.label")}
|
||||||
placeholder="API key"
|
placeholder={language.t("provider.custom.field.apiKey.placeholder")}
|
||||||
description="Optional. Leave empty if you manage auth via headers."
|
description={language.t("provider.custom.field.apiKey.description")}
|
||||||
value={form.apiKey}
|
value={form.apiKey}
|
||||||
onChange={setForm.bind(null, "apiKey")}
|
onChange={setForm.bind(null, "apiKey")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<label class="text-12-medium text-text-weak">Models</label>
|
<label class="text-12-medium text-text-weak">{language.t("provider.custom.models.label")}</label>
|
||||||
<For each={form.models}>
|
<For each={form.models}>
|
||||||
{(m, i) => (
|
{(m, i) => (
|
||||||
<div class="flex gap-2 items-start">
|
<div class="flex gap-2 items-start">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<TextField
|
<TextField
|
||||||
label="ID"
|
label={language.t("provider.custom.models.id.label")}
|
||||||
hideLabel
|
hideLabel
|
||||||
placeholder="model-id"
|
placeholder={language.t("provider.custom.models.id.placeholder")}
|
||||||
value={m.id}
|
value={m.id}
|
||||||
onChange={(v) => setForm("models", i(), "id", v)}
|
onChange={(v) => setForm("models", i(), "id", v)}
|
||||||
validationState={errors.models[i()]?.id ? "invalid" : undefined}
|
validationState={errors.models[i()]?.id ? "invalid" : undefined}
|
||||||
@@ -344,9 +344,9 @@ export function DialogCustomProvider(props: Props) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<TextField
|
<TextField
|
||||||
label="Name"
|
label={language.t("provider.custom.models.name.label")}
|
||||||
hideLabel
|
hideLabel
|
||||||
placeholder="Display Name"
|
placeholder={language.t("provider.custom.models.name.placeholder")}
|
||||||
value={m.name}
|
value={m.name}
|
||||||
onChange={(v) => setForm("models", i(), "name", v)}
|
onChange={(v) => setForm("models", i(), "name", v)}
|
||||||
validationState={errors.models[i()]?.name ? "invalid" : undefined}
|
validationState={errors.models[i()]?.name ? "invalid" : undefined}
|
||||||
@@ -360,26 +360,26 @@ export function DialogCustomProvider(props: Props) {
|
|||||||
class="mt-1.5"
|
class="mt-1.5"
|
||||||
onClick={() => removeModel(i())}
|
onClick={() => removeModel(i())}
|
||||||
disabled={form.models.length <= 1}
|
disabled={form.models.length <= 1}
|
||||||
aria-label="Remove model"
|
aria-label={language.t("provider.custom.models.remove")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
<Button type="button" size="small" variant="ghost" icon="plus-small" onClick={addModel} class="self-start">
|
<Button type="button" size="small" variant="ghost" icon="plus-small" onClick={addModel} class="self-start">
|
||||||
Add model
|
{language.t("provider.custom.models.add")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<label class="text-12-medium text-text-weak">Headers (optional)</label>
|
<label class="text-12-medium text-text-weak">{language.t("provider.custom.headers.label")}</label>
|
||||||
<For each={form.headers}>
|
<For each={form.headers}>
|
||||||
{(h, i) => (
|
{(h, i) => (
|
||||||
<div class="flex gap-2 items-start">
|
<div class="flex gap-2 items-start">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<TextField
|
<TextField
|
||||||
label="Header"
|
label={language.t("provider.custom.headers.key.label")}
|
||||||
hideLabel
|
hideLabel
|
||||||
placeholder="Header-Name"
|
placeholder={language.t("provider.custom.headers.key.placeholder")}
|
||||||
value={h.key}
|
value={h.key}
|
||||||
onChange={(v) => setForm("headers", i(), "key", v)}
|
onChange={(v) => setForm("headers", i(), "key", v)}
|
||||||
validationState={errors.headers[i()]?.key ? "invalid" : undefined}
|
validationState={errors.headers[i()]?.key ? "invalid" : undefined}
|
||||||
@@ -388,9 +388,9 @@ export function DialogCustomProvider(props: Props) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<TextField
|
<TextField
|
||||||
label="Value"
|
label={language.t("provider.custom.headers.value.label")}
|
||||||
hideLabel
|
hideLabel
|
||||||
placeholder="value"
|
placeholder={language.t("provider.custom.headers.value.placeholder")}
|
||||||
value={h.value}
|
value={h.value}
|
||||||
onChange={(v) => setForm("headers", i(), "value", v)}
|
onChange={(v) => setForm("headers", i(), "value", v)}
|
||||||
validationState={errors.headers[i()]?.value ? "invalid" : undefined}
|
validationState={errors.headers[i()]?.value ? "invalid" : undefined}
|
||||||
@@ -404,18 +404,18 @@ export function DialogCustomProvider(props: Props) {
|
|||||||
class="mt-1.5"
|
class="mt-1.5"
|
||||||
onClick={() => removeHeader(i())}
|
onClick={() => removeHeader(i())}
|
||||||
disabled={form.headers.length <= 1}
|
disabled={form.headers.length <= 1}
|
||||||
aria-label="Remove header"
|
aria-label={language.t("provider.custom.headers.remove")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
<Button type="button" size="small" variant="ghost" icon="plus-small" onClick={addHeader} class="self-start">
|
<Button type="button" size="small" variant="ghost" icon="plus-small" onClick={addHeader} class="self-start">
|
||||||
Add header
|
{language.t("provider.custom.headers.add")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button class="w-auto self-start" type="submit" size="large" variant="primary" disabled={form.saving}>
|
<Button class="w-auto self-start" type="submit" size="large" variant="primary" disabled={form.saving}>
|
||||||
{form.saving ? "Saving..." : language.t("common.submit")}
|
{form.saving ? language.t("common.saving") : language.t("common.submit")}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -158,22 +158,22 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
|||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 size-16 bg-black/60 rounded-[6px] z-10 pointer-events-none flex items-center justify-center transition-opacity"
|
class="absolute inset-0 size-16 bg-surface-raised-stronger-non-alpha/90 rounded-[6px] z-10 pointer-events-none flex items-center justify-center transition-opacity"
|
||||||
classList={{
|
classList={{
|
||||||
"opacity-100": store.iconHover && !store.iconUrl,
|
"opacity-100": store.iconHover && !store.iconUrl,
|
||||||
"opacity-0": !(store.iconHover && !store.iconUrl),
|
"opacity-0": !(store.iconHover && !store.iconUrl),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon name="cloud-upload" size="large" class="text-icon-invert-base" />
|
<Icon name="cloud-upload" size="large" class="text-icon-on-interactive-base drop-shadow-sm" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 size-16 bg-black/60 rounded-[6px] z-10 pointer-events-none flex items-center justify-center transition-opacity"
|
class="absolute inset-0 size-16 bg-surface-raised-stronger-non-alpha/90 rounded-[6px] z-10 pointer-events-none flex items-center justify-center transition-opacity"
|
||||||
classList={{
|
classList={{
|
||||||
"opacity-100": store.iconHover && !!store.iconUrl,
|
"opacity-100": store.iconHover && !!store.iconUrl,
|
||||||
"opacity-0": !(store.iconHover && !!store.iconUrl),
|
"opacity-0": !(store.iconHover && !!store.iconUrl),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon name="trash" size="large" class="text-icon-invert-base" />
|
<Icon name="trash" size="large" class="text-icon-on-interactive-base drop-shadow-sm" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input id="icon-upload" type="file" accept="image/*" class="hidden" onChange={handleInputChange} />
|
<input id="icon-upload" type="file" accept="image/*" class="hidden" onChange={handleInputChange} />
|
||||||
@@ -223,7 +223,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
|||||||
value={store.startup}
|
value={store.startup}
|
||||||
onChange={(v) => setStore("startup", v)}
|
onChange={(v) => setStore("startup", v)}
|
||||||
spellcheck={false}
|
spellcheck={false}
|
||||||
class="max-h-40 w-full font-mono text-xs no-scrollbar"
|
class="max-h-14 w-full overflow-y-auto font-mono text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import { FileIcon } from "@opencode-ai/ui/file-icon"
|
|||||||
import { List } from "@opencode-ai/ui/list"
|
import { List } from "@opencode-ai/ui/list"
|
||||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||||
import fuzzysort from "fuzzysort"
|
import fuzzysort from "fuzzysort"
|
||||||
import { createMemo } from "solid-js"
|
import { createMemo, createResource, createSignal } from "solid-js"
|
||||||
import { useGlobalSDK } from "@/context/global-sdk"
|
import { useGlobalSDK } from "@/context/global-sdk"
|
||||||
import { useGlobalSync } from "@/context/global-sync"
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
import type { ListRef } from "@opencode-ai/ui/list"
|
||||||
|
|
||||||
interface DialogSelectDirectoryProps {
|
interface DialogSelectDirectoryProps {
|
||||||
title?: string
|
title?: string
|
||||||
@@ -15,18 +16,47 @@ interface DialogSelectDirectoryProps {
|
|||||||
onSelect: (result: string | string[] | null) => void
|
onSelect: (result: string | string[] | null) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Row = {
|
||||||
|
absolute: string
|
||||||
|
search: string
|
||||||
|
}
|
||||||
|
|
||||||
export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
|
export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
|
||||||
const sync = useGlobalSync()
|
const sync = useGlobalSync()
|
||||||
const sdk = useGlobalSDK()
|
const sdk = useGlobalSDK()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
|
|
||||||
const home = createMemo(() => sync.data.path.home)
|
const [filter, setFilter] = createSignal("")
|
||||||
|
|
||||||
const start = createMemo(() => sync.data.path.home || sync.data.path.directory)
|
let list: ListRef | undefined
|
||||||
|
|
||||||
|
const missingBase = createMemo(() => !(sync.data.path.home || sync.data.path.directory))
|
||||||
|
|
||||||
|
const [fallbackPath] = createResource(
|
||||||
|
() => (missingBase() ? true : undefined),
|
||||||
|
async () => {
|
||||||
|
return sdk.client.path
|
||||||
|
.get()
|
||||||
|
.then((x) => x.data)
|
||||||
|
.catch(() => undefined)
|
||||||
|
},
|
||||||
|
{ initialValue: undefined },
|
||||||
|
)
|
||||||
|
|
||||||
|
const home = createMemo(() => sync.data.path.home || fallbackPath()?.home || "")
|
||||||
|
|
||||||
|
const start = createMemo(
|
||||||
|
() => sync.data.path.home || sync.data.path.directory || fallbackPath()?.home || fallbackPath()?.directory,
|
||||||
|
)
|
||||||
|
|
||||||
const cache = new Map<string, Promise<Array<{ name: string; absolute: string }>>>()
|
const cache = new Map<string, Promise<Array<{ name: string; absolute: string }>>>()
|
||||||
|
|
||||||
|
const clean = (value: string) => {
|
||||||
|
const first = (value ?? "").split(/\r?\n/)[0] ?? ""
|
||||||
|
return first.replace(/[\u0000-\u001F\u007F]/g, "").trim()
|
||||||
|
}
|
||||||
|
|
||||||
function normalize(input: string) {
|
function normalize(input: string) {
|
||||||
const v = input.replaceAll("\\", "/")
|
const v = input.replaceAll("\\", "/")
|
||||||
if (v.startsWith("//") && !v.startsWith("///")) return "//" + v.slice(2).replace(/\/+/g, "/")
|
if (v.startsWith("//") && !v.startsWith("///")) return "//" + v.slice(2).replace(/\/+/g, "/")
|
||||||
@@ -64,24 +94,67 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
function display(path: string) {
|
function parentOf(input: string) {
|
||||||
|
const v = trimTrailing(input)
|
||||||
|
if (v === "/") return v
|
||||||
|
if (v === "//") return v
|
||||||
|
if (/^[A-Za-z]:\/$/.test(v)) return v
|
||||||
|
|
||||||
|
const i = v.lastIndexOf("/")
|
||||||
|
if (i <= 0) return "/"
|
||||||
|
if (i === 2 && /^[A-Za-z]:/.test(v)) return v.slice(0, 3)
|
||||||
|
return v.slice(0, i)
|
||||||
|
}
|
||||||
|
|
||||||
|
function modeOf(input: string) {
|
||||||
|
const raw = normalizeDriveRoot(input.trim())
|
||||||
|
if (!raw) return "relative" as const
|
||||||
|
if (raw.startsWith("~")) return "tilde" as const
|
||||||
|
if (rootOf(raw)) return "absolute" as const
|
||||||
|
return "relative" as const
|
||||||
|
}
|
||||||
|
|
||||||
|
function display(path: string, input: string) {
|
||||||
const full = trimTrailing(path)
|
const full = trimTrailing(path)
|
||||||
|
if (modeOf(input) === "absolute") return full
|
||||||
|
|
||||||
|
return tildeOf(full) || full
|
||||||
|
}
|
||||||
|
|
||||||
|
function tildeOf(absolute: string) {
|
||||||
|
const full = trimTrailing(absolute)
|
||||||
const h = home()
|
const h = home()
|
||||||
if (!h) return full
|
if (!h) return ""
|
||||||
|
|
||||||
const hn = trimTrailing(h)
|
const hn = trimTrailing(h)
|
||||||
const lc = full.toLowerCase()
|
const lc = full.toLowerCase()
|
||||||
const hc = hn.toLowerCase()
|
const hc = hn.toLowerCase()
|
||||||
if (lc === hc) return "~"
|
if (lc === hc) return "~"
|
||||||
if (lc.startsWith(hc + "/")) return "~" + full.slice(hn.length)
|
if (lc.startsWith(hc + "/")) return "~" + full.slice(hn.length)
|
||||||
return full
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
function scoped(filter: string) {
|
function row(absolute: string): Row {
|
||||||
|
const full = trimTrailing(absolute)
|
||||||
|
const tilde = tildeOf(full)
|
||||||
|
|
||||||
|
const withSlash = (value: string) => {
|
||||||
|
if (!value) return ""
|
||||||
|
if (value.endsWith("/")) return value
|
||||||
|
return value + "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
const search = Array.from(
|
||||||
|
new Set([full, withSlash(full), tilde, withSlash(tilde), getFilename(full)].filter(Boolean)),
|
||||||
|
).join("\n")
|
||||||
|
return { absolute: full, search }
|
||||||
|
}
|
||||||
|
|
||||||
|
function scoped(value: string) {
|
||||||
const base = start()
|
const base = start()
|
||||||
if (!base) return
|
if (!base) return
|
||||||
|
|
||||||
const raw = normalizeDriveRoot(filter.trim())
|
const raw = normalizeDriveRoot(value)
|
||||||
if (!raw) return { directory: trimTrailing(base), path: "" }
|
if (!raw) return { directory: trimTrailing(base), path: "" }
|
||||||
|
|
||||||
const h = home()
|
const h = home()
|
||||||
@@ -122,21 +195,25 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const directories = async (filter: string) => {
|
const directories = async (filter: string) => {
|
||||||
const input = scoped(filter)
|
const value = clean(filter)
|
||||||
if (!input) return [] as string[]
|
const scopedInput = scoped(value)
|
||||||
|
if (!scopedInput) return [] as string[]
|
||||||
|
|
||||||
const raw = normalizeDriveRoot(filter.trim())
|
const raw = normalizeDriveRoot(value)
|
||||||
const isPath = raw.startsWith("~") || !!rootOf(raw) || raw.includes("/")
|
const isPath = raw.startsWith("~") || !!rootOf(raw) || raw.includes("/")
|
||||||
|
|
||||||
const query = normalizeDriveRoot(input.path)
|
const query = normalizeDriveRoot(scopedInput.path)
|
||||||
|
|
||||||
if (!isPath) {
|
const find = () =>
|
||||||
const results = await sdk.client.find
|
sdk.client.find
|
||||||
.files({ directory: input.directory, query, type: "directory", limit: 50 })
|
.files({ directory: scopedInput.directory, query, type: "directory", limit: 50 })
|
||||||
.then((x) => x.data ?? [])
|
.then((x) => x.data ?? [])
|
||||||
.catch(() => [])
|
.catch(() => [])
|
||||||
|
|
||||||
return results.map((rel) => join(input.directory, rel)).slice(0, 50)
|
if (!isPath) {
|
||||||
|
const results = await find()
|
||||||
|
|
||||||
|
return results.map((rel) => join(scopedInput.directory, rel)).slice(0, 50)
|
||||||
}
|
}
|
||||||
|
|
||||||
const segments = query.replace(/^\/+/, "").split("/")
|
const segments = query.replace(/^\/+/, "").split("/")
|
||||||
@@ -145,17 +222,10 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
|
|||||||
|
|
||||||
const cap = 12
|
const cap = 12
|
||||||
const branch = 4
|
const branch = 4
|
||||||
let paths = [input.directory]
|
let paths = [scopedInput.directory]
|
||||||
for (const part of head) {
|
for (const part of head) {
|
||||||
if (part === "..") {
|
if (part === "..") {
|
||||||
paths = paths.map((p) => {
|
paths = paths.map(parentOf)
|
||||||
const v = trimTrailing(p)
|
|
||||||
if (v === "/") return v
|
|
||||||
if (/^[A-Za-z]:\/$/.test(v)) return v
|
|
||||||
const i = v.lastIndexOf("/")
|
|
||||||
if (i <= 0) return "/"
|
|
||||||
return v.slice(0, i)
|
|
||||||
})
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,7 +235,27 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const out = (await Promise.all(paths.map((p) => match(p, tail, 50)))).flat()
|
const out = (await Promise.all(paths.map((p) => match(p, tail, 50)))).flat()
|
||||||
return Array.from(new Set(out)).slice(0, 50)
|
const deduped = Array.from(new Set(out))
|
||||||
|
const base = raw.startsWith("~") ? trimTrailing(scopedInput.directory) : ""
|
||||||
|
const expand = !raw.endsWith("/")
|
||||||
|
if (!expand || !tail) {
|
||||||
|
const items = base ? Array.from(new Set([base, ...deduped])) : deduped
|
||||||
|
return items.slice(0, 50)
|
||||||
|
}
|
||||||
|
|
||||||
|
const needle = tail.toLowerCase()
|
||||||
|
const exact = deduped.filter((p) => getFilename(p).toLowerCase() === needle)
|
||||||
|
const target = exact[0]
|
||||||
|
if (!target) return deduped.slice(0, 50)
|
||||||
|
|
||||||
|
const children = await match(target, "", 30)
|
||||||
|
const items = Array.from(new Set([...deduped, ...children]))
|
||||||
|
return (base ? Array.from(new Set([base, ...items])) : items).slice(0, 50)
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = async (value: string) => {
|
||||||
|
const results = await directories(value)
|
||||||
|
return results.map(row)
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolve(absolute: string) {
|
function resolve(absolute: string) {
|
||||||
@@ -179,24 +269,52 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
|
|||||||
search={{ placeholder: language.t("dialog.directory.search.placeholder"), autofocus: true }}
|
search={{ placeholder: language.t("dialog.directory.search.placeholder"), autofocus: true }}
|
||||||
emptyMessage={language.t("dialog.directory.empty")}
|
emptyMessage={language.t("dialog.directory.empty")}
|
||||||
loadingMessage={language.t("common.loading")}
|
loadingMessage={language.t("common.loading")}
|
||||||
items={directories}
|
items={items}
|
||||||
key={(x) => x}
|
key={(x) => x.absolute}
|
||||||
|
filterKeys={["search"]}
|
||||||
|
ref={(r) => (list = r)}
|
||||||
|
onFilter={(value) => setFilter(clean(value))}
|
||||||
|
onKeyEvent={(e, item) => {
|
||||||
|
if (e.key !== "Tab") return
|
||||||
|
if (e.shiftKey) return
|
||||||
|
if (!item) return
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
const value = display(item.absolute, filter())
|
||||||
|
list?.setFilter(value.endsWith("/") ? value : value + "/")
|
||||||
|
}}
|
||||||
onSelect={(path) => {
|
onSelect={(path) => {
|
||||||
if (!path) return
|
if (!path) return
|
||||||
resolve(path)
|
resolve(path.absolute)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(absolute) => {
|
{(item) => {
|
||||||
const path = display(absolute)
|
const path = display(item.absolute, filter())
|
||||||
|
if (path === "~") {
|
||||||
|
return (
|
||||||
|
<div class="w-full flex items-center justify-between rounded-md">
|
||||||
|
<div class="flex items-center gap-x-3 grow min-w-0">
|
||||||
|
<FileIcon node={{ path: item.absolute, type: "directory" }} class="shrink-0 size-4" />
|
||||||
|
<div class="flex items-center text-14-regular min-w-0">
|
||||||
|
<span class="text-text-strong whitespace-nowrap">~</span>
|
||||||
|
<span class="text-text-weak whitespace-nowrap">/</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div class="w-full flex items-center justify-between rounded-md">
|
<div class="w-full flex items-center justify-between rounded-md">
|
||||||
<div class="flex items-center gap-x-3 grow min-w-0">
|
<div class="flex items-center gap-x-3 grow min-w-0">
|
||||||
<FileIcon node={{ path: absolute, type: "directory" }} class="shrink-0 size-4" />
|
<FileIcon node={{ path: item.absolute, type: "directory" }} class="shrink-0 size-4" />
|
||||||
<div class="flex items-center text-14-regular min-w-0">
|
<div class="flex items-center text-14-regular min-w-0">
|
||||||
<span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
|
<span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
|
||||||
{getDirectory(path)}
|
{getDirectory(path)}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-text-strong whitespace-nowrap">{getFilename(path)}</span>
|
<span class="text-text-strong whitespace-nowrap">{getFilename(path)}</span>
|
||||||
|
<span class="text-text-weak whitespace-nowrap">/</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,17 +1,23 @@
|
|||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||||
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { Keybind } from "@opencode-ai/ui/keybind"
|
import { Keybind } from "@opencode-ai/ui/keybind"
|
||||||
import { List } from "@opencode-ai/ui/list"
|
import { List } from "@opencode-ai/ui/list"
|
||||||
|
import { base64Encode } from "@opencode-ai/util/encode"
|
||||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useNavigate, useParams } from "@solidjs/router"
|
||||||
import { createMemo, createSignal, onCleanup, Show } from "solid-js"
|
import { createMemo, createSignal, Match, onCleanup, Show, Switch } from "solid-js"
|
||||||
import { formatKeybind, useCommand, type CommandOption } from "@/context/command"
|
import { formatKeybind, useCommand, type CommandOption } from "@/context/command"
|
||||||
|
import { useGlobalSDK } from "@/context/global-sdk"
|
||||||
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
import { useFile } from "@/context/file"
|
import { useFile } from "@/context/file"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { decode64 } from "@/utils/base64"
|
||||||
|
import { getRelativeTime } from "@/utils/time"
|
||||||
|
|
||||||
type EntryType = "command" | "file"
|
type EntryType = "command" | "file" | "session"
|
||||||
|
|
||||||
type Entry = {
|
type Entry = {
|
||||||
id: string
|
id: string
|
||||||
@@ -22,6 +28,10 @@ type Entry = {
|
|||||||
category: string
|
category: string
|
||||||
option?: CommandOption
|
option?: CommandOption
|
||||||
path?: string
|
path?: string
|
||||||
|
directory?: string
|
||||||
|
sessionID?: string
|
||||||
|
archived?: number
|
||||||
|
updated?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
type DialogSelectFileMode = "all" | "files"
|
type DialogSelectFileMode = "all" | "files"
|
||||||
@@ -33,9 +43,13 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
|||||||
const file = useFile()
|
const file = useFile()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const globalSDK = useGlobalSDK()
|
||||||
|
const globalSync = useGlobalSync()
|
||||||
const filesOnly = () => props.mode === "files"
|
const filesOnly = () => props.mode === "files"
|
||||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||||
const tabs = createMemo(() => layout.tabs(sessionKey))
|
const tabs = createMemo(() => layout.tabs(sessionKey))
|
||||||
|
const view = createMemo(() => layout.view(sessionKey))
|
||||||
const state = { cleanup: undefined as (() => void) | void, committed: false }
|
const state = { cleanup: undefined as (() => void) | void, committed: false }
|
||||||
const [grouped, setGrouped] = createSignal(false)
|
const [grouped, setGrouped] = createSignal(false)
|
||||||
const common = [
|
const common = [
|
||||||
@@ -73,6 +87,54 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
|||||||
path,
|
path,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const projectDirectory = createMemo(() => decode64(params.dir) ?? "")
|
||||||
|
const project = createMemo(() => {
|
||||||
|
const directory = projectDirectory()
|
||||||
|
if (!directory) return
|
||||||
|
return layout.projects.list().find((p) => p.worktree === directory || p.sandboxes?.includes(directory))
|
||||||
|
})
|
||||||
|
const workspaces = createMemo(() => {
|
||||||
|
const directory = projectDirectory()
|
||||||
|
const current = project()
|
||||||
|
if (!current) return directory ? [directory] : []
|
||||||
|
|
||||||
|
const dirs = [current.worktree, ...(current.sandboxes ?? [])]
|
||||||
|
if (directory && !dirs.includes(directory)) return [...dirs, directory]
|
||||||
|
return dirs
|
||||||
|
})
|
||||||
|
const homedir = createMemo(() => globalSync.data.path.home)
|
||||||
|
const label = (directory: string) => {
|
||||||
|
const current = project()
|
||||||
|
const kind =
|
||||||
|
current && directory === current.worktree
|
||||||
|
? language.t("workspace.type.local")
|
||||||
|
: language.t("workspace.type.sandbox")
|
||||||
|
const [store] = globalSync.child(directory, { bootstrap: false })
|
||||||
|
const home = homedir()
|
||||||
|
const path = home ? directory.replace(home, "~") : directory
|
||||||
|
const name = store.vcs?.branch ?? getFilename(directory)
|
||||||
|
return `${kind} : ${name || path}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionItem = (input: {
|
||||||
|
directory: string
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
archived?: number
|
||||||
|
updated?: number
|
||||||
|
}): Entry => ({
|
||||||
|
id: `session:${input.directory}:${input.id}`,
|
||||||
|
type: "session",
|
||||||
|
title: input.title,
|
||||||
|
description: input.description,
|
||||||
|
category: language.t("command.category.session"),
|
||||||
|
directory: input.directory,
|
||||||
|
sessionID: input.id,
|
||||||
|
archived: input.archived,
|
||||||
|
updated: input.updated,
|
||||||
|
})
|
||||||
|
|
||||||
const list = createMemo(() => allowed().map(commandItem))
|
const list = createMemo(() => allowed().map(commandItem))
|
||||||
|
|
||||||
const picks = createMemo(() => {
|
const picks = createMemo(() => {
|
||||||
@@ -122,6 +184,69 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sessionToken = { value: 0 }
|
||||||
|
let sessionInflight: Promise<Entry[]> | undefined
|
||||||
|
let sessionAll: Entry[] | undefined
|
||||||
|
|
||||||
|
const sessions = (text: string) => {
|
||||||
|
const query = text.trim()
|
||||||
|
if (!query) {
|
||||||
|
sessionToken.value += 1
|
||||||
|
sessionInflight = undefined
|
||||||
|
sessionAll = undefined
|
||||||
|
return [] as Entry[]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sessionAll) return sessionAll
|
||||||
|
if (sessionInflight) return sessionInflight
|
||||||
|
|
||||||
|
const current = sessionToken.value
|
||||||
|
const dirs = workspaces()
|
||||||
|
if (dirs.length === 0) return [] as Entry[]
|
||||||
|
|
||||||
|
sessionInflight = Promise.all(
|
||||||
|
dirs.map((directory) => {
|
||||||
|
const description = label(directory)
|
||||||
|
return globalSDK.client.session
|
||||||
|
.list({ directory, roots: true })
|
||||||
|
.then((x) =>
|
||||||
|
(x.data ?? [])
|
||||||
|
.filter((s) => !!s?.id)
|
||||||
|
.map((s) => ({
|
||||||
|
id: s.id,
|
||||||
|
title: s.title ?? language.t("command.session.new"),
|
||||||
|
description,
|
||||||
|
directory,
|
||||||
|
archived: s.time?.archived,
|
||||||
|
updated: s.time?.updated,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.catch(() => [] as { id: string; title: string; description: string; directory: string; archived?: number }[])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.then((results) => {
|
||||||
|
if (sessionToken.value !== current) return [] as Entry[]
|
||||||
|
const seen = new Set<string>()
|
||||||
|
const next = results
|
||||||
|
.flat()
|
||||||
|
.filter((item) => {
|
||||||
|
const key = `${item.directory}:${item.id}`
|
||||||
|
if (seen.has(key)) return false
|
||||||
|
seen.add(key)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
.map(sessionItem)
|
||||||
|
sessionAll = next
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
.catch(() => [] as Entry[])
|
||||||
|
.finally(() => {
|
||||||
|
sessionInflight = undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
return sessionInflight
|
||||||
|
}
|
||||||
|
|
||||||
const items = async (text: string) => {
|
const items = async (text: string) => {
|
||||||
const query = text.trim()
|
const query = text.trim()
|
||||||
setGrouped(query.length > 0)
|
setGrouped(query.length > 0)
|
||||||
@@ -146,9 +271,10 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
|||||||
const files = await file.searchFiles(query)
|
const files = await file.searchFiles(query)
|
||||||
return files.map(fileItem)
|
return files.map(fileItem)
|
||||||
}
|
}
|
||||||
const files = await file.searchFiles(query)
|
|
||||||
|
const [files, nextSessions] = await Promise.all([file.searchFiles(query), Promise.resolve(sessions(query))])
|
||||||
const entries = files.map(fileItem)
|
const entries = files.map(fileItem)
|
||||||
return [...list(), ...entries]
|
return [...list(), ...nextSessions, ...entries]
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMove = (item: Entry | undefined) => {
|
const handleMove = (item: Entry | undefined) => {
|
||||||
@@ -162,6 +288,7 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
|||||||
const value = file.tab(path)
|
const value = file.tab(path)
|
||||||
tabs().open(value)
|
tabs().open(value)
|
||||||
file.load(path)
|
file.load(path)
|
||||||
|
if (!view().reviewPanel.opened()) view().reviewPanel.open()
|
||||||
layout.fileTree.open()
|
layout.fileTree.open()
|
||||||
layout.fileTree.setTab("all")
|
layout.fileTree.setTab("all")
|
||||||
props.onOpenFile?.(path)
|
props.onOpenFile?.(path)
|
||||||
@@ -178,6 +305,12 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (item.type === "session") {
|
||||||
|
if (!item.directory || !item.sessionID) return
|
||||||
|
navigate(`/${base64Encode(item.directory)}/session/${item.sessionID}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!item.path) return
|
if (!item.path) return
|
||||||
open(item.path)
|
open(item.path)
|
||||||
}
|
}
|
||||||
@@ -202,13 +335,12 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
|||||||
items={items}
|
items={items}
|
||||||
key={(item) => item.id}
|
key={(item) => item.id}
|
||||||
filterKeys={["title", "description", "category"]}
|
filterKeys={["title", "description", "category"]}
|
||||||
groupBy={(item) => item.category}
|
groupBy={grouped() ? (item) => item.category : () => ""}
|
||||||
onMove={handleMove}
|
onMove={handleMove}
|
||||||
onSelect={handleSelect}
|
onSelect={handleSelect}
|
||||||
>
|
>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<Show
|
<Switch
|
||||||
when={item.type === "command"}
|
|
||||||
fallback={
|
fallback={
|
||||||
<div class="w-full flex items-center justify-between rounded-md pl-1">
|
<div class="w-full flex items-center justify-between rounded-md pl-1">
|
||||||
<div class="flex items-center gap-x-3 grow min-w-0">
|
<div class="flex items-center gap-x-3 grow min-w-0">
|
||||||
@@ -223,18 +355,48 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div class="w-full flex items-center justify-between gap-4">
|
<Match when={item.type === "command"}>
|
||||||
<div class="flex items-center gap-2 min-w-0">
|
<div class="w-full flex items-center justify-between gap-4">
|
||||||
<span class="text-14-regular text-text-strong whitespace-nowrap">{item.title}</span>
|
<div class="flex items-center gap-2 min-w-0">
|
||||||
<Show when={item.description}>
|
<span class="text-14-regular text-text-strong whitespace-nowrap">{item.title}</span>
|
||||||
<span class="text-14-regular text-text-weak truncate">{item.description}</span>
|
<Show when={item.description}>
|
||||||
|
<span class="text-14-regular text-text-weak truncate">{item.description}</span>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<Show when={item.keybind}>
|
||||||
|
<Keybind class="rounded-[4px]">{formatKeybind(item.keybind ?? "")}</Keybind>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<Show when={item.keybind}>
|
</Match>
|
||||||
<Keybind class="rounded-[4px]">{formatKeybind(item.keybind ?? "")}</Keybind>
|
<Match when={item.type === "session"}>
|
||||||
</Show>
|
<div class="w-full flex items-center justify-between rounded-md pl-1">
|
||||||
</div>
|
<div class="flex items-center gap-x-3 grow min-w-0">
|
||||||
</Show>
|
<Icon name="bubble-5" size="small" class="shrink-0 text-icon-weak" />
|
||||||
|
<div class="flex items-center gap-2 min-w-0">
|
||||||
|
<span
|
||||||
|
class="text-14-regular text-text-strong truncate"
|
||||||
|
classList={{ "opacity-70": !!item.archived }}
|
||||||
|
>
|
||||||
|
{item.title}
|
||||||
|
</span>
|
||||||
|
<Show when={item.description}>
|
||||||
|
<span
|
||||||
|
class="text-14-regular text-text-weak truncate"
|
||||||
|
classList={{ "opacity-70": !!item.archived }}
|
||||||
|
>
|
||||||
|
{item.description}
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Show when={item.updated}>
|
||||||
|
<span class="text-12-regular text-text-weak whitespace-nowrap ml-2">
|
||||||
|
{getRelativeTime(new Date(item.updated!).toISOString())}
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
)}
|
)}
|
||||||
</List>
|
</List>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ const ModelList: Component<{
|
|||||||
class="w-full"
|
class="w-full"
|
||||||
placement="right-start"
|
placement="right-start"
|
||||||
gutter={12}
|
gutter={12}
|
||||||
forceMount={false}
|
|
||||||
value={
|
value={
|
||||||
<ModelTooltip
|
<ModelTooltip
|
||||||
model={item}
|
model={item}
|
||||||
@@ -88,11 +87,13 @@ const ModelList: Component<{
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ModelSelectorPopover<T extends ValidComponent = "div">(props: {
|
type ModelSelectorTriggerProps = Omit<ComponentProps<typeof Kobalte.Trigger>, "as" | "ref">
|
||||||
|
|
||||||
|
export function ModelSelectorPopover(props: {
|
||||||
provider?: string
|
provider?: string
|
||||||
children?: JSX.Element
|
children?: JSX.Element
|
||||||
triggerAs?: T
|
triggerAs?: ValidComponent
|
||||||
triggerProps?: ComponentProps<T>
|
triggerProps?: ModelSelectorTriggerProps
|
||||||
}) {
|
}) {
|
||||||
const [store, setStore] = createStore<{
|
const [store, setStore] = createStore<{
|
||||||
open: boolean
|
open: boolean
|
||||||
@@ -177,11 +178,7 @@ export function ModelSelectorPopover<T extends ValidComponent = "div">(props: {
|
|||||||
placement="top-start"
|
placement="top-start"
|
||||||
gutter={8}
|
gutter={8}
|
||||||
>
|
>
|
||||||
<Kobalte.Trigger
|
<Kobalte.Trigger ref={(el) => setStore("trigger", el)} as={props.triggerAs ?? "div"} {...props.triggerProps}>
|
||||||
ref={(el) => setStore("trigger", el)}
|
|
||||||
as={props.triggerAs ?? "div"}
|
|
||||||
{...(props.triggerProps as any)}
|
|
||||||
>
|
|
||||||
{props.children}
|
{props.children}
|
||||||
</Kobalte.Trigger>
|
</Kobalte.Trigger>
|
||||||
<Kobalte.Portal>
|
<Kobalte.Portal>
|
||||||
@@ -214,7 +211,7 @@ export function ModelSelectorPopover<T extends ValidComponent = "div">(props: {
|
|||||||
class="p-1"
|
class="p-1"
|
||||||
action={
|
action={
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<Tooltip placement="top" forceMount={false} value={language.t("command.provider.connect")}>
|
<Tooltip placement="top" value={language.t("command.provider.connect")}>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon="plus-small"
|
icon="plus-small"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -224,7 +221,7 @@ export function ModelSelectorPopover<T extends ValidComponent = "div">(props: {
|
|||||||
onClick={handleConnectProvider}
|
onClick={handleConnectProvider}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip placement="top" forceMount={false} value={language.t("dialog.model.manage")}>
|
<Tooltip placement="top" value={language.t("dialog.model.manage")}>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon="sliders"
|
icon="sliders"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createResource, createEffect, createMemo, onCleanup, Show, createSignal } from "solid-js"
|
import { createResource, createEffect, createMemo, onCleanup, Show } from "solid-js"
|
||||||
import { createStore, reconcile } from "solid-js/store"
|
import { createStore, reconcile } from "solid-js/store"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
@@ -6,17 +6,15 @@ import { List } from "@opencode-ai/ui/list"
|
|||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { TextField } from "@opencode-ai/ui/text-field"
|
import { TextField } from "@opencode-ai/ui/text-field"
|
||||||
import { normalizeServerUrl, serverDisplayName, useServer } from "@/context/server"
|
import { normalizeServerUrl, useServer } from "@/context/server"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
|
||||||
import { useNavigate } from "@solidjs/router"
|
import { useNavigate } from "@solidjs/router"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
|
||||||
import { useGlobalSDK } from "@/context/global-sdk"
|
import { useGlobalSDK } from "@/context/global-sdk"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
|
import { ServerRow } from "@/components/server/server-row"
|
||||||
type ServerStatus = { healthy: boolean; version?: string }
|
import { checkServerHealth, type ServerHealth } from "@/utils/server-health"
|
||||||
|
|
||||||
interface AddRowProps {
|
interface AddRowProps {
|
||||||
value: string
|
value: string
|
||||||
@@ -40,19 +38,6 @@ interface EditRowProps {
|
|||||||
onBlur: () => void
|
onBlur: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkHealth(url: string, platform: ReturnType<typeof usePlatform>): Promise<ServerStatus> {
|
|
||||||
const signal = (AbortSignal as unknown as { timeout?: (ms: number) => AbortSignal }).timeout?.(3000)
|
|
||||||
const sdk = createOpencodeClient({
|
|
||||||
baseUrl: url,
|
|
||||||
fetch: platform.fetch,
|
|
||||||
signal,
|
|
||||||
})
|
|
||||||
return sdk.global
|
|
||||||
.health()
|
|
||||||
.then((x) => ({ healthy: x.data?.healthy === true, version: x.data?.version }))
|
|
||||||
.catch(() => ({ healthy: false }))
|
|
||||||
}
|
|
||||||
|
|
||||||
function AddRow(props: AddRowProps) {
|
function AddRow(props: AddRowProps) {
|
||||||
return (
|
return (
|
||||||
<div class="flex items-center px-4 min-h-14 py-3 min-w-0 flex-1">
|
<div class="flex items-center px-4 min-h-14 py-3 min-w-0 flex-1">
|
||||||
@@ -131,7 +116,7 @@ export function DialogSelectServer() {
|
|||||||
const globalSDK = useGlobalSDK()
|
const globalSDK = useGlobalSDK()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
status: {} as Record<string, ServerStatus | undefined>,
|
status: {} as Record<string, ServerHealth | undefined>,
|
||||||
addServer: {
|
addServer: {
|
||||||
url: "",
|
url: "",
|
||||||
adding: false,
|
adding: false,
|
||||||
@@ -165,6 +150,7 @@ export function DialogSelectServer() {
|
|||||||
{ initialValue: null },
|
{ initialValue: null },
|
||||||
)
|
)
|
||||||
const canDefault = createMemo(() => !!platform.getDefaultServerUrl && !!platform.setDefaultServerUrl)
|
const canDefault = createMemo(() => !!platform.getDefaultServerUrl && !!platform.setDefaultServerUrl)
|
||||||
|
const fetcher = platform.fetch ?? globalThis.fetch
|
||||||
|
|
||||||
const looksComplete = (value: string) => {
|
const looksComplete = (value: string) => {
|
||||||
const normalized = normalizeServerUrl(value)
|
const normalized = normalizeServerUrl(value)
|
||||||
@@ -180,7 +166,7 @@ export function DialogSelectServer() {
|
|||||||
if (!looksComplete(value)) return
|
if (!looksComplete(value)) return
|
||||||
const normalized = normalizeServerUrl(value)
|
const normalized = normalizeServerUrl(value)
|
||||||
if (!normalized) return
|
if (!normalized) return
|
||||||
const result = await checkHealth(normalized, platform)
|
const result = await checkServerHealth(normalized, fetcher)
|
||||||
setStatus(result.healthy)
|
setStatus(result.healthy)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,7 +213,7 @@ export function DialogSelectServer() {
|
|||||||
if (!list.length) return list
|
if (!list.length) return list
|
||||||
const active = current()
|
const active = current()
|
||||||
const order = new Map(list.map((url, index) => [url, index] as const))
|
const order = new Map(list.map((url, index) => [url, index] as const))
|
||||||
const rank = (value?: ServerStatus) => {
|
const rank = (value?: ServerHealth) => {
|
||||||
if (value?.healthy === true) return 0
|
if (value?.healthy === true) return 0
|
||||||
if (value?.healthy === false) return 2
|
if (value?.healthy === false) return 2
|
||||||
return 1
|
return 1
|
||||||
@@ -242,10 +228,10 @@ export function DialogSelectServer() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
async function refreshHealth() {
|
async function refreshHealth() {
|
||||||
const results: Record<string, ServerStatus> = {}
|
const results: Record<string, ServerHealth> = {}
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
items().map(async (url) => {
|
items().map(async (url) => {
|
||||||
results[url] = await checkHealth(url, platform)
|
results[url] = await checkServerHealth(url, fetcher)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
setStore("status", reconcile(results))
|
setStore("status", reconcile(results))
|
||||||
@@ -300,7 +286,7 @@ export function DialogSelectServer() {
|
|||||||
|
|
||||||
setStore("addServer", { adding: true, error: "" })
|
setStore("addServer", { adding: true, error: "" })
|
||||||
|
|
||||||
const result = await checkHealth(normalized, platform)
|
const result = await checkServerHealth(normalized, fetcher)
|
||||||
setStore("addServer", { adding: false })
|
setStore("addServer", { adding: false })
|
||||||
|
|
||||||
if (!result.healthy) {
|
if (!result.healthy) {
|
||||||
@@ -327,7 +313,7 @@ export function DialogSelectServer() {
|
|||||||
|
|
||||||
setStore("editServer", { busy: true, error: "" })
|
setStore("editServer", { busy: true, error: "" })
|
||||||
|
|
||||||
const result = await checkHealth(normalized, platform)
|
const result = await checkServerHealth(normalized, fetcher)
|
||||||
setStore("editServer", { busy: false })
|
setStore("editServer", { busy: false })
|
||||||
|
|
||||||
if (!result.healthy) {
|
if (!result.healthy) {
|
||||||
@@ -369,6 +355,9 @@ export function DialogSelectServer() {
|
|||||||
|
|
||||||
async function handleRemove(url: string) {
|
async function handleRemove(url: string) {
|
||||||
server.remove(url)
|
server.remove(url)
|
||||||
|
if ((await platform.getDefaultServerUrl?.()) === url) {
|
||||||
|
platform.setDefaultServerUrl?.(null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -410,35 +399,6 @@ export function DialogSelectServer() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{(i) => {
|
{(i) => {
|
||||||
const [truncated, setTruncated] = createSignal(false)
|
|
||||||
let nameRef: HTMLSpanElement | undefined
|
|
||||||
let versionRef: HTMLSpanElement | undefined
|
|
||||||
|
|
||||||
const check = () => {
|
|
||||||
const nameTruncated = nameRef ? nameRef.scrollWidth > nameRef.clientWidth : false
|
|
||||||
const versionTruncated = versionRef ? versionRef.scrollWidth > versionRef.clientWidth : false
|
|
||||||
setTruncated(nameTruncated || versionTruncated)
|
|
||||||
}
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
check()
|
|
||||||
window.addEventListener("resize", check)
|
|
||||||
onCleanup(() => window.removeEventListener("resize", check))
|
|
||||||
})
|
|
||||||
|
|
||||||
const tooltipValue = () => {
|
|
||||||
const name = serverDisplayName(i)
|
|
||||||
const version = store.status[i]?.version
|
|
||||||
return (
|
|
||||||
<span class="flex items-center gap-2">
|
|
||||||
<span>{name}</span>
|
|
||||||
<Show when={version}>
|
|
||||||
<span class="text-text-invert-base">{version}</span>
|
|
||||||
</Show>
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex items-center gap-3 min-w-0 flex-1 group/item">
|
<div class="flex items-center gap-3 min-w-0 flex-1 group/item">
|
||||||
<Show
|
<Show
|
||||||
@@ -456,34 +416,19 @@ export function DialogSelectServer() {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Tooltip value={tooltipValue()} placement="top" inactive={!truncated()}>
|
<ServerRow
|
||||||
<div
|
url={i}
|
||||||
class="flex items-center gap-3 px-4 min-w-0 flex-1"
|
status={store.status[i]}
|
||||||
classList={{ "opacity-50": store.status[i]?.healthy === false }}
|
dimmed={store.status[i]?.healthy === false}
|
||||||
>
|
class="flex items-center gap-3 px-4 min-w-0 flex-1"
|
||||||
<div
|
badge={
|
||||||
classList={{
|
|
||||||
"size-1.5 rounded-full shrink-0": true,
|
|
||||||
"bg-icon-success-base": store.status[i]?.healthy === true,
|
|
||||||
"bg-icon-critical-base": store.status[i]?.healthy === false,
|
|
||||||
"bg-border-weak-base": store.status[i] === undefined,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span ref={nameRef} class="truncate">
|
|
||||||
{serverDisplayName(i)}
|
|
||||||
</span>
|
|
||||||
<Show when={store.status[i]?.version}>
|
|
||||||
<span ref={versionRef} class="text-text-weak text-14-regular truncate">
|
|
||||||
{store.status[i]?.version}
|
|
||||||
</span>
|
|
||||||
</Show>
|
|
||||||
<Show when={defaultUrl() === i}>
|
<Show when={defaultUrl() === i}>
|
||||||
<span class="text-text-weak bg-surface-base text-14-regular px-1.5 rounded-xs">
|
<span class="text-text-weak bg-surface-base text-14-regular px-1.5 rounded-xs">
|
||||||
{language.t("dialog.server.status.default")}
|
{language.t("dialog.server.status.default")}
|
||||||
</span>
|
</span>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
}
|
||||||
</Tooltip>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={store.editServer.id !== i}>
|
<Show when={store.editServer.id !== i}>
|
||||||
<div class="flex items-center justify-center gap-5 pl-4">
|
<div class="flex items-center justify-center gap-5 pl-4">
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { beforeAll, describe, expect, mock, test } from "bun:test"
|
||||||
|
|
||||||
|
let shouldListRoot: typeof import("./file-tree").shouldListRoot
|
||||||
|
let shouldListExpanded: typeof import("./file-tree").shouldListExpanded
|
||||||
|
let dirsToExpand: typeof import("./file-tree").dirsToExpand
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
mock.module("@solidjs/router", () => ({
|
||||||
|
useNavigate: () => () => undefined,
|
||||||
|
useParams: () => ({}),
|
||||||
|
}))
|
||||||
|
mock.module("@/context/file", () => ({
|
||||||
|
useFile: () => ({
|
||||||
|
tree: {
|
||||||
|
state: () => undefined,
|
||||||
|
list: () => Promise.resolve(),
|
||||||
|
children: () => [],
|
||||||
|
expand: () => {},
|
||||||
|
collapse: () => {},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
mock.module("@opencode-ai/ui/collapsible", () => ({
|
||||||
|
Collapsible: {
|
||||||
|
Trigger: (props: { children?: unknown }) => props.children,
|
||||||
|
Content: (props: { children?: unknown }) => props.children,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
mock.module("@opencode-ai/ui/file-icon", () => ({ FileIcon: () => null }))
|
||||||
|
mock.module("@opencode-ai/ui/icon", () => ({ Icon: () => null }))
|
||||||
|
mock.module("@opencode-ai/ui/tooltip", () => ({ Tooltip: (props: { children?: unknown }) => props.children }))
|
||||||
|
const mod = await import("./file-tree")
|
||||||
|
shouldListRoot = mod.shouldListRoot
|
||||||
|
shouldListExpanded = mod.shouldListExpanded
|
||||||
|
dirsToExpand = mod.dirsToExpand
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("file tree fetch discipline", () => {
|
||||||
|
test("root lists on mount unless already loaded or loading", () => {
|
||||||
|
expect(shouldListRoot({ level: 0 })).toBe(true)
|
||||||
|
expect(shouldListRoot({ level: 0, dir: { loaded: true } })).toBe(false)
|
||||||
|
expect(shouldListRoot({ level: 0, dir: { loading: true } })).toBe(false)
|
||||||
|
expect(shouldListRoot({ level: 1 })).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("nested dirs list only when expanded and stale", () => {
|
||||||
|
expect(shouldListExpanded({ level: 1 })).toBe(false)
|
||||||
|
expect(shouldListExpanded({ level: 1, dir: { expanded: false } })).toBe(false)
|
||||||
|
expect(shouldListExpanded({ level: 1, dir: { expanded: true } })).toBe(true)
|
||||||
|
expect(shouldListExpanded({ level: 1, dir: { expanded: true, loaded: true } })).toBe(false)
|
||||||
|
expect(shouldListExpanded({ level: 1, dir: { expanded: true, loading: true } })).toBe(false)
|
||||||
|
expect(shouldListExpanded({ level: 0, dir: { expanded: true } })).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("allowed auto-expand picks only collapsed dirs", () => {
|
||||||
|
const expanded = new Set<string>()
|
||||||
|
const filter = { dirs: new Set(["src", "src/components"]) }
|
||||||
|
|
||||||
|
const first = dirsToExpand({
|
||||||
|
level: 0,
|
||||||
|
filter,
|
||||||
|
expanded: (dir) => expanded.has(dir),
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(first).toEqual(["src", "src/components"])
|
||||||
|
|
||||||
|
for (const dir of first) expanded.add(dir)
|
||||||
|
|
||||||
|
const second = dirsToExpand({
|
||||||
|
level: 0,
|
||||||
|
filter,
|
||||||
|
expanded: (dir) => expanded.has(dir),
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(second).toEqual([])
|
||||||
|
expect(dirsToExpand({ level: 1, filter, expanded: () => false })).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useFile } from "@/context/file"
|
import { useFile } from "@/context/file"
|
||||||
|
import { encodeFilePath } from "@/context/file/path"
|
||||||
import { Collapsible } from "@opencode-ai/ui/collapsible"
|
import { Collapsible } from "@opencode-ai/ui/collapsible"
|
||||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
@@ -8,6 +9,7 @@ import {
|
|||||||
createMemo,
|
createMemo,
|
||||||
For,
|
For,
|
||||||
Match,
|
Match,
|
||||||
|
on,
|
||||||
Show,
|
Show,
|
||||||
splitProps,
|
splitProps,
|
||||||
Switch,
|
Switch,
|
||||||
@@ -18,6 +20,10 @@ import {
|
|||||||
import { Dynamic } from "solid-js/web"
|
import { Dynamic } from "solid-js/web"
|
||||||
import type { FileNode } from "@opencode-ai/sdk/v2"
|
import type { FileNode } from "@opencode-ai/sdk/v2"
|
||||||
|
|
||||||
|
function pathToFileUrl(filepath: string): string {
|
||||||
|
return `file://${encodeFilePath(filepath)}`
|
||||||
|
}
|
||||||
|
|
||||||
type Kind = "add" | "del" | "mix"
|
type Kind = "add" | "del" | "mix"
|
||||||
|
|
||||||
type Filter = {
|
type Filter = {
|
||||||
@@ -25,6 +31,34 @@ type Filter = {
|
|||||||
dirs: Set<string>
|
dirs: Set<string>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function shouldListRoot(input: { level: number; dir?: { loaded?: boolean; loading?: boolean } }) {
|
||||||
|
if (input.level !== 0) return false
|
||||||
|
if (input.dir?.loaded) return false
|
||||||
|
if (input.dir?.loading) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldListExpanded(input: {
|
||||||
|
level: number
|
||||||
|
dir?: { expanded?: boolean; loaded?: boolean; loading?: boolean }
|
||||||
|
}) {
|
||||||
|
if (input.level === 0) return false
|
||||||
|
if (!input.dir?.expanded) return false
|
||||||
|
if (input.dir.loaded) return false
|
||||||
|
if (input.dir.loading) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dirsToExpand(input: {
|
||||||
|
level: number
|
||||||
|
filter?: { dirs: Set<string> }
|
||||||
|
expanded: (dir: string) => boolean
|
||||||
|
}) {
|
||||||
|
if (input.level !== 0) return []
|
||||||
|
if (!input.filter) return []
|
||||||
|
return [...input.filter.dirs].filter((dir) => !input.expanded(dir))
|
||||||
|
}
|
||||||
|
|
||||||
export default function FileTree(props: {
|
export default function FileTree(props: {
|
||||||
path: string
|
path: string
|
||||||
class?: string
|
class?: string
|
||||||
@@ -111,29 +145,89 @@ export default function FileTree(props: {
|
|||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const current = filter()
|
const current = filter()
|
||||||
if (!current) return
|
const dirs = dirsToExpand({
|
||||||
if (level !== 0) return
|
level,
|
||||||
|
filter: current,
|
||||||
for (const dir of current.dirs) {
|
expanded: (dir) => untrack(() => file.tree.state(dir)?.expanded) ?? false,
|
||||||
const expanded = untrack(() => file.tree.state(dir)?.expanded) ?? false
|
})
|
||||||
if (expanded) continue
|
for (const dir of dirs) file.tree.expand(dir)
|
||||||
file.tree.expand(dir)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
createEffect(
|
||||||
|
on(
|
||||||
|
() => props.path,
|
||||||
|
(path) => {
|
||||||
|
const dir = untrack(() => file.tree.state(path))
|
||||||
|
if (!shouldListRoot({ level, dir })) return
|
||||||
|
void file.tree.list(path)
|
||||||
|
},
|
||||||
|
{ defer: false },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const path = props.path
|
const dir = file.tree.state(props.path)
|
||||||
untrack(() => void file.tree.list(path))
|
if (!shouldListExpanded({ level, dir })) return
|
||||||
|
void file.tree.list(props.path)
|
||||||
})
|
})
|
||||||
|
|
||||||
const nodes = createMemo(() => {
|
const nodes = createMemo(() => {
|
||||||
const nodes = file.tree.children(props.path)
|
const nodes = file.tree.children(props.path)
|
||||||
const current = filter()
|
const current = filter()
|
||||||
if (!current) return nodes
|
if (!current) return nodes
|
||||||
return nodes.filter((node) => {
|
|
||||||
|
const parent = (path: string) => {
|
||||||
|
const idx = path.lastIndexOf("/")
|
||||||
|
if (idx === -1) return ""
|
||||||
|
return path.slice(0, idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
const leaf = (path: string) => {
|
||||||
|
const idx = path.lastIndexOf("/")
|
||||||
|
return idx === -1 ? path : path.slice(idx + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const out = nodes.filter((node) => {
|
||||||
if (node.type === "file") return current.files.has(node.path)
|
if (node.type === "file") return current.files.has(node.path)
|
||||||
return current.dirs.has(node.path)
|
return current.dirs.has(node.path)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const seen = new Set(out.map((node) => node.path))
|
||||||
|
|
||||||
|
for (const dir of current.dirs) {
|
||||||
|
if (parent(dir) !== props.path) continue
|
||||||
|
if (seen.has(dir)) continue
|
||||||
|
out.push({
|
||||||
|
name: leaf(dir),
|
||||||
|
path: dir,
|
||||||
|
absolute: dir,
|
||||||
|
type: "directory",
|
||||||
|
ignored: false,
|
||||||
|
})
|
||||||
|
seen.add(dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of current.files) {
|
||||||
|
if (parent(item) !== props.path) continue
|
||||||
|
if (seen.has(item)) continue
|
||||||
|
out.push({
|
||||||
|
name: leaf(item),
|
||||||
|
path: item,
|
||||||
|
absolute: item,
|
||||||
|
type: "file",
|
||||||
|
ignored: false,
|
||||||
|
})
|
||||||
|
seen.add(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
out.sort((a, b) => {
|
||||||
|
if (a.type !== b.type) {
|
||||||
|
return a.type === "directory" ? -1 : 1
|
||||||
|
}
|
||||||
|
return a.name.localeCompare(b.name)
|
||||||
|
})
|
||||||
|
|
||||||
|
return out
|
||||||
})
|
})
|
||||||
|
|
||||||
const Node = (
|
const Node = (
|
||||||
@@ -160,7 +254,7 @@ export default function FileTree(props: {
|
|||||||
onDragStart={(e: DragEvent) => {
|
onDragStart={(e: DragEvent) => {
|
||||||
if (!draggable()) return
|
if (!draggable()) return
|
||||||
e.dataTransfer?.setData("text/plain", `file:${local.node.path}`)
|
e.dataTransfer?.setData("text/plain", `file:${local.node.path}`)
|
||||||
e.dataTransfer?.setData("text/uri-list", `file://${local.node.path}`)
|
e.dataTransfer?.setData("text/uri-list", pathToFileUrl(local.node.path))
|
||||||
if (e.dataTransfer) e.dataTransfer.effectAllowed = "copy"
|
if (e.dataTransfer) e.dataTransfer.effectAllowed = "copy"
|
||||||
|
|
||||||
const dragImage = document.createElement("div")
|
const dragImage = document.createElement("div")
|
||||||
@@ -194,7 +288,7 @@ export default function FileTree(props: {
|
|||||||
: kind === "del"
|
: kind === "del"
|
||||||
? "color: var(--icon-diff-delete-base)"
|
? "color: var(--icon-diff-delete-base)"
|
||||||
: kind === "mix"
|
: kind === "mix"
|
||||||
? "color: var(--icon-diff-modified-base)"
|
? "color: var(--icon-warning-active)"
|
||||||
: undefined
|
: undefined
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
@@ -221,7 +315,7 @@ export default function FileTree(props: {
|
|||||||
? "color: var(--icon-diff-add-base)"
|
? "color: var(--icon-diff-add-base)"
|
||||||
: kind === "del"
|
: kind === "del"
|
||||||
? "color: var(--icon-diff-delete-base)"
|
? "color: var(--icon-diff-delete-base)"
|
||||||
: "color: var(--icon-diff-modified-base)"
|
: "color: var(--icon-warning-active)"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span class="shrink-0 w-4 text-center text-12-medium" style={color}>
|
<span class="shrink-0 w-4 text-center text-12-medium" style={color}>
|
||||||
@@ -236,7 +330,7 @@ export default function FileTree(props: {
|
|||||||
? "background-color: var(--icon-diff-add-base)"
|
? "background-color: var(--icon-diff-add-base)"
|
||||||
: kind === "del"
|
: kind === "del"
|
||||||
? "background-color: var(--icon-diff-delete-base)"
|
? "background-color: var(--icon-diff-delete-base)"
|
||||||
: "background-color: var(--icon-diff-modified-base)"
|
: "background-color: var(--icon-warning-active)"
|
||||||
|
|
||||||
return <div class="shrink-0 size-1.5 mr-1.5 rounded-full" style={color} />
|
return <div class="shrink-0 size-1.5 mr-1.5 rounded-full" style={color} />
|
||||||
}
|
}
|
||||||
@@ -274,7 +368,6 @@ export default function FileTree(props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
forceMount={false}
|
|
||||||
openDelay={2000}
|
openDelay={2000}
|
||||||
placement="bottom-start"
|
placement="bottom-start"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,157 @@
|
|||||||
|
import { onCleanup, onMount } from "solid-js"
|
||||||
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
|
import { usePrompt, type ContentPart, type ImageAttachmentPart } from "@/context/prompt"
|
||||||
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { uuid } from "@/utils/uuid"
|
||||||
|
import { getCursorPosition } from "./editor-dom"
|
||||||
|
|
||||||
|
export const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"]
|
||||||
|
export const ACCEPTED_FILE_TYPES = [...ACCEPTED_IMAGE_TYPES, "application/pdf"]
|
||||||
|
|
||||||
|
type PromptAttachmentsInput = {
|
||||||
|
editor: () => HTMLDivElement | undefined
|
||||||
|
isFocused: () => boolean
|
||||||
|
isDialogActive: () => boolean
|
||||||
|
setDraggingType: (type: "image" | "@mention" | null) => void
|
||||||
|
focusEditor: () => void
|
||||||
|
addPart: (part: ContentPart) => void
|
||||||
|
readClipboardImage?: () => Promise<File | null>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createPromptAttachments(input: PromptAttachmentsInput) {
|
||||||
|
const prompt = usePrompt()
|
||||||
|
const language = useLanguage()
|
||||||
|
|
||||||
|
const addImageAttachment = async (file: File) => {
|
||||||
|
if (!ACCEPTED_FILE_TYPES.includes(file.type)) return
|
||||||
|
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = () => {
|
||||||
|
const editor = input.editor()
|
||||||
|
if (!editor) return
|
||||||
|
const dataUrl = reader.result as string
|
||||||
|
const attachment: ImageAttachmentPart = {
|
||||||
|
type: "image",
|
||||||
|
id: uuid(),
|
||||||
|
filename: file.name,
|
||||||
|
mime: file.type,
|
||||||
|
dataUrl,
|
||||||
|
}
|
||||||
|
const cursorPosition = prompt.cursor() ?? getCursorPosition(editor)
|
||||||
|
prompt.set([...prompt.current(), attachment], cursorPosition)
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeImageAttachment = (id: string) => {
|
||||||
|
const current = prompt.current()
|
||||||
|
const next = current.filter((part) => part.type !== "image" || part.id !== id)
|
||||||
|
prompt.set(next, prompt.cursor())
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePaste = async (event: ClipboardEvent) => {
|
||||||
|
if (!input.isFocused()) return
|
||||||
|
const clipboardData = event.clipboardData
|
||||||
|
if (!clipboardData) return
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
|
const items = Array.from(clipboardData.items)
|
||||||
|
const fileItems = items.filter((item) => item.kind === "file")
|
||||||
|
const imageItems = fileItems.filter((item) => ACCEPTED_FILE_TYPES.includes(item.type))
|
||||||
|
|
||||||
|
if (imageItems.length > 0) {
|
||||||
|
for (const item of imageItems) {
|
||||||
|
const file = item.getAsFile()
|
||||||
|
if (file) await addImageAttachment(file)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileItems.length > 0) {
|
||||||
|
showToast({
|
||||||
|
title: language.t("prompt.toast.pasteUnsupported.title"),
|
||||||
|
description: language.t("prompt.toast.pasteUnsupported.description"),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const plainText = clipboardData.getData("text/plain") ?? ""
|
||||||
|
|
||||||
|
// Desktop: Browser clipboard has no images and no text, try platform's native clipboard for images
|
||||||
|
if (input.readClipboardImage && !plainText) {
|
||||||
|
const file = await input.readClipboardImage()
|
||||||
|
if (file) {
|
||||||
|
await addImageAttachment(file)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!plainText) return
|
||||||
|
input.addPart({ type: "text", content: plainText, start: 0, end: 0 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleGlobalDragOver = (event: DragEvent) => {
|
||||||
|
if (input.isDialogActive()) return
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
const hasFiles = event.dataTransfer?.types.includes("Files")
|
||||||
|
const hasText = event.dataTransfer?.types.includes("text/plain")
|
||||||
|
if (hasFiles) {
|
||||||
|
input.setDraggingType("image")
|
||||||
|
} else if (hasText) {
|
||||||
|
input.setDraggingType("@mention")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleGlobalDragLeave = (event: DragEvent) => {
|
||||||
|
if (input.isDialogActive()) return
|
||||||
|
if (!event.relatedTarget) {
|
||||||
|
input.setDraggingType(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleGlobalDrop = async (event: DragEvent) => {
|
||||||
|
if (input.isDialogActive()) return
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
input.setDraggingType(null)
|
||||||
|
|
||||||
|
const plainText = event.dataTransfer?.getData("text/plain")
|
||||||
|
const filePrefix = "file:"
|
||||||
|
if (plainText?.startsWith(filePrefix)) {
|
||||||
|
const filePath = plainText.slice(filePrefix.length)
|
||||||
|
input.focusEditor()
|
||||||
|
input.addPart({ type: "file", path: filePath, content: "@" + filePath, start: 0, end: 0 })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const dropped = event.dataTransfer?.files
|
||||||
|
if (!dropped) return
|
||||||
|
|
||||||
|
for (const file of Array.from(dropped)) {
|
||||||
|
if (ACCEPTED_FILE_TYPES.includes(file.type)) {
|
||||||
|
await addImageAttachment(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
document.addEventListener("dragover", handleGlobalDragOver)
|
||||||
|
document.addEventListener("dragleave", handleGlobalDragLeave)
|
||||||
|
document.addEventListener("drop", handleGlobalDrop)
|
||||||
|
})
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
document.removeEventListener("dragover", handleGlobalDragOver)
|
||||||
|
document.removeEventListener("dragleave", handleGlobalDragLeave)
|
||||||
|
document.removeEventListener("drop", handleGlobalDrop)
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
addImageAttachment,
|
||||||
|
removeImageAttachment,
|
||||||
|
handlePaste,
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user