mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-09 18:34:21 +00:00
Compare commits
1131 Commits
github-v1.
...
github-v1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a092f567b7 | ||
|
|
883a6577d5 | ||
|
|
20b52cad2a | ||
|
|
528291532b | ||
|
|
2b77a84c4f | ||
|
|
7d0b52dc29 | ||
|
|
c86c2acf4c | ||
|
|
29bf731d47 | ||
|
|
3c5a256f0f | ||
|
|
067338bc25 | ||
|
|
736cd10847 | ||
|
|
a03daa4252 | ||
|
|
b01eec38d1 | ||
|
|
35cb06e0e4 | ||
|
|
f3b7d2f786 | ||
|
|
1facf7d8e4 | ||
|
|
ddd9c71cca | ||
|
|
520a814fc2 | ||
|
|
2072c8681a | ||
|
|
21990621e2 | ||
|
|
68a0947292 | ||
|
|
f1f44644e2 | ||
|
|
afb8a0d28a | ||
|
|
f05f175842 | ||
|
|
c0b214232d | ||
|
|
f4f8f2d151 | ||
|
|
b4ad5c138e | ||
|
|
789e111a0f | ||
|
|
eaa76dad0c | ||
|
|
66f9bdab32 | ||
|
|
efaa9166fb | ||
|
|
eb2044989e | ||
|
|
5d37e58d34 | ||
|
|
20088a87b0 | ||
|
|
05019dae76 | ||
|
|
bf37a88f7f | ||
|
|
d839f70834 | ||
|
|
498a4ab408 | ||
|
|
835e48cd28 | ||
|
|
1a5a63843e | ||
|
|
d954e1e3b6 | ||
|
|
8f22a6b69d | ||
|
|
fd37d5b54e | ||
|
|
e146083b73 | ||
|
|
d7a1c268d9 | ||
|
|
5c4345da4f | ||
|
|
0be37cc2c6 | ||
|
|
64f0205f97 | ||
|
|
b3a1360ad9 | ||
|
|
08d4d6d4af | ||
|
|
9c69c1de9f | ||
|
|
735f3d17bc | ||
|
|
db7243c364 | ||
|
|
f0912ee838 | ||
|
|
983f8ffeca | ||
|
|
c474380684 | ||
|
|
7ca767de55 | ||
|
|
1954c1255e | ||
|
|
b4f33485a7 | ||
|
|
f2504d8eb2 | ||
|
|
e47f383137 | ||
|
|
cd56845dce | ||
|
|
1c24dd02a6 | ||
|
|
b7b09fdfc2 | ||
|
|
62702fbd11 | ||
|
|
22c68a6992 | ||
|
|
ca1b597b01 | ||
|
|
d527ceeb2b | ||
|
|
2e9c22d911 | ||
|
|
762c58b756 | ||
|
|
61d0b3e4dc | ||
|
|
71a7ad1a4e | ||
|
|
20399bbdfe | ||
|
|
547a975707 | ||
|
|
c009cab15b | ||
|
|
e6bc3b253b | ||
|
|
f1a13f25a4 | ||
|
|
4695e685cf | ||
|
|
c6092e4ad9 | ||
|
|
e6045ca925 | ||
|
|
ebbb4dd88a | ||
|
|
087473be6e | ||
|
|
e0eb460fc8 | ||
|
|
7c6b3f981e | ||
|
|
a5b6c57a76 | ||
|
|
65724b693b | ||
|
|
8b9a85b7e7 | ||
|
|
7cbec9a1a7 | ||
|
|
c4ba5961c8 | ||
|
|
82b432349e | ||
|
|
68ed664a3f | ||
|
|
3a30773874 | ||
|
|
0c0057a7de | ||
|
|
fa79736b87 | ||
|
|
2e0c2c9db7 | ||
|
|
3e9366487a | ||
|
|
025ed04da0 | ||
|
|
b81eca4ebc | ||
|
|
20c18689c0 | ||
|
|
a803cf8aee | ||
|
|
c526e2d908 | ||
|
|
bdbbcd8a0c | ||
|
|
43c2da24d0 | ||
|
|
3205db9c16 | ||
|
|
efbab087df | ||
|
|
9280db3297 | ||
|
|
0cc3c3bc78 | ||
|
|
ee8b38ab26 | ||
|
|
44fa3d5392 | ||
|
|
a457828a67 | ||
|
|
4752c83155 | ||
|
|
f94ee5ce90 | ||
|
|
76386f5cfc | ||
|
|
a9275def43 | ||
|
|
50ed4c6b5d | ||
|
|
f882cca98a | ||
|
|
2d2a044961 | ||
|
|
b41fbda68f | ||
|
|
794c5981a5 | ||
|
|
d5738f542c | ||
|
|
02b7eb59f8 | ||
|
|
a8f23fb548 | ||
|
|
1a642a79a6 | ||
|
|
b6b0097755 | ||
|
|
afb1cad26d | ||
|
|
58186004db | ||
|
|
a593ed4c9b | ||
|
|
449270aacc | ||
|
|
e20535655b | ||
|
|
1662e149b3 | ||
|
|
dfe3e79304 | ||
|
|
e92a2ec9db | ||
|
|
7cba1ff793 | ||
|
|
8c3cc0d447 | ||
|
|
e30562d5f4 | ||
|
|
7c06ef2477 | ||
|
|
58eccf7f53 | ||
|
|
6da60bd5d9 | ||
|
|
8a43c24934 | ||
|
|
b03172d723 | ||
|
|
e342795bd0 | ||
|
|
a44d4acb3f | ||
|
|
8b8a358de1 | ||
|
|
075fa2c0e8 | ||
|
|
8b287caaad | ||
|
|
b3e6b7a985 | ||
|
|
5c74bff8e1 | ||
|
|
172bbdaced | ||
|
|
563b4c33f2 | ||
|
|
982b71e861 | ||
|
|
75df5040ea | ||
|
|
f2b2940298 | ||
|
|
22f51c6b47 | ||
|
|
de286b08f6 | ||
|
|
0f2124db32 | ||
|
|
e30a159264 | ||
|
|
a5edf3a311 | ||
|
|
7c2907cbb4 | ||
|
|
bce9dc040f | ||
|
|
44297ffe78 | ||
|
|
cbb3141130 | ||
|
|
18cf4df6c6 | ||
|
|
f3e8a275b8 | ||
|
|
7df36cf0fa | ||
|
|
e82b112759 | ||
|
|
030b14ac4e | ||
|
|
b1e381cff7 | ||
|
|
0433d4d064 | ||
|
|
d34fdac854 | ||
|
|
ec828619ca | ||
|
|
559013e124 | ||
|
|
8e3ab4afa7 | ||
|
|
13305966e5 | ||
|
|
a618fbe8cf | ||
|
|
07dc1f8ecc | ||
|
|
445c8631a2 | ||
|
|
c4eacd0ccf | ||
|
|
dd5ec26c8c | ||
|
|
8b062ed621 | ||
|
|
ab97a95032 | ||
|
|
a98d108d2e | ||
|
|
2e875b2d65 | ||
|
|
790baec41f | ||
|
|
52fbd16e08 | ||
|
|
cf97633d7d | ||
|
|
3fe2e89d55 | ||
|
|
eb5c113cff | ||
|
|
bd9c13bb27 | ||
|
|
3019b1f825 | ||
|
|
119cc8c795 | ||
|
|
be9b2bab15 | ||
|
|
c949e5b390 | ||
|
|
1c717d62e4 | ||
|
|
27675dfd70 | ||
|
|
374275eeb6 | ||
|
|
faa848cfb1 | ||
|
|
80772e5ac2 | ||
|
|
be34747526 | ||
|
|
49d9f99924 | ||
|
|
1f9e195cd8 | ||
|
|
539d6baa8c | ||
|
|
f6fc693c1f | ||
|
|
50d8396c9a | ||
|
|
22dd70b75b | ||
|
|
b4f8de0c0a | ||
|
|
768e0553bd | ||
|
|
f3e79235fb | ||
|
|
eacf3ad361 | ||
|
|
72062d22a0 | ||
|
|
9f90f0e8ed | ||
|
|
c74bc323b6 | ||
|
|
c7b825a42a | ||
|
|
b1a613b3b9 | ||
|
|
958f1edfef | ||
|
|
2bb299d741 | ||
|
|
9930ac6929 | ||
|
|
1906a347f3 | ||
|
|
e5d0c63b29 | ||
|
|
970796b832 | ||
|
|
3c5043497c | ||
|
|
4d09c5618e | ||
|
|
adae0d1853 | ||
|
|
61aeb2a2a7 | ||
|
|
4b0f7b82ba | ||
|
|
9fb24074c8 | ||
|
|
542c9d5346 | ||
|
|
d5f0e3fccc | ||
|
|
7d2bb5cb2b | ||
|
|
ca7a70b628 | ||
|
|
b3a2f9fb4e | ||
|
|
8be5a29870 | ||
|
|
68092f22e1 | ||
|
|
83f3c729e9 | ||
|
|
e37fd9c105 | ||
|
|
2e4fe973c9 | ||
|
|
1b82511fbd | ||
|
|
f24314438b | ||
|
|
361a962673 | ||
|
|
fa9c283fcf | ||
|
|
947b864d96 | ||
|
|
03eabb10e4 | ||
|
|
34c9d106ee | ||
|
|
fe57d7bb38 | ||
|
|
68cf6b04a0 | ||
|
|
9ffaf81fb3 | ||
|
|
50530b1ea7 | ||
|
|
a160eee499 | ||
|
|
d9aef1d73d | ||
|
|
4ba0b22b04 | ||
|
|
662d2b205a | ||
|
|
75960ae00c | ||
|
|
528f198c39 | ||
|
|
184834da98 | ||
|
|
008a5c10cc | ||
|
|
2d5b9a5cc6 | ||
|
|
fb3ca895d6 | ||
|
|
d3d379fe2e | ||
|
|
b41626049c | ||
|
|
e59be27810 | ||
|
|
1e2992244f | ||
|
|
fd22b26478 | ||
|
|
ea2ee46f45 | ||
|
|
4e1b6b3417 | ||
|
|
2d52a461a0 | ||
|
|
9cce0cf4f4 | ||
|
|
a41c8508da | ||
|
|
4f7458b47d | ||
|
|
270cd05195 | ||
|
|
24c933ae60 | ||
|
|
2b7a021ba3 | ||
|
|
cbf87c50b9 | ||
|
|
3c375b971e | ||
|
|
6590c1641f | ||
|
|
0ffe496869 | ||
|
|
ce4e595881 | ||
|
|
e91cc7e514 | ||
|
|
c961072d20 | ||
|
|
429240f439 | ||
|
|
a0dc90bfcc | ||
|
|
6bac501be5 | ||
|
|
b5be883758 | ||
|
|
0021a09ba8 | ||
|
|
a8c2928a87 | ||
|
|
79f6910697 | ||
|
|
04cea9cf11 | ||
|
|
61c334f1fb | ||
|
|
85ed329318 | ||
|
|
37decee795 | ||
|
|
5d0007ade4 | ||
|
|
31dd9fd13a | ||
|
|
23fc675ad5 | ||
|
|
22b058a33d | ||
|
|
939c0940aa | ||
|
|
fd7b7eacd3 | ||
|
|
eaa0826e7f | ||
|
|
f6055ad3d2 | ||
|
|
761863ae35 | ||
|
|
dadc08ddc7 | ||
|
|
d7afb01d13 | ||
|
|
5168988156 | ||
|
|
9a3bd0ade1 | ||
|
|
4e5b0b00b0 | ||
|
|
7672b722ca | ||
|
|
488c3502a7 | ||
|
|
a2f80f7c0d | ||
|
|
dee0226741 | ||
|
|
58aba1c797 | ||
|
|
9906d42e1c | ||
|
|
43eefbc349 | ||
|
|
625c9dae5c | ||
|
|
b2341c2d9a | ||
|
|
dadddcaf57 | ||
|
|
f7b3371b02 | ||
|
|
409f8f678e | ||
|
|
dc62f9393a | ||
|
|
32e0b612d9 | ||
|
|
7d6ce6fc5e | ||
|
|
ba105246ea | ||
|
|
a10cc63403 | ||
|
|
cde06e90d0 | ||
|
|
d4e7a88bba | ||
|
|
630476afc0 | ||
|
|
5181e4e90a | ||
|
|
5db78f20e9 | ||
|
|
5fc4472921 | ||
|
|
3049ac576a | ||
|
|
494e03490e | ||
|
|
675eba6588 | ||
|
|
01eadf3ded | ||
|
|
c7a2c737e8 | ||
|
|
bb09df0c77 | ||
|
|
ecbcbfbe90 | ||
|
|
485aadcbfa | ||
|
|
d76d6db589 | ||
|
|
aa612b27d4 | ||
|
|
a35c278424 | ||
|
|
8265621d48 | ||
|
|
1016a52cf1 | ||
|
|
d0a1e6fa46 | ||
|
|
f0e559c0ed | ||
|
|
528c6c1a75 | ||
|
|
6092f8792e | ||
|
|
4142e1bcf6 | ||
|
|
49d837e0c1 | ||
|
|
b88bcd49fd | ||
|
|
3f463bc916 | ||
|
|
0b02f6d22f | ||
|
|
4ecb305820 | ||
|
|
e5a868157e | ||
|
|
f510d17bd3 | ||
|
|
45fea6587e | ||
|
|
a721810682 | ||
|
|
d486c1c7c8 | ||
|
|
9197a2a7a1 | ||
|
|
8da890649f | ||
|
|
21053732e7 | ||
|
|
cf069dd046 | ||
|
|
4dc3cb9115 | ||
|
|
5c66c8b8e1 | ||
|
|
2ca0ae7755 | ||
|
|
19123b6803 | ||
|
|
4137c66581 | ||
|
|
48d14d4cac | ||
|
|
8996185f3b | ||
|
|
4f49967518 | ||
|
|
ec637aa21e | ||
|
|
2ff9a757b6 | ||
|
|
cb8533ef5b | ||
|
|
cdbb009ab0 | ||
|
|
001b486356 | ||
|
|
d315026abc | ||
|
|
d1191675c6 | ||
|
|
362a657b4f | ||
|
|
0276885181 | ||
|
|
5a38a6f248 | ||
|
|
aef01003e7 | ||
|
|
a38e1701ee | ||
|
|
bf9ee32d4a | ||
|
|
0917991361 | ||
|
|
c6a241e331 | ||
|
|
4b7301e8ca | ||
|
|
1bf20f0a2b | ||
|
|
e3b4d4ad49 | ||
|
|
6b207b09d6 | ||
|
|
9771325026 | ||
|
|
bbd1c071c4 | ||
|
|
8e9a0c4ad0 | ||
|
|
ced093e646 | ||
|
|
283bdce358 | ||
|
|
91d5ce8bf3 | ||
|
|
7f870cc9d4 | ||
|
|
2cb3b0484b | ||
|
|
11b0df6b86 | ||
|
|
e15af828fa | ||
|
|
265cbaea7c | ||
|
|
d39ebbc947 | ||
|
|
06acd70670 | ||
|
|
c285304acf | ||
|
|
4d187af9d2 | ||
|
|
7e14cc687a | ||
|
|
2f5b2b23d5 | ||
|
|
035baa4b38 | ||
|
|
9f38af44db | ||
|
|
7324b2260a | ||
|
|
166f169dbf | ||
|
|
9c55cb729b | ||
|
|
f2e65e40ea | ||
|
|
8b3ae08a55 | ||
|
|
555d7fcdde | ||
|
|
2410a6bc9e | ||
|
|
59ed8ccbd8 | ||
|
|
91ed101378 | ||
|
|
fb60f9c396 | ||
|
|
e93699b741 | ||
|
|
9ac00f55bc | ||
|
|
393cf78ca6 | ||
|
|
478fec61ab | ||
|
|
52ad134d55 | ||
|
|
3e09abbfda | ||
|
|
5450644c67 | ||
|
|
0c2ccf25dc | ||
|
|
65c7168492 | ||
|
|
c74c66e6b4 | ||
|
|
c545fa2a28 | ||
|
|
80235f325e | ||
|
|
88c306efd2 | ||
|
|
554572bc39 | ||
|
|
e5abe1e78b | ||
|
|
1d54f90330 | ||
|
|
5f10243e91 | ||
|
|
226a5c2000 | ||
|
|
f8442ad016 | ||
|
|
1e28d10610 | ||
|
|
7304ba616e | ||
|
|
cdd6ea514b | ||
|
|
24d9c1d18d | ||
|
|
5ca2f6c5a9 | ||
|
|
12ffb270fb | ||
|
|
dc25669b6e | ||
|
|
0f9130b649 | ||
|
|
a76570b5dd | ||
|
|
97977f6ad4 | ||
|
|
555a5ccb59 | ||
|
|
24dedb4f7b | ||
|
|
21dc3c24d9 | ||
|
|
e00621cb17 | ||
|
|
2d074f0472 | ||
|
|
f3cd3b8941 | ||
|
|
1f8dab50be | ||
|
|
29672e7b95 | ||
|
|
4f3ac709a4 | ||
|
|
8aa56dc01d | ||
|
|
d72d7ab510 | ||
|
|
5053822bd6 | ||
|
|
177b01a853 | ||
|
|
c9f907caec | ||
|
|
7ce0520f8d | ||
|
|
4486174e43 | ||
|
|
41cf45a16e | ||
|
|
3611260405 | ||
|
|
c3fd3c8656 | ||
|
|
4d7d28c30a | ||
|
|
96a00ffea9 | ||
|
|
02540b2464 | ||
|
|
5aa4fd0042 | ||
|
|
b934c22d8d | ||
|
|
72cef0d9e7 | ||
|
|
d3fd6d1a10 | ||
|
|
6b12a0084c | ||
|
|
a5a19197f5 | ||
|
|
74d0d2b942 | ||
|
|
235837d2d9 | ||
|
|
dcf37000e4 | ||
|
|
5944443a60 | ||
|
|
81e8d29ad2 | ||
|
|
8b6cf7081f | ||
|
|
0b4af95223 | ||
|
|
f6cc84747a | ||
|
|
586e7347bd | ||
|
|
69d4ef038b | ||
|
|
c7c1790da8 | ||
|
|
12eea69f2e | ||
|
|
308e8060dc | ||
|
|
5f93beed77 | ||
|
|
527553ada2 | ||
|
|
5c5e636030 | ||
|
|
da6df3d432 | ||
|
|
b9b0e3475c | ||
|
|
77fcefca0e | ||
|
|
47c670aea9 | ||
|
|
2b66b31d96 | ||
|
|
f991fbbde8 | ||
|
|
401b498c7d | ||
|
|
f2ec036027 | ||
|
|
a235aec9ab | ||
|
|
052de3c556 | ||
|
|
f6fe709f6e | ||
|
|
ff0bd84870 | ||
|
|
b4af8a65ec | ||
|
|
49c5c2b1df | ||
|
|
4956ee3ebd | ||
|
|
1261b7d333 | ||
|
|
a3f38e0533 | ||
|
|
681a257df6 | ||
|
|
586207adb4 | ||
|
|
a58dbb3b5c | ||
|
|
131d8e5778 | ||
|
|
0cf0294787 | ||
|
|
3c41e4e8f1 | ||
|
|
66bc046503 | ||
|
|
6e68ea034c | ||
|
|
c51fa7cb24 | ||
|
|
a4c67515c9 | ||
|
|
1d2d710fce | ||
|
|
2fd97377f6 | ||
|
|
47ebb2973f | ||
|
|
49d7ccd1db | ||
|
|
c996f3d847 | ||
|
|
70881b2937 | ||
|
|
d8753cda02 | ||
|
|
2685de2a33 | ||
|
|
ddb1ec294e | ||
|
|
fbd9677932 | ||
|
|
814e513db7 | ||
|
|
c600114db9 | ||
|
|
038cff4a93 | ||
|
|
741cb9c0ef | ||
|
|
38e5adc491 | ||
|
|
389a5fc017 | ||
|
|
d60393835c | ||
|
|
e6ba241045 | ||
|
|
cd2c160cf6 | ||
|
|
0f34634c52 | ||
|
|
a5a569f892 | ||
|
|
afc1825cf5 | ||
|
|
6b4c433e14 | ||
|
|
797d8425e0 | ||
|
|
260eef2d66 | ||
|
|
93f1e1afb8 | ||
|
|
6acd16dde4 | ||
|
|
6647b1e22f | ||
|
|
b8872d9d20 | ||
|
|
78940d5b7e | ||
|
|
b84a1f714b | ||
|
|
07c008fe3d | ||
|
|
dad9c917d2 | ||
|
|
2aaea71eb3 | ||
|
|
db8d83b53d | ||
|
|
963f407062 | ||
|
|
4f1ef93910 | ||
|
|
05eee679a3 | ||
|
|
154c52c4d9 | ||
|
|
680db7b9e4 | ||
|
|
7aa1dbe873 | ||
|
|
76186d19f3 | ||
|
|
7760b33956 | ||
|
|
99794c25b0 | ||
|
|
351ddeed91 | ||
|
|
dccb8875ad | ||
|
|
5f2be55e54 | ||
|
|
8b35d56a48 | ||
|
|
9be944a2d2 | ||
|
|
5138f9250e | ||
|
|
e503654252 | ||
|
|
8ebc601ea2 | ||
|
|
7a3ff5b98f | ||
|
|
3b03324578 | ||
|
|
35fff0ca70 | ||
|
|
dc8586371c | ||
|
|
41f9a58c27 | ||
|
|
01237c5325 | ||
|
|
6e7fc30f94 | ||
|
|
03733b0505 | ||
|
|
d1a4295a32 | ||
|
|
6341ed506c | ||
|
|
ed745df375 | ||
|
|
80db008419 | ||
|
|
4039670a24 | ||
|
|
d59357c89b | ||
|
|
3331b0600a | ||
|
|
c131dd0829 | ||
|
|
1c25f1fae0 | ||
|
|
2da71e0a50 | ||
|
|
87978b1c17 | ||
|
|
63d2b21b8f | ||
|
|
9a1dc1ffe4 | ||
|
|
c93e7621be | ||
|
|
e842205550 | ||
|
|
b2aa387376 | ||
|
|
34aecda47c | ||
|
|
b419b0ec55 | ||
|
|
538ac208e1 | ||
|
|
16957fd107 | ||
|
|
7f3a0b8e5c | ||
|
|
d4a2652eda | ||
|
|
7a4bfbe56d | ||
|
|
31e2c8b5e9 | ||
|
|
eab23738a8 | ||
|
|
93845db462 | ||
|
|
65bc72098b | ||
|
|
b5546dce80 | ||
|
|
3807364e73 | ||
|
|
3a1cfa6c73 | ||
|
|
a2857bba83 | ||
|
|
97a0fd1d54 | ||
|
|
87f9ebd17c | ||
|
|
e7422ee782 | ||
|
|
dc3e731afe | ||
|
|
de50e7f9b9 | ||
|
|
f3db966317 | ||
|
|
decc616c80 | ||
|
|
e0450e74fb | ||
|
|
094af4dc7d | ||
|
|
2dc14718aa | ||
|
|
b97d20f252 | ||
|
|
fcfcdd95e9 | ||
|
|
c42bd492ea | ||
|
|
840fe030ab | ||
|
|
a7c4f83ca2 | ||
|
|
ed70a07201 | ||
|
|
18a5eb205f | ||
|
|
5249f04ea0 | ||
|
|
6e3ead198e | ||
|
|
4309d439f5 | ||
|
|
2ec6a21cc0 | ||
|
|
ebf5ad25c5 | ||
|
|
8aa34ab9f3 | ||
|
|
50ef866a02 | ||
|
|
3650fefe2d | ||
|
|
22091c29f1 | ||
|
|
e7e89dc5a6 | ||
|
|
34e9392bb4 | ||
|
|
05c3bc27ff | ||
|
|
b1a6333d17 | ||
|
|
5c9d619620 | ||
|
|
dfb9caa2a9 | ||
|
|
57a2b5f444 | ||
|
|
977c9a3e2c | ||
|
|
db84ee17f4 | ||
|
|
0b1f6a7d2d | ||
|
|
a6d225558c | ||
|
|
2434965b7f | ||
|
|
d4cf78bceb | ||
|
|
ed4ce67cdc | ||
|
|
94dca309e9 | ||
|
|
52e4dd110b | ||
|
|
1e74560796 | ||
|
|
48f2419d9d | ||
|
|
b9ef09a0f4 | ||
|
|
eb81994a18 | ||
|
|
a3819e088c | ||
|
|
324ae9c471 | ||
|
|
7349626757 | ||
|
|
76c25ef286 | ||
|
|
c8b3b31d27 | ||
|
|
81fef60266 | ||
|
|
3fe5d91372 | ||
|
|
7adb6e495a | ||
|
|
2039c6936f | ||
|
|
a02fefe9dc | ||
|
|
cb0e05db26 | ||
|
|
b9cdcaa9db | ||
|
|
94453eb1bd | ||
|
|
8f629db988 | ||
|
|
585378cba0 | ||
|
|
8cd8393339 | ||
|
|
b184b2fb73 | ||
|
|
c88c2da9be | ||
|
|
9b04081ae0 | ||
|
|
7d2d87fa2c | ||
|
|
787f37b382 | ||
|
|
8fa1af851c | ||
|
|
73bc3e704e | ||
|
|
8d2feed30e | ||
|
|
2d8d4e5dee | ||
|
|
b3784588ae | ||
|
|
104d52bc38 | ||
|
|
dff1fe2d28 | ||
|
|
72ab4260ee | ||
|
|
9e9b4a0555 | ||
|
|
e53192889c | ||
|
|
23bbfb3d15 | ||
|
|
37da005a01 | ||
|
|
8b708242f1 | ||
|
|
339d2dcb98 | ||
|
|
bbc8678164 | ||
|
|
a1d54475fe | ||
|
|
55c601d13a | ||
|
|
9115fac4c4 | ||
|
|
cfcb2c1fd8 | ||
|
|
221fc62135 | ||
|
|
faaef45384 | ||
|
|
2d18d80ac3 | ||
|
|
e0e07c5d48 | ||
|
|
281f9e6236 | ||
|
|
f88903a901 | ||
|
|
ad425a6a6a | ||
|
|
e635d37027 | ||
|
|
97081484d5 | ||
|
|
e451504496 | ||
|
|
53211c5d37 | ||
|
|
98b6817e20 | ||
|
|
f54d5377a4 | ||
|
|
a576fdb5e4 | ||
|
|
ae53f876f1 | ||
|
|
a7beba5aa9 | ||
|
|
e9ef72c20f | ||
|
|
fa1ac7bc95 | ||
|
|
c82ab649e2 | ||
|
|
abc7eed92b | ||
|
|
1670d220da | ||
|
|
ddc4e34731 | ||
|
|
af99d83709 | ||
|
|
ed0c0d90be | ||
|
|
e1dd9c4ccb | ||
|
|
4657fa823f | ||
|
|
1d589c7ac7 | ||
|
|
6b5a0fb261 | ||
|
|
6d93a7bf55 | ||
|
|
4ca7ab6be8 | ||
|
|
713d996b9f | ||
|
|
aa16610021 | ||
|
|
d98568fe7e | ||
|
|
1da3550c4d | ||
|
|
0c48e6a116 | ||
|
|
ef266b2c74 | ||
|
|
0a1cdc7a58 | ||
|
|
2dec956a17 | ||
|
|
ef8388f0ee | ||
|
|
e5c5b5e872 | ||
|
|
a1c9a1b8c5 | ||
|
|
76b012139a | ||
|
|
02e5a19242 | ||
|
|
af967648cb | ||
|
|
504a668a26 | ||
|
|
5efb1c7b2d | ||
|
|
fd973d242e | ||
|
|
c3d8672753 | ||
|
|
fe8ef041f6 | ||
|
|
c841de947e | ||
|
|
825dfd48b1 | ||
|
|
923d114ffa | ||
|
|
b157fd10a7 | ||
|
|
67ebe68160 | ||
|
|
7b63c14154 | ||
|
|
cdc11cde2e | ||
|
|
9721223b7e | ||
|
|
35a626e711 | ||
|
|
bb7b0ff221 | ||
|
|
68b4038196 | ||
|
|
3109214900 | ||
|
|
86ccc3409b | ||
|
|
a89089c88f | ||
|
|
e617c5d689 | ||
|
|
31983ca5ff | ||
|
|
59e3b7409f | ||
|
|
b7ce46f7a1 | ||
|
|
82b8d8fa5d | ||
|
|
77c837eb1a | ||
|
|
db77cc9845 | ||
|
|
68043edae6 | ||
|
|
337681dbbf | ||
|
|
66afc034d1 | ||
|
|
11ab8de59f | ||
|
|
5f074edc3a | ||
|
|
56b5cdf883 | ||
|
|
fb0e1e4d8d | ||
|
|
b745b1593f | ||
|
|
7376c3f8e7 | ||
|
|
831e9bce51 | ||
|
|
5de73abd82 | ||
|
|
3adbbc1b23 | ||
|
|
c6c29b3dcf | ||
|
|
a687d7c15f | ||
|
|
0c6da69f39 | ||
|
|
c4930eb6b2 | ||
|
|
a24549fce7 | ||
|
|
c0f9b13630 | ||
|
|
98fd53fd5f | ||
|
|
5b02a3029e | ||
|
|
94e851c2a2 | ||
|
|
1658a3ff59 | ||
|
|
9c8bc64138 | ||
|
|
80f704ebbf | ||
|
|
4dae6d1fcf | ||
|
|
5d2cab39da | ||
|
|
6963f96d4b | ||
|
|
05a9e7ce7a | ||
|
|
896d18ab3f | ||
|
|
893888536a | ||
|
|
c6221fc8b3 | ||
|
|
ae67f43ff0 | ||
|
|
76880dce0d | ||
|
|
aafffb5b4b | ||
|
|
a71c9e3f2e | ||
|
|
0156f03e0e | ||
|
|
e0bb96a9f9 | ||
|
|
82e5d6d458 | ||
|
|
a4411c21b6 | ||
|
|
9d61370ac4 | ||
|
|
f3febd6e39 | ||
|
|
f12d55bf1e | ||
|
|
0c19b71f42 | ||
|
|
70fa66397e | ||
|
|
6e8cd3174c | ||
|
|
5bfffbe083 | ||
|
|
29d8557d41 | ||
|
|
ffd20b4477 | ||
|
|
2abaa46e23 | ||
|
|
0cbbb20d22 | ||
|
|
81c5e7b9ed | ||
|
|
ddf4897eaa | ||
|
|
040939fb72 | ||
|
|
f89b83a6d7 | ||
|
|
82a876da4d | ||
|
|
69a15ae9c1 | ||
|
|
18c8e5f451 | ||
|
|
ba3a1cfa0b | ||
|
|
d8563160f7 | ||
|
|
4a9ff9412e | ||
|
|
d6db6ff198 | ||
|
|
79c263494f | ||
|
|
1b5bf32ce5 | ||
|
|
2e972b3fdc | ||
|
|
d70e9fb01e | ||
|
|
fc082a0f14 | ||
|
|
953e4e9446 | ||
|
|
7ea0d37ee3 | ||
|
|
e35d97f9d7 | ||
|
|
2c0d9a46cb | ||
|
|
2fe7a7f2d3 | ||
|
|
8a2f4ddf70 | ||
|
|
7a94d7a2c5 | ||
|
|
de28fafb47 | ||
|
|
9d485dd307 | ||
|
|
613813ac12 | ||
|
|
7617f59441 | ||
|
|
7aecb43e84 | ||
|
|
21eba5f987 | ||
|
|
c523ca4127 | ||
|
|
685f3ea324 | ||
|
|
4667d57e3c | ||
|
|
e6b9988fa4 | ||
|
|
3c02d5d338 | ||
|
|
bfb9787361 | ||
|
|
1bcc72c477 | ||
|
|
4385fa4dd7 | ||
|
|
2b054bec95 | ||
|
|
2cdc88d295 | ||
|
|
f8fb08b3b4 | ||
|
|
ed06de5e30 | ||
|
|
52b99622ad | ||
|
|
a15397cd89 | ||
|
|
da394439a1 | ||
|
|
390b0a79b3 | ||
|
|
b2f45d574f | ||
|
|
1e2ef07c97 | ||
|
|
664e6bf2d0 | ||
|
|
160c8ab7cc | ||
|
|
1626341a4a | ||
|
|
61ddd1716d | ||
|
|
053a10e515 | ||
|
|
e1c1b1340b | ||
|
|
7a5fbdf67c | ||
|
|
9afc451020 | ||
|
|
f4fdf0eb03 | ||
|
|
505068d5a6 | ||
|
|
2e10ffac6b | ||
|
|
4abaa052db | ||
|
|
1bcf8d8806 | ||
|
|
25c68c8061 | ||
|
|
b0e4408ecf | ||
|
|
8416db03ef | ||
|
|
d5b47d9128 | ||
|
|
634559760a | ||
|
|
155ba794cf | ||
|
|
f1ab427f0e | ||
|
|
2333af6ed3 | ||
|
|
54588b4570 | ||
|
|
26e7043718 | ||
|
|
dd569c927a | ||
|
|
cf38884778 | ||
|
|
2946a6d9a7 | ||
|
|
3522c460e3 | ||
|
|
b6a264819e | ||
|
|
46c7a41d5f | ||
|
|
7cc4b24ac2 | ||
|
|
281ce4c0c3 | ||
|
|
f59d274d0f | ||
|
|
8886c78dce | ||
|
|
d9f0f58277 | ||
|
|
effa7b45cf | ||
|
|
b307075063 | ||
|
|
aaf9a5d434 | ||
|
|
e9c2f1f3f3 | ||
|
|
7469cba7cf | ||
|
|
5420702f69 | ||
|
|
583751ecae | ||
|
|
d0a1b5ef96 | ||
|
|
42f2bc7199 | ||
|
|
603dae562a | ||
|
|
650bd76370 | ||
|
|
8aa3520683 | ||
|
|
5b5b8c57d9 | ||
|
|
f057b22e20 | ||
|
|
388d40e41f | ||
|
|
f397c92ddf | ||
|
|
6f9bea4e1f | ||
|
|
5c49b4cbfc | ||
|
|
b746e831e2 | ||
|
|
2178deef91 | ||
|
|
b1d2fb5319 | ||
|
|
2284a4e6df | ||
|
|
ad852d9186 | ||
|
|
8a9b4245b4 | ||
|
|
76ac1ccb6b | ||
|
|
e71bc8c0b0 | ||
|
|
a5301e2ab7 | ||
|
|
8eac72341f | ||
|
|
bd139b4bd6 | ||
|
|
508578bf17 | ||
|
|
607d8aafb7 | ||
|
|
5843eca7d6 | ||
|
|
ff3b68bd36 | ||
|
|
474b6fd3d1 | ||
|
|
6145b197f3 | ||
|
|
918eff9233 | ||
|
|
987e444828 | ||
|
|
99633cb299 | ||
|
|
f822331eb8 | ||
|
|
0f053769db | ||
|
|
ceeaf494c4 | ||
|
|
126d887e57 | ||
|
|
e5cfc24d6b | ||
|
|
7f8d659737 | ||
|
|
4b061653f2 | ||
|
|
eeed89f985 | ||
|
|
8ab533b616 | ||
|
|
09a399d8d6 | ||
|
|
b75575884a | ||
|
|
5688c9fd61 | ||
|
|
08a075df61 | ||
|
|
a2e8737114 | ||
|
|
776a394b02 | ||
|
|
5788b33fdf | ||
|
|
0f270c3da4 | ||
|
|
376019e347 | ||
|
|
44b773a6f6 | ||
|
|
df97774f7f | ||
|
|
eeff62a912 | ||
|
|
3fc6c42f5f | ||
|
|
967d8238be | ||
|
|
bff7518a24 | ||
|
|
8eab677094 | ||
|
|
db57e7023a | ||
|
|
ede4e467db | ||
|
|
aa1c560e5e | ||
|
|
3aca9e5fa5 | ||
|
|
9e96d83164 | ||
|
|
4275907df6 | ||
|
|
6097d6af86 | ||
|
|
09d2febe27 | ||
|
|
2c5c1ecb5e | ||
|
|
99e2112807 | ||
|
|
4b6575999d | ||
|
|
1a9ee3080c | ||
|
|
f4d61be8bd | ||
|
|
8b40e38cd7 | ||
|
|
7396d495ee | ||
|
|
f9b5ce180a | ||
|
|
12ee9d51c3 | ||
|
|
2730e0c9cd | ||
|
|
d6c81d6e14 | ||
|
|
e8ac0b663b | ||
|
|
2806f240ea | ||
|
|
9898fbe8ef | ||
|
|
1bd8e61719 | ||
|
|
b6c07cb1b8 | ||
|
|
83f23817ce | ||
|
|
23b1d7c755 | ||
|
|
ef033db9c2 | ||
|
|
e30d5d8e34 | ||
|
|
698cfb57a1 | ||
|
|
27e72f2652 | ||
|
|
10eed6ee7e | ||
|
|
59b87f60f7 | ||
|
|
d10089a0bf | ||
|
|
ae7286c031 | ||
|
|
52048c327d | ||
|
|
4e1a9b6216 | ||
|
|
1995be3599 | ||
|
|
86b9b7b15a | ||
|
|
a90f2b9723 | ||
|
|
c73a17f8af | ||
|
|
48898fda07 | ||
|
|
c573732ddb | ||
|
|
ab2a6c45a3 | ||
|
|
66563fb974 | ||
|
|
fbece0dc4d | ||
|
|
1d9e181da0 | ||
|
|
c81721e9fc | ||
|
|
a94899ed36 | ||
|
|
b18d22498c | ||
|
|
c75584a31b | ||
|
|
b474f65547 | ||
|
|
c352999b41 | ||
|
|
f4cd708ca0 | ||
|
|
c20f2731ab | ||
|
|
01ca1a384a | ||
|
|
f330dadd89 | ||
|
|
43e92b4932 | ||
|
|
83397ebde2 | ||
|
|
fde74a72bb | ||
|
|
10ee8e5b3d | ||
|
|
96d3f1fe7c | ||
|
|
1a2b656c4d | ||
|
|
161e9287a8 | ||
|
|
968543af39 | ||
|
|
5af35117db | ||
|
|
eab177f5e7 | ||
|
|
279dc04b3c | ||
|
|
cbc5903aa1 | ||
|
|
81c3c63895 | ||
|
|
b76bd4141d | ||
|
|
794fe8f381 | ||
|
|
a4eebf9f08 | ||
|
|
680a63e3de | ||
|
|
3a54ab68d1 | ||
|
|
44fd0eee64 | ||
|
|
ac371d2987 | ||
|
|
a7baa5ce18 | ||
|
|
b129f809b9 | ||
|
|
92c0ab51e2 | ||
|
|
b25418e68b | ||
|
|
046e351140 | ||
|
|
b9029afa22 | ||
|
|
b229aeec0b | ||
|
|
c9140c6bab | ||
|
|
38551bda38 | ||
|
|
cd16d31510 | ||
|
|
54ba1af5d6 | ||
|
|
fe3144ce5b | ||
|
|
a1c0bae3af | ||
|
|
85f8655dfd | ||
|
|
9b6c9f64f7 | ||
|
|
1aae1c795d | ||
|
|
526c723e62 | ||
|
|
6011200128 | ||
|
|
740fcd243c | ||
|
|
e4d8a117c4 | ||
|
|
8c4a816cf6 | ||
|
|
5605fc3f38 | ||
|
|
009b096004 | ||
|
|
64f898601b | ||
|
|
224e5466c1 | ||
|
|
87b5b34280 | ||
|
|
855fd07d22 | ||
|
|
f9be2bab3a | ||
|
|
25f1643e8e | ||
|
|
e015bea462 | ||
|
|
7dc55ac3ca | ||
|
|
cd8ecf9722 | ||
|
|
eb021a5f92 | ||
|
|
7f5e30834f | ||
|
|
750a936ae1 | ||
|
|
8dfef670b3 | ||
|
|
1b1b73b5b3 | ||
|
|
6baee0791f | ||
|
|
291b65977c | ||
|
|
90f232d7f1 | ||
|
|
af214d35cb | ||
|
|
3f0afd7cf6 | ||
|
|
0545c5da2d | ||
|
|
4a32fa6f02 | ||
|
|
29c99ed4ab | ||
|
|
753abbe164 | ||
|
|
8e01f6cc13 | ||
|
|
33c0b125cb | ||
|
|
dab2e54df8 | ||
|
|
60db171b44 | ||
|
|
c6e9a5c800 | ||
|
|
2c16b9fa61 | ||
|
|
240ad31edd | ||
|
|
a97631f769 | ||
|
|
dbaac79039 | ||
|
|
a05915ddc8 | ||
|
|
eebbd73346 | ||
|
|
d4c981495a | ||
|
|
653c206688 | ||
|
|
580f46b589 | ||
|
|
986d12fd20 | ||
|
|
d04a72a4ad | ||
|
|
5fd873a35a | ||
|
|
a9fbd786b3 | ||
|
|
abde984b3e | ||
|
|
a95aa037a3 | ||
|
|
11a92b24c2 | ||
|
|
f9c10c62d8 | ||
|
|
6339f39871 | ||
|
|
68b09b30a1 | ||
|
|
92ade2a320 | ||
|
|
cb1a1fb26c | ||
|
|
af5ebabd03 | ||
|
|
fe2626a4ea | ||
|
|
45447e3336 | ||
|
|
7a3e82ec5d | ||
|
|
345f4801e8 | ||
|
|
ac4b8d62e3 | ||
|
|
236ce7a8c0 | ||
|
|
8bdc0c8f79 | ||
|
|
04650f01fe | ||
|
|
02d4594abf | ||
|
|
c1894b4e3d | ||
|
|
2062247e72 | ||
|
|
8785bec29c | ||
|
|
d4b7f75ce3 | ||
|
|
4f73d58031 | ||
|
|
b906f2de88 | ||
|
|
4035afe5c8 | ||
|
|
8fe0715928 | ||
|
|
cb8af962cd | ||
|
|
c333ffa38b | ||
|
|
3456f4ed80 | ||
|
|
2536e9f45b | ||
|
|
9188bc542c | ||
|
|
cbaba10994 | ||
|
|
85d3604309 | ||
|
|
507ba644cf | ||
|
|
3d6f62746a | ||
|
|
2f48c8c05f | ||
|
|
4828fd1eac |
4
.github/CODEOWNERS
vendored
Normal file
4
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# web + desktop packages
|
||||
packages/app/ @adamdotdevin
|
||||
packages/tauri/ @adamdotdevin
|
||||
packages/desktop/ @adamdotdevin
|
||||
8
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
8
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -11,6 +11,14 @@ body:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: plugins
|
||||
attributes:
|
||||
label: Plugins
|
||||
description: What plugins are you using?
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: opencode-version
|
||||
attributes:
|
||||
|
||||
3
.github/pull_request_template.md
vendored
Normal file
3
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
### What does this PR do?
|
||||
|
||||
### How did you verify your code works?
|
||||
72
.github/workflows/docs-update.yml
vendored
Normal file
72
.github/workflows/docs-update.yml
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
name: Docs Update
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 */12 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
LOOKBACK_HOURS: 4
|
||||
|
||||
jobs:
|
||||
update-docs:
|
||||
if: github.repository == 'sst/opencode'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Fetch full history to access commits
|
||||
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Get recent commits
|
||||
id: commits
|
||||
run: |
|
||||
COMMITS=$(git log --since="${{ env.LOOKBACK_HOURS }} hours ago" --pretty=format:"- %h %s" 2>/dev/null || echo "")
|
||||
if [ -z "$COMMITS" ]; then
|
||||
echo "No commits in the last ${{ env.LOOKBACK_HOURS }} hours"
|
||||
echo "has_commits=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "has_commits=true" >> $GITHUB_OUTPUT
|
||||
{
|
||||
echo "list<<EOF"
|
||||
echo "$COMMITS"
|
||||
echo "EOF"
|
||||
} >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Run opencode
|
||||
if: steps.commits.outputs.has_commits == 'true'
|
||||
uses: sst/opencode/github@latest
|
||||
env:
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
with:
|
||||
model: opencode/gpt-5.2
|
||||
agent: docs
|
||||
prompt: |
|
||||
Review the following commits from the last ${{ env.LOOKBACK_HOURS }} hours and identify any new features that may need documentation.
|
||||
|
||||
<recent_commits>
|
||||
${{ steps.commits.outputs.list }}
|
||||
</recent_commits>
|
||||
|
||||
Steps:
|
||||
1. For each commit that looks like a new feature or significant change:
|
||||
- Read the changed files to understand what was added
|
||||
- Check if the feature is already documented in packages/web/src/content/docs/*
|
||||
2. If you find undocumented features:
|
||||
- Update the relevant documentation files in packages/web/src/content/docs/*
|
||||
- Follow the existing documentation style and structure
|
||||
- Make sure to document the feature clearly with examples where appropriate
|
||||
3. If all new features are already documented, report that no updates are needed
|
||||
4. If you are creating a new documentation file be sure to update packages/web/astro.config.mjs too.
|
||||
|
||||
Focus on user-facing features and API changes. Skip internal refactors, bug fixes, and test updates unless they affect user-facing behavior.
|
||||
Don't feel the need to document every little thing. It is perfectly okay to make 0 changes at all.
|
||||
Try to keep documentation only for large features or changes that already have a good spot to be documented.
|
||||
4
.github/workflows/duplicate-issues.yml
vendored
4
.github/workflows/duplicate-issues.yml
vendored
@@ -28,8 +28,8 @@ jobs:
|
||||
OPENCODE_PERMISSION: |
|
||||
{
|
||||
"bash": {
|
||||
"gh issue*": "allow",
|
||||
"*": "deny"
|
||||
"*": "deny",
|
||||
"gh issue*": "allow"
|
||||
},
|
||||
"webfetch": "deny"
|
||||
}
|
||||
|
||||
65
.github/workflows/duplicate-prs.yml
vendored
Normal file
65
.github/workflows/duplicate-prs.yml
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
name: Duplicate PR Check
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
check-duplicates:
|
||||
if: |
|
||||
github.event.pull_request.user.login != 'actions-user' &&
|
||||
github.event.pull_request.user.login != 'opencode' &&
|
||||
github.event.pull_request.user.login != 'rekram1-node' &&
|
||||
github.event.pull_request.user.login != 'thdxr' &&
|
||||
github.event.pull_request.user.login != 'kommander' &&
|
||||
github.event.pull_request.user.login != 'jayair' &&
|
||||
github.event.pull_request.user.login != 'fwang' &&
|
||||
github.event.pull_request.user.login != 'adamdotdevin' &&
|
||||
github.event.pull_request.user.login != 'iamdavidhill' &&
|
||||
github.event.pull_request.user.login != 'opencode-agent[bot]'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
||||
- name: Install opencode
|
||||
run: curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
- name: Build prompt
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
run: |
|
||||
{
|
||||
echo "Check for duplicate PRs related to this new PR:"
|
||||
echo ""
|
||||
echo "CURRENT_PR_NUMBER: $PR_NUMBER"
|
||||
echo ""
|
||||
echo "Title: $(gh pr view "$PR_NUMBER" --json title --jq .title)"
|
||||
echo ""
|
||||
echo "Description:"
|
||||
gh pr view "$PR_NUMBER" --json body --jq .body
|
||||
} > pr_info.txt
|
||||
|
||||
- name: Check for duplicate PRs
|
||||
env:
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
run: |
|
||||
COMMENT=$(bun script/duplicate-pr.ts -f pr_info.txt "Check the attached file for PR details and search for duplicates")
|
||||
|
||||
gh pr comment "$PR_NUMBER" --body "_The following comment was made by an LLM, it may be inaccurate:_
|
||||
|
||||
$COMMENT"
|
||||
42
.github/workflows/nix-desktop.yml
vendored
Normal file
42
.github/workflows/nix-desktop.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
name: nix desktop
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [dev]
|
||||
paths:
|
||||
- "flake.nix"
|
||||
- "flake.lock"
|
||||
- "nix/**"
|
||||
- "packages/app/**"
|
||||
- "packages/desktop/**"
|
||||
pull_request:
|
||||
paths:
|
||||
- "flake.nix"
|
||||
- "flake.lock"
|
||||
- "nix/**"
|
||||
- "packages/app/**"
|
||||
- "packages/desktop/**"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-desktop:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os:
|
||||
- blacksmith-4vcpu-ubuntu-2404
|
||||
- macos-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Nix
|
||||
uses: DeterminateSystems/nix-installer-action@v21
|
||||
|
||||
- name: Build desktop via flake
|
||||
run: |
|
||||
set -euo pipefail
|
||||
nix --version
|
||||
nix build .#desktop -L
|
||||
2
.github/workflows/opencode.yml
vendored
2
.github/workflows/opencode.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Run opencode
|
||||
uses: sst/opencode/github@latest
|
||||
uses: anomalyco/opencode/github@latest
|
||||
env:
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
OPENCODE_PERMISSION: '{"bash": "deny"}'
|
||||
|
||||
139
.github/workflows/pr-standards.yml
vendored
Normal file
139
.github/workflows/pr-standards.yml
vendored
Normal file
@@ -0,0 +1,139 @@
|
||||
name: PR Standards
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, edited, synchronize]
|
||||
|
||||
jobs:
|
||||
check-standards:
|
||||
if: |
|
||||
github.event.pull_request.user.login != 'actions-user' &&
|
||||
github.event.pull_request.user.login != 'opencode' &&
|
||||
github.event.pull_request.user.login != 'rekram1-node' &&
|
||||
github.event.pull_request.user.login != 'thdxr' &&
|
||||
github.event.pull_request.user.login != 'kommander' &&
|
||||
github.event.pull_request.user.login != 'jayair' &&
|
||||
github.event.pull_request.user.login != 'fwang' &&
|
||||
github.event.pull_request.user.login != 'adamdotdevin' &&
|
||||
github.event.pull_request.user.login != 'iamdavidhill' &&
|
||||
github.event.pull_request.user.login != 'opencode-agent[bot]'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Check PR standards
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const pr = context.payload.pull_request;
|
||||
const title = pr.title;
|
||||
|
||||
async function addLabel(label) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr.number,
|
||||
labels: [label]
|
||||
});
|
||||
}
|
||||
|
||||
async function removeLabel(label) {
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr.number,
|
||||
name: label
|
||||
});
|
||||
} catch (e) {
|
||||
// Label wasn't present, ignore
|
||||
}
|
||||
}
|
||||
|
||||
async function comment(marker, body) {
|
||||
const markerText = `<!-- pr-standards:${marker} -->`;
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr.number
|
||||
});
|
||||
|
||||
const existing = comments.find(c => c.body.includes(markerText));
|
||||
if (existing) return;
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr.number,
|
||||
body: markerText + '\n' + body
|
||||
});
|
||||
}
|
||||
|
||||
// Step 1: Check title format
|
||||
// Matches: feat:, feat(scope):, feat (scope):, etc.
|
||||
const titlePattern = /^(feat|fix|docs|chore|refactor|test)\s*(\([a-zA-Z0-9-]+\))?\s*:/;
|
||||
const hasValidTitle = titlePattern.test(title);
|
||||
|
||||
if (!hasValidTitle) {
|
||||
await addLabel('needs:title');
|
||||
await comment('title', `Hey! Your PR title \`${title}\` doesn't follow conventional commit format.
|
||||
|
||||
Please update it to start with one of:
|
||||
- \`feat:\` or \`feat(scope):\` new feature
|
||||
- \`fix:\` or \`fix(scope):\` bug fix
|
||||
- \`docs:\` or \`docs(scope):\` documentation changes
|
||||
- \`chore:\` or \`chore(scope):\` maintenance tasks
|
||||
- \`refactor:\` or \`refactor(scope):\` code refactoring
|
||||
- \`test:\` or \`test(scope):\` adding or updating tests
|
||||
|
||||
Where \`scope\` is the package name (e.g., \`app\`, \`desktop\`, \`opencode\`).
|
||||
|
||||
See [CONTRIBUTING.md](../blob/dev/CONTRIBUTING.md#pr-titles) for details.`);
|
||||
return;
|
||||
}
|
||||
|
||||
await removeLabel('needs:title');
|
||||
|
||||
// Step 2: Check for linked issue (skip for docs/refactor PRs)
|
||||
const skipIssueCheck = /^(docs|refactor)\s*(\([a-zA-Z0-9-]+\))?\s*:/.test(title);
|
||||
if (skipIssueCheck) {
|
||||
await removeLabel('needs:issue');
|
||||
console.log('Skipping issue check for docs/refactor PR');
|
||||
return;
|
||||
}
|
||||
const query = `
|
||||
query($owner: String!, $repo: String!, $number: Int!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequest(number: $number) {
|
||||
closingIssuesReferences(first: 1) {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const result = await github.graphql(query, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
number: pr.number
|
||||
});
|
||||
|
||||
const linkedIssues = result.repository.pullRequest.closingIssuesReferences.totalCount;
|
||||
|
||||
if (linkedIssues === 0) {
|
||||
await addLabel('needs:issue');
|
||||
await comment('issue', `Thanks for your contribution!
|
||||
|
||||
This PR doesn't have a linked issue. All PRs must reference an existing issue.
|
||||
|
||||
Please:
|
||||
1. Open an issue describing the bug/feature (if one doesn't exist)
|
||||
2. Add \`Fixes #<number>\` or \`Closes #<number>\` to this PR description
|
||||
|
||||
See [CONTRIBUTING.md](../blob/dev/CONTRIBUTING.md#issue-first-policy) for details.`);
|
||||
return;
|
||||
}
|
||||
|
||||
await removeLabel('needs:issue');
|
||||
console.log('PR meets all standards');
|
||||
46
.github/workflows/publish.yml
vendored
46
.github/workflows/publish.yml
vendored
@@ -31,7 +31,7 @@ permissions:
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
if: github.repository == 'sst/opencode'
|
||||
if: github.repository == 'anomalyco/opencode'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
@@ -79,6 +79,12 @@ jobs:
|
||||
AUR_KEY: ${{ secrets.AUR_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
|
||||
NPM_CONFIG_PROVENANCE: false
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: opencode-cli
|
||||
path: packages/opencode/dist
|
||||
|
||||
outputs:
|
||||
release: ${{ steps.publish.outputs.release }}
|
||||
tag: ${{ steps.publish.outputs.tag }}
|
||||
@@ -86,7 +92,7 @@ jobs:
|
||||
|
||||
publish-tauri:
|
||||
needs: publish
|
||||
continue-on-error: true
|
||||
continue-on-error: false
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -99,6 +105,8 @@ jobs:
|
||||
target: x86_64-pc-windows-msvc
|
||||
- host: blacksmith-4vcpu-ubuntu-2404
|
||||
target: x86_64-unknown-linux-gnu
|
||||
- host: blacksmith-4vcpu-ubuntu-2404-arm
|
||||
target: aarch64-unknown-linux-gnu
|
||||
runs-on: ${{ matrix.settings.host }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -143,13 +151,12 @@ jobs:
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: packages/tauri/src-tauri
|
||||
workspaces: packages/desktop/src-tauri
|
||||
shared-key: ${{ matrix.settings.target }}
|
||||
|
||||
- name: Prepare
|
||||
if: inputs.bump || inputs.version
|
||||
run: |
|
||||
cd packages/tauri
|
||||
cd packages/desktop
|
||||
bun ./scripts/prepare.ts
|
||||
env:
|
||||
OPENCODE_VERSION: ${{ needs.publish.outputs.version }}
|
||||
@@ -159,6 +166,7 @@ jobs:
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
RUST_TARGET: ${{ matrix.settings.target }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||
|
||||
# Fixes AppImage build issues, can be removed when https://github.com/tauri-apps/tauri/pull/12491 is released
|
||||
- name: Install tauri-cli from portable appimage branch
|
||||
@@ -169,8 +177,22 @@ jobs:
|
||||
cargo tauri --version
|
||||
|
||||
- name: Build and upload artifacts
|
||||
timeout-minutes: 20
|
||||
uses: tauri-apps/tauri-action@390cbe447412ced1303d35abe75287949e43437a
|
||||
uses: Wandalen/wretry.action@v3
|
||||
timeout-minutes: 60
|
||||
with:
|
||||
attempt_limit: 3
|
||||
attempt_delay: 10000
|
||||
action: tauri-apps/tauri-action@390cbe447412ced1303d35abe75287949e43437a
|
||||
with: |
|
||||
projectPath: packages/desktop
|
||||
uploadWorkflowArtifacts: true
|
||||
tauriScript: ${{ (contains(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }}
|
||||
args: --target ${{ matrix.settings.target }} --config ./src-tauri/tauri.prod.conf.json --verbose
|
||||
updaterJsonPreferNsis: true
|
||||
releaseId: ${{ needs.publish.outputs.release }}
|
||||
tagName: ${{ needs.publish.outputs.tag }}
|
||||
releaseAssetNamePattern: opencode-desktop-[platform]-[arch][ext]
|
||||
releaseDraft: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_BUNDLER_NEW_APPIMAGE_FORMAT: true
|
||||
@@ -182,16 +204,6 @@ jobs:
|
||||
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
||||
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
|
||||
APPLE_API_KEY_PATH: ${{ runner.temp }}/apple-api-key.p8
|
||||
with:
|
||||
projectPath: packages/tauri
|
||||
uploadWorkflowArtifacts: true
|
||||
tauriScript: ${{ (contains(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }}
|
||||
args: --target ${{ matrix.settings.target }} --config src-tauri/tauri.prod.conf.json
|
||||
updaterJsonPreferNsis: true
|
||||
releaseId: ${{ needs.publish.outputs.release }}
|
||||
tagName: ${{ needs.publish.outputs.tag }}
|
||||
releaseAssetNamePattern: opencode-desktop-[platform]-[arch][ext]
|
||||
releaseDraft: true
|
||||
|
||||
publish-release:
|
||||
needs:
|
||||
|
||||
4
.github/workflows/review.yml
vendored
4
.github/workflows/review.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
OPENCODE_PERMISSION: '{ "bash": { "gh*": "allow", "gh pr review*": "deny", "*": "deny" } }'
|
||||
OPENCODE_PERMISSION: '{ "bash": { "*": "deny", "gh*": "allow", "gh pr review*": "deny" } }'
|
||||
PR_TITLE: ${{ steps.pr-details.outputs.title }}
|
||||
run: |
|
||||
PR_BODY=$(jq -r .body pr_data.json)
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
Please check all the code changes in this pull request against the style guide, also look for any bugs if they exist. Diffs are important but make sure you read the entire file to get proper context. Make it clear the suggestions are merely suggestions and the human can decide what to do
|
||||
|
||||
When critiquing code against the style guide, be sure that the code is ACTUALLY in violation, don't complain about else statements if they already use early returns there. You may complain about excessive nesting though, regardless of else statement usage.
|
||||
When critiquing code style don't be a zealot, we don't like "let" statements but sometimes they are the simpliest option, if someone does a bunch of nesting with let, they should consider using iife (see packages/opencode/src/util.iife.ts)
|
||||
When critiquing code style don't be a zealot, we don't like "let" statements but sometimes they are the simplest option, if someone does a bunch of nesting with let, they should consider using iife (see packages/opencode/src/util.iife.ts)
|
||||
|
||||
Use the gh cli to create comments on the files for the violations. Try to leave the comment on the exact line number. If you have a suggested fix include it in a suggestion code block.
|
||||
If you are writing suggested fixes, BE SURE THAT the change you are recommending is actually valid typescript, often I have seen missing closing "}" or other syntax errors.
|
||||
|
||||
33
.github/workflows/stale-issues.yml
vendored
Normal file
33
.github/workflows/stale-issues.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: "Auto-close stale issues"
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "30 1 * * *" # Daily at 1:30 AM
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
DAYS_BEFORE_STALE: 90
|
||||
DAYS_BEFORE_CLOSE: 7
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/stale@v10
|
||||
with:
|
||||
days-before-stale: ${{ env.DAYS_BEFORE_STALE }}
|
||||
days-before-close: ${{ env.DAYS_BEFORE_CLOSE }}
|
||||
stale-issue-label: "stale"
|
||||
close-issue-message: |
|
||||
[automated] Closing due to ${{ env.DAYS_BEFORE_STALE }}+ days of inactivity.
|
||||
|
||||
Feel free to reopen if you still need this!
|
||||
stale-issue-message: |
|
||||
[automated] This issue has had no activity for ${{ env.DAYS_BEFORE_STALE }} days.
|
||||
|
||||
It will be closed in ${{ env.DAYS_BEFORE_CLOSE }} days if there's no new activity.
|
||||
remove-stale-when-updated: true
|
||||
exempt-issue-labels: "pinned,security,feature-request,on-hold"
|
||||
start-date: "2025-12-27"
|
||||
3
.github/workflows/stats.yml
vendored
3
.github/workflows/stats.yml
vendored
@@ -5,8 +5,11 @@ on:
|
||||
- cron: "0 12 * * *" # Run daily at 12:00 UTC
|
||||
workflow_dispatch: # Allow manual trigger
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
stats:
|
||||
if: github.repository == 'anomalyco/opencode'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
7
.github/workflows/sync-zed-extension.yml
vendored
7
.github/workflows/sync-zed-extension.yml
vendored
@@ -2,8 +2,8 @@ name: "sync-zed-extension"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
# release:
|
||||
# types: [published]
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
zed:
|
||||
@@ -31,4 +31,5 @@ jobs:
|
||||
run: |
|
||||
./script/sync-zed.ts ${{ steps.get_tag.outputs.tag }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
|
||||
ZED_EXTENSIONS_PAT: ${{ secrets.ZED_EXTENSIONS_PAT }}
|
||||
ZED_PR_PAT: ${{ secrets.ZED_PR_PAT }}
|
||||
|
||||
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
@@ -2,11 +2,9 @@ name: test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- production
|
||||
branches:
|
||||
- dev
|
||||
pull_request:
|
||||
branches-ignore:
|
||||
- production
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
test:
|
||||
|
||||
99
.github/workflows/update-nix-hashes.yml
vendored
99
.github/workflows/update-nix-hashes.yml
vendored
@@ -17,7 +17,7 @@ on:
|
||||
- "packages/*/package.json"
|
||||
|
||||
jobs:
|
||||
update:
|
||||
update-linux:
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
env:
|
||||
@@ -47,14 +47,14 @@ jobs:
|
||||
nix flake update
|
||||
echo "✅ flake.lock updated successfully"
|
||||
|
||||
- name: Update node_modules hash
|
||||
- name: Update node_modules hash for x86_64-linux
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "🔄 Updating node_modules hash..."
|
||||
echo "🔄 Updating node_modules hash for x86_64-linux..."
|
||||
nix/scripts/update-hashes.sh
|
||||
echo "✅ node_modules hash updated successfully"
|
||||
echo "✅ node_modules hash for x86_64-linux updated successfully"
|
||||
|
||||
- name: Commit hash changes
|
||||
- name: Commit Linux hash changes
|
||||
env:
|
||||
TARGET_BRANCH: ${{ github.head_ref || github.ref_name }}
|
||||
run: |
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
summarize() {
|
||||
local status="$1"
|
||||
{
|
||||
echo "### Nix Hash Update"
|
||||
echo "### Nix Hash Update (x86_64-linux)"
|
||||
echo ""
|
||||
echo "- ref: ${GITHUB_REF_NAME}"
|
||||
echo "- status: ${status}"
|
||||
@@ -89,7 +89,92 @@ jobs:
|
||||
echo "🔗 Staging files..."
|
||||
git add "${FILES[@]}"
|
||||
echo "💾 Committing changes..."
|
||||
git commit -m "Update Nix flake.lock and hashes"
|
||||
git commit -m "Update Nix flake.lock and x86_64-linux hash"
|
||||
echo "✅ Changes committed"
|
||||
|
||||
BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}"
|
||||
echo "🌳 Pulling latest from branch: $BRANCH"
|
||||
git pull --rebase origin "$BRANCH"
|
||||
echo "🚀 Pushing changes to branch: $BRANCH"
|
||||
git push origin HEAD:"$BRANCH"
|
||||
echo "✅ Changes pushed successfully"
|
||||
|
||||
summarize "committed $(git rev-parse --short HEAD)"
|
||||
|
||||
update-macos:
|
||||
needs: update-linux
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: macos-latest
|
||||
env:
|
||||
SYSTEM: aarch64-darwin
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.head_ref || github.ref_name }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
|
||||
|
||||
- name: Setup Nix
|
||||
uses: DeterminateSystems/nix-installer-action@v20
|
||||
|
||||
- name: Configure git
|
||||
run: |
|
||||
git config --global user.email "action@github.com"
|
||||
git config --global user.name "Github Action"
|
||||
|
||||
- name: Pull latest changes
|
||||
env:
|
||||
TARGET_BRANCH: ${{ github.head_ref || github.ref_name }}
|
||||
run: |
|
||||
BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}"
|
||||
git pull origin "$BRANCH"
|
||||
|
||||
- name: Update node_modules hash for aarch64-darwin
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "🔄 Updating node_modules hash for aarch64-darwin..."
|
||||
nix/scripts/update-hashes.sh
|
||||
echo "✅ node_modules hash for aarch64-darwin updated successfully"
|
||||
|
||||
- name: Commit macOS hash changes
|
||||
env:
|
||||
TARGET_BRANCH: ${{ github.head_ref || github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
echo "🔍 Checking for changes in tracked Nix files..."
|
||||
|
||||
summarize() {
|
||||
local status="$1"
|
||||
{
|
||||
echo "### Nix Hash Update (aarch64-darwin)"
|
||||
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=(nix/hashes.json)
|
||||
STATUS="$(git status --short -- "${FILES[@]}" || true)"
|
||||
if [ -z "$STATUS" ]; then
|
||||
echo "✅ No changes detected. Hash is already up to date."
|
||||
summarize "no changes"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "📝 Changes detected:"
|
||||
echo "$STATUS"
|
||||
echo "🔗 Staging files..."
|
||||
git add "${FILES[@]}"
|
||||
echo "💾 Committing changes..."
|
||||
git commit -m "Update aarch64-darwin hash"
|
||||
echo "✅ Changes committed"
|
||||
|
||||
BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}"
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -20,3 +20,8 @@ opencode.json
|
||||
a.out
|
||||
target
|
||||
.scripts
|
||||
|
||||
# Local dev files
|
||||
opencode-dev
|
||||
logs/
|
||||
*.bun-build
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
---
|
||||
description: ALWAYS use this when writing docs
|
||||
color: "#38A3EE"
|
||||
---
|
||||
|
||||
You are an expert technical documentation writer
|
||||
|
||||
You are not verbose
|
||||
|
||||
Use a relaxed and friendly tone
|
||||
|
||||
The title of the page should be a word or a 2-3 word phrase
|
||||
|
||||
The description should be one short line, should not start with "The", should
|
||||
|
||||
26
.opencode/agent/duplicate-pr.md
Normal file
26
.opencode/agent/duplicate-pr.md
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
mode: primary
|
||||
hidden: true
|
||||
model: opencode/claude-haiku-4-5
|
||||
color: "#E67E22"
|
||||
tools:
|
||||
"*": false
|
||||
"github-pr-search": true
|
||||
---
|
||||
|
||||
You are a duplicate PR detection agent. When a PR is opened, your job is to search for potentially duplicate or related open PRs.
|
||||
|
||||
Use the github-pr-search tool to search for PRs that might be addressing the same issue or feature.
|
||||
|
||||
IMPORTANT: The input will contain a line `CURRENT_PR_NUMBER: NNNN`. This is the current PR number, you should not mark that the current PR as a duplicate of itself.
|
||||
|
||||
Search using keywords from the PR title and description. Try multiple searches with different relevant terms.
|
||||
|
||||
If you find potential duplicates:
|
||||
|
||||
- List them with their titles and URLs
|
||||
- Briefly explain why they might be related
|
||||
|
||||
If no duplicates are found, say so clearly. BUT ONLY SAY "No duplicate PRs found" (don't say anything else if no dups)
|
||||
|
||||
Keep your response concise and actionable.
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
description: Use this agent when you are asked to commit and push code changes to a git repository.
|
||||
mode: subagent
|
||||
---
|
||||
|
||||
You commit and push to git
|
||||
|
||||
Commit messages should be brief since they are used to generate release notes.
|
||||
|
||||
Messages should say WHY the change was made and not WHAT was changed.
|
||||
@@ -2,6 +2,7 @@
|
||||
mode: primary
|
||||
hidden: true
|
||||
model: opencode/claude-haiku-4-5
|
||||
color: "#44BA81"
|
||||
tools:
|
||||
"*": false
|
||||
"github-triage": true
|
||||
@@ -44,9 +45,9 @@ Desktop app issues:
|
||||
|
||||
#### zen
|
||||
|
||||
**Only** add if the issue mentions "zen" or "opencode zen". Zen is our gateway for coding models. **Do not** add for other gateways or inference providers.
|
||||
**Only** add if the issue mentions "zen" or "opencode zen" or "opencode black".
|
||||
|
||||
If the issue doesn't have "zen" in it then don't add zen label
|
||||
If the issue doesn't have "zen" or "opencode black" in it then don't add zen label
|
||||
|
||||
#### docs
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ description: "find issue(s) on github"
|
||||
model: opencode/claude-haiku-4-5
|
||||
---
|
||||
|
||||
Search through existing issues in sst/opencode using the gh cli to find issues matching this query:
|
||||
Search through existing issues in anomalyco/opencode using the gh cli to find issues matching this query:
|
||||
|
||||
$ARGUMENTS
|
||||
|
||||
|
||||
@@ -10,8 +10,14 @@
|
||||
"options": {},
|
||||
},
|
||||
},
|
||||
"mcp": {},
|
||||
"mcp": {
|
||||
"context7": {
|
||||
"type": "remote",
|
||||
"url": "https://mcp.context7.com/mcp",
|
||||
},
|
||||
},
|
||||
"tools": {
|
||||
"github-triage": false,
|
||||
"github-pr-search": false,
|
||||
},
|
||||
}
|
||||
|
||||
6
.opencode/skill/test-skill/SKILL.md
Normal file
6
.opencode/skill/test-skill/SKILL.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
name: test-skill
|
||||
description: use this when asked to test skill
|
||||
---
|
||||
|
||||
woah this is a test skill
|
||||
57
.opencode/tool/github-pr-search.ts
Normal file
57
.opencode/tool/github-pr-search.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/// <reference path="../env.d.ts" />
|
||||
import { tool } from "@opencode-ai/plugin"
|
||||
import DESCRIPTION from "./github-pr-search.txt"
|
||||
|
||||
async function githubFetch(endpoint: string, options: RequestInit = {}) {
|
||||
const response = await fetch(`https://api.github.com${endpoint}`, {
|
||||
...options,
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
|
||||
Accept: "application/vnd.github+json",
|
||||
"Content-Type": "application/json",
|
||||
...options.headers,
|
||||
},
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
return response.json()
|
||||
}
|
||||
|
||||
interface PR {
|
||||
title: string
|
||||
html_url: string
|
||||
}
|
||||
|
||||
export default tool({
|
||||
description: DESCRIPTION,
|
||||
args: {
|
||||
query: tool.schema.string().describe("Search query for PR titles and descriptions"),
|
||||
limit: tool.schema.number().describe("Maximum number of results to return").default(10),
|
||||
offset: tool.schema.number().describe("Number of results to skip for pagination").default(0),
|
||||
},
|
||||
async execute(args) {
|
||||
const owner = "anomalyco"
|
||||
const repo = "opencode"
|
||||
|
||||
const page = Math.floor(args.offset / args.limit) + 1
|
||||
const searchQuery = encodeURIComponent(`${args.query} repo:${owner}/${repo} type:pr state:open`)
|
||||
const result = await githubFetch(
|
||||
`/search/issues?q=${searchQuery}&per_page=${args.limit}&page=${page}&sort=updated&order=desc`,
|
||||
)
|
||||
|
||||
if (result.total_count === 0) {
|
||||
return `No PRs found matching "${args.query}"`
|
||||
}
|
||||
|
||||
const prs = result.items as PR[]
|
||||
|
||||
if (prs.length === 0) {
|
||||
return `No other PRs found matching "${args.query}"`
|
||||
}
|
||||
|
||||
const formatted = prs.map((pr) => `${pr.title}\n${pr.html_url}`).join("\n\n")
|
||||
|
||||
return `Found ${result.total_count} PRs (showing ${prs.length}):\n\n${formatted}`
|
||||
},
|
||||
})
|
||||
10
.opencode/tool/github-pr-search.txt
Normal file
10
.opencode/tool/github-pr-search.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
Use this tool to search GitHub pull requests by title and description.
|
||||
|
||||
This tool searches PRs in the sst/opencode repository and returns LLM-friendly results including:
|
||||
- PR number and title
|
||||
- Author
|
||||
- State (open/closed/merged)
|
||||
- Labels
|
||||
- Description snippet
|
||||
|
||||
Use the query parameter to search for keywords that might appear in PR titles or descriptions.
|
||||
@@ -40,7 +40,7 @@ export default tool({
|
||||
async execute(args) {
|
||||
const issue = getIssueNumber()
|
||||
// const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN })
|
||||
const owner = "sst"
|
||||
const owner = "anomalyco"
|
||||
const repo = "opencode"
|
||||
|
||||
const results: string[] = []
|
||||
|
||||
38
AGENTS.md
38
AGENTS.md
@@ -1,34 +1,4 @@
|
||||
## Debugging
|
||||
|
||||
- To test opencode in the `packages/opencode` directory you can run `bun dev`
|
||||
|
||||
## Tool Calling
|
||||
|
||||
- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE. Here is an example illustrating how to execute 3 parallel file reads in this chat environment:
|
||||
|
||||
json
|
||||
{
|
||||
"recipient_name": "multi_tool_use.parallel",
|
||||
"parameters": {
|
||||
"tool_uses": [
|
||||
{
|
||||
"recipient_name": "functions.read",
|
||||
"parameters": {
|
||||
"filePath": "path/to/file.tsx"
|
||||
}
|
||||
},
|
||||
{
|
||||
"recipient_name": "functions.read",
|
||||
"parameters": {
|
||||
"filePath": "path/to/file.ts"
|
||||
}
|
||||
},
|
||||
{
|
||||
"recipient_name": "functions.read",
|
||||
"parameters": {
|
||||
"filePath": "path/to/file.md"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
- To test opencode in `packages/opencode`, run `bun dev`.
|
||||
- To regenerate the JavaScript SDK, run `./packages/sdk/js/script/build.ts`.
|
||||
- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE.
|
||||
- The default branch in this repo is `dev`.
|
||||
|
||||
147
CONTRIBUTING.md
147
CONTRIBUTING.md
@@ -14,10 +14,10 @@ However, any UI or core product feature must go through a design review with the
|
||||
|
||||
If you are unsure if a PR would be accepted, feel free to ask a maintainer or look for issues with any of the following labels:
|
||||
|
||||
- [`help wanted`](https://github.com/sst/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3Ahelp-wanted)
|
||||
- [`good first issue`](https://github.com/sst/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22)
|
||||
- [`bug`](https://github.com/sst/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3Abug)
|
||||
- [`perf`](https://github.com/sst/opencode/issues?q=is%3Aopen%20is%3Aissue%20label%3A%22perf%22)
|
||||
- [`help wanted`](https://github.com/anomalyco/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3Ahelp-wanted)
|
||||
- [`good first issue`](https://github.com/anomalyco/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22)
|
||||
- [`bug`](https://github.com/anomalyco/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3Abug)
|
||||
- [`perf`](https://github.com/anomalyco/opencode/issues?q=is%3Aopen%20is%3Aissue%20label%3A%22perf%22)
|
||||
|
||||
> [!NOTE]
|
||||
> PRs that ignore these guardrails will likely be closed.
|
||||
@@ -34,11 +34,82 @@ Want to take on an issue? Leave a comment and a maintainer may assign it to you
|
||||
bun dev
|
||||
```
|
||||
|
||||
### Running against a different directory
|
||||
|
||||
By default, `bun dev` runs OpenCode in the `packages/opencode` directory. To run it against a different directory or repository:
|
||||
|
||||
```bash
|
||||
bun dev <directory>
|
||||
```
|
||||
|
||||
To run OpenCode in the root of the opencode repo itself:
|
||||
|
||||
```bash
|
||||
bun dev .
|
||||
```
|
||||
|
||||
### Building a "localcode"
|
||||
|
||||
To compile a standalone executable:
|
||||
|
||||
```bash
|
||||
./packages/opencode/script/build.ts --single
|
||||
```
|
||||
|
||||
Then run it with:
|
||||
|
||||
```bash
|
||||
./packages/opencode/dist/opencode-<platform>/bin/opencode
|
||||
```
|
||||
|
||||
Replace `<platform>` with your platform (e.g., `darwin-arm64`, `linux-x64`).
|
||||
|
||||
- Core pieces:
|
||||
- `packages/opencode`: OpenCode core business logic & server.
|
||||
- `packages/opencode/src/cli/cmd/tui/`: The TUI code, written in SolidJS with [opentui](https://github.com/sst/opentui)
|
||||
- `packages/app`: The shared web UI components, written in SolidJS
|
||||
- `packages/desktop`: The native desktop app, built with Tauri (wraps `packages/app`)
|
||||
- `packages/plugin`: Source for `@opencode-ai/plugin`
|
||||
|
||||
### Running the Web App
|
||||
|
||||
To test UI changes during development, run the web app:
|
||||
|
||||
```bash
|
||||
bun run --cwd packages/app dev
|
||||
```
|
||||
|
||||
This starts a local dev server at http://localhost:5173 (or similar port shown in output). Most UI changes can be tested here.
|
||||
|
||||
### Running the Desktop App
|
||||
|
||||
The desktop app is a native Tauri application that wraps the web UI.
|
||||
|
||||
To run the native desktop app:
|
||||
|
||||
```bash
|
||||
bun run --cwd packages/desktop tauri dev
|
||||
```
|
||||
|
||||
This starts the web dev server on http://localhost:1420 and opens the native window.
|
||||
|
||||
If you only want the web dev server (no native shell):
|
||||
|
||||
```bash
|
||||
bun run --cwd packages/desktop dev
|
||||
```
|
||||
|
||||
To create a production `dist/` and build the native app bundle:
|
||||
|
||||
```bash
|
||||
bun run --cwd packages/desktop tauri build
|
||||
```
|
||||
|
||||
This runs `bun run --cwd packages/desktop build` automatically via Tauri’s `beforeBuildCommand`.
|
||||
|
||||
> [!NOTE]
|
||||
> Running the desktop app requires additional Tauri dependencies (Rust toolchain, platform-specific libraries). See the [Tauri prerequisites](https://v2.tauri.app/start/prerequisites/) for setup instructions.
|
||||
|
||||
> [!NOTE]
|
||||
> If you make changes to the API or SDK (e.g. `packages/opencode/src/server/server.ts`), run `./script/generate.ts` to regenerate the SDK and related files.
|
||||
|
||||
@@ -53,12 +124,12 @@ your debugger via that URL. Other methods can result in breakpoints being mapped
|
||||
|
||||
Caveats:
|
||||
|
||||
- `*.tsx` files won't have their breakpoints correctly mapped. This seems due to Bun currently not supporting source maps on code transformed
|
||||
via `BunPlugin`s (currently necessary due to our dependency on `@opentui/solid`). Currently, the best you can do in terms of debugging `*.tsx`
|
||||
files is writing a `debugger;` statement. Debugging facilities like stepping won't work, but at least you will be informed if a specific code
|
||||
is triggered.
|
||||
- If you want to run the OpenCode TUI and have breakpoints triggered in the server code, you might need to run `bun dev spawn` instead of
|
||||
the usual `bun dev`. This is because `bun dev` runs the server in a worker thread and breakpoints might not work there.
|
||||
- If `spawn` does not work for you, you can debug the server separately:
|
||||
- Debug server: `bun run --inspect=ws://localhost:6499/ ./src/index.ts serve --port 4096`,
|
||||
then attach TUI with `opencode attach http://localhost:4096`
|
||||
- Debug TUI: `bun run --inspect=ws://localhost:6499/ --conditions=browser ./src/index.ts`
|
||||
|
||||
Other tips and tricks:
|
||||
|
||||
@@ -78,11 +149,63 @@ With that said, you may want to try these methods, as they might work for you.
|
||||
|
||||
## Pull Request Expectations
|
||||
|
||||
- Try to keep pull requests small and focused.
|
||||
- Link relevant issue(s) in the description
|
||||
### Issue First Policy
|
||||
|
||||
**All PRs must reference an existing issue.** Before opening a PR, open an issue describing the bug or feature. This helps maintainers triage and prevents duplicate work. PRs without a linked issue may be closed without review.
|
||||
|
||||
- Use `Fixes #123` or `Closes #123` in your PR description to link the issue
|
||||
- For small fixes, a brief issue is fine - just enough context for maintainers to understand the problem
|
||||
|
||||
### General Requirements
|
||||
|
||||
- Keep pull requests small and focused
|
||||
- Explain the issue and why your change fixes it
|
||||
- Avoid having verbose LLM generated PR descriptions
|
||||
- Before adding new functions or functionality, ensure that such behavior doesn't already exist elsewhere in the codebase.
|
||||
- Before adding new functionality, ensure it doesn't already exist elsewhere in the codebase
|
||||
|
||||
### UI Changes
|
||||
|
||||
If your PR includes UI changes, please include screenshots or videos showing the before and after. This helps maintainers review faster and gives you quicker feedback.
|
||||
|
||||
### Logic Changes
|
||||
|
||||
For non-UI changes (bug fixes, new features, refactors), explain **how you verified it works**:
|
||||
|
||||
- What did you test?
|
||||
- How can a reviewer reproduce/confirm the fix?
|
||||
|
||||
### No AI-Generated Walls of Text
|
||||
|
||||
Long, AI-generated PR descriptions and issues are not acceptable and may be ignored. Respect the maintainers' time:
|
||||
|
||||
- Write short, focused descriptions
|
||||
- Explain what changed and why in your own words
|
||||
- If you can't explain it briefly, your PR might be too large
|
||||
|
||||
### PR Titles
|
||||
|
||||
PR titles should follow conventional commit standards:
|
||||
|
||||
- `feat:` new feature or functionality
|
||||
- `fix:` bug fix
|
||||
- `docs:` documentation or README changes
|
||||
- `chore:` maintenance tasks, dependency updates, etc.
|
||||
- `refactor:` code refactoring without changing behavior
|
||||
- `test:` adding or updating tests
|
||||
|
||||
You can optionally include a scope to indicate which package is affected:
|
||||
|
||||
- `feat(app):` feature in the app package
|
||||
- `fix(desktop):` bug fix in the desktop package
|
||||
- `chore(opencode):` maintenance in the opencode package
|
||||
|
||||
Examples:
|
||||
|
||||
- `docs: update contributing guidelines`
|
||||
- `fix: resolve crash on startup`
|
||||
- `feat: add dark mode support`
|
||||
- `feat(app): add dark mode support`
|
||||
- `fix(desktop): resolve crash on startup`
|
||||
- `chore: bump dependency versions`
|
||||
|
||||
### Style Preferences
|
||||
|
||||
|
||||
22
README.md
22
README.md
@@ -11,7 +11,7 @@
|
||||
<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/sst/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/sst/opencode/publish.yml?style=flat-square&branch=dev" /></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>
|
||||
|
||||
[](https://opencode.ai)
|
||||
@@ -28,10 +28,11 @@ curl -fsSL https://opencode.ai/install | bash
|
||||
npm i -g opencode-ai@latest # or bun/pnpm/yarn
|
||||
scoop bucket add extras; scoop install extras/opencode # Windows
|
||||
choco install opencode # Windows
|
||||
brew install opencode # macOS and Linux
|
||||
brew install anomalyco/tap/opencode # macOS and Linux (recommended, always up to date)
|
||||
brew install opencode # macOS and Linux (official brew formula, updated less)
|
||||
paru -S opencode-bin # Arch Linux
|
||||
mise use -g github:sst/opencode # Any OS
|
||||
nix run nixpkgs#opencode # or github:sst/opencode for latest dev branch
|
||||
mise use -g opencode # Any OS
|
||||
nix run nixpkgs#opencode # or github:anomalyco/opencode for latest dev branch
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
@@ -39,7 +40,7 @@ nix run nixpkgs#opencode # or github:sst/opencode for latest dev branc
|
||||
|
||||
### Desktop App (BETA)
|
||||
|
||||
OpenCode is also available as a desktop application. Download directly from the [releases page](https://github.com/sst/opencode/releases) or [opencode.ai/download](https://opencode.ai/download).
|
||||
OpenCode is also available as a desktop application. Download directly from the [releases page](https://github.com/anomalyco/opencode/releases) or [opencode.ai/download](https://opencode.ai/download).
|
||||
|
||||
| Platform | Download |
|
||||
| --------------------- | ------------------------------------- |
|
||||
@@ -70,8 +71,7 @@ XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
### Agents
|
||||
|
||||
OpenCode includes two built-in agents you can switch between,
|
||||
you can switch between these using 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
|
||||
- **plan** - Read-only agent for analysis and code exploration
|
||||
@@ -79,7 +79,7 @@ you can switch between these using the `Tab` key.
|
||||
- Asks permission before running bash commands
|
||||
- Ideal for exploring unfamiliar codebases or planning changes
|
||||
|
||||
Also, included is a **general** subagent for complex searches and multi-step 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.
|
||||
|
||||
Learn more about [agents](https://opencode.ai/docs/agents).
|
||||
@@ -98,7 +98,7 @@ If you are working on a project that's related to OpenCode and is using "opencod
|
||||
|
||||
### FAQ
|
||||
|
||||
#### How is this different than Claude Code?
|
||||
#### How is this different from Claude Code?
|
||||
|
||||
It's very similar to Claude Code in terms of capability. Here are the key differences:
|
||||
|
||||
@@ -108,10 +108,6 @@ It's very similar to Claude Code in terms of capability. Here are the key differ
|
||||
- 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.
|
||||
|
||||
#### What's the other repo?
|
||||
|
||||
The other confusingly named repo has no relation to this one. You can [read the story behind it here](https://x.com/thdxr/status/1933561254481666466).
|
||||
|
||||
---
|
||||
|
||||
**Join our community** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode)
|
||||
|
||||
116
README.zh-CN.md
Normal file
116
README.zh-CN.md
Normal file
@@ -0,0 +1,116 @@
|
||||
<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">开源的 AI Coding Agent。</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>
|
||||
|
||||
[](https://opencode.ai)
|
||||
|
||||
---
|
||||
|
||||
### 安装
|
||||
|
||||
```bash
|
||||
# 直接安装 (YOLO)
|
||||
curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
# 软件包管理器
|
||||
npm i -g opencode-ai@latest # 也可使用 bun/pnpm/yarn
|
||||
scoop bucket add extras; scoop install extras/opencode # Windows
|
||||
choco install opencode # Windows
|
||||
brew install anomalyco/tap/opencode # macOS 和 Linux(推荐,始终保持最新)
|
||||
brew install opencode # macOS 和 Linux(官方 brew formula,更新频率较低)
|
||||
paru -S opencode-bin # Arch Linux
|
||||
mise use -g opencode # 任意系统
|
||||
nix run nixpkgs#opencode # 或用 github:anomalyco/opencode 获取最新 dev 分支
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> 安装前请先移除 0.1.x 之前的旧版本。
|
||||
|
||||
### 桌面应用程序 (BETA)
|
||||
|
||||
OpenCode 也提供桌面版应用。可直接从 [发布页 (releases page)](https://github.com/anomalyco/opencode/releases) 或 [opencode.ai/download](https://opencode.ai/download) 下载。
|
||||
|
||||
| 平台 | 下载文件 |
|
||||
| --------------------- | ------------------------------------- |
|
||||
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
|
||||
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
|
||||
| Windows | `opencode-desktop-windows-x64.exe` |
|
||||
| Linux | `.deb`、`.rpm` 或 AppImage |
|
||||
|
||||
```bash
|
||||
# macOS (Homebrew Cask)
|
||||
brew install --cask opencode-desktop
|
||||
```
|
||||
|
||||
#### 安装目录
|
||||
|
||||
安装脚本按照以下优先级决定安装路径:
|
||||
|
||||
1. `$OPENCODE_INSTALL_DIR` - 自定义安装目录
|
||||
2. `$XDG_BIN_DIR` - 符合 XDG 基础目录规范的路径
|
||||
3. `$HOME/bin` - 如果存在或可创建的用户二进制目录
|
||||
4. `$HOME/.opencode/bin` - 默认备用路径
|
||||
|
||||
```bash
|
||||
# 示例
|
||||
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
|
||||
```
|
||||
|
||||
### Agents
|
||||
|
||||
OpenCode 内置两种 Agent,可用 `Tab` 键快速切换:
|
||||
|
||||
- **build** - 默认模式,具备完整权限,适合开发工作
|
||||
- **plan** - 只读模式,适合代码分析与探索
|
||||
- 默认拒绝修改文件
|
||||
- 运行 bash 命令前会询问
|
||||
- 便于探索未知代码库或规划改动
|
||||
|
||||
另外还包含一个 **general** 子 Agent,用于复杂搜索和多步任务,内部使用,也可在消息中输入 `@general` 调用。
|
||||
|
||||
了解更多 [Agents](https://opencode.ai/docs/agents) 相关信息。
|
||||
|
||||
### 文档
|
||||
|
||||
更多配置说明请查看我们的 [**官方文档**](https://opencode.ai/docs)。
|
||||
|
||||
### 参与贡献
|
||||
|
||||
如有兴趣贡献代码,请在提交 PR 前阅读 [贡献指南 (Contributing Docs)](./CONTRIBUTING.md)。
|
||||
|
||||
### 基于 OpenCode 进行开发
|
||||
|
||||
如果你在项目名中使用了 “opencode”(如 “opencode-dashboard” 或 “opencode-mobile”),请在 README 里注明该项目不是 OpenCode 团队官方开发,且不存在隶属关系。
|
||||
|
||||
### 常见问题 (FAQ)
|
||||
|
||||
#### 这和 Claude Code 有什么不同?
|
||||
|
||||
功能上很相似,关键差异:
|
||||
|
||||
- 100% 开源。
|
||||
- 不绑定特定提供商。推荐使用 [OpenCode Zen](https://opencode.ai/zen) 的模型,但也可搭配 Claude、OpenAI、Google 甚至本地模型。模型迭代会缩小差异、降低成本,因此保持 provider-agnostic 很重要。
|
||||
- 内置 LSP 支持。
|
||||
- 聚焦终端界面 (TUI)。OpenCode 由 Neovim 爱好者和 [terminal.shop](https://terminal.shop) 的创建者打造,会持续探索终端的极限。
|
||||
- 客户端/服务器架构。可在本机运行,同时用移动设备远程驱动。TUI 只是众多潜在客户端之一。
|
||||
|
||||
#### 另一个同名的仓库是什么?
|
||||
|
||||
另一个名字相近的仓库与本项目无关。[点击这里了解背后故事](https://x.com/thdxr/status/1933561254481666466)。
|
||||
|
||||
---
|
||||
|
||||
**加入我们的社区** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode)
|
||||
@@ -11,7 +11,7 @@
|
||||
<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/sst/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/sst/opencode/publish.yml?style=flat-square&branch=dev" /></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>
|
||||
|
||||
[](https://opencode.ai)
|
||||
@@ -28,10 +28,11 @@ curl -fsSL https://opencode.ai/install | bash
|
||||
npm i -g opencode-ai@latest # 也可使用 bun/pnpm/yarn
|
||||
scoop bucket add extras; scoop install extras/opencode # Windows
|
||||
choco install opencode # Windows
|
||||
brew install opencode # macOS 與 Linux
|
||||
brew install anomalyco/tap/opencode # macOS 與 Linux(推薦,始終保持最新)
|
||||
brew install opencode # macOS 與 Linux(官方 brew formula,更新頻率較低)
|
||||
paru -S opencode-bin # Arch Linux
|
||||
mise use -g github:sst/opencode # 任何作業系統
|
||||
nix run nixpkgs#opencode # 或使用 github:sst/opencode 以取得最新開發分支
|
||||
mise use -g github:anomalyco/opencode # 任何作業系統
|
||||
nix run nixpkgs#opencode # 或使用 github:anomalyco/opencode 以取得最新開發分支
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
@@ -39,7 +40,7 @@ nix run nixpkgs#opencode # 或使用 github:sst/opencode 以取得最
|
||||
|
||||
### 桌面應用程式 (BETA)
|
||||
|
||||
OpenCode 也提供桌面版應用程式。您可以直接從 [發佈頁面 (releases page)](https://github.com/sst/opencode/releases) 或 [opencode.ai/download](https://opencode.ai/download) 下載。
|
||||
OpenCode 也提供桌面版應用程式。您可以直接從 [發佈頁面 (releases page)](https://github.com/anomalyco/opencode/releases) 或 [opencode.ai/download](https://opencode.ai/download) 下載。
|
||||
|
||||
| 平台 | 下載連結 |
|
||||
| --------------------- | ------------------------------------- |
|
||||
|
||||
11
SECURITY.md
Normal file
11
SECURITY.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Reporting Security Issues
|
||||
|
||||
We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions.
|
||||
|
||||
To report a security issue, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/anomalyco/opencode/security/advisories/new) tab.
|
||||
|
||||
The team will send a response indicating the next steps in handling your report. After the initial reply to your report, the security team will keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance.
|
||||
|
||||
## Escalation
|
||||
|
||||
If you do not receive an acknowledgement of your report within 6 business days, you may send an email to security@anoma.ly
|
||||
377
STATS.md
377
STATS.md
@@ -1,179 +1,202 @@
|
||||
# Download Stats
|
||||
|
||||
| Date | GitHub Downloads | npm Downloads | Total |
|
||||
| ---------- | ------------------- | ------------------- | ------------------- |
|
||||
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
|
||||
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
|
||||
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
|
||||
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
|
||||
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
|
||||
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
|
||||
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
|
||||
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
|
||||
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
|
||||
| 2025-07-09 | 40,924 (+2,872) | 67,935 (+3,467) | 108,859 (+6,339) |
|
||||
| 2025-07-10 | 43,796 (+2,872) | 71,402 (+3,467) | 115,198 (+6,339) |
|
||||
| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
|
||||
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
|
||||
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
|
||||
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
|
||||
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
|
||||
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
|
||||
| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
|
||||
| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
|
||||
| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) |
|
||||
| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) |
|
||||
| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) |
|
||||
| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) |
|
||||
| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
|
||||
| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) |
|
||||
| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) |
|
||||
| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) |
|
||||
| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) |
|
||||
| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) |
|
||||
| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) |
|
||||
| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) |
|
||||
| 2025-07-31 | 118,339 (+4,795) | 143,344 (+3,027) | 261,683 (+7,822) |
|
||||
| 2025-08-01 | 123,539 (+5,200) | 146,680 (+3,336) | 270,219 (+8,536) |
|
||||
| 2025-08-02 | 127,864 (+4,325) | 149,236 (+2,556) | 277,100 (+6,881) |
|
||||
| 2025-08-03 | 131,397 (+3,533) | 150,451 (+1,215) | 281,848 (+4,748) |
|
||||
| 2025-08-04 | 136,266 (+4,869) | 153,260 (+2,809) | 289,526 (+7,678) |
|
||||
| 2025-08-05 | 141,596 (+5,330) | 155,752 (+2,492) | 297,348 (+7,822) |
|
||||
| 2025-08-06 | 147,067 (+5,471) | 158,309 (+2,557) | 305,376 (+8,028) |
|
||||
| 2025-08-07 | 152,591 (+5,524) | 160,889 (+2,580) | 313,480 (+8,104) |
|
||||
| 2025-08-08 | 158,187 (+5,596) | 163,448 (+2,559) | 321,635 (+8,155) |
|
||||
| 2025-08-09 | 162,770 (+4,583) | 165,721 (+2,273) | 328,491 (+6,856) |
|
||||
| 2025-08-10 | 165,695 (+2,925) | 167,109 (+1,388) | 332,804 (+4,313) |
|
||||
| 2025-08-11 | 169,297 (+3,602) | 167,953 (+844) | 337,250 (+4,446) |
|
||||
| 2025-08-12 | 176,307 (+7,010) | 171,876 (+3,923) | 348,183 (+10,933) |
|
||||
| 2025-08-13 | 182,997 (+6,690) | 177,182 (+5,306) | 360,179 (+11,996) |
|
||||
| 2025-08-14 | 189,063 (+6,066) | 179,741 (+2,559) | 368,804 (+8,625) |
|
||||
| 2025-08-15 | 193,608 (+4,545) | 181,792 (+2,051) | 375,400 (+6,596) |
|
||||
| 2025-08-16 | 198,118 (+4,510) | 184,558 (+2,766) | 382,676 (+7,276) |
|
||||
| 2025-08-17 | 201,299 (+3,181) | 186,269 (+1,711) | 387,568 (+4,892) |
|
||||
| 2025-08-18 | 204,559 (+3,260) | 187,399 (+1,130) | 391,958 (+4,390) |
|
||||
| 2025-08-19 | 209,814 (+5,255) | 189,668 (+2,269) | 399,482 (+7,524) |
|
||||
| 2025-08-20 | 214,497 (+4,683) | 191,481 (+1,813) | 405,978 (+6,496) |
|
||||
| 2025-08-21 | 220,465 (+5,968) | 194,784 (+3,303) | 415,249 (+9,271) |
|
||||
| 2025-08-22 | 225,899 (+5,434) | 197,204 (+2,420) | 423,103 (+7,854) |
|
||||
| 2025-08-23 | 229,005 (+3,106) | 199,238 (+2,034) | 428,243 (+5,140) |
|
||||
| 2025-08-24 | 232,098 (+3,093) | 201,157 (+1,919) | 433,255 (+5,012) |
|
||||
| 2025-08-25 | 236,607 (+4,509) | 202,650 (+1,493) | 439,257 (+6,002) |
|
||||
| 2025-08-26 | 242,783 (+6,176) | 205,242 (+2,592) | 448,025 (+8,768) |
|
||||
| 2025-08-27 | 248,409 (+5,626) | 205,242 (+0) | 453,651 (+5,626) |
|
||||
| 2025-08-28 | 252,796 (+4,387) | 205,242 (+0) | 458,038 (+4,387) |
|
||||
| 2025-08-29 | 256,045 (+3,249) | 211,075 (+5,833) | 467,120 (+9,082) |
|
||||
| 2025-08-30 | 258,863 (+2,818) | 212,397 (+1,322) | 471,260 (+4,140) |
|
||||
| 2025-08-31 | 262,004 (+3,141) | 213,944 (+1,547) | 475,948 (+4,688) |
|
||||
| 2025-09-01 | 265,359 (+3,355) | 215,115 (+1,171) | 480,474 (+4,526) |
|
||||
| 2025-09-02 | 270,483 (+5,124) | 217,075 (+1,960) | 487,558 (+7,084) |
|
||||
| 2025-09-03 | 274,793 (+4,310) | 219,755 (+2,680) | 494,548 (+6,990) |
|
||||
| 2025-09-04 | 280,430 (+5,637) | 222,103 (+2,348) | 502,533 (+7,985) |
|
||||
| 2025-09-05 | 283,769 (+3,339) | 223,793 (+1,690) | 507,562 (+5,029) |
|
||||
| 2025-09-06 | 286,245 (+2,476) | 225,036 (+1,243) | 511,281 (+3,719) |
|
||||
| 2025-09-07 | 288,623 (+2,378) | 225,866 (+830) | 514,489 (+3,208) |
|
||||
| 2025-09-08 | 293,341 (+4,718) | 227,073 (+1,207) | 520,414 (+5,925) |
|
||||
| 2025-09-09 | 300,036 (+6,695) | 229,788 (+2,715) | 529,824 (+9,410) |
|
||||
| 2025-09-10 | 307,287 (+7,251) | 233,435 (+3,647) | 540,722 (+10,898) |
|
||||
| 2025-09-11 | 314,083 (+6,796) | 237,356 (+3,921) | 551,439 (+10,717) |
|
||||
| 2025-09-12 | 321,046 (+6,963) | 240,728 (+3,372) | 561,774 (+10,335) |
|
||||
| 2025-09-13 | 324,894 (+3,848) | 245,539 (+4,811) | 570,433 (+8,659) |
|
||||
| 2025-09-14 | 328,876 (+3,982) | 248,245 (+2,706) | 577,121 (+6,688) |
|
||||
| 2025-09-15 | 334,201 (+5,325) | 250,983 (+2,738) | 585,184 (+8,063) |
|
||||
| 2025-09-16 | 342,609 (+8,408) | 255,264 (+4,281) | 597,873 (+12,689) |
|
||||
| 2025-09-17 | 351,117 (+8,508) | 260,970 (+5,706) | 612,087 (+14,214) |
|
||||
| 2025-09-18 | 358,717 (+7,600) | 266,922 (+5,952) | 625,639 (+13,552) |
|
||||
| 2025-09-19 | 365,401 (+6,684) | 271,859 (+4,937) | 637,260 (+11,621) |
|
||||
| 2025-09-20 | 372,092 (+6,691) | 276,917 (+5,058) | 649,009 (+11,749) |
|
||||
| 2025-09-21 | 377,079 (+4,987) | 280,261 (+3,344) | 657,340 (+8,331) |
|
||||
| 2025-09-22 | 382,492 (+5,413) | 284,009 (+3,748) | 666,501 (+9,161) |
|
||||
| 2025-09-23 | 387,008 (+4,516) | 289,129 (+5,120) | 676,137 (+9,636) |
|
||||
| 2025-09-24 | 393,325 (+6,317) | 294,927 (+5,798) | 688,252 (+12,115) |
|
||||
| 2025-09-25 | 398,879 (+5,554) | 301,663 (+6,736) | 700,542 (+12,290) |
|
||||
| 2025-09-26 | 404,334 (+5,455) | 306,713 (+5,050) | 711,047 (+10,505) |
|
||||
| 2025-09-27 | 411,618 (+7,284) | 317,763 (+11,050) | 729,381 (+18,334) |
|
||||
| 2025-09-28 | 414,910 (+3,292) | 322,522 (+4,759) | 737,432 (+8,051) |
|
||||
| 2025-09-29 | 419,919 (+5,009) | 328,033 (+5,511) | 747,952 (+10,520) |
|
||||
| 2025-09-30 | 427,991 (+8,072) | 336,472 (+8,439) | 764,463 (+16,511) |
|
||||
| 2025-10-01 | 433,591 (+5,600) | 341,742 (+5,270) | 775,333 (+10,870) |
|
||||
| 2025-10-02 | 440,852 (+7,261) | 348,099 (+6,357) | 788,951 (+13,618) |
|
||||
| 2025-10-03 | 446,829 (+5,977) | 359,937 (+11,838) | 806,766 (+17,815) |
|
||||
| 2025-10-04 | 452,561 (+5,732) | 370,386 (+10,449) | 822,947 (+16,181) |
|
||||
| 2025-10-05 | 455,559 (+2,998) | 374,745 (+4,359) | 830,304 (+7,357) |
|
||||
| 2025-10-06 | 460,927 (+5,368) | 379,489 (+4,744) | 840,416 (+10,112) |
|
||||
| 2025-10-07 | 467,336 (+6,409) | 385,438 (+5,949) | 852,774 (+12,358) |
|
||||
| 2025-10-08 | 474,643 (+7,307) | 394,139 (+8,701) | 868,782 (+16,008) |
|
||||
| 2025-10-09 | 479,203 (+4,560) | 400,526 (+6,387) | 879,729 (+10,947) |
|
||||
| 2025-10-10 | 484,374 (+5,171) | 406,015 (+5,489) | 890,389 (+10,660) |
|
||||
| 2025-10-11 | 488,427 (+4,053) | 414,699 (+8,684) | 903,126 (+12,737) |
|
||||
| 2025-10-12 | 492,125 (+3,698) | 418,745 (+4,046) | 910,870 (+7,744) |
|
||||
| 2025-10-14 | 505,130 (+13,005) | 429,286 (+10,541) | 934,416 (+23,546) |
|
||||
| 2025-10-15 | 512,717 (+7,587) | 439,290 (+10,004) | 952,007 (+17,591) |
|
||||
| 2025-10-16 | 517,719 (+5,002) | 447,137 (+7,847) | 964,856 (+12,849) |
|
||||
| 2025-10-17 | 526,239 (+8,520) | 457,467 (+10,330) | 983,706 (+18,850) |
|
||||
| 2025-10-18 | 531,564 (+5,325) | 465,272 (+7,805) | 996,836 (+13,130) |
|
||||
| 2025-10-19 | 536,209 (+4,645) | 469,078 (+3,806) | 1,005,287 (+8,451) |
|
||||
| 2025-10-20 | 541,264 (+5,055) | 472,952 (+3,874) | 1,014,216 (+8,929) |
|
||||
| 2025-10-21 | 548,721 (+7,457) | 479,703 (+6,751) | 1,028,424 (+14,208) |
|
||||
| 2025-10-22 | 557,949 (+9,228) | 491,395 (+11,692) | 1,049,344 (+20,920) |
|
||||
| 2025-10-23 | 564,716 (+6,767) | 498,736 (+7,341) | 1,063,452 (+14,108) |
|
||||
| 2025-10-24 | 572,692 (+7,976) | 506,905 (+8,169) | 1,079,597 (+16,145) |
|
||||
| 2025-10-25 | 578,927 (+6,235) | 516,129 (+9,224) | 1,095,056 (+15,459) |
|
||||
| 2025-10-26 | 584,409 (+5,482) | 521,179 (+5,050) | 1,105,588 (+10,532) |
|
||||
| 2025-10-27 | 589,999 (+5,590) | 526,001 (+4,822) | 1,116,000 (+10,412) |
|
||||
| 2025-10-28 | 595,776 (+5,777) | 532,438 (+6,437) | 1,128,214 (+12,214) |
|
||||
| 2025-10-29 | 606,259 (+10,483) | 542,064 (+9,626) | 1,148,323 (+20,109) |
|
||||
| 2025-10-30 | 613,746 (+7,487) | 542,064 (+0) | 1,155,810 (+7,487) |
|
||||
| 2025-10-30 | 617,846 (+4,100) | 555,026 (+12,962) | 1,172,872 (+17,062) |
|
||||
| 2025-10-31 | 626,612 (+8,766) | 564,579 (+9,553) | 1,191,191 (+18,319) |
|
||||
| 2025-11-01 | 636,100 (+9,488) | 581,806 (+17,227) | 1,217,906 (+26,715) |
|
||||
| 2025-11-02 | 644,067 (+7,967) | 590,004 (+8,198) | 1,234,071 (+16,165) |
|
||||
| 2025-11-03 | 653,130 (+9,063) | 597,139 (+7,135) | 1,250,269 (+16,198) |
|
||||
| 2025-11-04 | 663,912 (+10,782) | 608,056 (+10,917) | 1,271,968 (+21,699) |
|
||||
| 2025-11-05 | 675,074 (+11,162) | 619,690 (+11,634) | 1,294,764 (+22,796) |
|
||||
| 2025-11-06 | 686,252 (+11,178) | 630,885 (+11,195) | 1,317,137 (+22,373) |
|
||||
| 2025-11-07 | 696,646 (+10,394) | 642,146 (+11,261) | 1,338,792 (+21,655) |
|
||||
| 2025-11-08 | 706,035 (+9,389) | 653,489 (+11,343) | 1,359,524 (+20,732) |
|
||||
| 2025-11-09 | 713,462 (+7,427) | 660,459 (+6,970) | 1,373,921 (+14,397) |
|
||||
| 2025-11-10 | 722,288 (+8,826) | 668,225 (+7,766) | 1,390,513 (+16,592) |
|
||||
| 2025-11-11 | 729,769 (+7,481) | 677,501 (+9,276) | 1,407,270 (+16,757) |
|
||||
| 2025-11-12 | 740,180 (+10,411) | 686,454 (+8,953) | 1,426,634 (+19,364) |
|
||||
| 2025-11-13 | 749,905 (+9,725) | 696,157 (+9,703) | 1,446,062 (+19,428) |
|
||||
| 2025-11-14 | 759,928 (+10,023) | 705,237 (+9,080) | 1,465,165 (+19,103) |
|
||||
| 2025-11-15 | 765,955 (+6,027) | 712,870 (+7,633) | 1,478,825 (+13,660) |
|
||||
| 2025-11-16 | 771,069 (+5,114) | 716,596 (+3,726) | 1,487,665 (+8,840) |
|
||||
| 2025-11-17 | 780,161 (+9,092) | 723,339 (+6,743) | 1,503,500 (+15,835) |
|
||||
| 2025-11-18 | 791,563 (+11,402) | 732,544 (+9,205) | 1,524,107 (+20,607) |
|
||||
| 2025-11-19 | 804,409 (+12,846) | 747,624 (+15,080) | 1,552,033 (+27,926) |
|
||||
| 2025-11-20 | 814,620 (+10,211) | 757,907 (+10,283) | 1,572,527 (+20,494) |
|
||||
| 2025-11-21 | 826,309 (+11,689) | 769,307 (+11,400) | 1,595,616 (+23,089) |
|
||||
| 2025-11-22 | 837,269 (+10,960) | 780,996 (+11,689) | 1,618,265 (+22,649) |
|
||||
| 2025-11-23 | 846,609 (+9,340) | 795,069 (+14,073) | 1,641,678 (+23,413) |
|
||||
| 2025-11-24 | 856,733 (+10,124) | 804,033 (+8,964) | 1,660,766 (+19,088) |
|
||||
| 2025-11-25 | 869,423 (+12,690) | 817,339 (+13,306) | 1,686,762 (+25,996) |
|
||||
| 2025-11-26 | 881,414 (+11,991) | 832,518 (+15,179) | 1,713,932 (+27,170) |
|
||||
| 2025-11-27 | 893,960 (+12,546) | 846,180 (+13,662) | 1,740,140 (+26,208) |
|
||||
| 2025-11-28 | 901,741 (+7,781) | 856,482 (+10,302) | 1,758,223 (+18,083) |
|
||||
| 2025-11-29 | 908,689 (+6,948) | 863,361 (+6,879) | 1,772,050 (+13,827) |
|
||||
| 2025-11-30 | 916,116 (+7,427) | 870,194 (+6,833) | 1,786,310 (+14,260) |
|
||||
| 2025-12-01 | 925,898 (+9,782) | 876,500 (+6,306) | 1,802,398 (+16,088) |
|
||||
| 2025-12-02 | 939,250 (+13,352) | 890,919 (+14,419) | 1,830,169 (+27,771) |
|
||||
| 2025-12-03 | 952,249 (+12,999) | 903,713 (+12,794) | 1,855,962 (+25,793) |
|
||||
| 2025-12-04 | 965,611 (+13,362) | 916,471 (+12,758) | 1,882,082 (+26,120) |
|
||||
| 2025-12-05 | 977,996 (+12,385) | 930,616 (+14,145) | 1,908,612 (+26,530) |
|
||||
| 2025-12-06 | 987,884 (+9,888) | 943,773 (+13,157) | 1,931,657 (+23,045) |
|
||||
| 2025-12-07 | 994,046 (+6,162) | 951,425 (+7,652) | 1,945,471 (+13,814) |
|
||||
| 2025-12-08 | 1,000,898 (+6,852) | 957,149 (+5,724) | 1,958,047 (+12,576) |
|
||||
| 2025-12-09 | 1,011,488 (+10,590) | 973,922 (+16,773) | 1,985,410 (+27,363) |
|
||||
| 2025-12-10 | 1,025,891 (+14,403) | 991,708 (+17,786) | 2,017,599 (+32,189) |
|
||||
| 2025-12-11 | 1,045,110 (+19,219) | 1,010,559 (+18,851) | 2,055,669 (+38,070) |
|
||||
| 2025-12-12 | 1,061,340 (+16,230) | 1,030,838 (+20,279) | 2,092,178 (+36,509) |
|
||||
| 2025-12-13 | 1,073,561 (+12,221) | 1,044,608 (+13,770) | 2,118,169 (+25,991) |
|
||||
| 2025-12-14 | 1,082,042 (+8,481) | 1,052,425 (+7,817) | 2,134,467 (+16,298) |
|
||||
| 2025-12-15 | 1,093,632 (+11,590) | 1,059,078 (+6,653) | 2,152,710 (+18,243) |
|
||||
| 2025-12-16 | 1,120,477 (+26,845) | 1,078,022 (+18,944) | 2,198,499 (+45,789) |
|
||||
| 2025-12-17 | 1,151,067 (+30,590) | 1,097,661 (+19,639) | 2,248,728 (+50,229) |
|
||||
| 2025-12-18 | 1,178,658 (+27,591) | 1,113,418 (+15,757) | 2,292,076 (+43,348) |
|
||||
| 2025-12-19 | 1,203,485 (+24,827) | 1,129,698 (+16,280) | 2,333,183 (+41,107) |
|
||||
| 2025-12-20 | 1,223,000 (+19,515) | 1,146,258 (+16,560) | 2,369,258 (+36,075) |
|
||||
| 2025-12-21 | 1,242,675 (+19,675) | 1,158,909 (+12,651) | 2,401,584 (+32,326) |
|
||||
| Date | GitHub Downloads | npm Downloads | Total |
|
||||
| ---------- | -------------------- | ------------------- | -------------------- |
|
||||
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
|
||||
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
|
||||
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
|
||||
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
|
||||
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
|
||||
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
|
||||
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
|
||||
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
|
||||
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
|
||||
| 2025-07-09 | 40,924 (+2,872) | 67,935 (+3,467) | 108,859 (+6,339) |
|
||||
| 2025-07-10 | 43,796 (+2,872) | 71,402 (+3,467) | 115,198 (+6,339) |
|
||||
| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
|
||||
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
|
||||
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
|
||||
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
|
||||
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
|
||||
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
|
||||
| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
|
||||
| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
|
||||
| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) |
|
||||
| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) |
|
||||
| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) |
|
||||
| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) |
|
||||
| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
|
||||
| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) |
|
||||
| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) |
|
||||
| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) |
|
||||
| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) |
|
||||
| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) |
|
||||
| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) |
|
||||
| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) |
|
||||
| 2025-07-31 | 118,339 (+4,795) | 143,344 (+3,027) | 261,683 (+7,822) |
|
||||
| 2025-08-01 | 123,539 (+5,200) | 146,680 (+3,336) | 270,219 (+8,536) |
|
||||
| 2025-08-02 | 127,864 (+4,325) | 149,236 (+2,556) | 277,100 (+6,881) |
|
||||
| 2025-08-03 | 131,397 (+3,533) | 150,451 (+1,215) | 281,848 (+4,748) |
|
||||
| 2025-08-04 | 136,266 (+4,869) | 153,260 (+2,809) | 289,526 (+7,678) |
|
||||
| 2025-08-05 | 141,596 (+5,330) | 155,752 (+2,492) | 297,348 (+7,822) |
|
||||
| 2025-08-06 | 147,067 (+5,471) | 158,309 (+2,557) | 305,376 (+8,028) |
|
||||
| 2025-08-07 | 152,591 (+5,524) | 160,889 (+2,580) | 313,480 (+8,104) |
|
||||
| 2025-08-08 | 158,187 (+5,596) | 163,448 (+2,559) | 321,635 (+8,155) |
|
||||
| 2025-08-09 | 162,770 (+4,583) | 165,721 (+2,273) | 328,491 (+6,856) |
|
||||
| 2025-08-10 | 165,695 (+2,925) | 167,109 (+1,388) | 332,804 (+4,313) |
|
||||
| 2025-08-11 | 169,297 (+3,602) | 167,953 (+844) | 337,250 (+4,446) |
|
||||
| 2025-08-12 | 176,307 (+7,010) | 171,876 (+3,923) | 348,183 (+10,933) |
|
||||
| 2025-08-13 | 182,997 (+6,690) | 177,182 (+5,306) | 360,179 (+11,996) |
|
||||
| 2025-08-14 | 189,063 (+6,066) | 179,741 (+2,559) | 368,804 (+8,625) |
|
||||
| 2025-08-15 | 193,608 (+4,545) | 181,792 (+2,051) | 375,400 (+6,596) |
|
||||
| 2025-08-16 | 198,118 (+4,510) | 184,558 (+2,766) | 382,676 (+7,276) |
|
||||
| 2025-08-17 | 201,299 (+3,181) | 186,269 (+1,711) | 387,568 (+4,892) |
|
||||
| 2025-08-18 | 204,559 (+3,260) | 187,399 (+1,130) | 391,958 (+4,390) |
|
||||
| 2025-08-19 | 209,814 (+5,255) | 189,668 (+2,269) | 399,482 (+7,524) |
|
||||
| 2025-08-20 | 214,497 (+4,683) | 191,481 (+1,813) | 405,978 (+6,496) |
|
||||
| 2025-08-21 | 220,465 (+5,968) | 194,784 (+3,303) | 415,249 (+9,271) |
|
||||
| 2025-08-22 | 225,899 (+5,434) | 197,204 (+2,420) | 423,103 (+7,854) |
|
||||
| 2025-08-23 | 229,005 (+3,106) | 199,238 (+2,034) | 428,243 (+5,140) |
|
||||
| 2025-08-24 | 232,098 (+3,093) | 201,157 (+1,919) | 433,255 (+5,012) |
|
||||
| 2025-08-25 | 236,607 (+4,509) | 202,650 (+1,493) | 439,257 (+6,002) |
|
||||
| 2025-08-26 | 242,783 (+6,176) | 205,242 (+2,592) | 448,025 (+8,768) |
|
||||
| 2025-08-27 | 248,409 (+5,626) | 205,242 (+0) | 453,651 (+5,626) |
|
||||
| 2025-08-28 | 252,796 (+4,387) | 205,242 (+0) | 458,038 (+4,387) |
|
||||
| 2025-08-29 | 256,045 (+3,249) | 211,075 (+5,833) | 467,120 (+9,082) |
|
||||
| 2025-08-30 | 258,863 (+2,818) | 212,397 (+1,322) | 471,260 (+4,140) |
|
||||
| 2025-08-31 | 262,004 (+3,141) | 213,944 (+1,547) | 475,948 (+4,688) |
|
||||
| 2025-09-01 | 265,359 (+3,355) | 215,115 (+1,171) | 480,474 (+4,526) |
|
||||
| 2025-09-02 | 270,483 (+5,124) | 217,075 (+1,960) | 487,558 (+7,084) |
|
||||
| 2025-09-03 | 274,793 (+4,310) | 219,755 (+2,680) | 494,548 (+6,990) |
|
||||
| 2025-09-04 | 280,430 (+5,637) | 222,103 (+2,348) | 502,533 (+7,985) |
|
||||
| 2025-09-05 | 283,769 (+3,339) | 223,793 (+1,690) | 507,562 (+5,029) |
|
||||
| 2025-09-06 | 286,245 (+2,476) | 225,036 (+1,243) | 511,281 (+3,719) |
|
||||
| 2025-09-07 | 288,623 (+2,378) | 225,866 (+830) | 514,489 (+3,208) |
|
||||
| 2025-09-08 | 293,341 (+4,718) | 227,073 (+1,207) | 520,414 (+5,925) |
|
||||
| 2025-09-09 | 300,036 (+6,695) | 229,788 (+2,715) | 529,824 (+9,410) |
|
||||
| 2025-09-10 | 307,287 (+7,251) | 233,435 (+3,647) | 540,722 (+10,898) |
|
||||
| 2025-09-11 | 314,083 (+6,796) | 237,356 (+3,921) | 551,439 (+10,717) |
|
||||
| 2025-09-12 | 321,046 (+6,963) | 240,728 (+3,372) | 561,774 (+10,335) |
|
||||
| 2025-09-13 | 324,894 (+3,848) | 245,539 (+4,811) | 570,433 (+8,659) |
|
||||
| 2025-09-14 | 328,876 (+3,982) | 248,245 (+2,706) | 577,121 (+6,688) |
|
||||
| 2025-09-15 | 334,201 (+5,325) | 250,983 (+2,738) | 585,184 (+8,063) |
|
||||
| 2025-09-16 | 342,609 (+8,408) | 255,264 (+4,281) | 597,873 (+12,689) |
|
||||
| 2025-09-17 | 351,117 (+8,508) | 260,970 (+5,706) | 612,087 (+14,214) |
|
||||
| 2025-09-18 | 358,717 (+7,600) | 266,922 (+5,952) | 625,639 (+13,552) |
|
||||
| 2025-09-19 | 365,401 (+6,684) | 271,859 (+4,937) | 637,260 (+11,621) |
|
||||
| 2025-09-20 | 372,092 (+6,691) | 276,917 (+5,058) | 649,009 (+11,749) |
|
||||
| 2025-09-21 | 377,079 (+4,987) | 280,261 (+3,344) | 657,340 (+8,331) |
|
||||
| 2025-09-22 | 382,492 (+5,413) | 284,009 (+3,748) | 666,501 (+9,161) |
|
||||
| 2025-09-23 | 387,008 (+4,516) | 289,129 (+5,120) | 676,137 (+9,636) |
|
||||
| 2025-09-24 | 393,325 (+6,317) | 294,927 (+5,798) | 688,252 (+12,115) |
|
||||
| 2025-09-25 | 398,879 (+5,554) | 301,663 (+6,736) | 700,542 (+12,290) |
|
||||
| 2025-09-26 | 404,334 (+5,455) | 306,713 (+5,050) | 711,047 (+10,505) |
|
||||
| 2025-09-27 | 411,618 (+7,284) | 317,763 (+11,050) | 729,381 (+18,334) |
|
||||
| 2025-09-28 | 414,910 (+3,292) | 322,522 (+4,759) | 737,432 (+8,051) |
|
||||
| 2025-09-29 | 419,919 (+5,009) | 328,033 (+5,511) | 747,952 (+10,520) |
|
||||
| 2025-09-30 | 427,991 (+8,072) | 336,472 (+8,439) | 764,463 (+16,511) |
|
||||
| 2025-10-01 | 433,591 (+5,600) | 341,742 (+5,270) | 775,333 (+10,870) |
|
||||
| 2025-10-02 | 440,852 (+7,261) | 348,099 (+6,357) | 788,951 (+13,618) |
|
||||
| 2025-10-03 | 446,829 (+5,977) | 359,937 (+11,838) | 806,766 (+17,815) |
|
||||
| 2025-10-04 | 452,561 (+5,732) | 370,386 (+10,449) | 822,947 (+16,181) |
|
||||
| 2025-10-05 | 455,559 (+2,998) | 374,745 (+4,359) | 830,304 (+7,357) |
|
||||
| 2025-10-06 | 460,927 (+5,368) | 379,489 (+4,744) | 840,416 (+10,112) |
|
||||
| 2025-10-07 | 467,336 (+6,409) | 385,438 (+5,949) | 852,774 (+12,358) |
|
||||
| 2025-10-08 | 474,643 (+7,307) | 394,139 (+8,701) | 868,782 (+16,008) |
|
||||
| 2025-10-09 | 479,203 (+4,560) | 400,526 (+6,387) | 879,729 (+10,947) |
|
||||
| 2025-10-10 | 484,374 (+5,171) | 406,015 (+5,489) | 890,389 (+10,660) |
|
||||
| 2025-10-11 | 488,427 (+4,053) | 414,699 (+8,684) | 903,126 (+12,737) |
|
||||
| 2025-10-12 | 492,125 (+3,698) | 418,745 (+4,046) | 910,870 (+7,744) |
|
||||
| 2025-10-14 | 505,130 (+13,005) | 429,286 (+10,541) | 934,416 (+23,546) |
|
||||
| 2025-10-15 | 512,717 (+7,587) | 439,290 (+10,004) | 952,007 (+17,591) |
|
||||
| 2025-10-16 | 517,719 (+5,002) | 447,137 (+7,847) | 964,856 (+12,849) |
|
||||
| 2025-10-17 | 526,239 (+8,520) | 457,467 (+10,330) | 983,706 (+18,850) |
|
||||
| 2025-10-18 | 531,564 (+5,325) | 465,272 (+7,805) | 996,836 (+13,130) |
|
||||
| 2025-10-19 | 536,209 (+4,645) | 469,078 (+3,806) | 1,005,287 (+8,451) |
|
||||
| 2025-10-20 | 541,264 (+5,055) | 472,952 (+3,874) | 1,014,216 (+8,929) |
|
||||
| 2025-10-21 | 548,721 (+7,457) | 479,703 (+6,751) | 1,028,424 (+14,208) |
|
||||
| 2025-10-22 | 557,949 (+9,228) | 491,395 (+11,692) | 1,049,344 (+20,920) |
|
||||
| 2025-10-23 | 564,716 (+6,767) | 498,736 (+7,341) | 1,063,452 (+14,108) |
|
||||
| 2025-10-24 | 572,692 (+7,976) | 506,905 (+8,169) | 1,079,597 (+16,145) |
|
||||
| 2025-10-25 | 578,927 (+6,235) | 516,129 (+9,224) | 1,095,056 (+15,459) |
|
||||
| 2025-10-26 | 584,409 (+5,482) | 521,179 (+5,050) | 1,105,588 (+10,532) |
|
||||
| 2025-10-27 | 589,999 (+5,590) | 526,001 (+4,822) | 1,116,000 (+10,412) |
|
||||
| 2025-10-28 | 595,776 (+5,777) | 532,438 (+6,437) | 1,128,214 (+12,214) |
|
||||
| 2025-10-29 | 606,259 (+10,483) | 542,064 (+9,626) | 1,148,323 (+20,109) |
|
||||
| 2025-10-30 | 613,746 (+7,487) | 542,064 (+0) | 1,155,810 (+7,487) |
|
||||
| 2025-10-30 | 617,846 (+4,100) | 555,026 (+12,962) | 1,172,872 (+17,062) |
|
||||
| 2025-10-31 | 626,612 (+8,766) | 564,579 (+9,553) | 1,191,191 (+18,319) |
|
||||
| 2025-11-01 | 636,100 (+9,488) | 581,806 (+17,227) | 1,217,906 (+26,715) |
|
||||
| 2025-11-02 | 644,067 (+7,967) | 590,004 (+8,198) | 1,234,071 (+16,165) |
|
||||
| 2025-11-03 | 653,130 (+9,063) | 597,139 (+7,135) | 1,250,269 (+16,198) |
|
||||
| 2025-11-04 | 663,912 (+10,782) | 608,056 (+10,917) | 1,271,968 (+21,699) |
|
||||
| 2025-11-05 | 675,074 (+11,162) | 619,690 (+11,634) | 1,294,764 (+22,796) |
|
||||
| 2025-11-06 | 686,252 (+11,178) | 630,885 (+11,195) | 1,317,137 (+22,373) |
|
||||
| 2025-11-07 | 696,646 (+10,394) | 642,146 (+11,261) | 1,338,792 (+21,655) |
|
||||
| 2025-11-08 | 706,035 (+9,389) | 653,489 (+11,343) | 1,359,524 (+20,732) |
|
||||
| 2025-11-09 | 713,462 (+7,427) | 660,459 (+6,970) | 1,373,921 (+14,397) |
|
||||
| 2025-11-10 | 722,288 (+8,826) | 668,225 (+7,766) | 1,390,513 (+16,592) |
|
||||
| 2025-11-11 | 729,769 (+7,481) | 677,501 (+9,276) | 1,407,270 (+16,757) |
|
||||
| 2025-11-12 | 740,180 (+10,411) | 686,454 (+8,953) | 1,426,634 (+19,364) |
|
||||
| 2025-11-13 | 749,905 (+9,725) | 696,157 (+9,703) | 1,446,062 (+19,428) |
|
||||
| 2025-11-14 | 759,928 (+10,023) | 705,237 (+9,080) | 1,465,165 (+19,103) |
|
||||
| 2025-11-15 | 765,955 (+6,027) | 712,870 (+7,633) | 1,478,825 (+13,660) |
|
||||
| 2025-11-16 | 771,069 (+5,114) | 716,596 (+3,726) | 1,487,665 (+8,840) |
|
||||
| 2025-11-17 | 780,161 (+9,092) | 723,339 (+6,743) | 1,503,500 (+15,835) |
|
||||
| 2025-11-18 | 791,563 (+11,402) | 732,544 (+9,205) | 1,524,107 (+20,607) |
|
||||
| 2025-11-19 | 804,409 (+12,846) | 747,624 (+15,080) | 1,552,033 (+27,926) |
|
||||
| 2025-11-20 | 814,620 (+10,211) | 757,907 (+10,283) | 1,572,527 (+20,494) |
|
||||
| 2025-11-21 | 826,309 (+11,689) | 769,307 (+11,400) | 1,595,616 (+23,089) |
|
||||
| 2025-11-22 | 837,269 (+10,960) | 780,996 (+11,689) | 1,618,265 (+22,649) |
|
||||
| 2025-11-23 | 846,609 (+9,340) | 795,069 (+14,073) | 1,641,678 (+23,413) |
|
||||
| 2025-11-24 | 856,733 (+10,124) | 804,033 (+8,964) | 1,660,766 (+19,088) |
|
||||
| 2025-11-25 | 869,423 (+12,690) | 817,339 (+13,306) | 1,686,762 (+25,996) |
|
||||
| 2025-11-26 | 881,414 (+11,991) | 832,518 (+15,179) | 1,713,932 (+27,170) |
|
||||
| 2025-11-27 | 893,960 (+12,546) | 846,180 (+13,662) | 1,740,140 (+26,208) |
|
||||
| 2025-11-28 | 901,741 (+7,781) | 856,482 (+10,302) | 1,758,223 (+18,083) |
|
||||
| 2025-11-29 | 908,689 (+6,948) | 863,361 (+6,879) | 1,772,050 (+13,827) |
|
||||
| 2025-11-30 | 916,116 (+7,427) | 870,194 (+6,833) | 1,786,310 (+14,260) |
|
||||
| 2025-12-01 | 925,898 (+9,782) | 876,500 (+6,306) | 1,802,398 (+16,088) |
|
||||
| 2025-12-02 | 939,250 (+13,352) | 890,919 (+14,419) | 1,830,169 (+27,771) |
|
||||
| 2025-12-03 | 952,249 (+12,999) | 903,713 (+12,794) | 1,855,962 (+25,793) |
|
||||
| 2025-12-04 | 965,611 (+13,362) | 916,471 (+12,758) | 1,882,082 (+26,120) |
|
||||
| 2025-12-05 | 977,996 (+12,385) | 930,616 (+14,145) | 1,908,612 (+26,530) |
|
||||
| 2025-12-06 | 987,884 (+9,888) | 943,773 (+13,157) | 1,931,657 (+23,045) |
|
||||
| 2025-12-07 | 994,046 (+6,162) | 951,425 (+7,652) | 1,945,471 (+13,814) |
|
||||
| 2025-12-08 | 1,000,898 (+6,852) | 957,149 (+5,724) | 1,958,047 (+12,576) |
|
||||
| 2025-12-09 | 1,011,488 (+10,590) | 973,922 (+16,773) | 1,985,410 (+27,363) |
|
||||
| 2025-12-10 | 1,025,891 (+14,403) | 991,708 (+17,786) | 2,017,599 (+32,189) |
|
||||
| 2025-12-11 | 1,045,110 (+19,219) | 1,010,559 (+18,851) | 2,055,669 (+38,070) |
|
||||
| 2025-12-12 | 1,061,340 (+16,230) | 1,030,838 (+20,279) | 2,092,178 (+36,509) |
|
||||
| 2025-12-13 | 1,073,561 (+12,221) | 1,044,608 (+13,770) | 2,118,169 (+25,991) |
|
||||
| 2025-12-14 | 1,082,042 (+8,481) | 1,052,425 (+7,817) | 2,134,467 (+16,298) |
|
||||
| 2025-12-15 | 1,093,632 (+11,590) | 1,059,078 (+6,653) | 2,152,710 (+18,243) |
|
||||
| 2025-12-16 | 1,120,477 (+26,845) | 1,078,022 (+18,944) | 2,198,499 (+45,789) |
|
||||
| 2025-12-17 | 1,151,067 (+30,590) | 1,097,661 (+19,639) | 2,248,728 (+50,229) |
|
||||
| 2025-12-18 | 1,178,658 (+27,591) | 1,113,418 (+15,757) | 2,292,076 (+43,348) |
|
||||
| 2025-12-19 | 1,203,485 (+24,827) | 1,129,698 (+16,280) | 2,333,183 (+41,107) |
|
||||
| 2025-12-20 | 1,223,000 (+19,515) | 1,146,258 (+16,560) | 2,369,258 (+36,075) |
|
||||
| 2025-12-21 | 1,242,675 (+19,675) | 1,158,909 (+12,651) | 2,401,584 (+32,326) |
|
||||
| 2025-12-22 | 1,262,522 (+19,847) | 1,169,121 (+10,212) | 2,431,643 (+30,059) |
|
||||
| 2025-12-23 | 1,286,548 (+24,026) | 1,186,439 (+17,318) | 2,472,987 (+41,344) |
|
||||
| 2025-12-24 | 1,309,323 (+22,775) | 1,203,767 (+17,328) | 2,513,090 (+40,103) |
|
||||
| 2025-12-25 | 1,333,032 (+23,709) | 1,217,283 (+13,516) | 2,550,315 (+37,225) |
|
||||
| 2025-12-26 | 1,352,411 (+19,379) | 1,227,615 (+10,332) | 2,580,026 (+29,711) |
|
||||
| 2025-12-27 | 1,371,771 (+19,360) | 1,238,236 (+10,621) | 2,610,007 (+29,981) |
|
||||
| 2025-12-28 | 1,390,388 (+18,617) | 1,245,690 (+7,454) | 2,636,078 (+26,071) |
|
||||
| 2025-12-29 | 1,415,560 (+25,172) | 1,257,101 (+11,411) | 2,672,661 (+36,583) |
|
||||
| 2025-12-30 | 1,445,450 (+29,890) | 1,272,689 (+15,588) | 2,718,139 (+45,478) |
|
||||
| 2025-12-31 | 1,479,598 (+34,148) | 1,293,235 (+20,546) | 2,772,833 (+54,694) |
|
||||
| 2026-01-01 | 1,508,883 (+29,285) | 1,309,874 (+16,639) | 2,818,757 (+45,924) |
|
||||
| 2026-01-02 | 1,563,474 (+54,591) | 1,320,959 (+11,085) | 2,884,433 (+65,676) |
|
||||
| 2026-01-03 | 1,618,065 (+54,591) | 1,331,914 (+10,955) | 2,949,979 (+65,546) |
|
||||
| 2026-01-04 | 1,672,656 (+39,702) | 1,339,883 (+7,969) | 3,012,539 (+62,560) |
|
||||
| 2026-01-05 | 1,738,171 (+65,515) | 1,353,043 (+13,160) | 3,091,214 (+78,675) |
|
||||
| 2026-01-06 | 1,960,988 (+222,817) | 1,377,377 (+24,334) | 3,338,365 (+247,151) |
|
||||
| 2026-01-07 | 2,123,239 (+162,251) | 1,398,648 (+21,271) | 3,521,887 (+183,522) |
|
||||
| 2026-01-08 | 2,272,630 (+149,391) | 1,432,480 (+33,832) | 3,705,110 (+183,223) |
|
||||
| 2026-01-09 | 2,443,565 (+170,935) | 1,469,451 (+36,971) | 3,913,016 (+207,906) |
|
||||
| 2026-01-10 | 2,632,023 (+188,458) | 1,503,670 (+34,219) | 4,135,693 (+222,677) |
|
||||
| 2026-01-11 | 2,836,394 (+204,371) | 1,530,479 (+26,809) | 4,366,873 (+231,180) |
|
||||
| 2026-01-12 | 3,053,594 (+217,200) | 1,553,671 (+23,192) | 4,607,265 (+240,392) |
|
||||
| 2026-01-13 | 3,297,078 (+243,484) | 1,595,062 (+41,391) | 4,892,140 (+284,875) |
|
||||
|
||||
@@ -1,12 +1,71 @@
|
||||
## Style Guide
|
||||
|
||||
- Try to keep things in one function unless composable or reusable
|
||||
- DO NOT do unnecessary destructuring of variables
|
||||
- DO NOT use `else` statements unless necessary
|
||||
- DO NOT use `try`/`catch` if it can be avoided
|
||||
- AVOID `try`/`catch` where possible
|
||||
- AVOID `else` statements
|
||||
- AVOID using `any` type
|
||||
- AVOID `let` statements
|
||||
- PREFER single word variable names where possible
|
||||
- Use as many bun apis as possible like Bun.file()
|
||||
- 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 using the `any` type
|
||||
- Prefer single word variable names where possible
|
||||
- Use Bun APIs when possible, like `Bun.file()`
|
||||
|
||||
# Avoid let statements
|
||||
|
||||
We don't like `let` statements, especially combined with if/else statements.
|
||||
Prefer `const`.
|
||||
|
||||
Good:
|
||||
|
||||
```ts
|
||||
const foo = condition ? 1 : 2
|
||||
```
|
||||
|
||||
Bad:
|
||||
|
||||
```ts
|
||||
let foo
|
||||
|
||||
if (condition) foo = 1
|
||||
else foo = 2
|
||||
```
|
||||
|
||||
# Avoid else statements
|
||||
|
||||
Prefer early returns or using an `iife` to avoid else statements.
|
||||
|
||||
Good:
|
||||
|
||||
```ts
|
||||
function foo() {
|
||||
if (condition) return 1
|
||||
return 2
|
||||
}
|
||||
```
|
||||
|
||||
Bad:
|
||||
|
||||
```ts
|
||||
function foo() {
|
||||
if (condition) return 1
|
||||
else return 2
|
||||
}
|
||||
```
|
||||
|
||||
# Prefer single word naming
|
||||
|
||||
Try your best to find a single word name for your variables, functions, etc.
|
||||
Only use multiple words if you cannot.
|
||||
|
||||
Good:
|
||||
|
||||
```ts
|
||||
const foo = 1
|
||||
const bar = 2
|
||||
const baz = 3
|
||||
```
|
||||
|
||||
Bad:
|
||||
|
||||
```ts
|
||||
const fooBar = 1
|
||||
const barBaz = 2
|
||||
const bazFoo = 3
|
||||
```
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
[install]
|
||||
exact = true
|
||||
|
||||
[test]
|
||||
root = "./do-not-run-tests-from-root"
|
||||
|
||||
|
||||
6
flake.lock
generated
6
flake.lock
generated
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1766125104,
|
||||
"narHash": "sha256-l/YGrEpLromL4viUo5GmFH3K5M1j0Mb9O+LiaeCPWEM=",
|
||||
"lastModified": 1768178648,
|
||||
"narHash": "sha256-kz/F6mhESPvU1diB7tOM3nLcBfQe7GU7GQCymRlTi/s=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "7d853e518814cca2a657b72eeba67ae20ebf7059",
|
||||
"rev": "3fbab70c6e69c87ea2b6e48aa6629da2aa6a23b0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
52
flake.nix
52
flake.nix
@@ -17,7 +17,7 @@
|
||||
"aarch64-darwin"
|
||||
"x86_64-darwin"
|
||||
];
|
||||
lib = nixpkgs.lib;
|
||||
inherit (nixpkgs) lib;
|
||||
forEachSystem = lib.genAttrs systems;
|
||||
pkgsFor = system: nixpkgs.legacyPackages.${system};
|
||||
packageJson = builtins.fromJSON (builtins.readFile ./packages/opencode/package.json);
|
||||
@@ -27,11 +27,28 @@
|
||||
"aarch64-darwin" = "bun-darwin-arm64";
|
||||
"x86_64-darwin" = "bun-darwin-x64";
|
||||
};
|
||||
defaultNodeModules = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
|
||||
|
||||
# Parse "bun-{os}-{cpu}" to {os, cpu}
|
||||
parseBunTarget =
|
||||
target:
|
||||
let
|
||||
parts = lib.splitString "-" target;
|
||||
in
|
||||
{
|
||||
os = builtins.elemAt parts 1;
|
||||
cpu = builtins.elemAt parts 2;
|
||||
};
|
||||
|
||||
hashesFile = "${./nix}/hashes.json";
|
||||
hashesData =
|
||||
if builtins.pathExists hashesFile then builtins.fromJSON (builtins.readFile hashesFile) else { };
|
||||
nodeModulesHash = hashesData.nodeModules or defaultNodeModules;
|
||||
# Lookup hash: supports per-system ({system: hash}) or legacy single hash
|
||||
nodeModulesHashFor =
|
||||
system:
|
||||
if builtins.isAttrs hashesData.nodeModules then
|
||||
hashesData.nodeModules.${system}
|
||||
else
|
||||
hashesData.nodeModules;
|
||||
modelsDev = forEachSystem (
|
||||
system:
|
||||
let
|
||||
@@ -63,20 +80,35 @@
|
||||
system:
|
||||
let
|
||||
pkgs = pkgsFor system;
|
||||
bunPlatform = parseBunTarget bunTarget.${system};
|
||||
mkNodeModules = pkgs.callPackage ./nix/node-modules.nix {
|
||||
hash = nodeModulesHash;
|
||||
hash = nodeModulesHashFor system;
|
||||
bunCpu = bunPlatform.cpu;
|
||||
bunOs = bunPlatform.os;
|
||||
};
|
||||
mkPackage = pkgs.callPackage ./nix/opencode.nix { };
|
||||
in
|
||||
{
|
||||
default = mkPackage {
|
||||
version = packageJson.version;
|
||||
mkOpencode = pkgs.callPackage ./nix/opencode.nix { };
|
||||
mkDesktop = pkgs.callPackage ./nix/desktop.nix { };
|
||||
|
||||
opencodePkg = mkOpencode {
|
||||
inherit (packageJson) version;
|
||||
src = ./.;
|
||||
scripts = ./nix/scripts;
|
||||
target = bunTarget.${system};
|
||||
modelsDev = "${modelsDev.${system}}/dist/_api.json";
|
||||
mkNodeModules = mkNodeModules;
|
||||
inherit mkNodeModules;
|
||||
};
|
||||
|
||||
desktopPkg = mkDesktop {
|
||||
inherit (packageJson) version;
|
||||
src = ./.;
|
||||
scripts = ./nix/scripts;
|
||||
mkNodeModules = mkNodeModules;
|
||||
opencode = opencodePkg;
|
||||
};
|
||||
in
|
||||
{
|
||||
default = opencodePkg;
|
||||
desktop = desktopPkg;
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -81,13 +81,14 @@ This will walk you through installing the GitHub app, creating the workflow, and
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run opencode
|
||||
uses: sst/opencode/github@latest
|
||||
- name: Run opencode
|
||||
uses: anomalyco/opencode/github@latest
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
with:
|
||||
@@ -98,7 +99,7 @@ This will walk you through installing the GitHub app, creating the workflow, and
|
||||
|
||||
## Support
|
||||
|
||||
This is an early release. If you encounter issues or have feedback, please create an issue at https://github.com/sst/opencode/issues.
|
||||
This is an early release. If you encounter issues or have feedback, please create an issue at https://github.com/anomalyco/opencode/issues.
|
||||
|
||||
## Development
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ runs:
|
||||
id: version
|
||||
shell: bash
|
||||
run: |
|
||||
VERSION=$(curl -sf https://api.github.com/repos/sst/opencode/releases/latest | grep -o '"tag_name": *"[^"]*"' | cut -d'"' -f4)
|
||||
VERSION=$(curl -sf https://api.github.com/repos/anomalyco/opencode/releases/latest | grep -o '"tag_name": *"[^"]*"' | cut -d'"' -f4)
|
||||
echo "version=${VERSION:-latest}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache opencode
|
||||
|
||||
@@ -281,7 +281,7 @@ async function assertOpencodeConnected() {
|
||||
connected = true
|
||||
break
|
||||
} catch (e) {}
|
||||
await new Promise((resolve) => setTimeout(resolve, 300))
|
||||
await Bun.sleep(300)
|
||||
} while (retry++ < 30)
|
||||
|
||||
if (!connected) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/bun": "catalog:"
|
||||
},
|
||||
|
||||
@@ -44,3 +44,12 @@ new sst.cloudflare.x.Astro("Web", {
|
||||
VITE_API_URL: api.url.apply((url) => url!),
|
||||
},
|
||||
})
|
||||
|
||||
new sst.cloudflare.StaticSite("WebApp", {
|
||||
domain: "app." + domain,
|
||||
path: "packages/app",
|
||||
build: {
|
||||
command: "bun turbo build",
|
||||
output: "./dist",
|
||||
},
|
||||
})
|
||||
|
||||
@@ -76,6 +76,7 @@ export const stripeWebhook = new stripe.WebhookEndpoint("StripeWebhookEndpoint",
|
||||
"checkout.session.completed",
|
||||
"checkout.session.expired",
|
||||
"charge.refunded",
|
||||
"invoice.payment_succeeded",
|
||||
"customer.created",
|
||||
"customer.deleted",
|
||||
"customer.updated",
|
||||
@@ -97,13 +98,29 @@ export const stripeWebhook = new stripe.WebhookEndpoint("StripeWebhookEndpoint",
|
||||
],
|
||||
})
|
||||
|
||||
const zenProduct = new stripe.Product("ZenBlack", {
|
||||
name: "OpenCode Black",
|
||||
})
|
||||
const zenPrice = new stripe.Price("ZenBlackPrice", {
|
||||
product: zenProduct.id,
|
||||
unitAmount: 20000,
|
||||
currency: "usd",
|
||||
recurring: {
|
||||
interval: "month",
|
||||
intervalCount: 1,
|
||||
},
|
||||
})
|
||||
|
||||
const ZEN_MODELS = [
|
||||
new sst.Secret("ZEN_MODELS1"),
|
||||
new sst.Secret("ZEN_MODELS2"),
|
||||
new sst.Secret("ZEN_MODELS3"),
|
||||
new sst.Secret("ZEN_MODELS4"),
|
||||
new sst.Secret("ZEN_MODELS5"),
|
||||
new sst.Secret("ZEN_MODELS6"),
|
||||
new sst.Secret("ZEN_MODELS7"),
|
||||
]
|
||||
const ZEN_BLACK = new sst.Secret("ZEN_BLACK")
|
||||
const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
|
||||
const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
|
||||
properties: { value: auth.url.apply((url) => url!) },
|
||||
@@ -118,6 +135,7 @@ const gatewayKv = new sst.cloudflare.Kv("GatewayKv")
|
||||
////////////////
|
||||
|
||||
const bucket = new sst.cloudflare.Bucket("ZenData")
|
||||
const bucketNew = new sst.cloudflare.Bucket("ZenDataNew")
|
||||
|
||||
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")
|
||||
@@ -136,6 +154,7 @@ new sst.cloudflare.x.SolidStart("Console", {
|
||||
path: "packages/console/app",
|
||||
link: [
|
||||
bucket,
|
||||
bucketNew,
|
||||
database,
|
||||
AUTH_API_URL,
|
||||
STRIPE_WEBHOOK_SECRET,
|
||||
@@ -143,6 +162,8 @@ new sst.cloudflare.x.SolidStart("Console", {
|
||||
EMAILOCTOPUS_API_KEY,
|
||||
AWS_SES_ACCESS_KEY_ID,
|
||||
AWS_SES_SECRET_ACCESS_KEY,
|
||||
ZEN_BLACK,
|
||||
new sst.Secret("ZEN_SESSION_SECRET"),
|
||||
...ZEN_MODELS,
|
||||
...($dev
|
||||
? [
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { domain } from "./stage"
|
||||
|
||||
new sst.cloudflare.StaticSite("Desktop", {
|
||||
domain: "desktop." + domain,
|
||||
path: "packages/desktop",
|
||||
build: {
|
||||
command: "bun turbo build",
|
||||
output: "./dist",
|
||||
},
|
||||
})
|
||||
369
install
369
install
@@ -7,112 +7,187 @@ RED='\033[0;31m'
|
||||
ORANGE='\033[38;5;214m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
OpenCode Installer
|
||||
|
||||
Usage: install.sh [options]
|
||||
|
||||
Options:
|
||||
-h, --help Display this help message
|
||||
-v, --version <version> Install a specific version (e.g., 1.0.180)
|
||||
-b, --binary <path> Install from a local binary instead of downloading
|
||||
--no-modify-path Don't modify shell config files (.zshrc, .bashrc, etc.)
|
||||
|
||||
Examples:
|
||||
curl -fsSL https://opencode.ai/install | bash
|
||||
curl -fsSL https://opencode.ai/install | bash -s -- --version 1.0.180
|
||||
./install --binary /path/to/opencode
|
||||
EOF
|
||||
}
|
||||
|
||||
requested_version=${VERSION:-}
|
||||
no_modify_path=false
|
||||
binary_path=""
|
||||
|
||||
raw_os=$(uname -s)
|
||||
os=$(echo "$raw_os" | tr '[:upper:]' '[:lower:]')
|
||||
case "$raw_os" in
|
||||
Darwin*) os="darwin" ;;
|
||||
Linux*) os="linux" ;;
|
||||
MINGW*|MSYS*|CYGWIN*) os="windows" ;;
|
||||
esac
|
||||
|
||||
arch=$(uname -m)
|
||||
if [[ "$arch" == "aarch64" ]]; then
|
||||
arch="arm64"
|
||||
fi
|
||||
if [[ "$arch" == "x86_64" ]]; then
|
||||
arch="x64"
|
||||
fi
|
||||
|
||||
if [ "$os" = "darwin" ] && [ "$arch" = "x64" ]; then
|
||||
rosetta_flag=$(sysctl -n sysctl.proc_translated 2>/dev/null || echo 0)
|
||||
if [ "$rosetta_flag" = "1" ]; then
|
||||
arch="arm64"
|
||||
fi
|
||||
fi
|
||||
|
||||
combo="$os-$arch"
|
||||
case "$combo" in
|
||||
linux-x64|linux-arm64|darwin-x64|darwin-arm64|windows-x64)
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}Unsupported OS/Arch: $os/$arch${NC}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
archive_ext=".zip"
|
||||
if [ "$os" = "linux" ]; then
|
||||
archive_ext=".tar.gz"
|
||||
fi
|
||||
|
||||
is_musl=false
|
||||
if [ "$os" = "linux" ]; then
|
||||
if [ -f /etc/alpine-release ]; then
|
||||
is_musl=true
|
||||
fi
|
||||
|
||||
if command -v ldd >/dev/null 2>&1; then
|
||||
if ldd --version 2>&1 | grep -qi musl; then
|
||||
is_musl=true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
needs_baseline=false
|
||||
if [ "$arch" = "x64" ]; then
|
||||
if [ "$os" = "linux" ]; then
|
||||
if ! grep -qi avx2 /proc/cpuinfo 2>/dev/null; then
|
||||
needs_baseline=true
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$os" = "darwin" ]; then
|
||||
avx2=$(sysctl -n hw.optional.avx2_0 2>/dev/null || echo 0)
|
||||
if [ "$avx2" != "1" ]; then
|
||||
needs_baseline=true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
target="$os-$arch"
|
||||
if [ "$needs_baseline" = "true" ]; then
|
||||
target="$target-baseline"
|
||||
fi
|
||||
if [ "$is_musl" = "true" ]; then
|
||||
target="$target-musl"
|
||||
fi
|
||||
|
||||
filename="$APP-$target$archive_ext"
|
||||
|
||||
|
||||
if [ "$os" = "linux" ]; then
|
||||
if ! command -v tar >/dev/null 2>&1; then
|
||||
echo -e "${RED}Error: 'tar' is required but not installed.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
if ! command -v unzip >/dev/null 2>&1; then
|
||||
echo -e "${RED}Error: 'unzip' is required but not installed.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
-v|--version)
|
||||
if [[ -n "${2:-}" ]]; then
|
||||
requested_version="$2"
|
||||
shift 2
|
||||
else
|
||||
echo -e "${RED}Error: --version requires a version argument${NC}"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
-b|--binary)
|
||||
if [[ -n "${2:-}" ]]; then
|
||||
binary_path="$2"
|
||||
shift 2
|
||||
else
|
||||
echo -e "${RED}Error: --binary requires a path argument${NC}"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
--no-modify-path)
|
||||
no_modify_path=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo -e "${ORANGE}Warning: Unknown option '$1'${NC}" >&2
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
INSTALL_DIR=$HOME/.opencode/bin
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
|
||||
if [ -z "$requested_version" ]; then
|
||||
url="https://github.com/sst/opencode/releases/latest/download/$filename"
|
||||
specific_version=$(curl -s https://api.github.com/repos/sst/opencode/releases/latest | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p')
|
||||
|
||||
if [[ $? -ne 0 || -z "$specific_version" ]]; then
|
||||
echo -e "${RED}Failed to fetch version information${NC}"
|
||||
# If --binary is provided, skip all download/detection logic
|
||||
if [ -n "$binary_path" ]; then
|
||||
if [ ! -f "$binary_path" ]; then
|
||||
echo -e "${RED}Error: Binary not found at ${binary_path}${NC}"
|
||||
exit 1
|
||||
fi
|
||||
specific_version="local"
|
||||
else
|
||||
url="https://github.com/sst/opencode/releases/download/v${requested_version}/$filename"
|
||||
specific_version=$requested_version
|
||||
raw_os=$(uname -s)
|
||||
os=$(echo "$raw_os" | tr '[:upper:]' '[:lower:]')
|
||||
case "$raw_os" in
|
||||
Darwin*) os="darwin" ;;
|
||||
Linux*) os="linux" ;;
|
||||
MINGW*|MSYS*|CYGWIN*) os="windows" ;;
|
||||
esac
|
||||
|
||||
arch=$(uname -m)
|
||||
if [[ "$arch" == "aarch64" ]]; then
|
||||
arch="arm64"
|
||||
fi
|
||||
if [[ "$arch" == "x86_64" ]]; then
|
||||
arch="x64"
|
||||
fi
|
||||
|
||||
if [ "$os" = "darwin" ] && [ "$arch" = "x64" ]; then
|
||||
rosetta_flag=$(sysctl -n sysctl.proc_translated 2>/dev/null || echo 0)
|
||||
if [ "$rosetta_flag" = "1" ]; then
|
||||
arch="arm64"
|
||||
fi
|
||||
fi
|
||||
|
||||
combo="$os-$arch"
|
||||
case "$combo" in
|
||||
linux-x64|linux-arm64|darwin-x64|darwin-arm64|windows-x64)
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}Unsupported OS/Arch: $os/$arch${NC}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
archive_ext=".zip"
|
||||
if [ "$os" = "linux" ]; then
|
||||
archive_ext=".tar.gz"
|
||||
fi
|
||||
|
||||
is_musl=false
|
||||
if [ "$os" = "linux" ]; then
|
||||
if [ -f /etc/alpine-release ]; then
|
||||
is_musl=true
|
||||
fi
|
||||
|
||||
if command -v ldd >/dev/null 2>&1; then
|
||||
if ldd --version 2>&1 | grep -qi musl; then
|
||||
is_musl=true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
needs_baseline=false
|
||||
if [ "$arch" = "x64" ]; then
|
||||
if [ "$os" = "linux" ]; then
|
||||
if ! grep -qi avx2 /proc/cpuinfo 2>/dev/null; then
|
||||
needs_baseline=true
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$os" = "darwin" ]; then
|
||||
avx2=$(sysctl -n hw.optional.avx2_0 2>/dev/null || echo 0)
|
||||
if [ "$avx2" != "1" ]; then
|
||||
needs_baseline=true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
target="$os-$arch"
|
||||
if [ "$needs_baseline" = "true" ]; then
|
||||
target="$target-baseline"
|
||||
fi
|
||||
if [ "$is_musl" = "true" ]; then
|
||||
target="$target-musl"
|
||||
fi
|
||||
|
||||
filename="$APP-$target$archive_ext"
|
||||
|
||||
|
||||
if [ "$os" = "linux" ]; then
|
||||
if ! command -v tar >/dev/null 2>&1; then
|
||||
echo -e "${RED}Error: 'tar' is required but not installed.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
if ! command -v unzip >/dev/null 2>&1; then
|
||||
echo -e "${RED}Error: 'unzip' is required but not installed.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$requested_version" ]; then
|
||||
url="https://github.com/anomalyco/opencode/releases/latest/download/$filename"
|
||||
specific_version=$(curl -s https://api.github.com/repos/anomalyco/opencode/releases/latest | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p')
|
||||
|
||||
if [[ $? -ne 0 || -z "$specific_version" ]]; then
|
||||
echo -e "${RED}Failed to fetch version information${NC}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
# Strip leading 'v' if present
|
||||
requested_version="${requested_version#v}"
|
||||
url="https://github.com/anomalyco/opencode/releases/download/v${requested_version}/$filename"
|
||||
specific_version=$requested_version
|
||||
|
||||
# Verify the release exists before downloading
|
||||
http_status=$(curl -sI -o /dev/null -w "%{http_code}" "https://github.com/anomalyco/opencode/releases/tag/v${requested_version}")
|
||||
if [ "$http_status" = "404" ]; then
|
||||
echo -e "${RED}Error: Release v${requested_version} not found${NC}"
|
||||
echo -e "${MUTED}Available releases: https://github.com/anomalyco/opencode/releases${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
print_message() {
|
||||
@@ -133,11 +208,8 @@ check_version() {
|
||||
if command -v opencode >/dev/null 2>&1; then
|
||||
opencode_path=$(which opencode)
|
||||
|
||||
|
||||
## TODO: check if version is installed
|
||||
# installed_version=$(opencode version)
|
||||
installed_version="0.0.1"
|
||||
installed_version=$(echo $installed_version | awk '{print $2}')
|
||||
## Check the installed version
|
||||
installed_version=$(opencode --version 2>/dev/null || echo "")
|
||||
|
||||
if [[ "$installed_version" != "$specific_version" ]]; then
|
||||
print_message info "${MUTED}Installed version: ${NC}$installed_version."
|
||||
@@ -213,11 +285,11 @@ download_with_progress() {
|
||||
{
|
||||
local length=0
|
||||
local bytes=0
|
||||
|
||||
|
||||
while IFS=" " read -r -a line; do
|
||||
[ "${#line[@]}" -lt 2 ] && continue
|
||||
local tag="${line[0]} ${line[1]}"
|
||||
|
||||
|
||||
if [ "$tag" = "0000: content-length:" ]; then
|
||||
length="${line[2]}"
|
||||
length=$(echo "$length" | tr -d '\r')
|
||||
@@ -242,7 +314,7 @@ download_and_install() {
|
||||
print_message info "\n${MUTED}Installing ${NC}opencode ${MUTED}version: ${NC}$specific_version"
|
||||
local tmp_dir="${TMPDIR:-/tmp}/opencode_install_$$"
|
||||
mkdir -p "$tmp_dir"
|
||||
|
||||
|
||||
if [[ "$os" == "windows" ]] || ! [ -t 2 ] || ! download_with_progress "$url" "$tmp_dir/$filename"; then
|
||||
# Fallback to standard curl on Windows, non-TTY environments, or if custom progress fails
|
||||
curl -# -L -o "$tmp_dir/$filename" "$url"
|
||||
@@ -253,14 +325,24 @@ download_and_install() {
|
||||
else
|
||||
unzip -q "$tmp_dir/$filename" -d "$tmp_dir"
|
||||
fi
|
||||
|
||||
|
||||
mv "$tmp_dir/opencode" "$INSTALL_DIR"
|
||||
chmod 755 "${INSTALL_DIR}/opencode"
|
||||
rm -rf "$tmp_dir"
|
||||
}
|
||||
|
||||
check_version
|
||||
download_and_install
|
||||
install_from_binary() {
|
||||
print_message info "\n${MUTED}Installing ${NC}opencode ${MUTED}from: ${NC}$binary_path"
|
||||
cp "$binary_path" "${INSTALL_DIR}/opencode"
|
||||
chmod 755 "${INSTALL_DIR}/opencode"
|
||||
}
|
||||
|
||||
if [ -n "$binary_path" ]; then
|
||||
install_from_binary
|
||||
else
|
||||
check_version
|
||||
download_and_install
|
||||
fi
|
||||
|
||||
|
||||
add_to_path() {
|
||||
@@ -304,42 +386,42 @@ case $current_shell in
|
||||
;;
|
||||
esac
|
||||
|
||||
config_file=""
|
||||
for file in $config_files; do
|
||||
if [[ -f $file ]]; then
|
||||
config_file=$file
|
||||
break
|
||||
if [[ "$no_modify_path" != "true" ]]; then
|
||||
config_file=""
|
||||
for file in $config_files; do
|
||||
if [[ -f $file ]]; then
|
||||
config_file=$file
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -z $config_file ]]; then
|
||||
print_message warning "No config file found for $current_shell. You may need to manually add to PATH:"
|
||||
print_message info " export PATH=$INSTALL_DIR:\$PATH"
|
||||
elif [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then
|
||||
case $current_shell in
|
||||
fish)
|
||||
add_to_path "$config_file" "fish_add_path $INSTALL_DIR"
|
||||
;;
|
||||
zsh)
|
||||
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
|
||||
;;
|
||||
bash)
|
||||
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
|
||||
;;
|
||||
ash)
|
||||
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
|
||||
;;
|
||||
sh)
|
||||
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
|
||||
;;
|
||||
*)
|
||||
export PATH=$INSTALL_DIR:$PATH
|
||||
print_message warning "Manually add the directory to $config_file (or similar):"
|
||||
print_message info " export PATH=$INSTALL_DIR:\$PATH"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -z $config_file ]]; then
|
||||
print_message error "No config file found for $current_shell. Checked files: ${config_files[@]}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then
|
||||
case $current_shell in
|
||||
fish)
|
||||
add_to_path "$config_file" "fish_add_path $INSTALL_DIR"
|
||||
;;
|
||||
zsh)
|
||||
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
|
||||
;;
|
||||
bash)
|
||||
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
|
||||
;;
|
||||
ash)
|
||||
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
|
||||
;;
|
||||
sh)
|
||||
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
|
||||
;;
|
||||
*)
|
||||
export PATH=$INSTALL_DIR:$PATH
|
||||
print_message warning "Manually add the directory to $config_file (or similar):"
|
||||
print_message info " export PATH=$INSTALL_DIR:\$PATH"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if [ -n "${GITHUB_ACTIONS-}" ] && [ "${GITHUB_ACTIONS}" == "true" ]; then
|
||||
@@ -362,4 +444,3 @@ echo -e ""
|
||||
echo -e "${MUTED}For more information visit ${NC}https://opencode.ai/docs"
|
||||
echo -e ""
|
||||
echo -e ""
|
||||
|
||||
|
||||
145
nix/desktop.nix
Normal file
145
nix/desktop.nix
Normal file
@@ -0,0 +1,145 @@
|
||||
{
|
||||
lib,
|
||||
stdenv,
|
||||
rustPlatform,
|
||||
bun,
|
||||
pkg-config,
|
||||
dbus ? null,
|
||||
openssl,
|
||||
glib ? null,
|
||||
gtk3 ? null,
|
||||
libsoup_3 ? null,
|
||||
webkitgtk_4_1 ? null,
|
||||
librsvg ? null,
|
||||
libappindicator-gtk3 ? null,
|
||||
cargo,
|
||||
rustc,
|
||||
makeBinaryWrapper,
|
||||
nodejs,
|
||||
jq,
|
||||
}:
|
||||
args:
|
||||
let
|
||||
scripts = args.scripts;
|
||||
mkModules =
|
||||
attrs:
|
||||
args.mkNodeModules (
|
||||
attrs
|
||||
// {
|
||||
canonicalizeScript = scripts + "/canonicalize-node-modules.ts";
|
||||
normalizeBinsScript = scripts + "/normalize-bun-binaries.ts";
|
||||
}
|
||||
);
|
||||
in
|
||||
rustPlatform.buildRustPackage rec {
|
||||
pname = "opencode-desktop";
|
||||
version = args.version;
|
||||
|
||||
src = args.src;
|
||||
|
||||
# We need to set the root for cargo, but we also need access to the whole repo.
|
||||
postUnpack = ''
|
||||
# Update sourceRoot to point to the tauri app
|
||||
sourceRoot+=/packages/desktop/src-tauri
|
||||
'';
|
||||
|
||||
cargoLock = {
|
||||
lockFile = ../packages/desktop/src-tauri/Cargo.lock;
|
||||
allowBuiltinFetchGit = true;
|
||||
};
|
||||
|
||||
node_modules = mkModules {
|
||||
version = version;
|
||||
src = src;
|
||||
};
|
||||
|
||||
nativeBuildInputs = [
|
||||
pkg-config
|
||||
bun
|
||||
makeBinaryWrapper
|
||||
cargo
|
||||
rustc
|
||||
nodejs
|
||||
jq
|
||||
];
|
||||
|
||||
buildInputs = [
|
||||
openssl
|
||||
]
|
||||
++ lib.optionals stdenv.isLinux [
|
||||
dbus
|
||||
glib
|
||||
gtk3
|
||||
libsoup_3
|
||||
webkitgtk_4_1
|
||||
librsvg
|
||||
libappindicator-gtk3
|
||||
];
|
||||
|
||||
preBuild = ''
|
||||
# Restore node_modules
|
||||
pushd ../../..
|
||||
|
||||
# Copy node_modules from the fixed-output derivation
|
||||
# We use cp -r --no-preserve=mode to ensure we can write to them if needed,
|
||||
# though we usually just read.
|
||||
cp -r ${node_modules}/node_modules .
|
||||
cp -r ${node_modules}/packages .
|
||||
|
||||
# Ensure node_modules is writable so patchShebangs can update script headers
|
||||
chmod -R u+w node_modules
|
||||
# Ensure workspace packages are writable for tsgo incremental outputs (.tsbuildinfo)
|
||||
chmod -R u+w packages
|
||||
# Patch shebangs so scripts can run
|
||||
patchShebangs node_modules
|
||||
|
||||
# Copy sidecar
|
||||
mkdir -p packages/desktop/src-tauri/sidecars
|
||||
targetTriple=${stdenv.hostPlatform.rust.rustcTarget}
|
||||
cp ${args.opencode}/bin/opencode packages/desktop/src-tauri/sidecars/opencode-cli-$targetTriple
|
||||
|
||||
# Merge prod config into tauri.conf.json
|
||||
if ! jq -s '.[0] * .[1]' \
|
||||
packages/desktop/src-tauri/tauri.conf.json \
|
||||
packages/desktop/src-tauri/tauri.prod.conf.json \
|
||||
> packages/desktop/src-tauri/tauri.conf.json.tmp; then
|
||||
echo "Error: failed to merge tauri.conf.json with tauri.prod.conf.json" >&2
|
||||
exit 1
|
||||
fi
|
||||
mv packages/desktop/src-tauri/tauri.conf.json.tmp packages/desktop/src-tauri/tauri.conf.json
|
||||
|
||||
# Build the frontend
|
||||
cd packages/desktop
|
||||
|
||||
# The 'build' script runs 'bun run typecheck && vite build'.
|
||||
bun run build
|
||||
|
||||
popd
|
||||
'';
|
||||
|
||||
# Tauri bundles the assets during the rust build phase (which happens after preBuild).
|
||||
# It looks for them in the location specified in tauri.conf.json.
|
||||
|
||||
postInstall = lib.optionalString stdenv.isLinux ''
|
||||
# Wrap the binary to ensure it finds the libraries
|
||||
wrapProgram $out/bin/opencode-desktop \
|
||||
--prefix LD_LIBRARY_PATH : ${
|
||||
lib.makeLibraryPath [
|
||||
gtk3
|
||||
webkitgtk_4_1
|
||||
librsvg
|
||||
glib
|
||||
libsoup_3
|
||||
]
|
||||
}
|
||||
'';
|
||||
|
||||
meta = with lib; {
|
||||
description = "OpenCode Desktop App";
|
||||
homepage = "https://opencode.ai";
|
||||
license = licenses.mit;
|
||||
maintainers = with maintainers; [ ];
|
||||
mainProgram = "opencode-desktop";
|
||||
platforms = platforms.linux ++ platforms.darwin;
|
||||
};
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
{
|
||||
"nodeModules": "sha256-cpXmqJQJeFj3eED/aOb4YLUdkZFV//7u4f0STBxzUhk="
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-UCPTTk4b7d2bets7KgCeYBHWAUwUAPUyKm+xDYkSexE=",
|
||||
"aarch64-darwin": "sha256-Y3o6lovahSWoG9un/l1qxu7hCmIlZXm2LxOLKNiPQfQ="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,28 @@
|
||||
{ hash, lib, stdenvNoCC, bun, cacert, curl }:
|
||||
{
|
||||
hash,
|
||||
lib,
|
||||
stdenvNoCC,
|
||||
bun,
|
||||
cacert,
|
||||
curl,
|
||||
bunCpu,
|
||||
bunOs,
|
||||
}:
|
||||
args:
|
||||
stdenvNoCC.mkDerivation {
|
||||
pname = "opencode-node_modules";
|
||||
version = args.version;
|
||||
src = args.src;
|
||||
inherit (args) version src;
|
||||
|
||||
impureEnvVars =
|
||||
lib.fetchers.proxyImpureEnvVars
|
||||
++ [
|
||||
"GIT_PROXY_COMMAND"
|
||||
"SOCKS_SERVER"
|
||||
];
|
||||
impureEnvVars = lib.fetchers.proxyImpureEnvVars ++ [
|
||||
"GIT_PROXY_COMMAND"
|
||||
"SOCKS_SERVER"
|
||||
];
|
||||
|
||||
nativeBuildInputs = [ bun cacert curl ];
|
||||
nativeBuildInputs = [
|
||||
bun
|
||||
cacert
|
||||
curl
|
||||
];
|
||||
|
||||
dontConfigure = true;
|
||||
|
||||
@@ -21,8 +31,8 @@ stdenvNoCC.mkDerivation {
|
||||
export HOME=$(mktemp -d)
|
||||
export BUN_INSTALL_CACHE_DIR=$(mktemp -d)
|
||||
bun install \
|
||||
--cpu="*" \
|
||||
--os="*" \
|
||||
--cpu="${bunCpu}" \
|
||||
--os="${bunOs}" \
|
||||
--frozen-lockfile \
|
||||
--ignore-scripts \
|
||||
--no-progress \
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
{ lib, stdenvNoCC, bun, ripgrep, makeBinaryWrapper }:
|
||||
{
|
||||
lib,
|
||||
stdenvNoCC,
|
||||
bun,
|
||||
ripgrep,
|
||||
makeBinaryWrapper,
|
||||
}:
|
||||
args:
|
||||
let
|
||||
scripts = args.scripts;
|
||||
inherit (args) scripts;
|
||||
mkModules =
|
||||
attrs:
|
||||
args.mkNodeModules (
|
||||
@@ -14,13 +20,10 @@ let
|
||||
in
|
||||
stdenvNoCC.mkDerivation (finalAttrs: {
|
||||
pname = "opencode";
|
||||
version = args.version;
|
||||
|
||||
src = args.src;
|
||||
inherit (args) version src;
|
||||
|
||||
node_modules = mkModules {
|
||||
version = finalAttrs.version;
|
||||
src = finalAttrs.src;
|
||||
inherit (finalAttrs) version src;
|
||||
};
|
||||
|
||||
nativeBuildInputs = [
|
||||
@@ -122,7 +125,7 @@ stdenvNoCC.mkDerivation (finalAttrs: {
|
||||
It combines a TypeScript/JavaScript core with a Go-based TUI
|
||||
to provide an interactive AI coding experience.
|
||||
'';
|
||||
homepage = "https://github.com/sst/opencode";
|
||||
homepage = "https://github.com/anomalyco/opencode";
|
||||
license = lib.licenses.mit;
|
||||
platforms = [
|
||||
"aarch64-linux"
|
||||
|
||||
@@ -60,7 +60,12 @@ const result = await Bun.build({
|
||||
compile: {
|
||||
target,
|
||||
outfile: "opencode",
|
||||
execArgv: ["--user-agent=opencode/" + version, '--env-file=""', "--"],
|
||||
autoloadBunfig: false,
|
||||
autoloadDotenv: false,
|
||||
//@ts-ignore (bun types aren't up to date)
|
||||
autoloadTsconfig: true,
|
||||
autoloadPackageJson: true,
|
||||
execArgv: ["--user-agent=opencode/" + version, "--use-system-ca", "--"],
|
||||
windows: {},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -31,9 +31,13 @@ for (const [name, wasmPath] of byName) {
|
||||
next = next.replaceAll("tree-sitter.wasm", mainWasm).replaceAll("web-tree-sitter/tree-sitter.wasm", mainWasm)
|
||||
|
||||
// Collapse any relative prefixes before absolute store paths (e.g., "../../../..//nix/store/...")
|
||||
const nixStorePrefix = process.env.NIX_STORE || "/nix/store"
|
||||
next = next.replace(/(\.\/)+/g, "./")
|
||||
next = next.replace(/(\.\.\/)+\/?(\/nix\/store[^"']+)/g, "/$2")
|
||||
next = next.replace(/(["'])\/{2,}(\/nix\/store[^"']+)(["'])/g, "$1/$2$3")
|
||||
next = next.replace(/(["'])\/\/(nix\/store[^"']+)(["'])/g, "$1/$2$3")
|
||||
next = next.replace(
|
||||
new RegExp(`(\\.\\.\\/)+\\/{1,2}(${nixStorePrefix.replace(/^\//, "").replace(/\//g, "\\/")}[^"']+)`, "g"),
|
||||
"/$2",
|
||||
)
|
||||
next = next.replace(new RegExp(`(["'])\\/{2,}(\\/${nixStorePrefix.replace(/\//g, "\\/")}[^"']+)(["'])`, "g"), "$1$2$3")
|
||||
next = next.replace(new RegExp(`(["'])\\/\\/(${nixStorePrefix.replace(/\//g, "\\/")}[^"']+)(["'])`, "g"), "$1$2$3")
|
||||
|
||||
if (next !== content) fs.writeFileSync(file, next)
|
||||
|
||||
@@ -10,7 +10,7 @@ HASH_FILE=${HASH_FILE:-$DEFAULT_HASH_FILE}
|
||||
if [ ! -f "$HASH_FILE" ]; then
|
||||
cat >"$HASH_FILE" <<EOF
|
||||
{
|
||||
"nodeModules": "$DUMMY"
|
||||
"nodeModules": {}
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
@@ -33,9 +33,16 @@ trap cleanup EXIT
|
||||
|
||||
write_node_modules_hash() {
|
||||
local value="$1"
|
||||
local system="${2:-$SYSTEM}"
|
||||
local temp
|
||||
temp=$(mktemp)
|
||||
jq --arg value "$value" '.nodeModules = $value' "$HASH_FILE" >"$temp"
|
||||
|
||||
if jq -e '.nodeModules | type == "object"' "$HASH_FILE" >/dev/null 2>&1; then
|
||||
jq --arg system "$system" --arg value "$value" '.nodeModules[$system] = $value' "$HASH_FILE" >"$temp"
|
||||
else
|
||||
jq --arg system "$system" --arg value "$value" '.nodeModules = {($system): $value}' "$HASH_FILE" >"$temp"
|
||||
fi
|
||||
|
||||
mv "$temp" "$HASH_FILE"
|
||||
}
|
||||
|
||||
@@ -104,7 +111,7 @@ fi
|
||||
|
||||
write_node_modules_hash "$CORRECT_HASH"
|
||||
|
||||
jq -e --arg hash "$CORRECT_HASH" '.nodeModules == $hash' "$HASH_FILE" >/dev/null
|
||||
jq -e --arg system "$SYSTEM" --arg hash "$CORRECT_HASH" '.nodeModules[$system] == $hash' "$HASH_FILE" >/dev/null
|
||||
|
||||
echo "node_modules hash updated for ${SYSTEM}: $CORRECT_HASH"
|
||||
|
||||
|
||||
16
package.json
16
package.json
@@ -10,7 +10,8 @@
|
||||
"typecheck": "bun turbo typecheck",
|
||||
"prepare": "husky",
|
||||
"random": "echo 'Random script'",
|
||||
"hello": "echo 'Hello World!'"
|
||||
"hello": "echo 'Hello World!'",
|
||||
"test": "echo 'do not run tests from root' && exit 1"
|
||||
},
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
@@ -31,19 +32,23 @@
|
||||
"@tsconfig/bun": "1.0.9",
|
||||
"@cloudflare/workers-types": "4.20251008.0",
|
||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||
"@pierre/diffs": "1.0.0-beta.3",
|
||||
"@pierre/diffs": "1.0.2",
|
||||
"@solid-primitives/storage": "4.3.3",
|
||||
"@tailwindcss/vite": "4.1.11",
|
||||
"diff": "8.0.2",
|
||||
"ai": "5.0.97",
|
||||
"dompurify": "3.3.1",
|
||||
"ai": "5.0.119",
|
||||
"hono": "4.10.7",
|
||||
"hono-openapi": "1.1.2",
|
||||
"fuzzysort": "3.1.0",
|
||||
"luxon": "3.6.1",
|
||||
"marked": "17.0.1",
|
||||
"marked-shiki": "1.2.1",
|
||||
"typescript": "5.8.2",
|
||||
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
||||
"zod": "4.1.8",
|
||||
"remeda": "2.26.0",
|
||||
"shiki": "3.20.0",
|
||||
"solid-list": "0.3.0",
|
||||
"tailwindcss": "4.1.11",
|
||||
"virtua": "0.42.3",
|
||||
@@ -56,6 +61,7 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/artifact": "5.0.1",
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"husky": "9.1.7",
|
||||
"prettier": "3.6.2",
|
||||
@@ -64,13 +70,14 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.933.0",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/script": "workspace:*",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"typescript": "catalog:"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sst/opencode"
|
||||
"url": "https://github.com/anomalyco/opencode"
|
||||
},
|
||||
"license": "MIT",
|
||||
"prettier": {
|
||||
@@ -80,7 +87,6 @@
|
||||
"trustedDependencies": [
|
||||
"esbuild",
|
||||
"protobufjs",
|
||||
"sharp",
|
||||
"tree-sitter",
|
||||
"tree-sitter-bash",
|
||||
"web-tree-sitter"
|
||||
|
||||
1
packages/app/.gitignore
vendored
Normal file
1
packages/app/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
src/assets/theme.css
|
||||
13
packages/app/AGENTS.md
Normal file
13
packages/app/AGENTS.md
Normal file
@@ -0,0 +1,13 @@
|
||||
## Debugging
|
||||
|
||||
- To test the opencode app, use the playwright MCP server, the app is already
|
||||
running at http://localhost:3000
|
||||
- NEVER try to restart the app, or the server process, EVER.
|
||||
|
||||
## SolidJS
|
||||
|
||||
- Always prefer `createStore` over multiple `createSignal` calls
|
||||
|
||||
## Tool Calling
|
||||
|
||||
- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE.
|
||||
34
packages/app/README.md
Normal file
34
packages/app/README.md
Normal file
@@ -0,0 +1,34 @@
|
||||
## Usage
|
||||
|
||||
Dependencies for these templates are managed with [pnpm](https://pnpm.io) using `pnpm up -Lri`.
|
||||
|
||||
This is the reason you see a `pnpm-lock.yaml`. That said, any package manager will work. This file can safely be removed once you clone a template.
|
||||
|
||||
```bash
|
||||
$ npm install # or pnpm install or yarn install
|
||||
```
|
||||
|
||||
### Learn more on the [Solid Website](https://solidjs.com) and come chat with us on our [Discord](https://discord.com/invite/solidjs)
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm run dev` or `npm start`
|
||||
|
||||
Runs the app in the development mode.<br>
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.<br>
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `dist` folder.<br>
|
||||
It correctly bundles Solid in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.<br>
|
||||
Your app is ready to be deployed!
|
||||
|
||||
## Deployment
|
||||
|
||||
You can deploy the `dist` folder to any static host provider (netlify, surge, now, etc.)
|
||||
@@ -1,5 +1,5 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="en" style="background-color: var(--background-base)">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
@@ -13,16 +13,12 @@
|
||||
<meta name="theme-color" content="#131010" media="(prefers-color-scheme: dark)" />
|
||||
<meta property="og:image" content="/social-share.png" />
|
||||
<meta property="twitter:image" content="/social-share.png" />
|
||||
<!-- Theme preload script - applies cached theme to avoid FOUC -->
|
||||
<script id="oc-theme-preload-script" src="/oc-theme-preload.js"></script>
|
||||
</head>
|
||||
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
|
||||
<script>
|
||||
;(function () {
|
||||
const savedTheme = localStorage.getItem("theme") || "oc-1"
|
||||
document.documentElement.setAttribute("data-theme", savedTheme)
|
||||
})()
|
||||
</script>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root" class="flex flex-col h-screen"></div>
|
||||
<script src="/src/index.tsx" type="module"></script>
|
||||
<div id="root" class="flex flex-col h-dvh"></div>
|
||||
<script src="/src/entry.tsx" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
62
packages/app/package.json
Normal file
62
packages/app/package.json
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.1.17",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./vite": "./vite.js"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsgo -b",
|
||||
"start": "vite",
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"serve": "vite preview"
|
||||
},
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@happy-dom/global-registrator": "20.0.11",
|
||||
"@tailwindcss/vite": "catalog:",
|
||||
"@tsconfig/bun": "1.0.9",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/luxon": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"vite": "catalog:",
|
||||
"vite-plugin-icons-spritesheet": "3.0.1",
|
||||
"vite-plugin-solid": "catalog:"
|
||||
},
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"@shikijs/transformers": "3.9.2",
|
||||
"@solid-primitives/active-element": "2.1.3",
|
||||
"@solid-primitives/audio": "1.4.2",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
"@solid-primitives/media": "2.3.3",
|
||||
"@solid-primitives/resize-observer": "2.1.3",
|
||||
"@solid-primitives/scroll": "2.1.3",
|
||||
"@solid-primitives/storage": "catalog:",
|
||||
"@solid-primitives/websocket": "1.3.1",
|
||||
"@solidjs/meta": "catalog:",
|
||||
"@solidjs/router": "catalog:",
|
||||
"@thisbeyond/solid-dnd": "0.7.5",
|
||||
"diff": "catalog:",
|
||||
"fuzzysort": "catalog:",
|
||||
"ghostty-web": "0.3.0",
|
||||
"luxon": "catalog:",
|
||||
"marked": "catalog:",
|
||||
"marked-shiki": "catalog:",
|
||||
"remeda": "catalog:",
|
||||
"shiki": "catalog:",
|
||||
"solid-js": "catalog:",
|
||||
"solid-list": "catalog:",
|
||||
"tailwindcss": "catalog:",
|
||||
"virtua": "catalog:",
|
||||
"zod": "catalog:"
|
||||
}
|
||||
}
|
||||
17
packages/app/public/_headers
Normal file
17
packages/app/public/_headers
Normal file
@@ -0,0 +1,17 @@
|
||||
/assets/*.js
|
||||
Content-Type: application/javascript
|
||||
|
||||
/assets/*.mjs
|
||||
Content-Type: application/javascript
|
||||
|
||||
/assets/*.css
|
||||
Content-Type: text/css
|
||||
|
||||
/*.js
|
||||
Content-Type: application/javascript
|
||||
|
||||
/*.mjs
|
||||
Content-Type: application/javascript
|
||||
|
||||
/*.css
|
||||
Content-Type: text/css
|
||||
28
packages/app/public/oc-theme-preload.js
Normal file
28
packages/app/public/oc-theme-preload.js
Normal file
@@ -0,0 +1,28 @@
|
||||
;(function () {
|
||||
var themeId = localStorage.getItem("opencode-theme-id")
|
||||
if (!themeId) return
|
||||
|
||||
var scheme = localStorage.getItem("opencode-color-scheme") || "system"
|
||||
var isDark = scheme === "dark" || (scheme === "system" && matchMedia("(prefers-color-scheme: dark)").matches)
|
||||
var mode = isDark ? "dark" : "light"
|
||||
|
||||
document.documentElement.dataset.theme = themeId
|
||||
document.documentElement.dataset.colorScheme = mode
|
||||
|
||||
if (themeId === "oc-1") return
|
||||
|
||||
var css = localStorage.getItem("opencode-theme-css-" + themeId + "-" + mode)
|
||||
if (css) {
|
||||
var style = document.createElement("style")
|
||||
style.id = "oc-theme-preload"
|
||||
style.textContent =
|
||||
":root{color-scheme:" +
|
||||
mode +
|
||||
";--text-mix-blend-mode:" +
|
||||
(isDark ? "plus-lighter" : "multiply") +
|
||||
";" +
|
||||
css +
|
||||
"}"
|
||||
document.head.appendChild(style)
|
||||
}
|
||||
})()
|
||||
@@ -242,6 +242,53 @@ describe("SerializeAddon", () => {
|
||||
expect(term2.buffer.active.getLine(2)?.translateToString(true)).toBe("total 42")
|
||||
})
|
||||
|
||||
test("serialized output should restore after Terminal.reset()", async () => {
|
||||
const { term, addon } = createTerminal()
|
||||
|
||||
const content = [
|
||||
"\x1b[1;32m❯\x1b[0m \x1b[34mcd\x1b[0m /some/path",
|
||||
"\x1b[1;32m❯\x1b[0m \x1b[34mls\x1b[0m -la",
|
||||
"total 42",
|
||||
].join("\r\n")
|
||||
|
||||
await writeAndWait(term, content)
|
||||
|
||||
const serialized = addon.serialize()
|
||||
|
||||
const { term: term2 } = createTerminal()
|
||||
terminals.push(term2)
|
||||
term2.reset()
|
||||
await writeAndWait(term2, serialized)
|
||||
|
||||
expect(term2.buffer.active.getLine(0)?.translateToString(true)).toContain("cd /some/path")
|
||||
expect(term2.buffer.active.getLine(1)?.translateToString(true)).toContain("ls -la")
|
||||
expect(term2.buffer.active.getLine(2)?.translateToString(true)).toBe("total 42")
|
||||
})
|
||||
|
||||
test("alternate buffer should round-trip without garbage", async () => {
|
||||
const { term, addon } = createTerminal(20, 5)
|
||||
|
||||
await writeAndWait(term, "normal\r\n")
|
||||
await writeAndWait(term, "\x1b[?1049h\x1b[HALT")
|
||||
|
||||
expect(term.buffer.active.type).toBe("alternate")
|
||||
|
||||
const serialized = addon.serialize()
|
||||
|
||||
const { term: term2 } = createTerminal(20, 5)
|
||||
terminals.push(term2)
|
||||
await writeAndWait(term2, serialized)
|
||||
|
||||
expect(term2.buffer.active.type).toBe("alternate")
|
||||
|
||||
const line = term2.buffer.active.getLine(0)
|
||||
expect(line?.translateToString(true)).toBe("ALT")
|
||||
|
||||
// Ensure a cell beyond content isn't garbage
|
||||
const cellCode = line?.getCell(10)?.getCode()
|
||||
expect(cellCode === 0 || cellCode === 32).toBe(true)
|
||||
})
|
||||
|
||||
test("serialized output written to new terminal should match original colors", async () => {
|
||||
const { term, addon } = createTerminal(40, 5)
|
||||
|
||||
@@ -157,23 +157,6 @@ function equalFlags(cell1: IBufferCell, cell2: IBufferCell): boolean {
|
||||
abstract class BaseSerializeHandler {
|
||||
constructor(protected readonly _buffer: IBuffer) {}
|
||||
|
||||
private _isRealContent(codepoint: number): boolean {
|
||||
if (codepoint === 0) return false
|
||||
if (codepoint >= 0xf000) return false
|
||||
return true
|
||||
}
|
||||
|
||||
private _findLastContentColumn(line: IBufferLine): number {
|
||||
let lastContent = -1
|
||||
for (let col = 0; col < line.length; col++) {
|
||||
const cell = line.getCell(col)
|
||||
if (cell && this._isRealContent(cell.getCode())) {
|
||||
lastContent = col
|
||||
}
|
||||
}
|
||||
return lastContent + 1
|
||||
}
|
||||
|
||||
public serialize(range: IBufferRange, excludeFinalCursorPosition?: boolean): string {
|
||||
let oldCell = this._buffer.getNullCell()
|
||||
|
||||
@@ -182,14 +165,14 @@ abstract class BaseSerializeHandler {
|
||||
const startColumn = range.start.x
|
||||
const endColumn = range.end.x
|
||||
|
||||
this._beforeSerialize(endRow - startRow, startRow, endRow)
|
||||
this._beforeSerialize(endRow - startRow + 1, startRow, endRow)
|
||||
|
||||
for (let row = startRow; row <= endRow; row++) {
|
||||
const line = this._buffer.getLine(row)
|
||||
if (line) {
|
||||
const startLineColumn = row === range.start.y ? startColumn : 0
|
||||
const maxColumn = row === range.end.y ? endColumn : this._findLastContentColumn(line)
|
||||
const endLineColumn = Math.min(maxColumn, line.length)
|
||||
const endLineColumn = Math.min(endColumn, line.length)
|
||||
|
||||
for (let col = startLineColumn; col < endLineColumn; col++) {
|
||||
const c = line.getCell(col)
|
||||
if (!c) {
|
||||
@@ -243,6 +226,13 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
||||
|
||||
protected _beforeSerialize(rows: number, start: number, _end: number): void {
|
||||
this._allRows = new Array<string>(rows)
|
||||
this._allRowSeparators = new Array<string>(rows)
|
||||
this._rowIndex = 0
|
||||
|
||||
this._currentRow = ""
|
||||
this._nullCellCount = 0
|
||||
this._cursorStyle = this._buffer.getNullCell()
|
||||
|
||||
this._lastContentCursorRow = start
|
||||
this._lastCursorRow = start
|
||||
this._firstRow = start
|
||||
@@ -251,6 +241,11 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
||||
protected _rowEnd(row: number, isLastRow: boolean): void {
|
||||
let rowSeparator = ""
|
||||
|
||||
if (this._nullCellCount > 0) {
|
||||
this._currentRow += " ".repeat(this._nullCellCount)
|
||||
this._nullCellCount = 0
|
||||
}
|
||||
|
||||
if (!isLastRow) {
|
||||
const nextLine = this._buffer.getLine(row + 1)
|
||||
|
||||
@@ -388,7 +383,8 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
||||
}
|
||||
|
||||
const codepoint = cell.getCode()
|
||||
const isGarbage = codepoint >= 0xf000
|
||||
const isInvalidCodepoint = codepoint > 0x10ffff || (codepoint >= 0xd800 && codepoint <= 0xdfff)
|
||||
const isGarbage = isInvalidCodepoint || (codepoint >= 0xf000 && cell.getWidth() === 1)
|
||||
const isEmptyCell = codepoint === 0 || cell.getChars() === "" || isGarbage
|
||||
|
||||
const sgrSeq = this._diffStyle(cell, this._cursorStyle)
|
||||
@@ -397,7 +393,7 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
||||
|
||||
if (styleChanged) {
|
||||
if (this._nullCellCount > 0) {
|
||||
this._currentRow += `\u001b[${this._nullCellCount}C`
|
||||
this._currentRow += " ".repeat(this._nullCellCount)
|
||||
this._nullCellCount = 0
|
||||
}
|
||||
|
||||
@@ -417,7 +413,7 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
||||
this._nullCellCount += cell.getWidth()
|
||||
} else {
|
||||
if (this._nullCellCount > 0) {
|
||||
this._currentRow += `\u001b[${this._nullCellCount}C`
|
||||
this._currentRow += " ".repeat(this._nullCellCount)
|
||||
this._nullCellCount = 0
|
||||
}
|
||||
|
||||
127
packages/app/src/app.tsx
Normal file
127
packages/app/src/app.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import "@/index.css"
|
||||
import { ErrorBoundary, Show, lazy, type ParentProps } from "solid-js"
|
||||
import { Router, Route, Navigate } from "@solidjs/router"
|
||||
import { MetaProvider } from "@solidjs/meta"
|
||||
import { Font } from "@opencode-ai/ui/font"
|
||||
import { MarkedProvider } from "@opencode-ai/ui/context/marked"
|
||||
import { DiffComponentProvider } from "@opencode-ai/ui/context/diff"
|
||||
import { CodeComponentProvider } from "@opencode-ai/ui/context/code"
|
||||
import { Diff } from "@opencode-ai/ui/diff"
|
||||
import { Code } from "@opencode-ai/ui/code"
|
||||
import { ThemeProvider } from "@opencode-ai/ui/theme"
|
||||
import { GlobalSyncProvider } from "@/context/global-sync"
|
||||
import { PermissionProvider } from "@/context/permission"
|
||||
import { LayoutProvider } from "@/context/layout"
|
||||
import { GlobalSDKProvider } from "@/context/global-sdk"
|
||||
import { ServerProvider, useServer } from "@/context/server"
|
||||
import { TerminalProvider } from "@/context/terminal"
|
||||
import { PromptProvider } from "@/context/prompt"
|
||||
import { FileProvider } from "@/context/file"
|
||||
import { NotificationProvider } from "@/context/notification"
|
||||
import { DialogProvider } from "@opencode-ai/ui/context/dialog"
|
||||
import { CommandProvider } from "@/context/command"
|
||||
import { Logo } from "@opencode-ai/ui/logo"
|
||||
import Layout from "@/pages/layout"
|
||||
import DirectoryLayout from "@/pages/directory-layout"
|
||||
import { ErrorPage } from "./pages/error"
|
||||
import { iife } from "@opencode-ai/util/iife"
|
||||
import { Suspense } from "solid-js"
|
||||
|
||||
const Home = lazy(() => import("@/pages/home"))
|
||||
const Session = lazy(() => import("@/pages/session"))
|
||||
const Loading = () => <div class="size-full flex items-center justify-center text-text-weak">Loading...</div>
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__OPENCODE__?: { updaterEnabled?: boolean; serverPassword?: string }
|
||||
}
|
||||
}
|
||||
|
||||
export function AppBaseProviders(props: ParentProps) {
|
||||
return (
|
||||
<MetaProvider>
|
||||
<Font />
|
||||
<ThemeProvider>
|
||||
<ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
|
||||
<DialogProvider>
|
||||
<MarkedProvider>
|
||||
<DiffComponentProvider component={Diff}>
|
||||
<CodeComponentProvider component={Code}>{props.children}</CodeComponentProvider>
|
||||
</DiffComponentProvider>
|
||||
</MarkedProvider>
|
||||
</DialogProvider>
|
||||
</ErrorBoundary>
|
||||
</ThemeProvider>
|
||||
</MetaProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function ServerKey(props: ParentProps) {
|
||||
const server = useServer()
|
||||
return (
|
||||
<Show when={server.url} keyed>
|
||||
{props.children}
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
export function AppInterface(props: { defaultUrl?: string }) {
|
||||
const defaultServerUrl = () => {
|
||||
if (props.defaultUrl) return props.defaultUrl
|
||||
if (location.hostname.includes("opencode.ai")) return "http://localhost:4096"
|
||||
if (import.meta.env.DEV)
|
||||
return `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}`
|
||||
|
||||
return window.location.origin
|
||||
}
|
||||
|
||||
return (
|
||||
<ServerProvider defaultUrl={defaultServerUrl()}>
|
||||
<ServerKey>
|
||||
<GlobalSDKProvider>
|
||||
<GlobalSyncProvider>
|
||||
<Router
|
||||
root={(props) => (
|
||||
<PermissionProvider>
|
||||
<LayoutProvider>
|
||||
<NotificationProvider>
|
||||
<CommandProvider>
|
||||
<Layout>{props.children}</Layout>
|
||||
</CommandProvider>
|
||||
</NotificationProvider>
|
||||
</LayoutProvider>
|
||||
</PermissionProvider>
|
||||
)}
|
||||
>
|
||||
<Route
|
||||
path="/"
|
||||
component={() => (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Home />
|
||||
</Suspense>
|
||||
)}
|
||||
/>
|
||||
<Route path="/:dir" component={DirectoryLayout}>
|
||||
<Route path="/" component={() => <Navigate href="session" />} />
|
||||
<Route
|
||||
path="/session/:id?"
|
||||
component={() => (
|
||||
<TerminalProvider>
|
||||
<FileProvider>
|
||||
<PromptProvider>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Session />
|
||||
</Suspense>
|
||||
</PromptProvider>
|
||||
</FileProvider>
|
||||
</TerminalProvider>
|
||||
)}
|
||||
/>
|
||||
</Route>
|
||||
</Router>
|
||||
</GlobalSyncProvider>
|
||||
</GlobalSDKProvider>
|
||||
</ServerKey>
|
||||
</ServerProvider>
|
||||
)
|
||||
}
|
||||
@@ -1,24 +1,24 @@
|
||||
import type { ProviderAuthAuthorization } from "@opencode-ai/sdk/v2/client"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import type { IconName } from "@opencode-ai/ui/icons/provider"
|
||||
import { List, type ListRef } from "@opencode-ai/ui/list"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import { Spinner } from "@opencode-ai/ui/spinner"
|
||||
import { TextField } from "@opencode-ai/ui/text-field"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { iife } from "@opencode-ai/util/iife"
|
||||
import { createMemo, Match, onCleanup, onMount, Switch } from "solid-js"
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { ProviderAuthAuthorization } from "@opencode-ai/sdk/v2/client"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { List, ListRef } from "@opencode-ai/ui/list"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { TextField } from "@opencode-ai/ui/text-field"
|
||||
import { Spinner } from "@opencode-ai/ui/spinner"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import { IconName } from "@opencode-ai/ui/icons/provider"
|
||||
import { iife } from "@opencode-ai/util/iife"
|
||||
import { Link } from "@/components/link"
|
||||
import { DialogSelectProvider } from "./dialog-select-provider"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { DialogSelectModel } from "./dialog-select-model"
|
||||
import { DialogSelectProvider } from "./dialog-select-provider"
|
||||
|
||||
export function DialogConnectProvider(props: { provider: string }) {
|
||||
const dialog = useDialog()
|
||||
@@ -154,7 +154,9 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
<div class="text-14-regular text-text-base">Select login method for {provider().name}.</div>
|
||||
<div class="">
|
||||
<List
|
||||
ref={(ref) => (listRef = ref)}
|
||||
ref={(ref) => {
|
||||
listRef = ref
|
||||
}}
|
||||
items={methods}
|
||||
key={(m) => m?.label}
|
||||
onSelect={async (method, index) => {
|
||||
@@ -163,7 +165,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
}}
|
||||
>
|
||||
{(i) => (
|
||||
<div class="w-full flex items-center gap-x-4">
|
||||
<div class="w-full flex items-center gap-x-2">
|
||||
<div class="w-4 h-2 rounded-[1px] bg-input-base shadow-xs-border-base flex items-center justify-center">
|
||||
<div class="w-2.5 h-0.5 bg-icon-strong-base hidden" data-slot="list-item-extra-icon" />
|
||||
</div>
|
||||
@@ -175,7 +177,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
</Match>
|
||||
<Match when={store.state === "pending"}>
|
||||
<div class="text-14-regular text-text-base">
|
||||
<div class="flex items-center gap-x-4">
|
||||
<div class="flex items-center gap-x-2">
|
||||
<Spinner />
|
||||
<span>Authorization in progress...</span>
|
||||
</div>
|
||||
@@ -183,7 +185,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
</Match>
|
||||
<Match when={store.state === "error"}>
|
||||
<div class="text-14-regular text-text-base">
|
||||
<div class="flex items-center gap-x-4">
|
||||
<div class="flex items-center gap-x-2">
|
||||
<Icon name="circle-ban-sign" class="text-icon-critical-base" />
|
||||
<span>Authorization failed: {store.error}</span>
|
||||
</div>
|
||||
176
packages/app/src/components/dialog-edit-project.tsx
Normal file
176
packages/app/src/components/dialog-edit-project.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { TextField } from "@opencode-ai/ui/text-field"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { createMemo, createSignal, For, Show } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { type LocalProject, getAvatarColors } from "@/context/layout"
|
||||
import { getFilename } from "@opencode-ai/util/path"
|
||||
import { Avatar } from "@opencode-ai/ui/avatar"
|
||||
|
||||
const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
|
||||
|
||||
export function DialogEditProject(props: { project: LocalProject }) {
|
||||
const dialog = useDialog()
|
||||
const globalSDK = useGlobalSDK()
|
||||
|
||||
const folderName = createMemo(() => getFilename(props.project.worktree))
|
||||
const defaultName = createMemo(() => props.project.name || folderName())
|
||||
|
||||
const [store, setStore] = createStore({
|
||||
name: defaultName(),
|
||||
color: props.project.icon?.color || "pink",
|
||||
iconUrl: props.project.icon?.url || "",
|
||||
saving: false,
|
||||
})
|
||||
|
||||
const [dragOver, setDragOver] = createSignal(false)
|
||||
|
||||
function handleFileSelect(file: File) {
|
||||
if (!file.type.startsWith("image/")) return
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => setStore("iconUrl", e.target?.result as string)
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
|
||||
function handleDrop(e: DragEvent) {
|
||||
e.preventDefault()
|
||||
setDragOver(false)
|
||||
const file = e.dataTransfer?.files[0]
|
||||
if (file) handleFileSelect(file)
|
||||
}
|
||||
|
||||
function handleDragOver(e: DragEvent) {
|
||||
e.preventDefault()
|
||||
setDragOver(true)
|
||||
}
|
||||
|
||||
function handleDragLeave() {
|
||||
setDragOver(false)
|
||||
}
|
||||
|
||||
function handleInputChange(e: Event) {
|
||||
const input = e.target as HTMLInputElement
|
||||
const file = input.files?.[0]
|
||||
if (file) handleFileSelect(file)
|
||||
}
|
||||
|
||||
function clearIcon() {
|
||||
setStore("iconUrl", "")
|
||||
}
|
||||
|
||||
async function handleSubmit(e: SubmitEvent) {
|
||||
e.preventDefault()
|
||||
if (!props.project.id) return
|
||||
|
||||
setStore("saving", true)
|
||||
const name = store.name.trim() === folderName() ? "" : store.name.trim()
|
||||
await globalSDK.client.project.update({
|
||||
projectID: props.project.id,
|
||||
name,
|
||||
icon: { color: store.color, url: store.iconUrl },
|
||||
})
|
||||
setStore("saving", false)
|
||||
dialog.close()
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog title="Edit project">
|
||||
<form onSubmit={handleSubmit} class="flex flex-col gap-6 px-2.5 pb-3">
|
||||
<div class="flex flex-col gap-4">
|
||||
<TextField
|
||||
autofocus
|
||||
type="text"
|
||||
label="Name"
|
||||
placeholder={folderName()}
|
||||
value={store.name}
|
||||
onChange={(v) => setStore("name", v)}
|
||||
/>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="text-12-medium text-text-weak">Icon</label>
|
||||
<div class="flex gap-3 items-start">
|
||||
<div class="relative">
|
||||
<div
|
||||
class="size-16 rounded-lg overflow-hidden border border-dashed transition-colors cursor-pointer"
|
||||
classList={{
|
||||
"border-text-interactive-base bg-surface-info-base/20": dragOver(),
|
||||
"border-border-base hover:border-border-strong": !dragOver(),
|
||||
}}
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onClick={() => document.getElementById("icon-upload")?.click()}
|
||||
>
|
||||
<Show
|
||||
when={store.iconUrl}
|
||||
fallback={
|
||||
<div class="size-full flex items-center justify-center">
|
||||
<Avatar
|
||||
fallback={store.name || defaultName()}
|
||||
{...getAvatarColors(store.color)}
|
||||
class="size-full"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<img src={store.iconUrl} alt="Project icon" class="size-full object-cover" />
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={store.iconUrl}>
|
||||
<button
|
||||
type="button"
|
||||
class="absolute -top-1.5 -right-1.5 size-5 rounded-full bg-surface-raised-base border border-border-base flex items-center justify-center hover:bg-surface-raised-base-hover"
|
||||
onClick={clearIcon}
|
||||
>
|
||||
<Icon name="close" class="size-3 text-icon-base" />
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
<input id="icon-upload" type="file" accept="image/*" class="hidden" onChange={handleInputChange} />
|
||||
<div class="flex flex-col gap-1.5 text-12-regular text-text-weak">
|
||||
<span>Click or drag an image</span>
|
||||
<span>Recommended: 128x128px</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={!store.iconUrl}>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="text-12-medium text-text-weak">Color</label>
|
||||
<div class="flex gap-2">
|
||||
<For each={AVATAR_COLOR_KEYS}>
|
||||
{(color) => (
|
||||
<button
|
||||
type="button"
|
||||
class="relative size-8 rounded-md transition-all"
|
||||
classList={{
|
||||
"ring-2 ring-offset-2 ring-offset-surface-base ring-text-interactive-base":
|
||||
store.color === color,
|
||||
}}
|
||||
style={{ background: getAvatarColors(color).background }}
|
||||
onClick={() => setStore("color", color)}
|
||||
>
|
||||
<Avatar fallback={store.name || defaultName()} {...getAvatarColors(color)} class="size-full" />
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button type="button" variant="ghost" size="large" onClick={() => dialog.close()}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" variant="primary" size="large" disabled={store.saving}>
|
||||
{store.saving ? "Saving..." : "Save"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
99
packages/app/src/components/dialog-fork.tsx
Normal file
99
packages/app/src/components/dialog-fork.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import { Component, createMemo } from "solid-js"
|
||||
import { useNavigate, useParams } from "@solidjs/router"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { usePrompt } from "@/context/prompt"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
import { extractPromptFromParts } from "@/utils/prompt"
|
||||
import type { TextPart as SDKTextPart } from "@opencode-ai/sdk/v2/client"
|
||||
import { base64Encode } from "@opencode-ai/util/encode"
|
||||
|
||||
interface ForkableMessage {
|
||||
id: string
|
||||
text: string
|
||||
time: string
|
||||
}
|
||||
|
||||
function formatTime(date: Date): string {
|
||||
return date.toLocaleTimeString(undefined, { timeStyle: "short" })
|
||||
}
|
||||
|
||||
export const DialogFork: Component = () => {
|
||||
const params = useParams()
|
||||
const navigate = useNavigate()
|
||||
const sync = useSync()
|
||||
const sdk = useSDK()
|
||||
const prompt = usePrompt()
|
||||
const dialog = useDialog()
|
||||
|
||||
const messages = createMemo((): ForkableMessage[] => {
|
||||
const sessionID = params.id
|
||||
if (!sessionID) return []
|
||||
|
||||
const msgs = sync.data.message[sessionID] ?? []
|
||||
const result: ForkableMessage[] = []
|
||||
|
||||
for (const message of msgs) {
|
||||
if (message.role !== "user") continue
|
||||
|
||||
const parts = sync.data.part[message.id] ?? []
|
||||
const textPart = parts.find((x): x is SDKTextPart => x.type === "text" && !x.synthetic && !x.ignored)
|
||||
if (!textPart) continue
|
||||
|
||||
result.push({
|
||||
id: message.id,
|
||||
text: textPart.text.replace(/\n/g, " ").slice(0, 200),
|
||||
time: formatTime(new Date(message.time.created)),
|
||||
})
|
||||
}
|
||||
|
||||
return result.reverse()
|
||||
})
|
||||
|
||||
const handleSelect = (item: ForkableMessage | undefined) => {
|
||||
if (!item) return
|
||||
|
||||
const sessionID = params.id
|
||||
if (!sessionID) return
|
||||
|
||||
const parts = sync.data.part[item.id] ?? []
|
||||
const restored = extractPromptFromParts(parts, { directory: sdk.directory })
|
||||
|
||||
dialog.close()
|
||||
|
||||
sdk.client.session.fork({ sessionID, messageID: item.id }).then((forked) => {
|
||||
if (!forked.data) return
|
||||
navigate(`/${base64Encode(sdk.directory)}/session/${forked.data.id}`)
|
||||
requestAnimationFrame(() => {
|
||||
prompt.set(restored)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog title="Fork from message">
|
||||
<List
|
||||
class="flex-1 min-h-0 [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:min-h-0"
|
||||
search={{ placeholder: "Search", autofocus: true }}
|
||||
emptyMessage="No messages to fork from"
|
||||
key={(x) => x.id}
|
||||
items={messages}
|
||||
filterKeys={["text"]}
|
||||
onSelect={handleSelect}
|
||||
>
|
||||
{(item) => (
|
||||
<div class="w-full flex items-center gap-2">
|
||||
<span class="truncate flex-1 min-w-0 text-left" style={{ "font-weight": "400" }}>
|
||||
{item.text}
|
||||
</span>
|
||||
<span class="text-text-weak shrink-0" style={{ "font-weight": "400" }}>
|
||||
{item.time}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</List>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
import { Component } from "solid-js"
|
||||
import { useLocal } from "@/context/local"
|
||||
import { popularProviders } from "@/hooks/use-providers"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
import { Switch } from "@opencode-ai/ui/switch"
|
||||
import type { Component } from "solid-js"
|
||||
import { useLocal } from "@/context/local"
|
||||
import { popularProviders } from "@/hooks/use-providers"
|
||||
|
||||
export const DialogManageModels: Component = () => {
|
||||
const local = useLocal()
|
||||
return (
|
||||
<Dialog title="Manage models" description="Customize which models appear in the model selector.">
|
||||
<List
|
||||
class="px-2.5"
|
||||
search={{ placeholder: "Search models", autofocus: true }}
|
||||
emptyMessage="No model results"
|
||||
key={(x) => `${x?.provider?.id}:${x?.id}`}
|
||||
@@ -27,16 +26,24 @@ export const DialogManageModels: Component = () => {
|
||||
}}
|
||||
onSelect={(x) => {
|
||||
if (!x) return
|
||||
const visible = local.model.visible({ modelID: x.id, providerID: x.provider.id })
|
||||
const visible = local.model.visible({
|
||||
modelID: x.id,
|
||||
providerID: x.provider.id,
|
||||
})
|
||||
local.model.setVisibility({ modelID: x.id, providerID: x.provider.id }, !visible)
|
||||
}}
|
||||
>
|
||||
{(i) => (
|
||||
<div class="w-full flex items-center justify-between gap-x-2.5">
|
||||
<div class="w-full flex items-center justify-between gap-x-3">
|
||||
<span>{i.name}</span>
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<Switch
|
||||
checked={!!local.model.visible({ modelID: i.id, providerID: i.provider.id })}
|
||||
checked={
|
||||
!!local.model.visible({
|
||||
modelID: i.id,
|
||||
providerID: i.provider.id,
|
||||
})
|
||||
}
|
||||
onChange={(checked) => {
|
||||
local.model.setVisibility({ modelID: i.id, providerID: i.provider.id }, checked)
|
||||
}}
|
||||
114
packages/app/src/components/dialog-select-directory.tsx
Normal file
114
packages/app/src/components/dialog-select-directory.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||
import { createMemo } from "solid-js"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
|
||||
interface DialogSelectDirectoryProps {
|
||||
title?: string
|
||||
multiple?: boolean
|
||||
onSelect: (result: string | string[] | null) => void
|
||||
}
|
||||
|
||||
export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
|
||||
const sync = useGlobalSync()
|
||||
const sdk = useGlobalSDK()
|
||||
const dialog = useDialog()
|
||||
|
||||
const home = createMemo(() => sync.data.path.home)
|
||||
const root = createMemo(() => sync.data.path.home || sync.data.path.directory)
|
||||
|
||||
function join(base: string | undefined, rel: string) {
|
||||
const b = (base ?? "").replace(/[\\/]+$/, "")
|
||||
const r = rel.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")
|
||||
if (!b) return r
|
||||
if (!r) return b
|
||||
return b + "/" + r
|
||||
}
|
||||
|
||||
function display(rel: string) {
|
||||
const full = join(root(), rel)
|
||||
const h = home()
|
||||
if (!h) return full
|
||||
if (full === h) return "~"
|
||||
if (full.startsWith(h + "/") || full.startsWith(h + "\\")) {
|
||||
return "~" + full.slice(h.length)
|
||||
}
|
||||
return full
|
||||
}
|
||||
|
||||
function normalizeQuery(query: string) {
|
||||
const h = home()
|
||||
|
||||
if (!query) return query
|
||||
if (query.startsWith("~/")) return query.slice(2)
|
||||
|
||||
if (h) {
|
||||
const lc = query.toLowerCase()
|
||||
const hc = h.toLowerCase()
|
||||
if (lc === hc || lc.startsWith(hc + "/") || lc.startsWith(hc + "\\")) {
|
||||
return query.slice(h.length).replace(/^[\\/]+/, "")
|
||||
}
|
||||
}
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
async function fetchDirs(query: string) {
|
||||
const directory = root()
|
||||
if (!directory) return [] as string[]
|
||||
|
||||
const results = await sdk.client.find
|
||||
.files({ directory, query, type: "directory", limit: 50 })
|
||||
.then((x) => x.data ?? [])
|
||||
.catch(() => [])
|
||||
|
||||
return results.map((x) => x.replace(/[\\/]+$/, ""))
|
||||
}
|
||||
|
||||
const directories = async (filter: string) => {
|
||||
const query = normalizeQuery(filter.trim())
|
||||
return fetchDirs(query)
|
||||
}
|
||||
|
||||
function resolve(rel: string) {
|
||||
const absolute = join(root(), rel)
|
||||
props.onSelect(props.multiple ? [absolute] : absolute)
|
||||
dialog.close()
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog title={props.title ?? "Open project"}>
|
||||
<List
|
||||
search={{ placeholder: "Search folders", autofocus: true }}
|
||||
emptyMessage="No folders found"
|
||||
items={directories}
|
||||
key={(x) => x}
|
||||
onSelect={(path) => {
|
||||
if (!path) return
|
||||
resolve(path)
|
||||
}}
|
||||
>
|
||||
{(rel) => {
|
||||
const path = display(rel)
|
||||
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: rel, type: "directory" }} class="shrink-0 size-4" />
|
||||
<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">
|
||||
{getDirectory(path)}
|
||||
</span>
|
||||
<span class="text-text-strong whitespace-nowrap">{getFilename(path)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</List>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -1,38 +1,41 @@
|
||||
import { useLocal } from "@/context/local"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { createMemo } from "solid-js"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { useFile } from "@/context/file"
|
||||
|
||||
export function DialogSelectFile() {
|
||||
const layout = useLayout()
|
||||
const local = useLocal()
|
||||
const file = useFile()
|
||||
const dialog = useDialog()
|
||||
const params = useParams()
|
||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||
const tabs = createMemo(() => layout.tabs(sessionKey()))
|
||||
const view = createMemo(() => layout.view(sessionKey()))
|
||||
return (
|
||||
<Dialog title="Select file">
|
||||
<List
|
||||
class="px-2.5"
|
||||
search={{ placeholder: "Search files", autofocus: true }}
|
||||
emptyMessage="No files found"
|
||||
items={local.file.searchFiles}
|
||||
items={file.searchFiles}
|
||||
key={(x) => x}
|
||||
onSelect={(path) => {
|
||||
if (path) {
|
||||
tabs().open("file://" + path)
|
||||
const value = file.tab(path)
|
||||
tabs().open(value)
|
||||
file.load(path)
|
||||
view().reviewPanel.open()
|
||||
}
|
||||
dialog.close()
|
||||
}}
|
||||
>
|
||||
{(i) => (
|
||||
<div class="w-full flex items-center justify-between rounded-md">
|
||||
<div class="flex items-center gap-x-2 grow min-w-0">
|
||||
<div class="flex items-center gap-x-3 grow min-w-0">
|
||||
<FileIcon node={{ path: i, type: "file" }} class="shrink-0 size-4" />
|
||||
<div class="flex items-center text-14-regular">
|
||||
<span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
|
||||
91
packages/app/src/components/dialog-select-mcp.tsx
Normal file
91
packages/app/src/components/dialog-select-mcp.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import { Component, createMemo, createSignal, Show } from "solid-js"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
import { Switch } from "@opencode-ai/ui/switch"
|
||||
|
||||
export const DialogSelectMcp: Component = () => {
|
||||
const sync = useSync()
|
||||
const sdk = useSDK()
|
||||
const [loading, setLoading] = createSignal<string | null>(null)
|
||||
|
||||
const items = createMemo(() =>
|
||||
Object.entries(sync.data.mcp ?? {})
|
||||
.map(([name, status]) => ({ name, status: status.status }))
|
||||
.sort((a, b) => a.name.localeCompare(b.name)),
|
||||
)
|
||||
|
||||
const toggle = async (name: string) => {
|
||||
if (loading()) return
|
||||
setLoading(name)
|
||||
const status = sync.data.mcp[name]
|
||||
if (status?.status === "connected") {
|
||||
await sdk.client.mcp.disconnect({ name })
|
||||
} else {
|
||||
await sdk.client.mcp.connect({ name })
|
||||
}
|
||||
const result = await sdk.client.mcp.status()
|
||||
if (result.data) sync.set("mcp", result.data)
|
||||
setLoading(null)
|
||||
}
|
||||
|
||||
const enabledCount = createMemo(() => items().filter((i) => i.status === "connected").length)
|
||||
const totalCount = createMemo(() => items().length)
|
||||
|
||||
return (
|
||||
<Dialog title="MCPs" description={`${enabledCount()} of ${totalCount()} enabled`}>
|
||||
<List
|
||||
search={{ placeholder: "Search", autofocus: true }}
|
||||
emptyMessage="No MCPs configured"
|
||||
key={(x) => x?.name ?? ""}
|
||||
items={items}
|
||||
filterKeys={["name", "status"]}
|
||||
sortBy={(a, b) => a.name.localeCompare(b.name)}
|
||||
onSelect={(x) => {
|
||||
if (x) toggle(x.name)
|
||||
}}
|
||||
>
|
||||
{(i) => {
|
||||
const mcpStatus = () => sync.data.mcp[i.name]
|
||||
const status = () => mcpStatus()?.status
|
||||
const error = () => {
|
||||
const s = mcpStatus()
|
||||
return s?.status === "failed" ? s.error : undefined
|
||||
}
|
||||
const enabled = () => status() === "connected"
|
||||
return (
|
||||
<div class="w-full flex items-center justify-between gap-x-3">
|
||||
<div class="flex flex-col gap-0.5 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="truncate">{i.name}</span>
|
||||
<Show when={status() === "connected"}>
|
||||
<span class="text-11-regular text-text-weaker">connected</span>
|
||||
</Show>
|
||||
<Show when={status() === "failed"}>
|
||||
<span class="text-11-regular text-text-weaker">failed</span>
|
||||
</Show>
|
||||
<Show when={status() === "needs_auth"}>
|
||||
<span class="text-11-regular text-text-weaker">needs auth</span>
|
||||
</Show>
|
||||
<Show when={status() === "disabled"}>
|
||||
<span class="text-11-regular text-text-weaker">disabled</span>
|
||||
</Show>
|
||||
<Show when={loading() === i.name}>
|
||||
<span class="text-11-regular text-text-weak">...</span>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={error()}>
|
||||
<span class="text-11-regular text-text-weaker truncate">{error()}</span>
|
||||
</Show>
|
||||
</div>
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<Switch checked={enabled()} disabled={loading() === i.name} onChange={() => toggle(i.name)} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</List>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
import { Component, onCleanup, onMount, Show } from "solid-js"
|
||||
import { useLocal } from "@/context/local"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { popularProviders, useProviders } from "@/hooks/use-providers"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { Tag } from "@opencode-ai/ui/tag"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { List, ListRef } from "@opencode-ai/ui/list"
|
||||
import type { IconName } from "@opencode-ai/ui/icons/provider"
|
||||
import { List, type ListRef } from "@opencode-ai/ui/list"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import { IconName } from "@opencode-ai/ui/icons/provider"
|
||||
import { DialogSelectProvider } from "./dialog-select-provider"
|
||||
import { Tag } from "@opencode-ai/ui/tag"
|
||||
import { type Component, onCleanup, onMount, Show } from "solid-js"
|
||||
import { useLocal } from "@/context/local"
|
||||
import { popularProviders, useProviders } from "@/hooks/use-providers"
|
||||
import { DialogConnectProvider } from "./dialog-connect-provider"
|
||||
import { DialogSelectProvider } from "./dialog-select-provider"
|
||||
|
||||
export const DialogSelectModelUnpaid: Component = () => {
|
||||
const local = useLocal()
|
||||
@@ -64,7 +64,7 @@ export const DialogSelectModelUnpaid: Component = () => {
|
||||
<div class="px-2 text-14-medium text-text-base">Add more models from popular providers</div>
|
||||
<div class="w-full">
|
||||
<List
|
||||
class="w-full"
|
||||
class="w-full px-0"
|
||||
key={(x) => x?.id}
|
||||
items={providers.popular}
|
||||
activeIcon="plus-small"
|
||||
@@ -79,17 +79,8 @@ export const DialogSelectModelUnpaid: Component = () => {
|
||||
}}
|
||||
>
|
||||
{(i) => (
|
||||
<div class="w-full flex items-center gap-x-4">
|
||||
<ProviderIcon
|
||||
data-slot="list-item-extra-icon"
|
||||
id={i.id as IconName}
|
||||
// TODO: clean this up after we update icon in models.dev
|
||||
classList={{
|
||||
"text-icon-weak-base": true,
|
||||
"size-4 mx-0.5": i.id === "opencode",
|
||||
"size-5": i.id !== "opencode",
|
||||
}}
|
||||
/>
|
||||
<div class="w-full flex items-center gap-x-3">
|
||||
<ProviderIcon data-slot="list-item-extra-icon" id={i.id as IconName} />
|
||||
<span>{i.name}</span>
|
||||
<Show when={i.id === "opencode"}>
|
||||
<Tag>Recommended</Tag>
|
||||
124
packages/app/src/components/dialog-select-model.tsx
Normal file
124
packages/app/src/components/dialog-select-model.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import { Popover as Kobalte } from "@kobalte/core/popover"
|
||||
import { Component, createMemo, createSignal, JSX, Show } from "solid-js"
|
||||
import { useLocal } from "@/context/local"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { popularProviders } from "@/hooks/use-providers"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { Tag } from "@opencode-ai/ui/tag"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import type { IconName } from "@opencode-ai/ui/icons/provider"
|
||||
import { DialogSelectProvider } from "./dialog-select-provider"
|
||||
import { DialogManageModels } from "./dialog-manage-models"
|
||||
|
||||
const ModelList: Component<{
|
||||
provider?: string
|
||||
class?: string
|
||||
onSelect: () => void
|
||||
}> = (props) => {
|
||||
const local = useLocal()
|
||||
|
||||
const models = createMemo(() =>
|
||||
local.model
|
||||
.list()
|
||||
.filter((m) => local.model.visible({ modelID: m.id, providerID: m.provider.id }))
|
||||
.filter((m) => (props.provider ? m.provider.id === props.provider : true)),
|
||||
)
|
||||
|
||||
return (
|
||||
<List
|
||||
class={`flex-1 min-h-0 [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:min-h-0 ${props.class ?? ""}`}
|
||||
search={{ placeholder: "Search models", autofocus: true }}
|
||||
emptyMessage="No model results"
|
||||
key={(x) => `${x.provider.id}:${x.id}`}
|
||||
items={models}
|
||||
current={local.model.current()}
|
||||
filterKeys={["provider.name", "name", "id"]}
|
||||
sortBy={(a, b) => a.name.localeCompare(b.name)}
|
||||
groupBy={(x) => x.provider.name}
|
||||
groupHeader={(group) => (
|
||||
<div class="flex items-center gap-x-3">
|
||||
<ProviderIcon data-slot="list-item-extra-icon" id={group.items[0].provider.id as IconName} />
|
||||
<span>{group.category}</span>
|
||||
</div>
|
||||
)}
|
||||
sortGroupsBy={(a, b) => {
|
||||
if (a.category === "Recent" && b.category !== "Recent") return -1
|
||||
if (b.category === "Recent" && a.category !== "Recent") return 1
|
||||
const aProvider = a.items[0].provider.id
|
||||
const bProvider = b.items[0].provider.id
|
||||
if (popularProviders.includes(aProvider) && !popularProviders.includes(bProvider)) return -1
|
||||
if (!popularProviders.includes(aProvider) && popularProviders.includes(bProvider)) return 1
|
||||
return popularProviders.indexOf(aProvider) - popularProviders.indexOf(bProvider)
|
||||
}}
|
||||
onSelect={(x) => {
|
||||
local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, {
|
||||
recent: true,
|
||||
})
|
||||
props.onSelect()
|
||||
}}
|
||||
>
|
||||
{(i) => (
|
||||
<div class="w-full flex items-center gap-x-3 pl-1 text-13-regular">
|
||||
<ProviderIcon data-slot="list-item-extra-icon" id={i.provider.id as IconName} />
|
||||
<span class="truncate">{i.name}</span>
|
||||
<Show when={i.provider.id === "opencode" && (!i.cost || i.cost?.input === 0)}>
|
||||
<Tag>Free</Tag>
|
||||
</Show>
|
||||
<Show when={i.latest}>
|
||||
<Tag>Latest</Tag>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</List>
|
||||
)
|
||||
}
|
||||
|
||||
export const ModelSelectorPopover: Component<{
|
||||
provider?: string
|
||||
children: JSX.Element
|
||||
}> = (props) => {
|
||||
const [open, setOpen] = createSignal(false)
|
||||
|
||||
return (
|
||||
<Kobalte open={open()} onOpenChange={setOpen} placement="top-start" gutter={8}>
|
||||
<Kobalte.Trigger as="div">{props.children}</Kobalte.Trigger>
|
||||
<Kobalte.Portal>
|
||||
<Kobalte.Content class="w-72 h-80 flex flex-col rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none overflow-hidden">
|
||||
<Kobalte.Title class="sr-only">Select model</Kobalte.Title>
|
||||
<ModelList provider={props.provider} onSelect={() => setOpen(false)} class="p-1" />
|
||||
</Kobalte.Content>
|
||||
</Kobalte.Portal>
|
||||
</Kobalte>
|
||||
)
|
||||
}
|
||||
|
||||
export const DialogSelectModel: Component<{ provider?: string }> = (props) => {
|
||||
const dialog = useDialog()
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title="Select model"
|
||||
action={
|
||||
<Button
|
||||
class="h-7 -my-1 text-14-medium"
|
||||
icon="plus-small"
|
||||
tabIndex={-1}
|
||||
onClick={() => dialog.show(() => <DialogSelectProvider />)}
|
||||
>
|
||||
Connect provider
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<ModelList provider={props.provider} onSelect={() => dialog.close()} />
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="ml-3 mt-5 mb-6 text-text-base self-start"
|
||||
onClick={() => dialog.show(() => <DialogManageModels />)}
|
||||
>
|
||||
Manage models
|
||||
</Button>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -15,7 +15,6 @@ export const DialogSelectProvider: Component = () => {
|
||||
return (
|
||||
<Dialog title="Connect provider">
|
||||
<List
|
||||
class="px-2.5"
|
||||
search={{ placeholder: "Search providers", autofocus: true }}
|
||||
activeIcon="plus-small"
|
||||
key={(x) => x?.id}
|
||||
@@ -38,17 +37,8 @@ export const DialogSelectProvider: Component = () => {
|
||||
}}
|
||||
>
|
||||
{(i) => (
|
||||
<div class="px-1.25 w-full flex items-center gap-x-4">
|
||||
<ProviderIcon
|
||||
data-slot="list-item-extra-icon"
|
||||
id={i.id as IconName}
|
||||
// TODO: clean this up after we update icon in models.dev
|
||||
classList={{
|
||||
"text-icon-weak-base": true,
|
||||
"size-4 mx-0.5": i.id === "opencode",
|
||||
"size-5": i.id !== "opencode",
|
||||
}}
|
||||
/>
|
||||
<div class="px-1.25 w-full flex items-center gap-x-3">
|
||||
<ProviderIcon data-slot="list-item-extra-icon" id={i.id as IconName} />
|
||||
<span>{i.name}</span>
|
||||
<Show when={i.id === "opencode"}>
|
||||
<Tag>Recommended</Tag>
|
||||
246
packages/app/src/components/dialog-select-server.tsx
Normal file
246
packages/app/src/components/dialog-select-server.tsx
Normal file
@@ -0,0 +1,246 @@
|
||||
import { createResource, createEffect, createMemo, onCleanup, Show } from "solid-js"
|
||||
import { createStore, reconcile } from "solid-js/store"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
import { TextField } from "@opencode-ai/ui/text-field"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { normalizeServerUrl, serverDisplayName, useServer } from "@/context/server"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
||||
import { useNavigate } from "@solidjs/router"
|
||||
|
||||
type ServerStatus = { healthy: boolean; version?: string }
|
||||
|
||||
async function checkHealth(url: string, fetch?: typeof globalThis.fetch): Promise<ServerStatus> {
|
||||
const sdk = createOpencodeClient({
|
||||
baseUrl: url,
|
||||
fetch,
|
||||
signal: AbortSignal.timeout(3000),
|
||||
})
|
||||
return sdk.global
|
||||
.health()
|
||||
.then((x) => ({ healthy: x.data?.healthy === true, version: x.data?.version }))
|
||||
.catch(() => ({ healthy: false }))
|
||||
}
|
||||
|
||||
export function DialogSelectServer() {
|
||||
const navigate = useNavigate()
|
||||
const dialog = useDialog()
|
||||
const server = useServer()
|
||||
const platform = usePlatform()
|
||||
const [store, setStore] = createStore({
|
||||
url: "",
|
||||
adding: false,
|
||||
error: "",
|
||||
status: {} as Record<string, ServerStatus | undefined>,
|
||||
})
|
||||
const [defaultUrl, defaultUrlActions] = createResource(() => platform.getDefaultServerUrl?.())
|
||||
const isDesktop = platform.platform === "desktop"
|
||||
|
||||
const items = createMemo(() => {
|
||||
const current = server.url
|
||||
const list = server.list
|
||||
if (!current) return list
|
||||
if (!list.includes(current)) return [current, ...list]
|
||||
return [current, ...list.filter((x) => x !== current)]
|
||||
})
|
||||
|
||||
const current = createMemo(() => items().find((x) => x === server.url) ?? items()[0])
|
||||
|
||||
const sortedItems = createMemo(() => {
|
||||
const list = items()
|
||||
if (!list.length) return list
|
||||
const active = current()
|
||||
const order = new Map(list.map((url, index) => [url, index] as const))
|
||||
const rank = (value?: ServerStatus) => {
|
||||
if (value?.healthy === true) return 0
|
||||
if (value?.healthy === false) return 2
|
||||
return 1
|
||||
}
|
||||
return list.slice().sort((a, b) => {
|
||||
if (a === active) return -1
|
||||
if (b === active) return 1
|
||||
const diff = rank(store.status[a]) - rank(store.status[b])
|
||||
if (diff !== 0) return diff
|
||||
return (order.get(a) ?? 0) - (order.get(b) ?? 0)
|
||||
})
|
||||
})
|
||||
|
||||
async function refreshHealth() {
|
||||
const results: Record<string, ServerStatus> = {}
|
||||
await Promise.all(
|
||||
items().map(async (url) => {
|
||||
results[url] = await checkHealth(url, platform.fetch)
|
||||
}),
|
||||
)
|
||||
setStore("status", reconcile(results))
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
items()
|
||||
refreshHealth()
|
||||
const interval = setInterval(refreshHealth, 10_000)
|
||||
onCleanup(() => clearInterval(interval))
|
||||
})
|
||||
|
||||
function select(value: string, persist?: boolean) {
|
||||
if (!persist && store.status[value]?.healthy === false) return
|
||||
dialog.close()
|
||||
if (persist) {
|
||||
server.add(value)
|
||||
navigate("/")
|
||||
return
|
||||
}
|
||||
server.setActive(value)
|
||||
navigate("/")
|
||||
}
|
||||
|
||||
async function handleSubmit(e: SubmitEvent) {
|
||||
e.preventDefault()
|
||||
const value = normalizeServerUrl(store.url)
|
||||
if (!value) return
|
||||
|
||||
setStore("adding", true)
|
||||
setStore("error", "")
|
||||
|
||||
const result = await checkHealth(value, platform.fetch)
|
||||
setStore("adding", false)
|
||||
|
||||
if (!result.healthy) {
|
||||
setStore("error", "Could not connect to server")
|
||||
return
|
||||
}
|
||||
|
||||
setStore("url", "")
|
||||
select(value, true)
|
||||
}
|
||||
|
||||
async function handleRemove(url: string) {
|
||||
server.remove(url)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog title="Servers" description="Switch which OpenCode server this app connects to.">
|
||||
<div class="flex flex-col gap-4 pb-4">
|
||||
<List
|
||||
search={{ placeholder: "Search servers", autofocus: true }}
|
||||
emptyMessage="No servers yet"
|
||||
items={sortedItems}
|
||||
key={(x) => x}
|
||||
current={current()}
|
||||
onSelect={(x) => {
|
||||
if (x) select(x)
|
||||
}}
|
||||
>
|
||||
{(i) => (
|
||||
<div class="flex items-center gap-2 min-w-0 flex-1 group/item">
|
||||
<div
|
||||
class="flex items-center gap-2 min-w-0 flex-1"
|
||||
classList={{ "opacity-50": store.status[i]?.healthy === false }}
|
||||
>
|
||||
<div
|
||||
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 class="truncate">{serverDisplayName(i)}</span>
|
||||
<span class="text-text-weak">{store.status[i]?.version}</span>
|
||||
</div>
|
||||
<Show when={current() !== i && server.list.includes(i)}>
|
||||
<IconButton
|
||||
icon="circle-x"
|
||||
variant="ghost"
|
||||
class="bg-transparent transition-opacity shrink-0 hover:scale-110"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleRemove(i)
|
||||
}}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</List>
|
||||
|
||||
<div class="mt-6 px-3 flex flex-col gap-1.5">
|
||||
<div class="px-3">
|
||||
<h3 class="text-14-regular text-text-weak">Add a server</h3>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div class="flex items-start gap-2">
|
||||
<div class="flex-1 min-w-0 h-auto">
|
||||
<TextField
|
||||
type="text"
|
||||
label="Server URL"
|
||||
hideLabel
|
||||
placeholder="http://localhost:4096"
|
||||
value={store.url}
|
||||
onChange={(v) => {
|
||||
setStore("url", v)
|
||||
setStore("error", "")
|
||||
}}
|
||||
validationState={store.error ? "invalid" : "valid"}
|
||||
error={store.error}
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" variant="secondary" icon="plus-small" size="large" disabled={store.adding}>
|
||||
{store.adding ? "Checking..." : "Add"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<Show when={isDesktop}>
|
||||
<div class="mt-6 px-3 flex flex-col gap-1.5">
|
||||
<div class="px-3">
|
||||
<h3 class="text-14-regular text-text-weak">Default server</h3>
|
||||
<p class="text-12-regular text-text-weak mt-1">
|
||||
Connect to this server on app launch instead of starting a local server. Requires restart.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 px-3 py-2">
|
||||
<Show
|
||||
when={defaultUrl()}
|
||||
fallback={
|
||||
<Show
|
||||
when={server.url}
|
||||
fallback={<span class="text-14-regular text-text-weak">No server selected</span>}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
onClick={async () => {
|
||||
await platform.setDefaultServerUrl?.(server.url)
|
||||
defaultUrlActions.refetch(server.url)
|
||||
}}
|
||||
>
|
||||
Set current server as default
|
||||
</Button>
|
||||
</Show>
|
||||
}
|
||||
>
|
||||
<div class="flex items-center gap-2 flex-1 min-w-0">
|
||||
<span class="truncate text-14-regular">{serverDisplayName(defaultUrl()!)}</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="small"
|
||||
onClick={async () => {
|
||||
await platform.setDefaultServerUrl?.(null)
|
||||
defaultUrlActions.refetch()
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { useLocal, type LocalFile } from "@/context/local"
|
||||
import { Collapsible } from "@opencode-ai/ui/collapsible"
|
||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { For, Match, Switch, Show, type ComponentProps, type ParentProps } from "solid-js"
|
||||
import { For, Match, Switch, type ComponentProps, type ParentProps } from "solid-js"
|
||||
import { Dynamic } from "solid-js/web"
|
||||
|
||||
export default function FileTree(props: {
|
||||
@@ -57,14 +57,14 @@ export default function FileTree(props: {
|
||||
"text-text-muted/40": p.node.ignored,
|
||||
"text-text-muted/80": !p.node.ignored,
|
||||
// "!text-text": local.file.active()?.path === p.node.path,
|
||||
"!text-primary": local.file.changed(p.node.path),
|
||||
// "!text-primary": local.file.changed(p.node.path),
|
||||
}}
|
||||
>
|
||||
{p.node.name}
|
||||
</span>
|
||||
<Show when={local.file.changed(p.node.path)}>
|
||||
<span class="ml-auto mr-1 w-1.5 h-1.5 rounded-full bg-primary/50 shrink-0" />
|
||||
</Show>
|
||||
{/* <Show when={local.file.changed(p.node.path)}> */}
|
||||
{/* <span class="ml-auto mr-1 w-1.5 h-1.5 rounded-full bg-primary/50 shrink-0" /> */}
|
||||
{/* </Show> */}
|
||||
</Dynamic>
|
||||
)
|
||||
|
||||
1788
packages/app/src/components/prompt-input.tsx
Normal file
1788
packages/app/src/components/prompt-input.tsx
Normal file
File diff suppressed because it is too large
Load Diff
103
packages/app/src/components/session-context-usage.tsx
Normal file
103
packages/app/src/components/session-context-usage.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import { Match, Show, Switch, createMemo } from "solid-js"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { ProgressCircle } from "@opencode-ai/ui/progress-circle"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { AssistantMessage } from "@opencode-ai/sdk/v2/client"
|
||||
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { useSync } from "@/context/sync"
|
||||
|
||||
interface SessionContextUsageProps {
|
||||
variant?: "button" | "indicator"
|
||||
}
|
||||
|
||||
export function SessionContextUsage(props: SessionContextUsageProps) {
|
||||
const sync = useSync()
|
||||
const params = useParams()
|
||||
const layout = useLayout()
|
||||
|
||||
const variant = createMemo(() => props.variant ?? "button")
|
||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||
const tabs = createMemo(() => layout.tabs(sessionKey()))
|
||||
const view = createMemo(() => layout.view(sessionKey()))
|
||||
const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
|
||||
|
||||
const cost = createMemo(() => {
|
||||
const total = messages().reduce((sum, x) => sum + (x.role === "assistant" ? x.cost : 0), 0)
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
}).format(total)
|
||||
})
|
||||
|
||||
const context = createMemo(() => {
|
||||
const last = messages().findLast((x) => {
|
||||
if (x.role !== "assistant") return false
|
||||
const total = x.tokens.input + x.tokens.output + x.tokens.reasoning + x.tokens.cache.read + x.tokens.cache.write
|
||||
return total > 0
|
||||
}) as AssistantMessage
|
||||
if (!last) return
|
||||
const total =
|
||||
last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
|
||||
const model = sync.data.provider.all.find((x) => x.id === last.providerID)?.models[last.modelID]
|
||||
return {
|
||||
tokens: total.toLocaleString(),
|
||||
percentage: model?.limit.context ? Math.round((total / model.limit.context) * 100) : null,
|
||||
}
|
||||
})
|
||||
|
||||
const openContext = () => {
|
||||
if (!params.id) return
|
||||
view().reviewPanel.open()
|
||||
tabs().open("context")
|
||||
tabs().setActive("context")
|
||||
}
|
||||
|
||||
const circle = () => (
|
||||
<div class="p-1">
|
||||
<ProgressCircle size={16} strokeWidth={2} percentage={context()?.percentage ?? 0} />
|
||||
</div>
|
||||
)
|
||||
|
||||
const tooltipValue = () => (
|
||||
<div>
|
||||
<Show when={context()}>
|
||||
{(ctx) => (
|
||||
<>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-text-invert-strong">{ctx().tokens}</span>
|
||||
<span class="text-text-invert-base">Tokens</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-text-invert-strong">{ctx().percentage ?? 0}%</span>
|
||||
<span class="text-text-invert-base">Usage</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Show>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-text-invert-strong">{cost()}</span>
|
||||
<span class="text-text-invert-base">Cost</span>
|
||||
</div>
|
||||
<Show when={variant() === "button"}>
|
||||
<div class="text-11-regular text-text-invert-base mt-1">Click to view context</div>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<Show when={params.id}>
|
||||
<Tooltip value={tooltipValue()} placement="top">
|
||||
<Switch>
|
||||
<Match when={variant() === "indicator"}>{circle()}</Match>
|
||||
<Match when={true}>
|
||||
<Button type="button" variant="ghost" class="size-6" onClick={openContext}>
|
||||
{circle()}
|
||||
</Button>
|
||||
</Match>
|
||||
</Switch>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
38
packages/app/src/components/session-lsp-indicator.tsx
Normal file
38
packages/app/src/components/session-lsp-indicator.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { createMemo, Show } from "solid-js"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
|
||||
export function SessionLspIndicator() {
|
||||
const sync = useSync()
|
||||
|
||||
const lspStats = createMemo(() => {
|
||||
const lsp = sync.data.lsp ?? []
|
||||
const connected = lsp.filter((s) => s.status === "connected").length
|
||||
const hasError = lsp.some((s) => s.status === "error")
|
||||
const total = lsp.length
|
||||
return { connected, hasError, total }
|
||||
})
|
||||
|
||||
const tooltipContent = createMemo(() => {
|
||||
const lsp = sync.data.lsp ?? []
|
||||
if (lsp.length === 0) return "No LSP servers"
|
||||
return lsp.map((s) => s.name).join(", ")
|
||||
})
|
||||
|
||||
return (
|
||||
<Show when={lspStats().total > 0}>
|
||||
<Tooltip placement="top" value={tooltipContent()}>
|
||||
<div class="flex items-center gap-1 px-2 cursor-default select-none">
|
||||
<div
|
||||
classList={{
|
||||
"size-1.5 rounded-full": true,
|
||||
"bg-icon-critical-base": lspStats().hasError,
|
||||
"bg-icon-success-base": !lspStats().hasError && lspStats().connected > 0,
|
||||
}}
|
||||
/>
|
||||
<span class="text-12-regular text-text-weak">{lspStats().connected} LSP</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
34
packages/app/src/components/session-mcp-indicator.tsx
Normal file
34
packages/app/src/components/session-mcp-indicator.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { createMemo, Show } from "solid-js"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { DialogSelectMcp } from "@/components/dialog-select-mcp"
|
||||
|
||||
export function SessionMcpIndicator() {
|
||||
const sync = useSync()
|
||||
const dialog = useDialog()
|
||||
|
||||
const mcpStats = createMemo(() => {
|
||||
const mcp = sync.data.mcp ?? {}
|
||||
const entries = Object.entries(mcp)
|
||||
const enabled = entries.filter(([, status]) => status.status === "connected").length
|
||||
const failed = entries.some(([, status]) => status.status === "failed")
|
||||
const total = entries.length
|
||||
return { enabled, failed, total }
|
||||
})
|
||||
|
||||
return (
|
||||
<Show when={mcpStats().total > 0}>
|
||||
<Button variant="ghost" onClick={() => dialog.show(() => <DialogSelectMcp />)}>
|
||||
<div
|
||||
classList={{
|
||||
"size-1.5 rounded-full": true,
|
||||
"bg-icon-critical-base": mcpStats().failed,
|
||||
"bg-icon-success-base": !mcpStats().failed && mcpStats().enabled > 0,
|
||||
}}
|
||||
/>
|
||||
<span class="text-12-regular text-text-weak">{mcpStats().enabled} MCP</span>
|
||||
</Button>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
5
packages/app/src/components/session/index.ts
Normal file
5
packages/app/src/components/session/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export { SessionHeader } from "./session-header"
|
||||
export { SessionContextTab } from "./session-context-tab"
|
||||
export { SortableTab, FileVisual } from "./session-sortable-tab"
|
||||
export { SortableTerminalTab } from "./session-sortable-terminal-tab"
|
||||
export { NewSessionView } from "./session-new-view"
|
||||
425
packages/app/src/components/session/session-context-tab.tsx
Normal file
425
packages/app/src/components/session/session-context-tab.tsx
Normal file
@@ -0,0 +1,425 @@
|
||||
import { createMemo, createEffect, on, onCleanup, For, Show } from "solid-js"
|
||||
import type { JSX } from "solid-js"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { DateTime } from "luxon"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { checksum } from "@opencode-ai/util/encode"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { Accordion } from "@opencode-ai/ui/accordion"
|
||||
import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header"
|
||||
import { Code } from "@opencode-ai/ui/code"
|
||||
import { Markdown } from "@opencode-ai/ui/markdown"
|
||||
import type { AssistantMessage, Message, Part, UserMessage } from "@opencode-ai/sdk/v2/client"
|
||||
|
||||
interface SessionContextTabProps {
|
||||
messages: () => Message[]
|
||||
visibleUserMessages: () => UserMessage[]
|
||||
view: () => ReturnType<ReturnType<typeof useLayout>["view"]>
|
||||
info: () => ReturnType<ReturnType<typeof useSync>["session"]["get"]>
|
||||
}
|
||||
|
||||
export function SessionContextTab(props: SessionContextTabProps) {
|
||||
const params = useParams()
|
||||
const sync = useSync()
|
||||
|
||||
const ctx = createMemo(() => {
|
||||
const last = props.messages().findLast((x) => {
|
||||
if (x.role !== "assistant") return false
|
||||
const total = x.tokens.input + x.tokens.output + x.tokens.reasoning + x.tokens.cache.read + x.tokens.cache.write
|
||||
return total > 0
|
||||
}) as AssistantMessage
|
||||
if (!last) return
|
||||
|
||||
const provider = sync.data.provider.all.find((x) => x.id === last.providerID)
|
||||
const model = provider?.models[last.modelID]
|
||||
const limit = model?.limit.context
|
||||
|
||||
const input = last.tokens.input
|
||||
const output = last.tokens.output
|
||||
const reasoning = last.tokens.reasoning
|
||||
const cacheRead = last.tokens.cache.read
|
||||
const cacheWrite = last.tokens.cache.write
|
||||
const total = input + output + reasoning + cacheRead + cacheWrite
|
||||
const usage = limit ? Math.round((total / limit) * 100) : null
|
||||
|
||||
return {
|
||||
message: last,
|
||||
provider,
|
||||
model,
|
||||
limit,
|
||||
input,
|
||||
output,
|
||||
reasoning,
|
||||
cacheRead,
|
||||
cacheWrite,
|
||||
total,
|
||||
usage,
|
||||
}
|
||||
})
|
||||
|
||||
const cost = createMemo(() => {
|
||||
const total = props.messages().reduce((sum, x) => sum + (x.role === "assistant" ? x.cost : 0), 0)
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
}).format(total)
|
||||
})
|
||||
|
||||
const counts = createMemo(() => {
|
||||
const all = props.messages()
|
||||
const user = all.reduce((count, x) => count + (x.role === "user" ? 1 : 0), 0)
|
||||
const assistant = all.reduce((count, x) => count + (x.role === "assistant" ? 1 : 0), 0)
|
||||
return {
|
||||
all: all.length,
|
||||
user,
|
||||
assistant,
|
||||
}
|
||||
})
|
||||
|
||||
const systemPrompt = createMemo(() => {
|
||||
const msg = props.visibleUserMessages().findLast((m) => !!m.system)
|
||||
const system = msg?.system
|
||||
if (!system) return
|
||||
const trimmed = system.trim()
|
||||
if (!trimmed) return
|
||||
return trimmed
|
||||
})
|
||||
|
||||
const number = (value: number | null | undefined) => {
|
||||
if (value === undefined) return "—"
|
||||
if (value === null) return "—"
|
||||
return value.toLocaleString()
|
||||
}
|
||||
|
||||
const percent = (value: number | null | undefined) => {
|
||||
if (value === undefined) return "—"
|
||||
if (value === null) return "—"
|
||||
return value.toString() + "%"
|
||||
}
|
||||
|
||||
const time = (value: number | undefined) => {
|
||||
if (!value) return "—"
|
||||
return DateTime.fromMillis(value).toLocaleString(DateTime.DATETIME_MED)
|
||||
}
|
||||
|
||||
const providerLabel = createMemo(() => {
|
||||
const c = ctx()
|
||||
if (!c) return "—"
|
||||
return c.provider?.name ?? c.message.providerID
|
||||
})
|
||||
|
||||
const modelLabel = createMemo(() => {
|
||||
const c = ctx()
|
||||
if (!c) return "—"
|
||||
if (c.model?.name) return c.model.name
|
||||
return c.message.modelID
|
||||
})
|
||||
|
||||
const breakdown = createMemo(
|
||||
on(
|
||||
() => [ctx()?.message.id, ctx()?.input, props.messages().length, systemPrompt()],
|
||||
() => {
|
||||
const c = ctx()
|
||||
if (!c) return []
|
||||
const input = c.input
|
||||
if (!input) return []
|
||||
|
||||
const out = {
|
||||
system: systemPrompt()?.length ?? 0,
|
||||
user: 0,
|
||||
assistant: 0,
|
||||
tool: 0,
|
||||
}
|
||||
|
||||
for (const msg of props.messages()) {
|
||||
const parts = (sync.data.part[msg.id] ?? []) as Part[]
|
||||
|
||||
if (msg.role === "user") {
|
||||
for (const part of parts) {
|
||||
if (part.type === "text") out.user += part.text.length
|
||||
if (part.type === "file") out.user += part.source?.text.value.length ?? 0
|
||||
if (part.type === "agent") out.user += part.source?.value.length ?? 0
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (msg.role === "assistant") {
|
||||
for (const part of parts) {
|
||||
if (part.type === "text") out.assistant += part.text.length
|
||||
if (part.type === "reasoning") out.assistant += part.text.length
|
||||
if (part.type === "tool") {
|
||||
out.tool += Object.keys(part.state.input).length * 16
|
||||
if (part.state.status === "pending") out.tool += part.state.raw.length
|
||||
if (part.state.status === "completed") out.tool += part.state.output.length
|
||||
if (part.state.status === "error") out.tool += part.state.error.length
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const estimateTokens = (chars: number) => Math.ceil(chars / 4)
|
||||
const system = estimateTokens(out.system)
|
||||
const user = estimateTokens(out.user)
|
||||
const assistant = estimateTokens(out.assistant)
|
||||
const tool = estimateTokens(out.tool)
|
||||
const estimated = system + user + assistant + tool
|
||||
|
||||
const pct = (tokens: number) => (tokens / input) * 100
|
||||
const pctLabel = (tokens: number) => (Math.round(pct(tokens) * 10) / 10).toString() + "%"
|
||||
|
||||
const build = (tokens: { system: number; user: number; assistant: number; tool: number; other: number }) => {
|
||||
return [
|
||||
{
|
||||
key: "system",
|
||||
label: "System",
|
||||
tokens: tokens.system,
|
||||
width: pct(tokens.system),
|
||||
percent: pctLabel(tokens.system),
|
||||
color: "var(--syntax-info)",
|
||||
},
|
||||
{
|
||||
key: "user",
|
||||
label: "User",
|
||||
tokens: tokens.user,
|
||||
width: pct(tokens.user),
|
||||
percent: pctLabel(tokens.user),
|
||||
color: "var(--syntax-success)",
|
||||
},
|
||||
{
|
||||
key: "assistant",
|
||||
label: "Assistant",
|
||||
tokens: tokens.assistant,
|
||||
width: pct(tokens.assistant),
|
||||
percent: pctLabel(tokens.assistant),
|
||||
color: "var(--syntax-property)",
|
||||
},
|
||||
{
|
||||
key: "tool",
|
||||
label: "Tool Calls",
|
||||
tokens: tokens.tool,
|
||||
width: pct(tokens.tool),
|
||||
percent: pctLabel(tokens.tool),
|
||||
color: "var(--syntax-warning)",
|
||||
},
|
||||
{
|
||||
key: "other",
|
||||
label: "Other",
|
||||
tokens: tokens.other,
|
||||
width: pct(tokens.other),
|
||||
percent: pctLabel(tokens.other),
|
||||
color: "var(--syntax-comment)",
|
||||
},
|
||||
].filter((x) => x.tokens > 0)
|
||||
}
|
||||
|
||||
if (estimated <= input) {
|
||||
return build({ system, user, assistant, tool, other: input - estimated })
|
||||
}
|
||||
|
||||
const scale = input / estimated
|
||||
const scaled = {
|
||||
system: Math.floor(system * scale),
|
||||
user: Math.floor(user * scale),
|
||||
assistant: Math.floor(assistant * scale),
|
||||
tool: Math.floor(tool * scale),
|
||||
}
|
||||
const scaledTotal = scaled.system + scaled.user + scaled.assistant + scaled.tool
|
||||
return build({ ...scaled, other: Math.max(0, input - scaledTotal) })
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
function Stat(statProps: { label: string; value: JSX.Element }) {
|
||||
return (
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="text-12-regular text-text-weak">{statProps.label}</div>
|
||||
<div class="text-12-medium text-text-strong">{statProps.value}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const stats = createMemo(() => {
|
||||
const c = ctx()
|
||||
const count = counts()
|
||||
return [
|
||||
{ label: "Session", value: props.info()?.title ?? params.id ?? "—" },
|
||||
{ label: "Messages", value: count.all.toLocaleString() },
|
||||
{ label: "Provider", value: providerLabel() },
|
||||
{ label: "Model", value: modelLabel() },
|
||||
{ label: "Context Limit", value: number(c?.limit) },
|
||||
{ label: "Total Tokens", value: number(c?.total) },
|
||||
{ label: "Usage", value: percent(c?.usage) },
|
||||
{ label: "Input Tokens", value: number(c?.input) },
|
||||
{ label: "Output Tokens", value: number(c?.output) },
|
||||
{ label: "Reasoning Tokens", value: number(c?.reasoning) },
|
||||
{ label: "Cache Tokens (read/write)", value: `${number(c?.cacheRead)} / ${number(c?.cacheWrite)}` },
|
||||
{ label: "User Messages", value: count.user.toLocaleString() },
|
||||
{ label: "Assistant Messages", value: count.assistant.toLocaleString() },
|
||||
{ label: "Total Cost", value: cost() },
|
||||
{ label: "Session Created", value: time(props.info()?.time.created) },
|
||||
{ label: "Last Activity", value: time(c?.message.time.created) },
|
||||
] satisfies { label: string; value: JSX.Element }[]
|
||||
})
|
||||
|
||||
function RawMessageContent(msgProps: { message: Message }) {
|
||||
const file = createMemo(() => {
|
||||
const parts = (sync.data.part[msgProps.message.id] ?? []) as Part[]
|
||||
const contents = JSON.stringify({ message: msgProps.message, parts }, null, 2)
|
||||
return {
|
||||
name: `${msgProps.message.role}-${msgProps.message.id}.json`,
|
||||
contents,
|
||||
cacheKey: checksum(contents),
|
||||
}
|
||||
})
|
||||
|
||||
return <Code file={file()} overflow="wrap" class="select-text" />
|
||||
}
|
||||
|
||||
function RawMessage(msgProps: { message: Message }) {
|
||||
return (
|
||||
<Accordion.Item value={msgProps.message.id}>
|
||||
<StickyAccordionHeader>
|
||||
<Accordion.Trigger>
|
||||
<div class="flex items-center justify-between gap-2 w-full">
|
||||
<div class="min-w-0 truncate">
|
||||
{msgProps.message.role} <span class="text-text-base">• {msgProps.message.id}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="shrink-0 text-12-regular text-text-weak">{time(msgProps.message.time.created)}</div>
|
||||
<Icon name="chevron-grabber-vertical" size="small" class="shrink-0 text-text-weak" />
|
||||
</div>
|
||||
</div>
|
||||
</Accordion.Trigger>
|
||||
</StickyAccordionHeader>
|
||||
<Accordion.Content class="bg-background-base">
|
||||
<div class="p-3">
|
||||
<RawMessageContent message={msgProps.message} />
|
||||
</div>
|
||||
</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
)
|
||||
}
|
||||
|
||||
let scroll: HTMLDivElement | undefined
|
||||
let frame: number | undefined
|
||||
let pending: { x: number; y: number } | undefined
|
||||
|
||||
const restoreScroll = (retries = 0) => {
|
||||
const el = scroll
|
||||
if (!el) return
|
||||
|
||||
const s = props.view()?.scroll("context")
|
||||
if (!s) return
|
||||
|
||||
// Wait for content to be scrollable - content may not have rendered yet
|
||||
if (el.scrollHeight <= el.clientHeight && retries < 10) {
|
||||
requestAnimationFrame(() => restoreScroll(retries + 1))
|
||||
return
|
||||
}
|
||||
|
||||
if (el.scrollTop !== s.y) el.scrollTop = s.y
|
||||
if (el.scrollLeft !== s.x) el.scrollLeft = s.x
|
||||
}
|
||||
|
||||
const handleScroll = (event: Event & { currentTarget: HTMLDivElement }) => {
|
||||
pending = {
|
||||
x: event.currentTarget.scrollLeft,
|
||||
y: event.currentTarget.scrollTop,
|
||||
}
|
||||
if (frame !== undefined) return
|
||||
|
||||
frame = requestAnimationFrame(() => {
|
||||
frame = undefined
|
||||
|
||||
const next = pending
|
||||
pending = undefined
|
||||
if (!next) return
|
||||
|
||||
props.view().setScroll("context", next)
|
||||
})
|
||||
}
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => props.messages().length,
|
||||
() => {
|
||||
requestAnimationFrame(restoreScroll)
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
onCleanup(() => {
|
||||
if (frame === undefined) return
|
||||
cancelAnimationFrame(frame)
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
class="@container h-full overflow-y-auto no-scrollbar pb-10"
|
||||
ref={(el) => {
|
||||
scroll = el
|
||||
restoreScroll()
|
||||
}}
|
||||
onScroll={handleScroll}
|
||||
>
|
||||
<div class="px-6 pt-4 flex flex-col gap-10">
|
||||
<div class="grid grid-cols-1 @[32rem]:grid-cols-2 gap-4">
|
||||
<For each={stats()}>{(stat) => <Stat label={stat.label} value={stat.value} />}</For>
|
||||
</div>
|
||||
|
||||
<Show when={breakdown().length > 0}>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="text-12-regular text-text-weak">Context Breakdown</div>
|
||||
<div class="h-2 w-full rounded-full bg-surface-base overflow-hidden flex">
|
||||
<For each={breakdown()}>
|
||||
{(segment) => (
|
||||
<div
|
||||
class="h-full"
|
||||
style={{
|
||||
width: `${segment.width}%`,
|
||||
"background-color": segment.color,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-x-3 gap-y-1">
|
||||
<For each={breakdown()}>
|
||||
{(segment) => (
|
||||
<div class="flex items-center gap-1 text-11-regular text-text-weak">
|
||||
<div class="size-2 rounded-sm" style={{ "background-color": segment.color }} />
|
||||
<div>{segment.label}</div>
|
||||
<div class="text-text-weaker">{segment.percent}</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<div class="hidden text-11-regular text-text-weaker">
|
||||
Approximate breakdown of input tokens. "Other" includes tool definitions and overhead.
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={systemPrompt()}>
|
||||
{(prompt) => (
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="text-12-regular text-text-weak">System Prompt</div>
|
||||
<div class="border border-border-base rounded-md bg-surface-base px-3 py-2">
|
||||
<Markdown text={prompt()} class="text-12-regular" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="text-12-regular text-text-weak">Raw messages</div>
|
||||
<Accordion multiple>
|
||||
<For each={props.messages()}>{(message) => <RawMessage message={message} />}</For>
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
267
packages/app/src/components/session/session-header.tsx
Normal file
267
packages/app/src/components/session/session-header.tsx
Normal file
@@ -0,0 +1,267 @@
|
||||
import { createMemo, createResource, Show } from "solid-js"
|
||||
import { A, useNavigate, useParams } from "@solidjs/router"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { useCommand } from "@/context/command"
|
||||
import { useServer } from "@/context/server"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { getFilename } from "@opencode-ai/util/path"
|
||||
import { base64Decode, base64Encode } from "@opencode-ai/util/encode"
|
||||
import { iife } from "@opencode-ai/util/iife"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||
import { Select } from "@opencode-ai/ui/select"
|
||||
import { Popover } from "@opencode-ai/ui/popover"
|
||||
import { TextField } from "@opencode-ai/ui/text-field"
|
||||
import { DialogSelectServer } from "@/components/dialog-select-server"
|
||||
import { SessionLspIndicator } from "@/components/session-lsp-indicator"
|
||||
import { SessionMcpIndicator } from "@/components/session-mcp-indicator"
|
||||
import type { Session } from "@opencode-ai/sdk/v2/client"
|
||||
import { same } from "@/utils/same"
|
||||
|
||||
export function SessionHeader() {
|
||||
const globalSDK = useGlobalSDK()
|
||||
const layout = useLayout()
|
||||
const params = useParams()
|
||||
const navigate = useNavigate()
|
||||
const command = useCommand()
|
||||
const server = useServer()
|
||||
const dialog = useDialog()
|
||||
const sync = useSync()
|
||||
|
||||
const projectDirectory = createMemo(() => base64Decode(params.dir ?? ""))
|
||||
|
||||
const sessions = createMemo(() => (sync.data.session ?? []).filter((s) => !s.parentID))
|
||||
const currentSession = createMemo(() => sync.data.session.find((s) => s.id === params.id))
|
||||
const parentSession = createMemo(() => {
|
||||
const current = currentSession()
|
||||
if (!current?.parentID) return undefined
|
||||
return sync.data.session.find((s) => s.id === current.parentID)
|
||||
})
|
||||
const shareEnabled = createMemo(() => sync.data.config.share !== "disabled")
|
||||
const worktrees = createMemo(() => layout.projects.list().map((p) => p.worktree), [], { equals: same })
|
||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||
const view = createMemo(() => layout.view(sessionKey()))
|
||||
|
||||
function navigateToProject(directory: string) {
|
||||
navigate(`/${base64Encode(directory)}`)
|
||||
}
|
||||
|
||||
function navigateToSession(session: Session | undefined) {
|
||||
if (!session) return
|
||||
// Only navigate if we're actually changing to a different session
|
||||
if (session.id === params.id) return
|
||||
navigate(`/${params.dir}/session/${session.id}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base flex">
|
||||
<button
|
||||
type="button"
|
||||
class="xl:hidden w-12 shrink-0 flex items-center justify-center border-r border-border-weak-base hover:bg-surface-raised-base-hover active:bg-surface-raised-base-active transition-colors"
|
||||
onClick={layout.mobileSidebar.toggle}
|
||||
>
|
||||
<Icon name="menu" size="small" />
|
||||
</button>
|
||||
<div class="px-4 flex items-center justify-between gap-4 w-full">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<div class="hidden xl:flex items-center gap-2">
|
||||
<Select
|
||||
options={worktrees()}
|
||||
current={sync.project?.worktree ?? projectDirectory()}
|
||||
label={(x) => getFilename(x)}
|
||||
onSelect={(x) => (x ? navigateToProject(x) : undefined)}
|
||||
class="text-14-regular text-text-base"
|
||||
variant="ghost"
|
||||
>
|
||||
{/* @ts-ignore */}
|
||||
{(i) => (
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon name="folder" size="small" />
|
||||
<div class="text-text-strong">{getFilename(i)}</div>
|
||||
</div>
|
||||
)}
|
||||
</Select>
|
||||
<div class="text-text-weaker">/</div>
|
||||
</div>
|
||||
<Show
|
||||
when={parentSession()}
|
||||
fallback={
|
||||
<>
|
||||
<Select
|
||||
options={sessions()}
|
||||
current={currentSession()}
|
||||
placeholder="New session"
|
||||
label={(x) => x.title}
|
||||
value={(x) => x.id}
|
||||
onSelect={navigateToSession}
|
||||
class="text-14-regular text-text-base max-w-[calc(100vw-180px)] md:max-w-md"
|
||||
variant="ghost"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<Select
|
||||
options={sessions()}
|
||||
current={parentSession()}
|
||||
placeholder="Back to parent session"
|
||||
label={(x) => x.title}
|
||||
value={(x) => x.id}
|
||||
onSelect={(session) => {
|
||||
// Only navigate if selecting a different session than current parent
|
||||
const currentParent = parentSession()
|
||||
if (session && currentParent && session.id !== currentParent.id) {
|
||||
navigateToSession(session)
|
||||
}
|
||||
}}
|
||||
class="text-14-regular text-text-base max-w-[calc(100vw-180px)] md:max-w-md"
|
||||
variant="ghost"
|
||||
/>
|
||||
<div class="text-text-weaker">/</div>
|
||||
<div class="flex items-center gap-1.5 min-w-0">
|
||||
<Tooltip value="Back to parent session">
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center gap-1 p-1 rounded hover:bg-surface-raised-base-hover active:bg-surface-raised-base-active transition-colors flex-shrink-0"
|
||||
onClick={() => navigateToSession(parentSession())}
|
||||
>
|
||||
<Icon name="arrow-left" size="small" class="text-icon-base" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={currentSession() && !parentSession()}>
|
||||
<TooltipKeybind class="hidden xl:block" title="New session" keybind={command.keybind("session.new")}>
|
||||
<IconButton as={A} href={`/${params.dir}/session`} icon="edit-small-2" variant="ghost" />
|
||||
</TooltipKeybind>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="hidden md:flex items-center gap-1">
|
||||
<Button
|
||||
size="small"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
dialog.show(() => <DialogSelectServer />)
|
||||
}}
|
||||
>
|
||||
<div
|
||||
classList={{
|
||||
"size-1.5 rounded-full": true,
|
||||
"bg-icon-success-base": server.healthy() === true,
|
||||
"bg-icon-critical-base": server.healthy() === false,
|
||||
"bg-border-weak-base": server.healthy() === undefined,
|
||||
}}
|
||||
/>
|
||||
<Icon name="server" size="small" class="text-icon-weak" />
|
||||
<span class="text-12-regular text-text-weak truncate max-w-[200px]">{server.name}</span>
|
||||
</Button>
|
||||
<SessionLspIndicator />
|
||||
<SessionMcpIndicator />
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<Show when={currentSession()?.summary?.files}>
|
||||
<TooltipKeybind
|
||||
class="hidden md:block shrink-0"
|
||||
title="Toggle review"
|
||||
keybind={command.keybind("review.toggle")}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="group/review-toggle size-6 p-0"
|
||||
onClick={() => view().reviewPanel.toggle()}
|
||||
>
|
||||
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
||||
<Icon
|
||||
name={view().reviewPanel.opened() ? "layout-right" : "layout-left"}
|
||||
size="small"
|
||||
class="group-hover/review-toggle:hidden"
|
||||
/>
|
||||
<Icon
|
||||
name={view().reviewPanel.opened() ? "layout-right-partial" : "layout-left-partial"}
|
||||
size="small"
|
||||
class="hidden group-hover/review-toggle:inline-block"
|
||||
/>
|
||||
<Icon
|
||||
name={view().reviewPanel.opened() ? "layout-right-full" : "layout-left-full"}
|
||||
size="small"
|
||||
class="hidden group-active/review-toggle:inline-block"
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
</Show>
|
||||
<TooltipKeybind
|
||||
class="hidden md:block shrink-0"
|
||||
title="Toggle terminal"
|
||||
keybind={command.keybind("terminal.toggle")}
|
||||
>
|
||||
<Button variant="ghost" class="group/terminal-toggle size-6 p-0" onClick={() => view().terminal.toggle()}>
|
||||
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
||||
<Icon
|
||||
size="small"
|
||||
name={view().terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
|
||||
class="group-hover/terminal-toggle:hidden"
|
||||
/>
|
||||
<Icon
|
||||
size="small"
|
||||
name="layout-bottom-partial"
|
||||
class="hidden group-hover/terminal-toggle:inline-block"
|
||||
/>
|
||||
<Icon
|
||||
size="small"
|
||||
name={view().terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
|
||||
class="hidden group-active/terminal-toggle:inline-block"
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
<Show when={shareEnabled() && currentSession()}>
|
||||
<Popover
|
||||
title="Share session"
|
||||
trigger={
|
||||
<Tooltip class="shrink-0" value="Share session">
|
||||
<IconButton icon="share" variant="ghost" class="" />
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
{iife(() => {
|
||||
const [url] = createResource(
|
||||
() => currentSession(),
|
||||
async (session) => {
|
||||
if (!session) return
|
||||
let shareURL = session.share?.url
|
||||
if (!shareURL) {
|
||||
shareURL = await globalSDK.client.session
|
||||
.share({ sessionID: session.id, directory: projectDirectory() })
|
||||
.then((r) => r.data?.share?.url)
|
||||
.catch((e) => {
|
||||
console.error("Failed to share session", e)
|
||||
return undefined
|
||||
})
|
||||
}
|
||||
return shareURL
|
||||
},
|
||||
{ initialValue: "" },
|
||||
)
|
||||
return (
|
||||
<Show when={url.latest}>
|
||||
{(shareUrl) => <TextField value={shareUrl()} readOnly copyable class="w-72" />}
|
||||
</Show>
|
||||
)
|
||||
})}
|
||||
</Popover>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
89
packages/app/src/components/session/session-new-view.tsx
Normal file
89
packages/app/src/components/session/session-new-view.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import { Show, createMemo } from "solid-js"
|
||||
import { DateTime } from "luxon"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||
import { Select } from "@opencode-ai/ui/select"
|
||||
|
||||
const MAIN_WORKTREE = "main"
|
||||
const CREATE_WORKTREE = "create"
|
||||
|
||||
interface NewSessionViewProps {
|
||||
worktree: string
|
||||
onWorktreeChange: (value: string) => void
|
||||
}
|
||||
|
||||
export function NewSessionView(props: NewSessionViewProps) {
|
||||
const sync = useSync()
|
||||
|
||||
const sandboxes = createMemo(() => sync.project?.sandboxes ?? [])
|
||||
const options = createMemo(() => [MAIN_WORKTREE, ...sandboxes(), CREATE_WORKTREE])
|
||||
const current = createMemo(() => {
|
||||
const selection = props.worktree
|
||||
if (options().includes(selection)) return selection
|
||||
return MAIN_WORKTREE
|
||||
})
|
||||
const projectRoot = createMemo(() => sync.project?.worktree ?? sync.data.path.directory)
|
||||
const isWorktree = createMemo(() => {
|
||||
const project = sync.project
|
||||
if (!project) return false
|
||||
return sync.data.path.directory !== project.worktree
|
||||
})
|
||||
|
||||
const label = (value: string) => {
|
||||
if (value === MAIN_WORKTREE) {
|
||||
if (isWorktree()) return "Main branch"
|
||||
const branch = sync.data.vcs?.branch
|
||||
if (branch) return `Main branch (${branch})`
|
||||
return "Main branch"
|
||||
}
|
||||
|
||||
if (value === CREATE_WORKTREE) return "Create new worktree"
|
||||
|
||||
return getFilename(value)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
class="size-full flex flex-col pb-45 justify-end items-start gap-4 flex-[1_0_0] self-stretch max-w-200 mx-auto px-6"
|
||||
style={{ "padding-bottom": "calc(var(--prompt-height, 11.25rem) + 64px)" }}
|
||||
>
|
||||
<div class="text-20-medium text-text-weaker">New session</div>
|
||||
<div class="flex justify-center items-center gap-3">
|
||||
<Icon name="folder" size="small" />
|
||||
<div class="text-12-medium text-text-weak">
|
||||
{getDirectory(projectRoot())}
|
||||
<span class="text-text-strong">{getFilename(projectRoot())}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-center items-center gap-1">
|
||||
<Icon name="branch" size="small" />
|
||||
<Select
|
||||
options={options()}
|
||||
current={current()}
|
||||
value={(x) => x}
|
||||
label={label}
|
||||
onSelect={(value) => {
|
||||
props.onWorktreeChange(value ?? MAIN_WORKTREE)
|
||||
}}
|
||||
size="normal"
|
||||
variant="ghost"
|
||||
class="text-12-medium"
|
||||
/>
|
||||
</div>
|
||||
<Show when={sync.project}>
|
||||
{(project) => (
|
||||
<div class="flex justify-center items-center gap-3">
|
||||
<Icon name="pencil-line" size="small" />
|
||||
<div class="text-12-medium text-text-weak">
|
||||
Last modified
|
||||
<span class="text-text-strong">
|
||||
{DateTime.fromMillis(project().time.updated ?? project().time.created).toRelative()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
49
packages/app/src/components/session/session-sortable-tab.tsx
Normal file
49
packages/app/src/components/session/session-sortable-tab.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { createMemo, Show } from "solid-js"
|
||||
import type { JSX } from "solid-js"
|
||||
import { createSortable } from "@thisbeyond/solid-dnd"
|
||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||
import { getFilename } from "@opencode-ai/util/path"
|
||||
import { useFile } from "@/context/file"
|
||||
|
||||
export function FileVisual(props: { path: string; active?: boolean }): JSX.Element {
|
||||
return (
|
||||
<div class="flex items-center gap-x-1.5">
|
||||
<FileIcon
|
||||
node={{ path: props.path, type: "file" }}
|
||||
classList={{
|
||||
"grayscale-100 group-data-[selected]/tab:grayscale-0": !props.active,
|
||||
"grayscale-0": props.active,
|
||||
}}
|
||||
/>
|
||||
<span class="text-14-medium">{getFilename(props.path)}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function SortableTab(props: { tab: string; onTabClose: (tab: string) => void }): JSX.Element {
|
||||
const file = useFile()
|
||||
const sortable = createSortable(props.tab)
|
||||
const path = createMemo(() => file.pathFromTab(props.tab))
|
||||
return (
|
||||
// @ts-ignore
|
||||
<div use:sortable classList={{ "h-full": true, "opacity-0": sortable.isActiveDraggable }}>
|
||||
<div class="relative h-full">
|
||||
<Tabs.Trigger
|
||||
value={props.tab}
|
||||
closeButton={
|
||||
<Tooltip value="Close tab" placement="bottom">
|
||||
<IconButton icon="close" variant="ghost" onClick={() => props.onTabClose(props.tab)} />
|
||||
</Tooltip>
|
||||
}
|
||||
hideCloseButton
|
||||
onMiddleClick={() => props.onTabClose(props.tab)}
|
||||
>
|
||||
<Show when={path()}>{(p) => <FileVisual path={p()} />}</Show>
|
||||
</Tabs.Trigger>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { JSX } from "solid-js"
|
||||
import { createSortable } from "@thisbeyond/solid-dnd"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||
import { useTerminal, type LocalPTY } from "@/context/terminal"
|
||||
|
||||
export function SortableTerminalTab(props: { terminal: LocalPTY }): JSX.Element {
|
||||
const terminal = useTerminal()
|
||||
const sortable = createSortable(props.terminal.id)
|
||||
return (
|
||||
// @ts-ignore
|
||||
<div use:sortable classList={{ "h-full": true, "opacity-0": sortable.isActiveDraggable }}>
|
||||
<div class="relative h-full">
|
||||
<Tabs.Trigger
|
||||
value={props.terminal.id}
|
||||
closeButton={
|
||||
terminal.all().length > 1 && (
|
||||
<IconButton icon="close" variant="ghost" onClick={() => terminal.close(props.terminal.id)} />
|
||||
)
|
||||
}
|
||||
>
|
||||
{props.terminal.title}
|
||||
</Tabs.Trigger>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
296
packages/app/src/components/terminal.tsx
Normal file
296
packages/app/src/components/terminal.tsx
Normal file
@@ -0,0 +1,296 @@
|
||||
import type { Ghostty, Terminal as Term, FitAddon } from "ghostty-web"
|
||||
import { ComponentProps, createEffect, createSignal, onCleanup, onMount, splitProps } from "solid-js"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { SerializeAddon } from "@/addons/serialize"
|
||||
import { LocalPTY } from "@/context/terminal"
|
||||
import { resolveThemeVariant, useTheme, withAlpha, type HexColor } from "@opencode-ai/ui/theme"
|
||||
|
||||
export interface TerminalProps extends ComponentProps<"div"> {
|
||||
pty: LocalPTY
|
||||
onSubmit?: () => void
|
||||
onCleanup?: (pty: LocalPTY) => void
|
||||
onConnectError?: (error: unknown) => void
|
||||
}
|
||||
|
||||
type TerminalColors = {
|
||||
background: string
|
||||
foreground: string
|
||||
cursor: string
|
||||
selectionBackground: string
|
||||
}
|
||||
|
||||
const DEFAULT_TERMINAL_COLORS: Record<"light" | "dark", TerminalColors> = {
|
||||
light: {
|
||||
background: "#fcfcfc",
|
||||
foreground: "#211e1e",
|
||||
cursor: "#211e1e",
|
||||
selectionBackground: withAlpha("#211e1e", 0.2),
|
||||
},
|
||||
dark: {
|
||||
background: "#191515",
|
||||
foreground: "#d4d4d4",
|
||||
cursor: "#d4d4d4",
|
||||
selectionBackground: withAlpha("#d4d4d4", 0.25),
|
||||
},
|
||||
}
|
||||
|
||||
export const Terminal = (props: TerminalProps) => {
|
||||
const sdk = useSDK()
|
||||
const theme = useTheme()
|
||||
let container!: HTMLDivElement
|
||||
const [local, others] = splitProps(props, ["pty", "class", "classList", "onConnectError"])
|
||||
let ws: WebSocket | undefined
|
||||
let term: Term | undefined
|
||||
let ghostty: Ghostty
|
||||
let serializeAddon: SerializeAddon
|
||||
let fitAddon: FitAddon
|
||||
let handleResize: () => void
|
||||
let handleTextareaFocus: () => void
|
||||
let handleTextareaBlur: () => void
|
||||
let reconnect: number | undefined
|
||||
let disposed = false
|
||||
|
||||
const getTerminalColors = (): TerminalColors => {
|
||||
const mode = theme.mode()
|
||||
const fallback = DEFAULT_TERMINAL_COLORS[mode]
|
||||
const currentTheme = theme.themes()[theme.themeId()]
|
||||
if (!currentTheme) return fallback
|
||||
const variant = mode === "dark" ? currentTheme.dark : currentTheme.light
|
||||
if (!variant?.seeds) return fallback
|
||||
const resolved = resolveThemeVariant(variant, mode === "dark")
|
||||
const text = resolved["text-stronger"] ?? fallback.foreground
|
||||
const background = resolved["background-stronger"] ?? fallback.background
|
||||
const alpha = mode === "dark" ? 0.25 : 0.2
|
||||
const base = text.startsWith("#") ? (text as HexColor) : (fallback.foreground as HexColor)
|
||||
const selectionBackground = withAlpha(base, alpha)
|
||||
return {
|
||||
background,
|
||||
foreground: text,
|
||||
cursor: text,
|
||||
selectionBackground,
|
||||
}
|
||||
}
|
||||
|
||||
const [terminalColors, setTerminalColors] = createSignal<TerminalColors>(getTerminalColors())
|
||||
|
||||
createEffect(() => {
|
||||
const colors = getTerminalColors()
|
||||
setTerminalColors(colors)
|
||||
if (!term) return
|
||||
const setOption = (term as unknown as { setOption?: (key: string, value: TerminalColors) => void }).setOption
|
||||
if (!setOption) return
|
||||
setOption("theme", colors)
|
||||
})
|
||||
|
||||
const focusTerminal = () => {
|
||||
const t = term
|
||||
if (!t) return
|
||||
t.focus()
|
||||
setTimeout(() => t.textarea?.focus(), 0)
|
||||
}
|
||||
const handlePointerDown = () => {
|
||||
const activeElement = document.activeElement
|
||||
if (activeElement instanceof HTMLElement && activeElement !== container) {
|
||||
activeElement.blur()
|
||||
}
|
||||
focusTerminal()
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
const mod = await import("ghostty-web")
|
||||
ghostty = await mod.Ghostty.load()
|
||||
|
||||
const url = new URL(sdk.url + `/pty/${local.pty.id}/connect?directory=${encodeURIComponent(sdk.directory)}`)
|
||||
if (window.__OPENCODE__?.serverPassword) {
|
||||
url.username = "opencode"
|
||||
url.password = window.__OPENCODE__?.serverPassword
|
||||
}
|
||||
const socket = new WebSocket(url)
|
||||
ws = socket
|
||||
|
||||
const t = new mod.Terminal({
|
||||
cursorBlink: true,
|
||||
cursorStyle: "bar",
|
||||
fontSize: 14,
|
||||
fontFamily: "IBM Plex Mono, monospace",
|
||||
allowTransparency: true,
|
||||
theme: terminalColors(),
|
||||
scrollback: 10_000,
|
||||
ghostty,
|
||||
})
|
||||
term = t
|
||||
|
||||
const copy = () => {
|
||||
const selection = t.getSelection()
|
||||
if (!selection) return false
|
||||
|
||||
const body = document.body
|
||||
if (body) {
|
||||
const textarea = document.createElement("textarea")
|
||||
textarea.value = selection
|
||||
textarea.setAttribute("readonly", "")
|
||||
textarea.style.position = "fixed"
|
||||
textarea.style.opacity = "0"
|
||||
body.appendChild(textarea)
|
||||
textarea.select()
|
||||
const copied = document.execCommand("copy")
|
||||
body.removeChild(textarea)
|
||||
if (copied) return true
|
||||
}
|
||||
|
||||
const clipboard = navigator.clipboard
|
||||
if (clipboard?.writeText) {
|
||||
clipboard.writeText(selection).catch(() => {})
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
t.attachCustomKeyEventHandler((event) => {
|
||||
const key = event.key.toLowerCase()
|
||||
|
||||
if (event.ctrlKey && event.shiftKey && !event.metaKey && key === "c") {
|
||||
copy()
|
||||
return true
|
||||
}
|
||||
|
||||
if (event.metaKey && !event.ctrlKey && !event.altKey && key === "c") {
|
||||
if (!t.hasSelection()) return true
|
||||
copy()
|
||||
return true
|
||||
}
|
||||
|
||||
// allow for ctrl-` to toggle terminal in parent
|
||||
if (event.ctrlKey && key === "`") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
fitAddon = new mod.FitAddon()
|
||||
serializeAddon = new SerializeAddon()
|
||||
t.loadAddon(serializeAddon)
|
||||
t.loadAddon(fitAddon)
|
||||
|
||||
t.open(container)
|
||||
container.addEventListener("pointerdown", handlePointerDown)
|
||||
|
||||
handleTextareaFocus = () => {
|
||||
t.options.cursorBlink = true
|
||||
}
|
||||
handleTextareaBlur = () => {
|
||||
t.options.cursorBlink = false
|
||||
}
|
||||
|
||||
t.textarea?.addEventListener("focus", handleTextareaFocus)
|
||||
t.textarea?.addEventListener("blur", handleTextareaBlur)
|
||||
|
||||
focusTerminal()
|
||||
|
||||
if (local.pty.buffer) {
|
||||
if (local.pty.rows && local.pty.cols) {
|
||||
t.resize(local.pty.cols, local.pty.rows)
|
||||
}
|
||||
t.write(local.pty.buffer, () => {
|
||||
if (local.pty.scrollY) {
|
||||
t.scrollToLine(local.pty.scrollY)
|
||||
}
|
||||
fitAddon.fit()
|
||||
})
|
||||
}
|
||||
|
||||
fitAddon.observeResize()
|
||||
handleResize = () => fitAddon.fit()
|
||||
window.addEventListener("resize", handleResize)
|
||||
t.onResize(async (size) => {
|
||||
if (socket.readyState === WebSocket.OPEN) {
|
||||
await sdk.client.pty
|
||||
.update({
|
||||
ptyID: local.pty.id,
|
||||
size: {
|
||||
cols: size.cols,
|
||||
rows: size.rows,
|
||||
},
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
})
|
||||
t.onData((data) => {
|
||||
if (socket.readyState === WebSocket.OPEN) {
|
||||
socket.send(data)
|
||||
}
|
||||
})
|
||||
t.onKey((key) => {
|
||||
if (key.key == "Enter") {
|
||||
props.onSubmit?.()
|
||||
}
|
||||
})
|
||||
// t.onScroll((ydisp) => {
|
||||
// console.log("Scroll position:", ydisp)
|
||||
// })
|
||||
socket.addEventListener("open", () => {
|
||||
console.log("WebSocket connected")
|
||||
sdk.client.pty
|
||||
.update({
|
||||
ptyID: local.pty.id,
|
||||
size: {
|
||||
cols: t.cols,
|
||||
rows: t.rows,
|
||||
},
|
||||
})
|
||||
.catch(() => {})
|
||||
})
|
||||
socket.addEventListener("message", (event) => {
|
||||
t.write(event.data)
|
||||
})
|
||||
socket.addEventListener("error", (error) => {
|
||||
console.error("WebSocket error:", error)
|
||||
props.onConnectError?.(error)
|
||||
})
|
||||
socket.addEventListener("close", () => {
|
||||
console.log("WebSocket disconnected")
|
||||
})
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
if (handleResize) {
|
||||
window.removeEventListener("resize", handleResize)
|
||||
}
|
||||
container.removeEventListener("pointerdown", handlePointerDown)
|
||||
term?.textarea?.removeEventListener("focus", handleTextareaFocus)
|
||||
term?.textarea?.removeEventListener("blur", handleTextareaBlur)
|
||||
|
||||
const t = term
|
||||
if (serializeAddon && props.onCleanup && t) {
|
||||
const buffer = serializeAddon.serialize()
|
||||
props.onCleanup({
|
||||
...local.pty,
|
||||
buffer,
|
||||
rows: t.rows,
|
||||
cols: t.cols,
|
||||
scrollY: t.getViewportY(),
|
||||
})
|
||||
}
|
||||
|
||||
ws?.close()
|
||||
t?.dispose()
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={container}
|
||||
data-component="terminal"
|
||||
data-prevent-autofocus
|
||||
style={{ "background-color": terminalColors().background }}
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
"select-text": true,
|
||||
"size-full px-6 py-3 font-mono": true,
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
{...others}
|
||||
/>
|
||||
)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user