mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-09 18:34:21 +00:00
Compare commits
1918 Commits
fix-issue
...
queue-on-t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2311bbf81 | ||
|
|
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 | ||
|
|
10375263ef | ||
|
|
ae00001aa0 | ||
|
|
f53ebafbab | ||
|
|
23ebc50da9 | ||
|
|
673c6f97b7 | ||
|
|
ec46f71258 | ||
|
|
8865e524cb | ||
|
|
36bb02ae45 | ||
|
|
5072331f04 | ||
|
|
9d48fd4bbd | ||
|
|
bf66390557 | ||
|
|
184643f0db | ||
|
|
1bce898ca7 | ||
|
|
8c895570c6 | ||
|
|
6dc4e5ac93 | ||
|
|
d3922f0965 | ||
|
|
cfaac9f2e1 | ||
|
|
0b046d6cf0 | ||
|
|
3d822e5f79 | ||
|
|
f9cef22a53 | ||
|
|
b5d7d3dec1 | ||
|
|
182630e0d7 | ||
|
|
c81506b28d | ||
|
|
6c40bfe043 | ||
|
|
9caaae6a18 | ||
|
|
ad6a5e6157 | ||
|
|
7dd8ea58c2 | ||
|
|
3b261e0125 | ||
|
|
426791f68a | ||
|
|
c7cade2494 | ||
|
|
8f6c8844d7 | ||
|
|
da6e0e60c0 | ||
|
|
d89b567b47 | ||
|
|
34eb03f5b8 | ||
|
|
2f6d15a51e | ||
|
|
8ffea80980 | ||
|
|
c87d61b561 | ||
|
|
35c12e2053 | ||
|
|
33d8bfc937 | ||
|
|
f2343a6794 | ||
|
|
bab000eeb5 | ||
|
|
8e674ae053 | ||
|
|
6a4f4009d5 | ||
|
|
5e79b95927 | ||
|
|
a7a2bbb497 | ||
|
|
6e93d14bdb | ||
|
|
f29f284b3e | ||
|
|
b1b8f6cf71 | ||
|
|
4c3336bbe7 | ||
|
|
354ac0b493 | ||
|
|
1d159c6858 | ||
|
|
d70639b256 | ||
|
|
e4a92f0084 | ||
|
|
fdf5a70a27 | ||
|
|
f71da42520 | ||
|
|
f6bdeb9e3a | ||
|
|
2400354bab | ||
|
|
db348c46cc | ||
|
|
49567fe61a | ||
|
|
e5b3f796e4 | ||
|
|
a9700c8773 | ||
|
|
26cf5e003e | ||
|
|
742cf10dee | ||
|
|
7664453f94 | ||
|
|
460672aa93 | ||
|
|
b4e4fd9807 | ||
|
|
34bdfd0937 | ||
|
|
84591ca8ad | ||
|
|
fd4d0c5c0b | ||
|
|
9f5db46911 | ||
|
|
755ddbb223 | ||
|
|
701d470d01 | ||
|
|
1d9058d26b | ||
|
|
39e2a5f595 | ||
|
|
f862ab6722 | ||
|
|
129d4f0b1b | ||
|
|
3a1e50d1f8 | ||
|
|
e2fb690d8e | ||
|
|
0a7f58a811 | ||
|
|
dae0168ed8 | ||
|
|
edfe2e4f1c | ||
|
|
1bc1ea8b47 | ||
|
|
dacbbe3184 | ||
|
|
89285d8f5f | ||
|
|
2e853911c3 | ||
|
|
695fdecf23 | ||
|
|
054d22791d | ||
|
|
4a57cc69d8 | ||
|
|
7e0c8db029 | ||
|
|
ba4cc3bf86 | ||
|
|
b19a424c85 | ||
|
|
1689281c35 | ||
|
|
cdbb59fae8 | ||
|
|
4eb311e98f | ||
|
|
80eac96258 | ||
|
|
4bad6f9f1b | ||
|
|
d7db57e8e1 | ||
|
|
943fbf39a3 | ||
|
|
d8a34c2fcc | ||
|
|
5720ed1f44 | ||
|
|
bb20a359e4 | ||
|
|
0d472a49a0 | ||
|
|
203581e82f | ||
|
|
677631916c | ||
|
|
1aa1e8c904 | ||
|
|
55d62fbd9f | ||
|
|
e1ad2a355c | ||
|
|
4f318f913e | ||
|
|
2d814b6db2 | ||
|
|
e561f1ad68 | ||
|
|
ebfb985215 | ||
|
|
2646da50df | ||
|
|
50a5f6e53b | ||
|
|
d03fac52e7 | ||
|
|
6a802c01cd | ||
|
|
14146428dd | ||
|
|
26d0280f70 | ||
|
|
3274a5813e | ||
|
|
382905602c | ||
|
|
8b5cea7899 | ||
|
|
100c31cbb1 | ||
|
|
0b286f1b84 | ||
|
|
2f6ca958fe | ||
|
|
5218e7a546 | ||
|
|
7f8e799392 | ||
|
|
289f4abaaa | ||
|
|
7ce898ce43 | ||
|
|
0dd716a75e | ||
|
|
87171467fa | ||
|
|
b99afdad91 | ||
|
|
4fd576f3af | ||
|
|
2f41d0bedd | ||
|
|
5f03290534 | ||
|
|
427157c683 | ||
|
|
a0ab3d98b7 | ||
|
|
c8de766913 | ||
|
|
d57b963141 | ||
|
|
0ebcaff927 | ||
|
|
15931fa170 | ||
|
|
af4087d7b5 | ||
|
|
323ea1040c | ||
|
|
1fe87b0233 | ||
|
|
8d11df1b3b | ||
|
|
ecc5050838 | ||
|
|
606cf3b6f2 | ||
|
|
67cfd7f06b | ||
|
|
ab9ac7c87a | ||
|
|
ee9f979613 | ||
|
|
228b6444f8 | ||
|
|
9998efdae2 | ||
|
|
9427f56e1a | ||
|
|
a6dd35d73d | ||
|
|
faeaafa5f5 | ||
|
|
8b298a233e | ||
|
|
6f43d03043 | ||
|
|
c868a4088d | ||
|
|
83d8a88c90 | ||
|
|
268f37f8c9 | ||
|
|
b0aaf04957 | ||
|
|
b7875256f3 | ||
|
|
7bc47fb904 | ||
|
|
5cf8e54372 | ||
|
|
7437ccd6f4 | ||
|
|
4bf882ba81 | ||
|
|
d5dcc55a47 | ||
|
|
e1925f4fe8 | ||
|
|
ee3d034e16 | ||
|
|
257a4d5b86 | ||
|
|
1fc5836f64 | ||
|
|
2fb89161c8 | ||
|
|
251fbc0a99 | ||
|
|
0da901a188 | ||
|
|
17221e6ffe | ||
|
|
cc9f88ac8f | ||
|
|
fe65ed6a61 | ||
|
|
e37a75a411 | ||
|
|
194ff4919c | ||
|
|
83843a794f | ||
|
|
235a60d3c2 | ||
|
|
b70d186bd1 | ||
|
|
647331de28 | ||
|
|
57ef115375 | ||
|
|
942498211f | ||
|
|
e789fcf5e5 | ||
|
|
b9fb180bc6 | ||
|
|
7427b887f9 | ||
|
|
289b2b6a51 | ||
|
|
49b4b5907e | ||
|
|
f82442c123 | ||
|
|
e682cc9daf | ||
|
|
d359e086a4 | ||
|
|
f949755367 | ||
|
|
a168d854f4 | ||
|
|
31645f5578 | ||
|
|
a1b68daa9a | ||
|
|
ca65da2d9e | ||
|
|
e48d804d84 | ||
|
|
b4209582fb | ||
|
|
dbdea2f659 | ||
|
|
a50ab4b5b5 | ||
|
|
4d7c3f56fa | ||
|
|
16b41d2bea | ||
|
|
a8c499ae8f | ||
|
|
24430287c5 | ||
|
|
1f52731255 | ||
|
|
4a3ba58f65 | ||
|
|
2a3a8a1ec2 | ||
|
|
69e562125d | ||
|
|
b5e97eb338 | ||
|
|
16e6941495 | ||
|
|
f033e0317e | ||
|
|
ddd88f92cc | ||
|
|
99101edc13 | ||
|
|
6e85a07977 | ||
|
|
be1a3536ae | ||
|
|
1e4bfbcf6f | ||
|
|
204e3bf382 | ||
|
|
8fb014a48d | ||
|
|
57c3cf1f8b | ||
|
|
f9d0850c5e | ||
|
|
8864da7a77 | ||
|
|
1b39199083 | ||
|
|
b8204c0bb7 | ||
|
|
fe8c5c143e | ||
|
|
d6f86e9bb7 | ||
|
|
bf00b2bfc9 | ||
|
|
382ec8fb2c | ||
|
|
6454adcd69 | ||
|
|
99548554d7 | ||
|
|
751899eeec | ||
|
|
f8df1d3185 | ||
|
|
b07a47fc89 | ||
|
|
c6f84f32d7 | ||
|
|
ebe25c3e9a | ||
|
|
65d7fc3ccd | ||
|
|
4f3037d803 | ||
|
|
5c490c51ed | ||
|
|
5da1c0087b | ||
|
|
4375149e63 | ||
|
|
b695d3b6bb | ||
|
|
d7e133732c | ||
|
|
494e6fff01 | ||
|
|
0c7a297b1d | ||
|
|
9b1f9007c3 | ||
|
|
34ef5f4ece | ||
|
|
73ad20b90c | ||
|
|
340e80257a | ||
|
|
c23ea2a211 | ||
|
|
a5f964aec6 | ||
|
|
b8a8fb0de6 | ||
|
|
a6a8f41fd3 | ||
|
|
c137babea3 | ||
|
|
db2abc1b2c | ||
|
|
a0f9f8dabb | ||
|
|
8a185aa678 | ||
|
|
29aaf4f000 | ||
|
|
fc940dfcfb | ||
|
|
2f2ea98937 | ||
|
|
ef0fa2007b | ||
|
|
f07d4b933c | ||
|
|
5f57cee8e4 | ||
|
|
1755a3fe07 | ||
|
|
99680baf83 | ||
|
|
9aa5460a0e | ||
|
|
b4014e5baa | ||
|
|
96e4dcb521 | ||
|
|
7e682a95c4 | ||
|
|
5eeba76bc5 | ||
|
|
a2c91ebc32 | ||
|
|
1aee8b49e1 | ||
|
|
984f17ddd7 | ||
|
|
d556143e3b | ||
|
|
7e3ad770ac | ||
|
|
87524de265 | ||
|
|
ee10d9b898 | ||
|
|
bbd36e8441 | ||
|
|
4e2d1acf7d | ||
|
|
40d63cd1e3 | ||
|
|
77b2331428 | ||
|
|
2b7e2edee5 | ||
|
|
28aba35ff9 | ||
|
|
89219a77f7 | ||
|
|
20e3a74bad | ||
|
|
ff690350b1 | ||
|
|
ebefb26e8f | ||
|
|
0b1ee9ddd9 | ||
|
|
79599f351e | ||
|
|
8c9f6b1d3e | ||
|
|
83bcb9e95b | ||
|
|
96b9ff8d0e | ||
|
|
0af2254856 | ||
|
|
c2944024a8 | ||
|
|
5be4bda90f | ||
|
|
b78e2db013 | ||
|
|
3f4d1121a4 | ||
|
|
def910021d | ||
|
|
3ac42e9632 | ||
|
|
9c26bb7c6c | ||
|
|
53f20f7612 | ||
|
|
11b3927dc2 | ||
|
|
a190eda2c8 | ||
|
|
1f18f389c0 | ||
|
|
84e56ee614 | ||
|
|
59329a414d | ||
|
|
452c991f58 | ||
|
|
be8116e2ea | ||
|
|
f0ed1e38c9 | ||
|
|
ac0f1dbbdd | ||
|
|
275a352e81 | ||
|
|
9f3bc0e352 | ||
|
|
6c1a1a77b7 | ||
|
|
2e21c62320 | ||
|
|
19c6fec4d1 | ||
|
|
4779d99a13 | ||
|
|
05e0759878 | ||
|
|
2330ec6dc3 | ||
|
|
75e5130cf8 | ||
|
|
87efd27459 | ||
|
|
62f080b0e4 | ||
|
|
ae3990a557 | ||
|
|
d7b5b431d6 | ||
|
|
e2fbd098d2 | ||
|
|
ef78fd8bae | ||
|
|
72ebaeb8f7 | ||
|
|
0dc62d5dad | ||
|
|
d118782a10 | ||
|
|
ff05647350 | ||
|
|
0e1c711c4e | ||
|
|
bfb254dac6 | ||
|
|
92fe927785 | ||
|
|
2e25fe9d5d | ||
|
|
38c5f23f4a | ||
|
|
112c58abf5 | ||
|
|
0dce5173cc | ||
|
|
2c70c0b00f | ||
|
|
34024c2504 | ||
|
|
27e826eba6 | ||
|
|
89a4f1c1ae | ||
|
|
c0c61b25ff | ||
|
|
0d1c6e0ca9 | ||
|
|
002db3abf4 | ||
|
|
416a919c6d | ||
|
|
dbbcf0b8d0 | ||
|
|
efac8cebb3 | ||
|
|
4f2baf1a72 | ||
|
|
48b2bde6e5 | ||
|
|
88314148e6 | ||
|
|
56452d886d | ||
|
|
f3e64cfb19 | ||
|
|
8fcc80bc20 | ||
|
|
0beccc406e | ||
|
|
b82ea693db | ||
|
|
4fd9a19fbb | ||
|
|
e16487b804 | ||
|
|
5388192aac | ||
|
|
8010448ba1 | ||
|
|
66f3e69867 | ||
|
|
ca599ab8fc | ||
|
|
c3b3b133b0 | ||
|
|
300ec0e0af | ||
|
|
6632987827 | ||
|
|
e555e893c4 | ||
|
|
81134cf61e | ||
|
|
37e4c1e619 | ||
|
|
02b5e7d72c | ||
|
|
7abc2a947e | ||
|
|
337a7e9646 | ||
|
|
62cc532ecc | ||
|
|
d5a506d4ae | ||
|
|
9c5f94bd66 | ||
|
|
83390314d6 | ||
|
|
8b08e9cda5 | ||
|
|
b1b1df824c | ||
|
|
7d1733c752 | ||
|
|
cf05e6e02b | ||
|
|
7e49d0fb15 | ||
|
|
c4f63824df | ||
|
|
4236744fb5 | ||
|
|
284c045795 | ||
|
|
2c53abd70c | ||
|
|
b7a9cbfc68 | ||
|
|
46a35dfc1b | ||
|
|
b7597c12dd | ||
|
|
6830590183 | ||
|
|
b9b4349039 | ||
|
|
4107918909 | ||
|
|
6347ee9988 | ||
|
|
9daa4e04ea | ||
|
|
ed96ae9d45 | ||
|
|
8ce0966987 | ||
|
|
8cb26b6066 | ||
|
|
5cf6a1343c | ||
|
|
44d6c5780d | ||
|
|
5eaa8e1bf4 | ||
|
|
df2713a6c2 | ||
|
|
ff6864a7ca | ||
|
|
5e37a902ce | ||
|
|
df2ebfac7d | ||
|
|
5fbcb203f5 | ||
|
|
34db739442 | ||
|
|
ae8c4154aa | ||
|
|
315836c0b7 | ||
|
|
c0d009d5f3 | ||
|
|
c36f3b9dbe | ||
|
|
d31824320e | ||
|
|
88c0675148 | ||
|
|
82c4755fb0 | ||
|
|
40572eeba4 | ||
|
|
d81d63045a | ||
|
|
ece3bfd93d | ||
|
|
acd91bddf7 | ||
|
|
3a14ca044c | ||
|
|
d66d806700 | ||
|
|
e9b95b2e91 | ||
|
|
56dde2cc83 | ||
|
|
d2ce368a3f | ||
|
|
f492122d59 | ||
|
|
b0f77da56c | ||
|
|
274b86b19b | ||
|
|
2ca118db59 | ||
|
|
a0c0e2b5c3 | ||
|
|
d43fbec12d | ||
|
|
bb426112ed | ||
|
|
d2217bb825 | ||
|
|
ac495bd351 | ||
|
|
b913eb7acc | ||
|
|
ea65a91b2e | ||
|
|
ed6d749104 | ||
|
|
9eefcd1b41 | ||
|
|
7c1124199e | ||
|
|
5cf126d489 | ||
|
|
509f7d9617 | ||
|
|
ae1bf92c81 | ||
|
|
b021b26e77 | ||
|
|
9555d348de | ||
|
|
220c564047 | ||
|
|
cf5c0129ac | ||
|
|
543dbe71d2 | ||
|
|
54569b5552 | ||
|
|
6a09861806 | ||
|
|
79a4c65313 | ||
|
|
654534ac71 | ||
|
|
ba16bfdf3d | ||
|
|
ad5614bbb9 | ||
|
|
dda579c8ad | ||
|
|
4246cdb069 | ||
|
|
7ade6d386d | ||
|
|
2613f44961 | ||
|
|
62ffeb3987 | ||
|
|
4a8e8f537c | ||
|
|
a68bee7878 | ||
|
|
ed33d82535 | ||
|
|
2d63c22d1a | ||
|
|
e22af25076 | ||
|
|
622caae9c9 | ||
|
|
fed4776451 | ||
|
|
fdf560c343 | ||
|
|
fc3ffb2bf9 | ||
|
|
7368342bab | ||
|
|
c8fc910533 | ||
|
|
0f9ef84d55 | ||
|
|
74b5c285cf | ||
|
|
a34e67b518 | ||
|
|
0c7f0cfa2e | ||
|
|
10ee6d345b | ||
|
|
48ec68730f | ||
|
|
70e4efe429 | ||
|
|
92948ed8a4 | ||
|
|
6d412d8872 | ||
|
|
e6a0a005d6 | ||
|
|
90d44751e7 | ||
|
|
4d062ba1b2 | ||
|
|
f8bca50f00 | ||
|
|
3d2ef28fa8 | ||
|
|
210b3e905b | ||
|
|
96975ef8d6 | ||
|
|
b8b998be56 | ||
|
|
d8ac35f6e5 | ||
|
|
ed1eacfce0 | ||
|
|
629f475f63 | ||
|
|
43a7c1dd8c | ||
|
|
e288ce0fca | ||
|
|
67b3fcb31a | ||
|
|
aedb5550a8 | ||
|
|
1638ffde69 | ||
|
|
d4cfbd8219 | ||
|
|
c7bac83212 | ||
|
|
fc9789d7a7 | ||
|
|
a8957d8d16 | ||
|
|
0660433921 | ||
|
|
1a6f4f1c0d | ||
|
|
974a24ba02 | ||
|
|
5ebe29de1e | ||
|
|
6bdf8b1fe1 | ||
|
|
5bcc93851c | ||
|
|
d0789632b4 | ||
|
|
a6e297baad | ||
|
|
307af10c8b | ||
|
|
f254cf76d9 | ||
|
|
b4ffaa21ec | ||
|
|
7bf6f264e4 | ||
|
|
7434fbba8e | ||
|
|
b7581e01ea | ||
|
|
b46d4789fc | ||
|
|
199bd8a9a2 | ||
|
|
decf2452c4 | ||
|
|
d8663a44c2 | ||
|
|
8917a4c609 | ||
|
|
5d7a52f8b8 | ||
|
|
b7b827c5bd | ||
|
|
613e082358 | ||
|
|
b6856bd593 | ||
|
|
7cb5a77ba6 | ||
|
|
cd9898a565 | ||
|
|
a4ffa869cc | ||
|
|
dbc84ff4c3 | ||
|
|
c11ea3fd92 | ||
|
|
3c3a0f8afb | ||
|
|
b93614cb81 | ||
|
|
b84d513bd7 | ||
|
|
0554d03162 | ||
|
|
15caecdb45 | ||
|
|
91ab966921 | ||
|
|
bc3286de46 | ||
|
|
af45444496 | ||
|
|
43202f2820 | ||
|
|
ce37e11bfe | ||
|
|
6e9833acce | ||
|
|
379c4ecab3 | ||
|
|
f1db4b60c4 | ||
|
|
9846b26be7 | ||
|
|
d6ba6af6f3 | ||
|
|
d463ade028 | ||
|
|
6c3495a75a | ||
|
|
a16edb4ea0 | ||
|
|
9efe09564b | ||
|
|
ccdd77032a | ||
|
|
41e234c6d0 | ||
|
|
3e03646e42 | ||
|
|
f7acc34327 | ||
|
|
bf420e7df6 | ||
|
|
78484f545c | ||
|
|
ad008d2151 | ||
|
|
651a10d6db | ||
|
|
f9674793af | ||
|
|
f3a33d41f1 | ||
|
|
642eec3dfd | ||
|
|
73513612d4 | ||
|
|
9b77246246 | ||
|
|
cf3bc1e0a6 | ||
|
|
4550ad049e | ||
|
|
d51c6ca39f | ||
|
|
47c6a2430c | ||
|
|
909013320b | ||
|
|
770a4d87db | ||
|
|
2e417c4d8c | ||
|
|
2da527aaa6 | ||
|
|
0303eb0cc1 | ||
|
|
3f4a792c8a | ||
|
|
9629f7464b | ||
|
|
9017d10303 | ||
|
|
59d4041aa4 | ||
|
|
feb8c4f3c6 | ||
|
|
3f5cd2c4a8 | ||
|
|
a160eb76df | ||
|
|
e4b2028f91 | ||
|
|
ffc48e115b | ||
|
|
04b4dacee3 | ||
|
|
c0e30f48c6 | ||
|
|
99158e736b | ||
|
|
4c02d515a1 | ||
|
|
b803a9732d | ||
|
|
f9d5e18790 | ||
|
|
147111c9c6 | ||
|
|
9a70eb538b | ||
|
|
0b1731142e | ||
|
|
7ec48dfd15 | ||
|
|
57120e69ed | ||
|
|
11efda3f5c | ||
|
|
a5cb4e41f5 | ||
|
|
88b2382b97 | ||
|
|
237c0253c2 | ||
|
|
a9f27371cf | ||
|
|
9c126c5b64 | ||
|
|
e241aa21b9 | ||
|
|
1fd005838c | ||
|
|
61ba844234 | ||
|
|
ffec52a17c | ||
|
|
342595e0f7 | ||
|
|
b41051b4ee | ||
|
|
6f8746ab94 | ||
|
|
9d4ed5b04a | ||
|
|
e149b7c1e2 | ||
|
|
55957b2ac7 | ||
|
|
14291bff71 | ||
|
|
a0472c0312 | ||
|
|
0400024d02 | ||
|
|
d4dc142cc2 | ||
|
|
bfdb236581 | ||
|
|
4e92f54415 | ||
|
|
f8dc740c61 | ||
|
|
dea5111a5a | ||
|
|
4f5abe387d | ||
|
|
7d55aeee0a | ||
|
|
b34f434332 | ||
|
|
2a9269c347 | ||
|
|
0efdc3a8a0 | ||
|
|
fc9dfd054a | ||
|
|
013bf079cc | ||
|
|
5ffcde9dba | ||
|
|
e0f885ffc8 | ||
|
|
5b21334fdd | ||
|
|
755a79cd8e | ||
|
|
16b7370d8c | ||
|
|
634fd62b25 | ||
|
|
e845eedbc3 | ||
|
|
4ae7e1b19c | ||
|
|
0ca758e135 | ||
|
|
ea8508ee44 | ||
|
|
78d4f32d79 | ||
|
|
afcd547a16 | ||
|
|
521fbb93cd | ||
|
|
9d73096db0 | ||
|
|
c11bb440e6 | ||
|
|
80e04be84f | ||
|
|
639320b3e1 | ||
|
|
55ea20de84 | ||
|
|
21cf3a7c1b | ||
|
|
4f855072f2 | ||
|
|
11e6a181ad | ||
|
|
4c9208fbf1 | ||
|
|
3a9bbe2371 | ||
|
|
6382bda7d6 | ||
|
|
885a142ae3 | ||
|
|
4387602f9d | ||
|
|
71fc8b2115 | ||
|
|
137716e0dc | ||
|
|
95526fb9ed | ||
|
|
a5cc19068b | ||
|
|
48c7913431 | ||
|
|
89d9856ed2 | ||
|
|
c12be73bf7 | ||
|
|
de6fb3126c | ||
|
|
fad5cbe6c7 | ||
|
|
e271852bc3 | ||
|
|
4e02704f17 | ||
|
|
b17fdc7f4e | ||
|
|
df4d9236a6 | ||
|
|
0ee626ba9f | ||
|
|
dce4585d40 | ||
|
|
5ffc1617fe | ||
|
|
93dd01947d | ||
|
|
5e7d908dc9 | ||
|
|
eb6596cb97 | ||
|
|
8fcd31f353 | ||
|
|
dfdd1c9b20 | ||
|
|
d7b8dce6a7 | ||
|
|
f9f78122d0 | ||
|
|
1980113ee4 | ||
|
|
85c0311d38 | ||
|
|
740aa8e541 | ||
|
|
1b331548ba | ||
|
|
3bb546c94d | ||
|
|
8e15bcb68e | ||
|
|
ca08dc87dd | ||
|
|
4287552991 | ||
|
|
f1af54ada1 | ||
|
|
577cacb7db | ||
|
|
8346550d26 | ||
|
|
a8e3caca3f | ||
|
|
fd83578442 | ||
|
|
e91d121ef8 | ||
|
|
1b4975ba54 | ||
|
|
380d2c466e | ||
|
|
53d2edc0f2 | ||
|
|
02705e460f | ||
|
|
44cd384e3c | ||
|
|
8ac5204009 | ||
|
|
ddd5464081 | ||
|
|
fbad378966 | ||
|
|
c211b22a45 | ||
|
|
5d57d0385c | ||
|
|
f7e29a1acf | ||
|
|
a8f83cdcb5 | ||
|
|
9e0a2bc7d0 | ||
|
|
2d9c76baae | ||
|
|
85c01e8694 | ||
|
|
65a6b3d585 | ||
|
|
40f121c3e8 | ||
|
|
6251231e41 | ||
|
|
578072bb8e | ||
|
|
231390cb7b | ||
|
|
5955d20539 | ||
|
|
4309c078fb | ||
|
|
d14462f7a7 | ||
|
|
a02223a310 | ||
|
|
d93c8c7604 | ||
|
|
7eb509db14 | ||
|
|
f1b8707286 | ||
|
|
9b05217471 | ||
|
|
d88912abf0 | ||
|
|
28c6320cd6 | ||
|
|
13a77005f1 | ||
|
|
530b75a92a | ||
|
|
7b4f852f33 | ||
|
|
439aebb4e9 | ||
|
|
6f5f73a74a | ||
|
|
bd1f5f884e | ||
|
|
499ad4f84b | ||
|
|
01fd0d8209 | ||
|
|
df55ad89ab | ||
|
|
a5a3060208 | ||
|
|
a468044c9f | ||
|
|
f0274fd29f | ||
|
|
fadeed1fa4 | ||
|
|
e9d3e240c2 | ||
|
|
13611176b0 | ||
|
|
92fa66d76f | ||
|
|
fba0aad2f8 | ||
|
|
1a1874d8b3 | ||
|
|
56540f8312 | ||
|
|
89d51ad596 | ||
|
|
15b8c14542 | ||
|
|
85cfa226c3 | ||
|
|
cbb591eb7d | ||
|
|
e36c349222 | ||
|
|
b274371dbb | ||
|
|
72eb004057 | ||
|
|
e46080aa8c | ||
|
|
7d82f1769c | ||
|
|
7435d94f85 | ||
|
|
e060f968f5 | ||
|
|
86f7cc17ae | ||
|
|
58e66dd3d1 | ||
|
|
190fa4c87a | ||
|
|
91d743ef9a | ||
|
|
804ad5897f | ||
|
|
f20d6e8555 | ||
|
|
e694d4d880 | ||
|
|
ada40decd1 | ||
|
|
6866a060bc | ||
|
|
a4ec619c74 | ||
|
|
67a95c3cc8 | ||
|
|
8d3eac2347 | ||
|
|
9ad828dcd0 | ||
|
|
59fb3ae606 | ||
|
|
0ab3b88250 | ||
|
|
a1175bddcd | ||
|
|
936a6be5d6 | ||
|
|
03c6c3f4cb | ||
|
|
6288a032fd | ||
|
|
31e6ed6806 | ||
|
|
da56319af4 | ||
|
|
2198f9400f | ||
|
|
ffc4d53923 | ||
|
|
18d3c054a3 | ||
|
|
59c5da9b6c | ||
|
|
15880195a2 | ||
|
|
117de64f39 | ||
|
|
388156704a | ||
|
|
faf443132f | ||
|
|
36a9be040b | ||
|
|
1835d7526f | ||
|
|
946e4f0a61 | ||
|
|
ae60f41adf | ||
|
|
6b93d23642 | ||
|
|
cfa13df346 | ||
|
|
744a7159e4 | ||
|
|
80d1c62818 | ||
|
|
83aa42f510 | ||
|
|
183a1a181c | ||
|
|
bc7e7c2c4d | ||
|
|
7b5bd89570 | ||
|
|
ba1c6122b9 | ||
|
|
baed581a7c | ||
|
|
4a23052778 | ||
|
|
ee4190aa41 | ||
|
|
de8460cb99 | ||
|
|
f7b2beaaf1 | ||
|
|
9b0933187e | ||
|
|
862141e8b2 | ||
|
|
070ced0b3f | ||
|
|
cc3b699823 | ||
|
|
301f1a191b | ||
|
|
d149c25aab | ||
|
|
18d24b8f5f | ||
|
|
cf34981e8f | ||
|
|
e2ebe560ea | ||
|
|
6db822fd92 | ||
|
|
661122bab8 | ||
|
|
4a96836d11 | ||
|
|
e072f9605c | ||
|
|
9986031481 | ||
|
|
3d95848607 | ||
|
|
221c9028af | ||
|
|
b2057791aa | ||
|
|
c1ee6d6c41 | ||
|
|
a3fbbece9a | ||
|
|
e72c974c4c | ||
|
|
a762da7cab | ||
|
|
fa6c060324 | ||
|
|
8e33ac052b | ||
|
|
0759696ec0 | ||
|
|
59dce63471 | ||
|
|
1ae28090e3 | ||
|
|
0decdf6a55 | ||
|
|
09b402a274 | ||
|
|
150baf3e96 | ||
|
|
78c51371af | ||
|
|
6dbcacf3ea | ||
|
|
4ecebc2c83 | ||
|
|
38a79fa449 | ||
|
|
bafad6b8a8 | ||
|
|
5682dddd45 | ||
|
|
a9aacdb94a | ||
|
|
e7e32c946b | ||
|
|
fc9bc26d86 | ||
|
|
ee00b4e0ce | ||
|
|
f82156f0b1 | ||
|
|
2ed6298584 | ||
|
|
52ef8dea3e | ||
|
|
ebe6015db0 | ||
|
|
56526114e4 | ||
|
|
bb1c225027 | ||
|
|
e5af0dde08 | ||
|
|
3cf17bc24f | ||
|
|
4aa1b8de0e | ||
|
|
73e9534d08 | ||
|
|
cb188f907f | ||
|
|
63d9656ad8 | ||
|
|
3512d02e9e | ||
|
|
1efdceaf10 | ||
|
|
632a0fe009 | ||
|
|
6fb32cebec | ||
|
|
8b8b17d755 | ||
|
|
2c27afaaf5 | ||
|
|
4bdc7c1426 | ||
|
|
3c1e6c2c8f | ||
|
|
b8f5809f95 | ||
|
|
552ee81455 | ||
|
|
9fdbe193cd | ||
|
|
df64612d54 | ||
|
|
0aa3e6c270 | ||
|
|
44c17c1435 | ||
|
|
132e772c26 | ||
|
|
62cbed57cc | ||
|
|
ebab7e176e | ||
|
|
9c93853e22 | ||
|
|
8a9c7a4ef3 | ||
|
|
2dad56c9a2 | ||
|
|
41d78c1ecc | ||
|
|
16c4b02b69 | ||
|
|
35c04d9283 | ||
|
|
1fbd7a7f9a | ||
|
|
d7563d1694 | ||
|
|
b9fa7d9163 | ||
|
|
f736751ab2 | ||
|
|
dbcc779f0b | ||
|
|
c33a90320c | ||
|
|
802b862aae | ||
|
|
b0cd171c1b | ||
|
|
13755f4680 | ||
|
|
b242659cc3 | ||
|
|
5f6b2fdc6f | ||
|
|
e34f18991e | ||
|
|
209b0a06f7 | ||
|
|
a2e460bc4b | ||
|
|
fc9081afe4 | ||
|
|
1a3f7c3d84 | ||
|
|
06aa1f49b8 | ||
|
|
dfd67cd922 | ||
|
|
70f7287ca1 | ||
|
|
f1955b4d05 | ||
|
|
c5e5627cbd | ||
|
|
93378526b9 | ||
|
|
abf176a335 | ||
|
|
84a0868e66 | ||
|
|
75a9c42789 | ||
|
|
204fa54625 | ||
|
|
365584048f | ||
|
|
edffcc32cf | ||
|
|
238f441bcb | ||
|
|
0571a8302c | ||
|
|
8c07382382 | ||
|
|
fa32fbd187 | ||
|
|
0fd2ecd0ba | ||
|
|
7439a40b00 | ||
|
|
2ad99713f3 | ||
|
|
19ec970701 | ||
|
|
b48caec218 | ||
|
|
380c34af53 | ||
|
|
553d9013eb | ||
|
|
8bff3cdae8 | ||
|
|
0b40c3d37d | ||
|
|
1e3bdcc71c | ||
|
|
de577e17da | ||
|
|
8a9e258ad7 | ||
|
|
9a34965432 | ||
|
|
c944d19c3b | ||
|
|
fb1b6c5e6b | ||
|
|
ad0c4c5d89 | ||
|
|
a54b663a39 | ||
|
|
ae4993f39a | ||
|
|
aa638cec48 | ||
|
|
4db4a90559 | ||
|
|
e23a81097c | ||
|
|
76f4803d8d | ||
|
|
22e4649318 | ||
|
|
0ac70ff261 | ||
|
|
1bc1e56da3 | ||
|
|
0d0c20e673 | ||
|
|
a964824b22 | ||
|
|
2cf0d578fe | ||
|
|
13e8fb382f | ||
|
|
4090bc9dea | ||
|
|
cec1caf99e | ||
|
|
c74da97d52 | ||
|
|
1f2497ce69 | ||
|
|
986f14cb15 | ||
|
|
34f639d510 | ||
|
|
defe51c825 | ||
|
|
5a16acef8c | ||
|
|
2ce249dbc0 | ||
|
|
7ba6b18945 | ||
|
|
b8c0b393bf | ||
|
|
5442adb517 | ||
|
|
6b2ac20abc | ||
|
|
3efc95b157 | ||
|
|
cd9db8a81d | ||
|
|
036f5d4eef | ||
|
|
c4401290db | ||
|
|
4a6deb6420 | ||
|
|
87a03e1e30 | ||
|
|
01dc9d7ec6 | ||
|
|
e78e0f9841 | ||
|
|
8326640670 | ||
|
|
d079af4be2 | ||
|
|
82c9584382 | ||
|
|
d3b6de855b | ||
|
|
5ad000fd99 | ||
|
|
fe196da430 | ||
|
|
20662e2101 | ||
|
|
0a357be160 | ||
|
|
d29205e677 | ||
|
|
9d0630f094 | ||
|
|
b6844565e8 | ||
|
|
17d1b24def | ||
|
|
3d279edf44 | ||
|
|
0a47a3cea0 | ||
|
|
306d57fcde | ||
|
|
ff6f1abf61 | ||
|
|
331278a5be | ||
|
|
78547f3c59 | ||
|
|
d32671224f | ||
|
|
9ade416ad4 | ||
|
|
f8bd4ff705 | ||
|
|
2206e10d92 | ||
|
|
e282d5dc42 | ||
|
|
2b4a5aede1 | ||
|
|
654a2cd6a4 | ||
|
|
2252b5ca1b | ||
|
|
c78dad8db5 | ||
|
|
0569e8652c | ||
|
|
f2d5b32e52 | ||
|
|
b2f5ea7c30 | ||
|
|
8eb8f1a16c | ||
|
|
ad93d50ab7 | ||
|
|
73c5c328a8 | ||
|
|
72a13212e2 | ||
|
|
78bcf35f37 | ||
|
|
3a3ee3ecfd | ||
|
|
34e045c275 | ||
|
|
e187918ce0 | ||
|
|
7ac17c7833 | ||
|
|
b05d5b30fb | ||
|
|
57c08b01b7 | ||
|
|
08efb9cdf6 | ||
|
|
8b51da768c | ||
|
|
862407c674 | ||
|
|
a381aa7e0d | ||
|
|
4dcda3d53d | ||
|
|
479cca29a5 | ||
|
|
e38814c597 | ||
|
|
4d42daa9a3 | ||
|
|
130345bd5d | ||
|
|
0baee00be4 | ||
|
|
e0acd5d361 | ||
|
|
1e7d78a215 | ||
|
|
7a6cb85617 | ||
|
|
20530104ce | ||
|
|
d22754dd68 | ||
|
|
db0e1ebb80 | ||
|
|
e83a47debe | ||
|
|
eed48e76de | ||
|
|
ac70c1e813 | ||
|
|
ac0bed16a2 | ||
|
|
fab8ab2840 | ||
|
|
09ff8eba00 | ||
|
|
9bd2ea5e5f | ||
|
|
aa525482ae | ||
|
|
285605737d | ||
|
|
4201fe6e01 | ||
|
|
203f3312ee | ||
|
|
0c77c46dc7 | ||
|
|
52bb43eebd | ||
|
|
e020f5355e | ||
|
|
9b86d4e595 | ||
|
|
f3d3b41a3f | ||
|
|
a959199f09 | ||
|
|
5e3a59d5a2 | ||
|
|
9f23d85e20 | ||
|
|
54e15b38ea | ||
|
|
d66b903e7f | ||
|
|
4425c66732 | ||
|
|
0dd0250285 | ||
|
|
c347056246 | ||
|
|
2a5255ac8f | ||
|
|
d9175be989 | ||
|
|
cebbfcfbaa | ||
|
|
de415be4f6 | ||
|
|
e8ce113b7f | ||
|
|
8ca02b7664 | ||
|
|
3f5bb21f16 | ||
|
|
fe114c41b5 | ||
|
|
d90fd8a5d7 | ||
|
|
9363c15b4a | ||
|
|
3325823f23 | ||
|
|
4b4d8da1ad | ||
|
|
d531dff8d3 | ||
|
|
923bf36593 | ||
|
|
d6e499dd48 | ||
|
|
aec1497c6e | ||
|
|
f5a77c8cd8 | ||
|
|
bc524eeb44 | ||
|
|
725f658260 | ||
|
|
af1080dd42 | ||
|
|
63e54541fe | ||
|
|
c3f7a88c1c | ||
|
|
586a8b7b31 | ||
|
|
981744f802 | ||
|
|
a3bb4a3c85 | ||
|
|
06ba1f76dc | ||
|
|
076c8b2ca1 | ||
|
|
9a90939ac4 | ||
|
|
123a136093 | ||
|
|
4126fedbd4 | ||
|
|
145d185e6f | ||
|
|
6b32667c7d | ||
|
|
d8401e1937 | ||
|
|
55d6fcc350 | ||
|
|
9ff39503e9 | ||
|
|
f96c181afd | ||
|
|
f9b75a09df | ||
|
|
6111ed79b1 | ||
|
|
4cf2322b7f | ||
|
|
2c6fcc5dc1 | ||
|
|
81ee8541ab | ||
|
|
5b3550ab9f | ||
|
|
4fabce58d8 | ||
|
|
da7edb5f5c | ||
|
|
bf0f85e37f | ||
|
|
7b52160bff | ||
|
|
fde97ec4a7 | ||
|
|
ea7ec60f51 | ||
|
|
6667856ba5 | ||
|
|
13b2cf50ae | ||
|
|
93b0abfce9 | ||
|
|
f7e4c47113 | ||
|
|
509e43d6f8 | ||
|
|
e693192e06 | ||
|
|
ec27759f90 | ||
|
|
9c938eec73 | ||
|
|
238b907dd8 | ||
|
|
c16d8c6db8 | ||
|
|
9856e3b798 | ||
|
|
1d089272c8 | ||
|
|
c30b1130ee | ||
|
|
40ca222d09 | ||
|
|
0ecccbfd17 | ||
|
|
3f4862ced6 | ||
|
|
1574e2457b | ||
|
|
af33212f77 | ||
|
|
4eb82e8c04 | ||
|
|
a45f0aac90 | ||
|
|
eb4afdca65 | ||
|
|
9391749577 | ||
|
|
36a25660e9 | ||
|
|
d9fe722da1 | ||
|
|
da722e7db9 | ||
|
|
75a4dcbce8 | ||
|
|
3a179fcd34 | ||
|
|
ad22fe9fe7 | ||
|
|
6d622d91be | ||
|
|
aa884b003e | ||
|
|
e0f77940f9 | ||
|
|
3a1718f62b | ||
|
|
f7f9d3e5b9 | ||
|
|
68501d7799 | ||
|
|
f8a987b135 | ||
|
|
dfea6780d9 | ||
|
|
2ac8dd6361 | ||
|
|
dd0945b9ca | ||
|
|
1b05d5dd8e | ||
|
|
6923cc4a6a | ||
|
|
3feb69e63c | ||
|
|
6723792fbb | ||
|
|
3e36069f41 | ||
|
|
6a4ca92a6c | ||
|
|
3ec34ee3dd | ||
|
|
2e5c2d5e98 | ||
|
|
4dbe17d4f1 | ||
|
|
f18776cb49 | ||
|
|
429aa24275 | ||
|
|
741c9d3c63 | ||
|
|
419983c0f1 | ||
|
|
a59c80e076 | ||
|
|
55981205da | ||
|
|
c1e6037bda | ||
|
|
8e816a9283 | ||
|
|
d165c6b15a | ||
|
|
9111005165 | ||
|
|
3ceac25fb5 | ||
|
|
8ec771d5be | ||
|
|
659e5653bc | ||
|
|
3ff6de261c | ||
|
|
25dae77fcd | ||
|
|
68daadcb56 | ||
|
|
c9b1bb0285 | ||
|
|
ac5809e757 | ||
|
|
0db209a636 | ||
|
|
6c65f4acd1 | ||
|
|
3d447e8b12 | ||
|
|
f8807144d4 | ||
|
|
ebb4c8a724 | ||
|
|
893f232b2f | ||
|
|
a6aaf5429c | ||
|
|
4ef239a086 | ||
|
|
9362368fd3 | ||
|
|
b35e010e2a | ||
|
|
cc35e6a019 | ||
|
|
3281888160 | ||
|
|
c6d0ae892e | ||
|
|
df67ae9cbe | ||
|
|
ebe20efb29 | ||
|
|
b03b9b9017 | ||
|
|
73258c6193 | ||
|
|
dfe3fb8ed3 | ||
|
|
cd6bfb3f69 | ||
|
|
ba417d80b1 | ||
|
|
40eb8b93e1 | ||
|
|
6e6bd1e171 | ||
|
|
81ee2d2332 | ||
|
|
85974e9acd | ||
|
|
864c098701 | ||
|
|
cfbaf81ef8 | ||
|
|
87a791fdb9 | ||
|
|
ada7cca10d | ||
|
|
52db0f23a6 | ||
|
|
60388f7f03 | ||
|
|
53ed1c912b | ||
|
|
5f7ab83de4 | ||
|
|
05d2f70529 | ||
|
|
f950de95ba | ||
|
|
a4e5a72c36 | ||
|
|
03324d4277 | ||
|
|
e53580cb68 | ||
|
|
332ebe36c3 | ||
|
|
5013d64b28 | ||
|
|
767a81f930 | ||
|
|
78046dac8b | ||
|
|
95168b8267 | ||
|
|
c264e9c364 | ||
|
|
856e1e2948 | ||
|
|
bef4fdfc4b | ||
|
|
095a1ab041 | ||
|
|
71e578eac9 | ||
|
|
4380727727 | ||
|
|
392d46933b | ||
|
|
2bc0b46ff4 | ||
|
|
09f522f0aa | ||
|
|
d82bd430f6 | ||
|
|
49800a00bd | ||
|
|
f9dcd97936 | ||
|
|
d763c11a6d | ||
|
|
b1202ac6db | ||
|
|
d469d7d441 | ||
|
|
48dc520fb8 | ||
|
|
668d5a76d5 | ||
|
|
b9c1f10016 | ||
|
|
8a0c86cbdb | ||
|
|
7f86fe3f61 | ||
|
|
a32cf70d7e | ||
|
|
a607f33552 | ||
|
|
350a32274a | ||
|
|
27c99b46cb | ||
|
|
1d6e3d477b | ||
|
|
efbb973393 | ||
|
|
45bc7a6a9d | ||
|
|
088ebb967f | ||
|
|
bcf740f98a | ||
|
|
6b80fff2bb | ||
|
|
2e63fedb76 | ||
|
|
5a9f4e5c60 | ||
|
|
d0a48a09e2 | ||
|
|
c0a21e7025 | ||
|
|
10cc15aabe | ||
|
|
adf7681100 | ||
|
|
0237905b96 | ||
|
|
e8aa79bab6 | ||
|
|
4ff5783e59 | ||
|
|
dcfeb52983 | ||
|
|
46790e57e9 | ||
|
|
4bc3fa0826 | ||
|
|
88cfb979be | ||
|
|
32b5db754e | ||
|
|
f33f8ca109 | ||
|
|
598d63db63 | ||
|
|
38bff1b372 | ||
|
|
e8c9b21f20 | ||
|
|
6d3fc63658 | ||
|
|
ee4437ff32 | ||
|
|
7a4aa68706 | ||
|
|
f00380d285 | ||
|
|
c00d4885c6 | ||
|
|
70f4722356 | ||
|
|
8898bf7ca4 | ||
|
|
e5b13b767e | ||
|
|
3181c68cbb | ||
|
|
c3c9003dbb | ||
|
|
921b98066d | ||
|
|
c5b4cc80cc | ||
|
|
0bccd1d578 | ||
|
|
91db82c138 | ||
|
|
0eb97086fc | ||
|
|
5b34636afa | ||
|
|
f1138b9a55 | ||
|
|
23ff6dbba4 | ||
|
|
b457923970 | ||
|
|
66e4a5a64e | ||
|
|
6c25e64658 | ||
|
|
f2fd0f8f00 | ||
|
|
44cdde5422 | ||
|
|
88235dc618 | ||
|
|
4d2b671d7b | ||
|
|
8098031eac | ||
|
|
74c882d9d0 | ||
|
|
80636fec43 | ||
|
|
a8ad74aef3 | ||
|
|
e2e2b7934e | ||
|
|
28c802f399 | ||
|
|
bcfa63aa4e | ||
|
|
d40feafb01 | ||
|
|
2a8473891b | ||
|
|
a4e3451d5c | ||
|
|
53a7c2885b | ||
|
|
f354507d42 | ||
|
|
f17e1def32 | ||
|
|
3183e8b7d4 | ||
|
|
9d2b9ef2d4 | ||
|
|
733e5cd876 | ||
|
|
4ee4f7bcb3 | ||
|
|
da7ecda9ea | ||
|
|
1f11d4fb1a | ||
|
|
b308503ab5 | ||
|
|
3b10de4a28 | ||
|
|
6ce1de476a | ||
|
|
d9b0848a61 | ||
|
|
46dd3b8166 | ||
|
|
eca07be072 | ||
|
|
165d57b88e | ||
|
|
28c44f7e5a | ||
|
|
39d5bdff4b | ||
|
|
b9f8480b2f | ||
|
|
58b30d678a | ||
|
|
408cdaf5e0 | ||
|
|
cae23cde09 | ||
|
|
702fb2562c | ||
|
|
785d0b60b6 | ||
|
|
67ab9dc4d0 | ||
|
|
77494cb7df | ||
|
|
08f8856d6c | ||
|
|
79a4c1544d | ||
|
|
c0a35141e6 | ||
|
|
221bb64aeb | ||
|
|
2555dba188 | ||
|
|
6355ed6ae7 | ||
|
|
1864e8c863 | ||
|
|
61d0d66dae | ||
|
|
86522f1b3e | ||
|
|
dc32705bc9 | ||
|
|
1eaf5c31d3 | ||
|
|
677b19e22e | ||
|
|
8e248ae045 |
57
.github/guidelines-check.yml
vendored
57
.github/guidelines-check.yml
vendored
@@ -1,57 +0,0 @@
|
||||
#
|
||||
# This file is intentionally in the wrong dir, will move and add later....
|
||||
#
|
||||
|
||||
name: Guidelines Check
|
||||
|
||||
on:
|
||||
# Disabled - uncomment to re-enable
|
||||
# pull_request_target:
|
||||
# types: [opened, synchronize]
|
||||
|
||||
jobs:
|
||||
check-guidelines:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Install opencode
|
||||
run: curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
- name: Check PR guidelines compliance
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
OPENCODE_PERMISSION: '{ "bash": { "gh*": "allow", "gh pr review*": "deny", "*": "deny" } }'
|
||||
run: |
|
||||
opencode run -m anthropic/claude-sonnet-4-20250514 "A new pull request has been created: '${{ github.event.pull_request.title }}'
|
||||
|
||||
<pr-number>
|
||||
${{ github.event.pull_request.number }}
|
||||
</pr-number>
|
||||
|
||||
<pr-description>
|
||||
${{ github.event.pull_request.body }}
|
||||
</pr-description>
|
||||
|
||||
Please check all the code changes in this pull request against the guidelines in AGENTS.md file in this repository. 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
|
||||
|
||||
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.
|
||||
|
||||
Command MUST be like this.
|
||||
```
|
||||
gh api \
|
||||
--method POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/comments \
|
||||
-f 'body=[summary of issue]' -f 'commit_id=${{ github.event.pull_request.head.sha }}' -f 'path=[path-to-file]' -F "line=[line]" -f 'side=RIGHT'
|
||||
```
|
||||
|
||||
Only create comments for actual violations. If the code follows all guidelines, don't run any gh commands."
|
||||
63
.github/workflows/auto-label-tui.yml
vendored
63
.github/workflows/auto-label-tui.yml
vendored
@@ -1,63 +0,0 @@
|
||||
name: Auto-label TUI Issues
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
auto-label:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- name: Auto-label and assign issues
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const issue = context.payload.issue;
|
||||
const title = issue.title;
|
||||
const description = issue.body || '';
|
||||
|
||||
// Check for "opencode web" keyword
|
||||
const webPattern = /(opencode web)/i;
|
||||
const isWebRelated = webPattern.test(title) || webPattern.test(description);
|
||||
|
||||
// Check for version patterns like v1.0.x or 1.0.x
|
||||
const versionPattern = /[v]?1\.0\./i;
|
||||
const isVersionRelated = versionPattern.test(title) || versionPattern.test(description);
|
||||
|
||||
// Check for "nix" keyword
|
||||
const nixPattern = /\bnix\b/i;
|
||||
const isNixRelated = nixPattern.test(title) || nixPattern.test(description);
|
||||
|
||||
const labels = [];
|
||||
|
||||
if (isWebRelated) {
|
||||
labels.push('web');
|
||||
|
||||
// Assign to adamdotdevin
|
||||
await github.rest.issues.addAssignees({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
assignees: ['adamdotdevin']
|
||||
});
|
||||
} else if (isVersionRelated) {
|
||||
// Only add opentui if NOT web-related
|
||||
labels.push('opentui');
|
||||
}
|
||||
|
||||
if (isNixRelated) {
|
||||
labels.push('nix');
|
||||
}
|
||||
|
||||
if (labels.length > 0) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
labels: labels
|
||||
});
|
||||
}
|
||||
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.
|
||||
5
.github/workflows/duplicate-issues.yml
vendored
5
.github/workflows/duplicate-issues.yml
vendored
@@ -16,6 +16,8 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Install opencode
|
||||
run: curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
@@ -55,4 +57,7 @@ jobs:
|
||||
|
||||
Feel free to ignore if none of these address your specific case.'
|
||||
|
||||
Additionally, if the issue mentions keybinds, keyboard shortcuts, or key bindings, please add a comment mentioning the pinned keybinds issue #4997:
|
||||
'For keybind-related issues, please also check our pinned keybinds documentation: #4997'
|
||||
|
||||
If no clear duplicates are found, do not comment."
|
||||
|
||||
29
.github/workflows/format.yml
vendored
29
.github/workflows/format.yml
vendored
@@ -1,29 +0,0 @@
|
||||
name: format
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- production
|
||||
pull_request:
|
||||
branches-ignore:
|
||||
- production
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
format:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: run
|
||||
run: |
|
||||
./script/format.ts
|
||||
env:
|
||||
CI: true
|
||||
51
.github/workflows/generate.yml
vendored
Normal file
51
.github/workflows/generate.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
name: generate
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
generate:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
|
||||
ref: ${{ github.event.pull_request.head.ref || github.ref_name }}
|
||||
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Generate
|
||||
run: ./script/generate.ts
|
||||
|
||||
- name: Commit and push
|
||||
run: |
|
||||
if [ -z "$(git status --porcelain)" ]; then
|
||||
echo "No changes to commit"
|
||||
exit 0
|
||||
fi
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
git add -A
|
||||
git commit -m "chore: generate"
|
||||
git push origin HEAD:${{ github.ref_name }} --no-verify
|
||||
# if ! git push origin HEAD:${{ github.event.pull_request.head.ref || github.ref_name }} --no-verify; then
|
||||
# echo ""
|
||||
# echo "============================================"
|
||||
# echo "Failed to push generated code."
|
||||
# echo "Please run locally and push:"
|
||||
# echo ""
|
||||
# echo " ./script/generate.ts"
|
||||
# echo " git add -A && git commit -m \"chore: generate\" && git push"
|
||||
# echo ""
|
||||
# echo "============================================"
|
||||
# exit 1
|
||||
# fi
|
||||
2
.github/workflows/notify-discord.yml
vendored
2
.github/workflows/notify-discord.yml
vendored
@@ -2,7 +2,7 @@ name: discord
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published] # fires only when a release is published
|
||||
types: [released] # fires when a draft release is published
|
||||
|
||||
jobs:
|
||||
notify:
|
||||
|
||||
5
.github/workflows/opencode.yml
vendored
5
.github/workflows/opencode.yml
vendored
@@ -26,8 +26,9 @@ 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"}'
|
||||
with:
|
||||
model: opencode/claude-haiku-4-5
|
||||
model: opencode/claude-opus-4-5
|
||||
|
||||
210
.github/workflows/publish.yml
vendored
210
.github/workflows/publish.yml
vendored
@@ -2,11 +2,15 @@ name: publish
|
||||
run-name: "${{ format('release {0}', inputs.bump) }}"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- snapshot-*
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
bump:
|
||||
description: "Bump major, minor, or patch"
|
||||
required: true
|
||||
required: false
|
||||
type: choice
|
||||
options:
|
||||
- major
|
||||
@@ -17,15 +21,17 @@ on:
|
||||
required: false
|
||||
type: string
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.version || inputs.bump }}
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
if: github.repository == 'sst/opencode'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
@@ -33,33 +39,11 @@ jobs:
|
||||
|
||||
- run: git fetch --force --tags
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ">=1.24.0"
|
||||
cache: true
|
||||
cache-dependency-path: go.sum
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Install makepkg
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y pacman-package-manager
|
||||
- name: Setup SSH for AUR
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.AUR_KEY }}" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
git config --global user.email "opencode@sst.dev"
|
||||
git config --global user.name "opencode"
|
||||
ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts || true
|
||||
|
||||
- name: Install OpenCode
|
||||
run: curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
- name: Setup npm auth
|
||||
run: |
|
||||
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
|
||||
if: inputs.bump || inputs.version
|
||||
run: bun i -g opencode-ai@1.0.169
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
@@ -68,14 +52,182 @@ jobs:
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Publish
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Setup Git Identity
|
||||
run: |
|
||||
./script/publish.ts
|
||||
git config --global user.email "opencode@sst.dev"
|
||||
git config --global user.name "opencode"
|
||||
git remote set-url origin https://x-access-token:${{ secrets.SST_GITHUB_TOKEN }}@github.com/${{ github.repository }}
|
||||
|
||||
- name: Publish
|
||||
id: publish
|
||||
run: ./script/publish-start.ts
|
||||
env:
|
||||
OPENCODE_BUMP: ${{ inputs.bump }}
|
||||
OPENCODE_VERSION: ${{ inputs.version }}
|
||||
OPENCODE_CHANNEL: latest
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
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 }}
|
||||
version: ${{ steps.publish.outputs.version }}
|
||||
|
||||
publish-tauri:
|
||||
needs: publish
|
||||
continue-on-error: true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
settings:
|
||||
- host: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
- host: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
- host: blacksmith-4vcpu-windows-2025
|
||||
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
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ needs.publish.outputs.tag }}
|
||||
|
||||
- uses: apple-actions/import-codesign-certs@v2
|
||||
if: ${{ runner.os == 'macOS' }}
|
||||
with:
|
||||
keychain: build
|
||||
p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
|
||||
- name: Verify Certificate
|
||||
if: ${{ runner.os == 'macOS' }}
|
||||
run: |
|
||||
CERT_INFO=$(security find-identity -v -p codesigning build.keychain | grep "Developer ID Application")
|
||||
CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}')
|
||||
echo "CERT_ID=$CERT_ID" >> $GITHUB_ENV
|
||||
echo "Certificate imported."
|
||||
|
||||
- name: Setup Apple API Key
|
||||
if: ${{ runner.os == 'macOS' }}
|
||||
run: |
|
||||
echo "${{ secrets.APPLE_API_KEY_PATH }}" > $RUNNER_TEMP/apple-api-key.p8
|
||||
|
||||
- run: git fetch --force --tags
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: install dependencies (ubuntu only)
|
||||
if: contains(matrix.settings.host, 'ubuntu')
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.settings.target }}
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: packages/desktop/src-tauri
|
||||
shared-key: ${{ matrix.settings.target }}
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
cd packages/desktop
|
||||
bun ./scripts/prepare.ts
|
||||
env:
|
||||
OPENCODE_VERSION: ${{ needs.publish.outputs.version }}
|
||||
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
|
||||
AUR_KEY: ${{ secrets.AUR_KEY }}
|
||||
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
|
||||
if: contains(matrix.settings.host, 'ubuntu')
|
||||
run: |
|
||||
cargo install tauri-cli --git https://github.com/tauri-apps/tauri --branch feat/truly-portable-appimage --force
|
||||
echo "Installed tauri-cli version:"
|
||||
cargo tauri --version
|
||||
|
||||
- name: Build and upload artifacts
|
||||
timeout-minutes: 20
|
||||
uses: tauri-apps/tauri-action@390cbe447412ced1303d35abe75287949e43437a
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_BUNDLER_NEW_APPIMAGE_FORMAT: true
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ env.CERT_ID }}
|
||||
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/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
|
||||
|
||||
publish-release:
|
||||
needs:
|
||||
- publish
|
||||
- publish-tauri
|
||||
if: needs.publish.outputs.tag
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ needs.publish.outputs.tag }}
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Setup SSH for AUR
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y pacman-package-manager
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.AUR_KEY }}" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
git config --global user.email "opencode@sst.dev"
|
||||
git config --global user.name "opencode"
|
||||
ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts || true
|
||||
|
||||
- run: ./script/publish-complete.ts
|
||||
env:
|
||||
OPENCODE_VERSION: ${{ needs.publish.outputs.version }}
|
||||
AUR_KEY: ${{ secrets.AUR_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
|
||||
|
||||
29
.github/workflows/release-github-action.yml
vendored
Normal file
29
.github/workflows/release-github-action.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: release-github-action
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
paths:
|
||||
- "github/**"
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- run: git fetch --force --tags
|
||||
|
||||
- name: Release
|
||||
run: |
|
||||
git config --global user.email "opencode@sst.dev"
|
||||
git config --global user.name "opencode"
|
||||
./github/script/release
|
||||
83
.github/workflows/review.yml
vendored
Normal file
83
.github/workflows/review.yml
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
name: Guidelines Check
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
check-guidelines:
|
||||
if: |
|
||||
github.event.issue.pull_request &&
|
||||
startsWith(github.event.comment.body, '/review') &&
|
||||
contains(fromJson('["OWNER","MEMBER"]'), github.event.comment.author_association)
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Get PR number
|
||||
id: pr-number
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "pull_request_target" ]; then
|
||||
echo "number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "number=${{ github.event.issue.number }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Install opencode
|
||||
run: curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
- name: Get PR details
|
||||
id: pr-details
|
||||
run: |
|
||||
gh api /repos/${{ github.repository }}/pulls/${{ steps.pr-number.outputs.number }} > pr_data.json
|
||||
echo "title=$(jq -r .title pr_data.json)" >> $GITHUB_OUTPUT
|
||||
echo "sha=$(jq -r .head.sha pr_data.json)" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Check PR guidelines compliance
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
OPENCODE_PERMISSION: '{ "bash": { "gh*": "allow", "gh pr review*": "deny", "*": "deny" } }'
|
||||
PR_TITLE: ${{ steps.pr-details.outputs.title }}
|
||||
run: |
|
||||
PR_BODY=$(jq -r .body pr_data.json)
|
||||
opencode run -m anthropic/claude-opus-4-5 "A new pull request has been created: '${PR_TITLE}'
|
||||
|
||||
<pr-number>
|
||||
${{ steps.pr-number.outputs.number }}
|
||||
</pr-number>
|
||||
|
||||
<pr-description>
|
||||
$PR_BODY
|
||||
</pr-description>
|
||||
|
||||
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 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.
|
||||
Generally, write a comment instead of writing suggested change if you can help it.
|
||||
|
||||
Command MUST be like this.
|
||||
\`\`\`
|
||||
gh api \
|
||||
--method POST \
|
||||
-H \"Accept: application/vnd.github+json\" \
|
||||
-H \"X-GitHub-Api-Version: 2022-11-28\" \
|
||||
/repos/${{ github.repository }}/pulls/${{ steps.pr-number.outputs.number }}/comments \
|
||||
-f 'body=[summary of issue]' -f 'commit_id=${{ steps.pr-details.outputs.sha }}' -f 'path=[path-to-file]' -F \"line=[line]\" -f 'side=RIGHT'
|
||||
\`\`\`
|
||||
|
||||
Only create comments for actual violations. If the code follows all guidelines, comment on the issue using gh cli: 'lgtm' AND NOTHING ELSE!!!!."
|
||||
38
.github/workflows/snapshot.yml
vendored
38
.github/workflows/snapshot.yml
vendored
@@ -1,38 +0,0 @@
|
||||
name: snapshot
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- test-bedrock
|
||||
- v0
|
||||
- otui-diffs
|
||||
- snapshot-*
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- run: git fetch --force --tags
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ">=1.24.0"
|
||||
cache: true
|
||||
cache-dependency-path: go.sum
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Publish
|
||||
run: |
|
||||
./script/publish.ts
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
|
||||
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
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 == 'sst/opencode'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
3
.github/workflows/sync-zed-extension.yml
vendored
3
.github/workflows/sync-zed-extension.yml
vendored
@@ -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 }}
|
||||
|
||||
12
.github/workflows/test.yml
vendored
12
.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:
|
||||
@@ -28,9 +26,3 @@ jobs:
|
||||
bun turbo test
|
||||
env:
|
||||
CI: true
|
||||
|
||||
- name: Check SDK is up to date
|
||||
run: |
|
||||
bun ./packages/sdk/js/script/build.ts
|
||||
git diff --exit-code packages/sdk/js/src/gen packages/sdk/js/dist
|
||||
continue-on-error: false
|
||||
|
||||
37
.github/workflows/triage.yml
vendored
Normal file
37
.github/workflows/triage.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
name: Issue Triage
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Install opencode
|
||||
run: curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
- name: Triage issue
|
||||
env:
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
ISSUE_TITLE: ${{ github.event.issue.title }}
|
||||
ISSUE_BODY: ${{ github.event.issue.body }}
|
||||
run: |
|
||||
opencode run --agent triage "The following issue was just opened, triage it:
|
||||
|
||||
Title: $ISSUE_TITLE
|
||||
|
||||
$ISSUE_BODY"
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -6,10 +6,10 @@ node_modules
|
||||
.idea
|
||||
.vscode
|
||||
*~
|
||||
openapi.json
|
||||
playground
|
||||
tmp
|
||||
dist
|
||||
ts-dist
|
||||
.turbo
|
||||
**/.serena
|
||||
.serena/
|
||||
@@ -18,3 +18,9 @@ refs
|
||||
Session.vim
|
||||
opencode.json
|
||||
a.out
|
||||
target
|
||||
.scripts
|
||||
|
||||
# Local dev files
|
||||
opencode-dev
|
||||
logs/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
78
.opencode/agent/triage.md
Normal file
78
.opencode/agent/triage.md
Normal file
@@ -0,0 +1,78 @@
|
||||
---
|
||||
mode: primary
|
||||
hidden: true
|
||||
model: opencode/claude-haiku-4-5
|
||||
color: "#44BA81"
|
||||
tools:
|
||||
"*": false
|
||||
"github-triage": true
|
||||
---
|
||||
|
||||
You are a triage agent responsible for triaging github issues.
|
||||
|
||||
Use your github-triage tool to triage issues.
|
||||
|
||||
## Labels
|
||||
|
||||
### windows
|
||||
|
||||
Use for any issue that mentions Windows (the OS). Be sure they are saying that they are on Windows.
|
||||
|
||||
- Use if they mention WSL too
|
||||
|
||||
#### perf
|
||||
|
||||
Performance-related issues:
|
||||
|
||||
- Slow performance
|
||||
- High RAM usage
|
||||
- High CPU usage
|
||||
|
||||
**Only** add if it's likely a RAM or CPU issue. **Do not** add for LLM slowness.
|
||||
|
||||
#### desktop
|
||||
|
||||
Desktop app issues:
|
||||
|
||||
- `opencode web` command
|
||||
- The desktop app itself
|
||||
|
||||
**Only** add if it's specifically about the Desktop application or `opencode web` view. **Do not** add for terminal, TUI, or general opencode issues.
|
||||
|
||||
#### nix
|
||||
|
||||
**Only** add if the issue explicitly mentions nix.
|
||||
|
||||
#### 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.
|
||||
|
||||
If the issue doesn't have "zen" in it then don't add zen label
|
||||
|
||||
#### docs
|
||||
|
||||
Add if the issue requests better documentation or docs updates.
|
||||
|
||||
#### opentui
|
||||
|
||||
TUI issues potentially caused by our underlying TUI library:
|
||||
|
||||
- Keybindings not working
|
||||
- Scroll speed issues (too fast/slow/laggy)
|
||||
- Screen flickering
|
||||
- Crashes with opentui in the log
|
||||
|
||||
**Do not** add for general TUI bugs.
|
||||
|
||||
When assigning to people here are the following rules:
|
||||
|
||||
adamdotdev:
|
||||
ONLY assign adam if the issue will have the "desktop" label.
|
||||
|
||||
fwang:
|
||||
ONLY assign fwang if the issue will have the "zen" label.
|
||||
|
||||
jayair:
|
||||
ONLY assign jayair if the issue will have the "docs" label.
|
||||
|
||||
In all other cases use best judgment. Avoid assigning to kommander needlessly, when in doubt assign to rekram1-node.
|
||||
@@ -1,5 +1,7 @@
|
||||
---
|
||||
description: Git commit and push
|
||||
description: git commit and push
|
||||
model: opencode/glm-4.6
|
||||
subtask: true
|
||||
---
|
||||
|
||||
commit and push
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
description: hello world iaosd ioasjdoiasjd oisadjoisajd osiajd oisaj dosaij dsoajsajdaijdoisa jdoias jdoias jdoia jois jo jdois jdoias jdoias j djoasdj
|
||||
---
|
||||
|
||||
hey there $ARGUMENTS
|
||||
|
||||
!`ls`
|
||||
check out @README.md
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
description: "Find issue(s) on github"
|
||||
description: "find issue(s) on github"
|
||||
model: opencode/claude-haiku-4-5
|
||||
---
|
||||
|
||||
|
||||
15
.opencode/command/rmslop.md
Normal file
15
.opencode/command/rmslop.md
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
description: Remove AI code slop
|
||||
---
|
||||
|
||||
Check the diff against dev, and remove all AI generated slop introduced in this branch.
|
||||
|
||||
This includes:
|
||||
|
||||
- Extra comments that a human wouldn't add or is inconsistent with the rest of the file
|
||||
- Extra defensive checks or try/catch blocks that are abnormal for that area of the codebase (especially if called by trusted / validated codepaths)
|
||||
- Casts to any to get around type issues
|
||||
- Any other style that is inconsistent with the file
|
||||
- Unnecessary emoji usage
|
||||
|
||||
Report at the end with only a 1-3 sentence summary of what you changed
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
description: Spellcheck all markdown file changes
|
||||
description: spellcheck all markdown file changes
|
||||
---
|
||||
|
||||
Look at all the unstaged changes to markdown (.md, .mdx) files, pull out the lines that have changed, and check for spelling and grammar errors.
|
||||
|
||||
4
.opencode/env.d.ts
vendored
Normal file
4
.opencode/env.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
declare module "*.txt" {
|
||||
const content: string
|
||||
export default content
|
||||
}
|
||||
@@ -1,27 +1,27 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"plugin": ["opencode-openai-codex-auth"],
|
||||
"enterprise": {
|
||||
"url": "https://enterprise.dev.opencode.ai",
|
||||
},
|
||||
// "plugin": ["opencode-openai-codex-auth"],
|
||||
// "enterprise": {
|
||||
// "url": "https://enterprise.dev.opencode.ai",
|
||||
// },
|
||||
"instructions": ["STYLE_GUIDE.md"],
|
||||
"provider": {
|
||||
"opencode": {
|
||||
"options": {
|
||||
// "baseURL": "http://localhost:8080",
|
||||
},
|
||||
"options": {},
|
||||
},
|
||||
},
|
||||
"permission": {
|
||||
"bash": {
|
||||
"ls foo": "ask",
|
||||
},
|
||||
},
|
||||
"mcp": {
|
||||
"exa": {
|
||||
"context7": {
|
||||
"type": "remote",
|
||||
"url": "https://mcp.exa.ai/mcp",
|
||||
},
|
||||
"morph": {
|
||||
"type": "local",
|
||||
"command": ["bunx", "@morphllm/morphmcp"],
|
||||
"environment": {
|
||||
"ENABLED_TOOLS": "warp_grep",
|
||||
},
|
||||
"url": "https://mcp.context7.com/mcp",
|
||||
},
|
||||
},
|
||||
"tools": {
|
||||
"github-triage": 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
|
||||
90
.opencode/tool/github-triage.ts
Normal file
90
.opencode/tool/github-triage.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
/// <reference path="../env.d.ts" />
|
||||
// import { Octokit } from "@octokit/rest"
|
||||
import { tool } from "@opencode-ai/plugin"
|
||||
import DESCRIPTION from "./github-triage.txt"
|
||||
|
||||
function getIssueNumber(): number {
|
||||
const issue = parseInt(process.env.ISSUE_NUMBER ?? "", 10)
|
||||
if (!issue) throw new Error("ISSUE_NUMBER env var not set")
|
||||
return issue
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
export default tool({
|
||||
description: DESCRIPTION,
|
||||
args: {
|
||||
assignee: tool.schema
|
||||
.enum(["thdxr", "adamdotdevin", "rekram1-node", "fwang", "jayair", "kommander"])
|
||||
.describe("The username of the assignee")
|
||||
.default("rekram1-node"),
|
||||
labels: tool.schema
|
||||
.array(tool.schema.enum(["nix", "opentui", "perf", "desktop", "zen", "docs", "windows"]))
|
||||
.describe("The labels(s) to add to the issue")
|
||||
.default([]),
|
||||
},
|
||||
async execute(args) {
|
||||
const issue = getIssueNumber()
|
||||
// const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN })
|
||||
const owner = "sst"
|
||||
const repo = "opencode"
|
||||
|
||||
const results: string[] = []
|
||||
|
||||
if (args.assignee === "adamdotdevin" && !args.labels.includes("desktop")) {
|
||||
throw new Error("Only desktop issues should be assigned to adamdotdevin")
|
||||
}
|
||||
|
||||
if (args.assignee === "fwang" && !args.labels.includes("zen")) {
|
||||
throw new Error("Only zen issues should be assigned to fwang")
|
||||
}
|
||||
|
||||
if (args.assignee === "kommander" && !args.labels.includes("opentui")) {
|
||||
throw new Error("Only opentui issues should be assigned to kommander")
|
||||
}
|
||||
|
||||
// await octokit.rest.issues.addAssignees({
|
||||
// owner,
|
||||
// repo,
|
||||
// issue_number: issue,
|
||||
// assignees: [args.assignee],
|
||||
// })
|
||||
await githubFetch(`/repos/${owner}/${repo}/issues/${issue}/assignees`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ assignees: [args.assignee] }),
|
||||
})
|
||||
results.push(`Assigned @${args.assignee} to issue #${issue}`)
|
||||
|
||||
const labels: string[] = args.labels.map((label) => (label === "desktop" ? "web" : label))
|
||||
|
||||
if (labels.length > 0) {
|
||||
// await octokit.rest.issues.addLabels({
|
||||
// owner,
|
||||
// repo,
|
||||
// issue_number: issue,
|
||||
// labels,
|
||||
// })
|
||||
await githubFetch(`/repos/${owner}/${repo}/issues/${issue}/labels`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ labels }),
|
||||
})
|
||||
results.push(`Added labels: ${args.labels.join(", ")}`)
|
||||
}
|
||||
|
||||
return results.join("\n")
|
||||
},
|
||||
})
|
||||
88
.opencode/tool/github-triage.txt
Normal file
88
.opencode/tool/github-triage.txt
Normal file
@@ -0,0 +1,88 @@
|
||||
Use this tool to assign and/or label a Github issue.
|
||||
|
||||
You can assign the following users:
|
||||
- thdxr
|
||||
- adamdotdevin
|
||||
- fwang
|
||||
- jayair
|
||||
- kommander
|
||||
- rekram1-node
|
||||
|
||||
|
||||
You can use the following labels:
|
||||
- nix
|
||||
- opentui
|
||||
- perf
|
||||
- web
|
||||
- zen
|
||||
- docs
|
||||
|
||||
Always try to assign an issue, if in doubt, assign rekram1-node to it.
|
||||
|
||||
## Breakdown of responsibilities:
|
||||
|
||||
### thdxr
|
||||
|
||||
Dax is responsible for managing core parts of the application, for large feature requests, api changes, or things that require significant changes to the codebase assign him.
|
||||
|
||||
This relates to OpenCode server primarily but has overlap with just about anything
|
||||
|
||||
### adamdotdevin
|
||||
|
||||
Adam is responsible for managing the Desktop/Web app. If there is an issue relating to the desktop app or `opencode web` command. Assign him.
|
||||
|
||||
|
||||
### fwang
|
||||
|
||||
Frank is responsible for managing Zen, if you see complaints about OpenCode Zen, maybe it's the dashboard, the model quality, billing issues, etc. Assign him to the issue.
|
||||
|
||||
### jayair
|
||||
|
||||
Jay is responsible for documentation. If there is an issue relating to documentation assign him.
|
||||
|
||||
### kommander
|
||||
|
||||
Sebastian is responsible for managing an OpenTUI (a library for building terminal user interfaces). OpenCode's TUI is built with OpenTUI. If there are issues about:
|
||||
- random characters on screen
|
||||
- keybinds not working on different terminals
|
||||
- general terminal stuff
|
||||
Then assign the issue to Him.
|
||||
|
||||
### rekram1-node
|
||||
|
||||
ALL BUGS SHOULD BE assigned to rekram1-node unless they have the `opentui` label.
|
||||
|
||||
Assign Aiden to an issue as a catch all, if you can't assign anyone else. Most of the time this will be bugs/polish things.
|
||||
If no one else makes sense to assign, assign rekram1-node to it.
|
||||
|
||||
Always assign to aiden if the issue mentions "acp", "zed", or model performance issues
|
||||
|
||||
## Breakdown of Labels:
|
||||
|
||||
### nix
|
||||
|
||||
Any issue that mentions nix, or nixos should have a nix label
|
||||
|
||||
### opentui
|
||||
|
||||
Anything relating to the TUI itself should have an opentui label
|
||||
|
||||
### perf
|
||||
|
||||
Anything related to slow performance, high ram, high cpu usage, or any other performance related issue should have a perf label
|
||||
|
||||
### desktop
|
||||
|
||||
Anything related to `opencode web` command or the desktop app should have a desktop label. Never add this label for anything terminal/tui related
|
||||
|
||||
### zen
|
||||
|
||||
Anything related to OpenCode Zen, billing, or model quality from Zen should have a zen label
|
||||
|
||||
### docs
|
||||
|
||||
Anything related to the documentation should have a docs label
|
||||
|
||||
### windows
|
||||
|
||||
Use for any issue that involves the windows OS
|
||||
1
.prettierignore
Normal file
1
.prettierignore
Normal file
@@ -0,0 +1 @@
|
||||
sst-env.d.ts
|
||||
49
AGENTS.md
49
AGENTS.md
@@ -1,47 +1,4 @@
|
||||
## IMPORTANT
|
||||
|
||||
- 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()
|
||||
|
||||
## 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 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`
|
||||
|
||||
@@ -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,13 +34,45 @@ 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/plugin`: Source for `@opencode-ai/plugin`
|
||||
|
||||
> [!NOTE]
|
||||
> After touching `packages/opencode/src/server/server.ts`, run "./packages/sdk/js/script/build.ts" to regenerate the JS sdk.
|
||||
> 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.
|
||||
|
||||
Please try to follow the [style guide](./STYLE_GUIDE.md)
|
||||
|
||||
### Setting up a Debugger
|
||||
|
||||
@@ -51,12 +83,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:
|
||||
|
||||
|
||||
30
README.md
30
README.md
@@ -7,11 +7,11 @@
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
<p align="center">The AI coding agent built for the terminal.</p>
|
||||
<p align="center">The open source 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/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)
|
||||
@@ -30,13 +30,29 @@ scoop bucket add extras; scoop install extras/opencode # Windows
|
||||
choco install opencode # Windows
|
||||
brew install opencode # macOS and Linux
|
||||
paru -S opencode-bin # Arch Linux
|
||||
mise use --pin -g ubi: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]
|
||||
> Remove versions older than 0.1.x before installing.
|
||||
|
||||
### Desktop App (BETA)
|
||||
|
||||
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 |
|
||||
| --------------------- | ------------------------------------- |
|
||||
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
|
||||
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
|
||||
| Windows | `opencode-desktop-windows-x64.exe` |
|
||||
| Linux | `.deb`, `.rpm`, or AppImage |
|
||||
|
||||
```bash
|
||||
# macOS (Homebrew)
|
||||
brew install --cask opencode-desktop
|
||||
```
|
||||
|
||||
#### Installation Directory
|
||||
|
||||
The install script respects the following priority order for the installation path:
|
||||
@@ -63,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).
|
||||
@@ -78,11 +94,11 @@ If you're interested in contributing to OpenCode, please read our [contributing
|
||||
|
||||
### Building on OpenCode
|
||||
|
||||
If you are working on a project that's related to OpenCode and is using "opencode" as a part of its name; for example, "opencode-dashboard" or "opencode-mobile", please add a note to your README to clarify that it is not built by the OpenCode team and is not affiliated with us in anyway.
|
||||
If you are working on a project that's related to OpenCode and is using "opencode" as a part of its name; for example, "opencode-dashboard" or "opencode-mobile", please add a note to your README to clarify that it is not built by the OpenCode team and is not affiliated with us in any way.
|
||||
|
||||
### 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:
|
||||
|
||||
|
||||
115
README.zh-TW.md
Normal file
115
README.zh-TW.md
Normal file
@@ -0,0 +1,115 @@
|
||||
<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 opencode # macOS 與 Linux
|
||||
paru -S opencode-bin # Arch Linux
|
||||
mise use -g github:anomalyco/opencode # 任何作業系統
|
||||
nix run nixpkgs#opencode # 或使用 github:anomalyco/opencode 以取得最新開發分支
|
||||
```
|
||||
|
||||
> [!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** - 預設模式,具備完整權限的 Agent,適用於開發工作。
|
||||
- **plan** - 唯讀模式,適用於程式碼分析與探索。
|
||||
- 預設禁止修改檔案。
|
||||
- 執行 bash 指令前會詢問權限。
|
||||
- 非常適合用來探索陌生的程式碼庫或規劃變更。
|
||||
|
||||
此外,OpenCode 還包含一個 **general** 子 Agent,用於處理複雜搜尋與多步驟任務。此 Agent 供系統內部使用,亦可透過在訊息中輸入 `@general` 來呼叫。
|
||||
|
||||
了解更多關於 [Agents](https://opencode.ai/docs/agents) 的資訊。
|
||||
|
||||
### 線上文件
|
||||
|
||||
關於如何設定 OpenCode 的詳細資訊,請參閱我們的 [**官方文件**](https://opencode.ai/docs)。
|
||||
|
||||
### 參與貢獻
|
||||
|
||||
如果您有興趣參與 OpenCode 的開發,請在提交 Pull Request 前先閱讀我們的 [貢獻指南 (Contributing Docs)](./CONTRIBUTING.md)。
|
||||
|
||||
### 基於 OpenCode 進行開發
|
||||
|
||||
如果您正在開發與 OpenCode 相關的專案,並在名稱中使用了 "opencode"(例如 "opencode-dashboard" 或 "opencode-mobile"),請在您的 README 中加入聲明,說明該專案並非由 OpenCode 團隊開發,且與我們沒有任何隸屬關係。
|
||||
|
||||
### 常見問題 (FAQ)
|
||||
|
||||
#### 這跟 Claude Code 有什麼不同?
|
||||
|
||||
在功能面上與 Claude Code 非常相似。以下是關鍵差異:
|
||||
|
||||
- 100% 開源。
|
||||
- 不綁定特定的服務提供商。雖然我們推薦使用透過 [OpenCode Zen](https://opencode.ai/zen) 提供的模型,但 OpenCode 也可搭配 Claude, OpenAI, Google 甚至本地模型使用。隨著模型不斷演進,彼此間的差距會縮小且價格會下降,因此具備「不限廠商 (provider-agnostic)」的特性至關重要。
|
||||
- 內建 LSP (語言伺服器協定) 支援。
|
||||
- 專注於終端機介面 (TUI)。OpenCode 由 Neovim 愛好者與 [terminal.shop](https://terminal.shop) 的創作者打造;我們將不斷挑戰終端機介面的極限。
|
||||
- 客戶端/伺服器架構 (Client/Server Architecture)。這讓 OpenCode 能夠在您的電腦上運行的同時,由行動裝置進行遠端操控。這意味著 TUI 前端只是眾多可能的客戶端之一。
|
||||
|
||||
#### 另一個同名的 Repo 是什麼?
|
||||
|
||||
另一個名稱相近的儲存庫與本專案無關。您可以點此[閱讀背後的故事](https://x.com/thdxr/status/1933561254481666466)。
|
||||
|
||||
---
|
||||
|
||||
**加入我們的社群** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode)
|
||||
346
STATS.md
346
STATS.md
@@ -1,159 +1,191 @@
|
||||
# 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) |
|
||||
| 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) |
|
||||
|
||||
12
STYLE_GUIDE.md
Normal file
12
STYLE_GUIDE.md
Normal file
@@ -0,0 +1,12 @@
|
||||
## 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()
|
||||
@@ -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": 1764587062,
|
||||
"narHash": "sha256-hdFa0TAVQAQLDF31cEW3enWmBP+b592OvHs6WVe3D8k=",
|
||||
"lastModified": 1767273430,
|
||||
"narHash": "sha256-kDpoFwQ8GLrPiS3KL+sAwreXrph2KhdXuJzo5+vSLoo=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "c1cb7d097cb250f6e1904aacd5f2ba5ffd8a49ce",
|
||||
"rev": "76eec3925eb9bbe193934987d3285473dbcfad50",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -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);
|
||||
@@ -70,12 +70,12 @@
|
||||
in
|
||||
{
|
||||
default = mkPackage {
|
||||
version = packageJson.version;
|
||||
inherit (packageJson) version;
|
||||
src = ./.;
|
||||
scripts = ./nix/scripts;
|
||||
target = bunTarget.${system};
|
||||
modelsDev = "${modelsDev.${system}}/dist/_api.json";
|
||||
mkNodeModules = mkNodeModules;
|
||||
inherit mkNodeModules;
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -6,7 +6,7 @@ Mention `/opencode` in your comment, and opencode will execute tasks within your
|
||||
|
||||
## Features
|
||||
|
||||
#### Explain an issues
|
||||
#### Explain an issue
|
||||
|
||||
Leave the following comment on a GitHub issue. `opencode` will read the entire thread, including all comments, and reply with a clear explanation.
|
||||
|
||||
@@ -14,7 +14,7 @@ Leave the following comment on a GitHub issue. `opencode` will read the entire t
|
||||
/opencode explain this issue
|
||||
```
|
||||
|
||||
#### Fix an issues
|
||||
#### Fix an issue
|
||||
|
||||
Leave the following comment on a GitHub issue. opencode will create a new branch, implement the changes, and open a PR with the changes.
|
||||
|
||||
@@ -87,7 +87,7 @@ This will walk you through installing the GitHub app, creating the workflow, and
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run opencode
|
||||
uses: sst/opencode/github@latest
|
||||
uses: anomalyco/opencode/github@latest
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
with:
|
||||
@@ -98,7 +98,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
|
||||
|
||||
|
||||
@@ -9,21 +9,66 @@ inputs:
|
||||
description: "Model to use"
|
||||
required: true
|
||||
|
||||
agent:
|
||||
description: "Agent to use. Must be a primary agent. Falls back to default_agent from config or 'build' if not found."
|
||||
required: false
|
||||
|
||||
share:
|
||||
description: "Share the opencode session (defaults to true for public repos)"
|
||||
required: false
|
||||
|
||||
prompt:
|
||||
description: "Custom prompt to override the default prompt"
|
||||
required: false
|
||||
|
||||
use_github_token:
|
||||
description: "Use GITHUB_TOKEN directly instead of OpenCode App token exchange. When true, skips OIDC and uses the GITHUB_TOKEN env var."
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
mentions:
|
||||
description: "Comma-separated list of trigger phrases (case-insensitive). Defaults to '/opencode,/oc'"
|
||||
required: false
|
||||
|
||||
oidc_base_url:
|
||||
description: "Base URL for OIDC token exchange API. Only required when running a custom GitHub App install. Defaults to https://api.opencode.ai"
|
||||
required: false
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Get opencode version
|
||||
id: version
|
||||
shell: bash
|
||||
run: |
|
||||
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
|
||||
id: cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.opencode/bin
|
||||
key: opencode-${{ runner.os }}-${{ runner.arch }}-${{ steps.version.outputs.version }}
|
||||
|
||||
- name: Install opencode
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
run: curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
- name: Add opencode to PATH
|
||||
shell: bash
|
||||
run: echo "$HOME/.opencode/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Run opencode
|
||||
shell: bash
|
||||
id: run_opencode
|
||||
run: opencode github run
|
||||
env:
|
||||
MODEL: ${{ inputs.model }}
|
||||
AGENT: ${{ inputs.agent }}
|
||||
SHARE: ${{ inputs.share }}
|
||||
PROMPT: ${{ inputs.prompt }}
|
||||
USE_GITHUB_TOKEN: ${{ inputs.use_github_token }}
|
||||
MENTIONS: ${{ inputs.mentions }}
|
||||
OIDC_BASE_URL: ${{ inputs.oidc_base_url }}
|
||||
|
||||
@@ -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) {
|
||||
@@ -318,6 +318,10 @@ function useEnvRunUrl() {
|
||||
return `/${repo.owner}/${repo.repo}/actions/runs/${runId}`
|
||||
}
|
||||
|
||||
function useEnvAgent() {
|
||||
return process.env["AGENT"] || undefined
|
||||
}
|
||||
|
||||
function useEnvShare() {
|
||||
const value = process.env["SHARE"]
|
||||
if (!value) return undefined
|
||||
@@ -570,24 +574,49 @@ async function subscribeSessionEvents() {
|
||||
}
|
||||
|
||||
async function summarize(response: string) {
|
||||
const payload = useContext().payload as IssueCommentEvent
|
||||
try {
|
||||
return await chat(`Summarize the following in less than 40 characters:\n\n${response}`)
|
||||
} catch (e) {
|
||||
if (isScheduleEvent()) {
|
||||
return "Scheduled task changes"
|
||||
}
|
||||
const payload = useContext().payload as IssueCommentEvent
|
||||
return `Fix issue: ${payload.issue.title}`
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveAgent(): Promise<string | undefined> {
|
||||
const envAgent = useEnvAgent()
|
||||
if (!envAgent) return undefined
|
||||
|
||||
// Validate the agent exists and is a primary agent
|
||||
const agents = await client.agent.list<true>()
|
||||
const agent = agents.data?.find((a) => a.name === envAgent)
|
||||
|
||||
if (!agent) {
|
||||
console.warn(`agent "${envAgent}" not found. Falling back to default agent`)
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (agent.mode === "subagent") {
|
||||
console.warn(`agent "${envAgent}" is a subagent, not a primary agent. Falling back to default agent`)
|
||||
return undefined
|
||||
}
|
||||
|
||||
return envAgent
|
||||
}
|
||||
|
||||
async function chat(text: string, files: PromptFiles = []) {
|
||||
console.log("Sending message to opencode...")
|
||||
const { providerID, modelID } = useEnvModel()
|
||||
const agent = await resolveAgent()
|
||||
|
||||
const chat = await client.session.chat<true>({
|
||||
path: session,
|
||||
body: {
|
||||
providerID,
|
||||
modelID,
|
||||
agent: "build",
|
||||
agent,
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"@actions/core": "1.11.1",
|
||||
"@actions/github": "6.0.1",
|
||||
"@octokit/graphql": "9.0.1",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@octokit/rest": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
2
github/sst-env.d.ts
vendored
2
github/sst-env.d.ts
vendored
@@ -6,4 +6,4 @@
|
||||
/// <reference path="../sst-env.d.ts" />
|
||||
|
||||
import "sst"
|
||||
export {}
|
||||
export {}
|
||||
@@ -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",
|
||||
},
|
||||
})
|
||||
|
||||
@@ -102,6 +102,8 @@ const ZEN_MODELS = [
|
||||
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"),
|
||||
]
|
||||
const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
|
||||
const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
|
||||
@@ -116,7 +118,8 @@ const gatewayKv = new sst.cloudflare.Kv("GatewayKv")
|
||||
// CONSOLE
|
||||
////////////////
|
||||
|
||||
const bucket = new sst.cloudflare.Bucket("ConsoleData")
|
||||
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")
|
||||
@@ -135,6 +138,7 @@ new sst.cloudflare.x.SolidStart("Console", {
|
||||
path: "packages/console/app",
|
||||
link: [
|
||||
bucket,
|
||||
bucketNew,
|
||||
database,
|
||||
AUTH_API_URL,
|
||||
STRIPE_WEBHOOK_SECRET,
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
})
|
||||
@@ -1,10 +1,10 @@
|
||||
import { SECRET } from "./secret"
|
||||
import { domain } from "./stage"
|
||||
import { domain, shortDomain } from "./stage"
|
||||
|
||||
const storage = new sst.cloudflare.Bucket("EnterpriseStorage")
|
||||
|
||||
const enterprise = new sst.cloudflare.x.SolidStart("Enterprise", {
|
||||
domain: "enterprise." + domain,
|
||||
const teams = new sst.cloudflare.x.SolidStart("Teams", {
|
||||
domain: shortDomain,
|
||||
path: "packages/enterprise",
|
||||
buildCommand: "bun run build:cloudflare",
|
||||
environment: {
|
||||
|
||||
@@ -11,3 +11,9 @@ new cloudflare.RegionalHostname("RegionalHostname", {
|
||||
regionKey: "us",
|
||||
zoneId: zoneID,
|
||||
})
|
||||
|
||||
export const shortDomain = (() => {
|
||||
if ($app.stage === "production") return "opncd.ai"
|
||||
if ($app.stage === "dev") return "dev.opncd.ai"
|
||||
return `${$app.stage}.dev.opncd.ai`
|
||||
})()
|
||||
|
||||
157
install
157
install
@@ -4,10 +4,54 @@ APP=opencode
|
||||
|
||||
MUTED='\033[0;2m'
|
||||
RED='\033[0;31m'
|
||||
ORANGE='\033[38;2;255;140;0m'
|
||||
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)
|
||||
--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
|
||||
EOF
|
||||
}
|
||||
|
||||
requested_version=${VERSION:-}
|
||||
no_modify_path=false
|
||||
|
||||
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
|
||||
;;
|
||||
--no-modify-path)
|
||||
no_modify_path=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo -e "${ORANGE}Warning: Unknown option '$1'${NC}" >&2
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
raw_os=$(uname -s)
|
||||
os=$(echo "$raw_os" | tr '[:upper:]' '[:lower:]')
|
||||
@@ -103,16 +147,26 @@ 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')
|
||||
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
|
||||
url="https://github.com/sst/opencode/releases/download/v${requested_version}/$filename"
|
||||
# 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
|
||||
|
||||
print_message() {
|
||||
@@ -240,22 +294,23 @@ download_with_progress() {
|
||||
|
||||
download_and_install() {
|
||||
print_message info "\n${MUTED}Installing ${NC}opencode ${MUTED}version: ${NC}$specific_version"
|
||||
mkdir -p opencodetmp && cd opencodetmp
|
||||
local tmp_dir="${TMPDIR:-/tmp}/opencode_install_$$"
|
||||
mkdir -p "$tmp_dir"
|
||||
|
||||
if [[ "$os" == "windows" ]] || ! download_with_progress "$url" "$filename"; then
|
||||
# Fallback to standard curl on Windows or if custom progress fails
|
||||
curl -# -L -o "$filename" "$url"
|
||||
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"
|
||||
fi
|
||||
|
||||
if [ "$os" = "linux" ]; then
|
||||
tar -xzf "$filename"
|
||||
tar -xzf "$tmp_dir/$filename" -C "$tmp_dir"
|
||||
else
|
||||
unzip -q "$filename"
|
||||
unzip -q "$tmp_dir/$filename" -d "$tmp_dir"
|
||||
fi
|
||||
|
||||
mv opencode "$INSTALL_DIR"
|
||||
mv "$tmp_dir/opencode" "$INSTALL_DIR"
|
||||
chmod 755 "${INSTALL_DIR}/opencode"
|
||||
cd .. && rm -rf opencodetmp
|
||||
rm -rf "$tmp_dir"
|
||||
}
|
||||
|
||||
check_version
|
||||
@@ -303,42 +358,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
|
||||
@@ -353,10 +408,10 @@ echo -e "${MUTED}█░░█ █░░█ █▀▀▀ █░░█ ${NC}█░
|
||||
echo -e "${MUTED}▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ${NC}▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀"
|
||||
echo -e ""
|
||||
echo -e ""
|
||||
echo -e "${MUTED}To get started, navigate to a project and run:${NC}"
|
||||
echo -e "opencode ${MUTED}Use free models${NC}"
|
||||
echo -e "opencode auth login ${MUTED}Add paid provider API keys${NC}"
|
||||
echo -e "opencode help ${MUTED}List commands and options${NC}"
|
||||
echo -e "${MUTED}OpenCode includes free models, to start:${NC}"
|
||||
echo -e ""
|
||||
echo -e "cd <project> ${MUTED}# Open directory${NC}"
|
||||
echo -e "opencode ${MUTED}# Run command${NC}"
|
||||
echo -e ""
|
||||
echo -e "${MUTED}For more information visit ${NC}https://opencode.ai/docs"
|
||||
echo -e ""
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"nodeModules": "sha256-9BfJ3dFq/UYyhsnK3Sfx6rb6CT8bCvFOFOqD2+W1WQE="
|
||||
"nodeModules": "sha256-uJDhOieOdMQLORyuOWtgtjLoMnNEQPrDcyij9TX0aTw="
|
||||
}
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
{ hash, lib, stdenvNoCC, bun, cacert, curl }:
|
||||
{
|
||||
hash,
|
||||
lib,
|
||||
stdenvNoCC,
|
||||
bun,
|
||||
cacert,
|
||||
curl,
|
||||
}:
|
||||
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;
|
||||
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
{ lib, stdenvNoCC, bun, fzf, 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 = [
|
||||
@@ -97,7 +100,7 @@ stdenvNoCC.mkDerivation (finalAttrs: {
|
||||
makeWrapper ${bun}/bin/bun $out/bin/opencode \
|
||||
--add-flags "run" \
|
||||
--add-flags "$out/lib/opencode/dist/src/index.js" \
|
||||
--prefix PATH : ${lib.makeBinPath [ fzf ripgrep ]} \
|
||||
--prefix PATH : ${lib.makeBinPath [ ripgrep ]} \
|
||||
--argv0 opencode
|
||||
|
||||
runHook postInstall
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
28
package.json
28
package.json
@@ -4,13 +4,14 @@
|
||||
"description": "AI-powered development tool",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "bun@1.3.3",
|
||||
"packageManager": "bun@1.3.5",
|
||||
"scripts": {
|
||||
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
||||
"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": [
|
||||
@@ -20,7 +21,8 @@
|
||||
"packages/slack"
|
||||
],
|
||||
"catalog": {
|
||||
"@types/bun": "1.3.3",
|
||||
"@types/bun": "1.3.4",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
"ulid": "3.0.1",
|
||||
"@kobalte/core": "0.13.11",
|
||||
@@ -30,18 +32,22 @@
|
||||
"@tsconfig/bun": "1.0.9",
|
||||
"@cloudflare/workers-types": "4.20251008.0",
|
||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||
"@pierre/precision-diffs": "0.5.7",
|
||||
"@pierre/diffs": "1.0.2",
|
||||
"@solid-primitives/storage": "4.3.3",
|
||||
"@tailwindcss/vite": "4.1.11",
|
||||
"diff": "8.0.2",
|
||||
"ai": "5.0.97",
|
||||
"hono": "4.7.10",
|
||||
"hono-openapi": "1.1.1",
|
||||
"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.20251014.1",
|
||||
"@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",
|
||||
@@ -54,6 +60,7 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/artifact": "5.0.1",
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"husky": "9.1.7",
|
||||
"prettier": "3.6.2",
|
||||
@@ -62,13 +69,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": {
|
||||
@@ -78,7 +86,6 @@
|
||||
"trustedDependencies": [
|
||||
"esbuild",
|
||||
"protobufjs",
|
||||
"sharp",
|
||||
"tree-sitter",
|
||||
"tree-sitter-bash",
|
||||
"web-tree-sitter"
|
||||
@@ -86,5 +93,8 @@
|
||||
"overrides": {
|
||||
"@types/bun": "catalog:",
|
||||
"@types/node": "catalog:"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"ghostty-web@0.3.0": "patches/ghostty-web@0.3.0.patch"
|
||||
}
|
||||
}
|
||||
|
||||
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 playwrite 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
|
||||
|
||||
Those templates dependencies are maintained via [pnpm](https://pnpm.io) via `pnpm up -Lri`.
|
||||
|
||||
This is the reason you see a `pnpm-lock.yaml`. That being said, any package manager will work. This file can be 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.)
|
||||
2
packages/app/bunfig.toml
Normal file
2
packages/app/bunfig.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[test]
|
||||
preload = ["./happydom.ts"]
|
||||
75
packages/app/happydom.ts
Normal file
75
packages/app/happydom.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { GlobalRegistrator } from "@happy-dom/global-registrator"
|
||||
|
||||
GlobalRegistrator.register()
|
||||
|
||||
const originalGetContext = HTMLCanvasElement.prototype.getContext
|
||||
// @ts-expect-error - we're overriding with a simplified mock
|
||||
HTMLCanvasElement.prototype.getContext = function (contextType: string, _options?: unknown) {
|
||||
if (contextType === "2d") {
|
||||
return {
|
||||
canvas: this,
|
||||
fillStyle: "#000000",
|
||||
strokeStyle: "#000000",
|
||||
font: "12px monospace",
|
||||
textAlign: "start",
|
||||
textBaseline: "alphabetic",
|
||||
globalAlpha: 1,
|
||||
globalCompositeOperation: "source-over",
|
||||
imageSmoothingEnabled: true,
|
||||
lineWidth: 1,
|
||||
lineCap: "butt",
|
||||
lineJoin: "miter",
|
||||
miterLimit: 10,
|
||||
shadowBlur: 0,
|
||||
shadowColor: "rgba(0, 0, 0, 0)",
|
||||
shadowOffsetX: 0,
|
||||
shadowOffsetY: 0,
|
||||
fillRect: () => {},
|
||||
strokeRect: () => {},
|
||||
clearRect: () => {},
|
||||
fillText: () => {},
|
||||
strokeText: () => {},
|
||||
measureText: (text: string) => ({ width: text.length * 8 }),
|
||||
drawImage: () => {},
|
||||
save: () => {},
|
||||
restore: () => {},
|
||||
scale: () => {},
|
||||
rotate: () => {},
|
||||
translate: () => {},
|
||||
transform: () => {},
|
||||
setTransform: () => {},
|
||||
resetTransform: () => {},
|
||||
createLinearGradient: () => ({ addColorStop: () => {} }),
|
||||
createRadialGradient: () => ({ addColorStop: () => {} }),
|
||||
createPattern: () => null,
|
||||
beginPath: () => {},
|
||||
closePath: () => {},
|
||||
moveTo: () => {},
|
||||
lineTo: () => {},
|
||||
bezierCurveTo: () => {},
|
||||
quadraticCurveTo: () => {},
|
||||
arc: () => {},
|
||||
arcTo: () => {},
|
||||
ellipse: () => {},
|
||||
rect: () => {},
|
||||
fill: () => {},
|
||||
stroke: () => {},
|
||||
clip: () => {},
|
||||
isPointInPath: () => false,
|
||||
isPointInStroke: () => false,
|
||||
getTransform: () => ({}),
|
||||
getImageData: () => ({
|
||||
data: new Uint8ClampedArray(0),
|
||||
width: 0,
|
||||
height: 0,
|
||||
}),
|
||||
putImageData: () => {},
|
||||
createImageData: () => ({
|
||||
data: new Uint8ClampedArray(0),
|
||||
width: 0,
|
||||
height: 0,
|
||||
}),
|
||||
} as unknown as CanvasRenderingContext2D
|
||||
}
|
||||
return originalGetContext.call(this, contextType as "2d", _options)
|
||||
}
|
||||
53
packages/app/index.html
Normal file
53
packages/app/index.html
Normal file
@@ -0,0 +1,53 @@
|
||||
<!doctype html>
|
||||
<html lang="en" style="background-color: var(--background-base)">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>OpenCode</title>
|
||||
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<meta name="theme-color" content="#F8F7F7" />
|
||||
<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">
|
||||
;(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)
|
||||
}
|
||||
})()
|
||||
</script>
|
||||
</head>
|
||||
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root" class="flex flex-col h-screen"></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.0.224",
|
||||
"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
|
||||
1
packages/app/public/social-share-zen.png
Symbolic link
1
packages/app/public/social-share-zen.png
Symbolic link
@@ -0,0 +1 @@
|
||||
../../ui/src/assets/images/social-share-zen.png
|
||||
1
packages/app/public/social-share.png
Symbolic link
1
packages/app/public/social-share.png
Symbolic link
@@ -0,0 +1 @@
|
||||
../../ui/src/assets/images/social-share.png
|
||||
272
packages/app/src/addons/serialize.test.ts
Normal file
272
packages/app/src/addons/serialize.test.ts
Normal file
@@ -0,0 +1,272 @@
|
||||
import { describe, test, expect, beforeAll, afterEach } from "bun:test"
|
||||
import { Terminal, Ghostty } from "ghostty-web"
|
||||
import { SerializeAddon } from "./serialize"
|
||||
|
||||
let ghostty: Ghostty
|
||||
beforeAll(async () => {
|
||||
ghostty = await Ghostty.load()
|
||||
})
|
||||
|
||||
const terminals: Terminal[] = []
|
||||
|
||||
afterEach(() => {
|
||||
for (const term of terminals) {
|
||||
term.dispose()
|
||||
}
|
||||
terminals.length = 0
|
||||
document.body.innerHTML = ""
|
||||
})
|
||||
|
||||
function createTerminal(cols = 80, rows = 24): { term: Terminal; addon: SerializeAddon; container: HTMLElement } {
|
||||
const container = document.createElement("div")
|
||||
document.body.appendChild(container)
|
||||
|
||||
const term = new Terminal({ cols, rows, ghostty })
|
||||
const addon = new SerializeAddon()
|
||||
term.loadAddon(addon)
|
||||
term.open(container)
|
||||
terminals.push(term)
|
||||
|
||||
return { term, addon, container }
|
||||
}
|
||||
|
||||
function writeAndWait(term: Terminal, data: string): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
term.write(data, resolve)
|
||||
})
|
||||
}
|
||||
|
||||
describe("SerializeAddon", () => {
|
||||
describe("ANSI color preservation", () => {
|
||||
test("should preserve text attributes (bold, italic, underline)", async () => {
|
||||
const { term, addon } = createTerminal()
|
||||
|
||||
const input = "\x1b[1mBOLD\x1b[0m \x1b[3mITALIC\x1b[0m \x1b[4mUNDER\x1b[0m"
|
||||
await writeAndWait(term, input)
|
||||
|
||||
const origLine = term.buffer.active.getLine(0)
|
||||
expect(origLine!.getCell(0)!.isBold()).toBe(1)
|
||||
expect(origLine!.getCell(5)!.isItalic()).toBe(1)
|
||||
expect(origLine!.getCell(12)!.isUnderline()).toBe(1)
|
||||
|
||||
const serialized = addon.serialize({ range: { start: 0, end: 0 } })
|
||||
|
||||
const { term: term2 } = createTerminal()
|
||||
terminals.push(term2)
|
||||
await writeAndWait(term2, serialized)
|
||||
|
||||
const line = term2.buffer.active.getLine(0)
|
||||
|
||||
const boldCell = line!.getCell(0)
|
||||
expect(boldCell!.getChars()).toBe("B")
|
||||
expect(boldCell!.isBold()).toBe(1)
|
||||
|
||||
const italicCell = line!.getCell(5)
|
||||
expect(italicCell!.getChars()).toBe("I")
|
||||
expect(italicCell!.isItalic()).toBe(1)
|
||||
|
||||
const underCell = line!.getCell(12)
|
||||
expect(underCell!.getChars()).toBe("U")
|
||||
expect(underCell!.isUnderline()).toBe(1)
|
||||
})
|
||||
|
||||
test("should preserve basic 16-color foreground colors", async () => {
|
||||
const { term, addon } = createTerminal()
|
||||
|
||||
const input = "\x1b[31mRED\x1b[32mGREEN\x1b[34mBLUE\x1b[0mNORMAL"
|
||||
await writeAndWait(term, input)
|
||||
|
||||
const origLine = term.buffer.active.getLine(0)
|
||||
const origRedFg = origLine!.getCell(0)!.getFgColor()
|
||||
const origGreenFg = origLine!.getCell(3)!.getFgColor()
|
||||
const origBlueFg = origLine!.getCell(8)!.getFgColor()
|
||||
|
||||
const serialized = addon.serialize({ range: { start: 0, end: 0 } })
|
||||
|
||||
const { term: term2 } = createTerminal()
|
||||
terminals.push(term2)
|
||||
await writeAndWait(term2, serialized)
|
||||
|
||||
const line = term2.buffer.active.getLine(0)
|
||||
expect(line).toBeDefined()
|
||||
|
||||
const redCell = line!.getCell(0)
|
||||
expect(redCell!.getChars()).toBe("R")
|
||||
expect(redCell!.getFgColor()).toBe(origRedFg)
|
||||
|
||||
const greenCell = line!.getCell(3)
|
||||
expect(greenCell!.getChars()).toBe("G")
|
||||
expect(greenCell!.getFgColor()).toBe(origGreenFg)
|
||||
|
||||
const blueCell = line!.getCell(8)
|
||||
expect(blueCell!.getChars()).toBe("B")
|
||||
expect(blueCell!.getFgColor()).toBe(origBlueFg)
|
||||
})
|
||||
|
||||
test("should preserve 256-color palette colors", async () => {
|
||||
const { term, addon } = createTerminal()
|
||||
|
||||
const input = "\x1b[38;5;196mRED256\x1b[0mNORMAL"
|
||||
await writeAndWait(term, input)
|
||||
|
||||
const origLine = term.buffer.active.getLine(0)
|
||||
const origRedFg = origLine!.getCell(0)!.getFgColor()
|
||||
|
||||
const serialized = addon.serialize({ range: { start: 0, end: 0 } })
|
||||
|
||||
const { term: term2 } = createTerminal()
|
||||
terminals.push(term2)
|
||||
await writeAndWait(term2, serialized)
|
||||
|
||||
const line = term2.buffer.active.getLine(0)
|
||||
const redCell = line!.getCell(0)
|
||||
expect(redCell!.getChars()).toBe("R")
|
||||
expect(redCell!.getFgColor()).toBe(origRedFg)
|
||||
})
|
||||
|
||||
test("should preserve RGB/truecolor colors", async () => {
|
||||
const { term, addon } = createTerminal()
|
||||
|
||||
const input = "\x1b[38;2;255;128;64mRGB_TEXT\x1b[0mNORMAL"
|
||||
await writeAndWait(term, input)
|
||||
|
||||
const origLine = term.buffer.active.getLine(0)
|
||||
const origRgbFg = origLine!.getCell(0)!.getFgColor()
|
||||
|
||||
const serialized = addon.serialize({ range: { start: 0, end: 0 } })
|
||||
|
||||
const { term: term2 } = createTerminal()
|
||||
terminals.push(term2)
|
||||
await writeAndWait(term2, serialized)
|
||||
|
||||
const line = term2.buffer.active.getLine(0)
|
||||
const rgbCell = line!.getCell(0)
|
||||
expect(rgbCell!.getChars()).toBe("R")
|
||||
expect(rgbCell!.getFgColor()).toBe(origRgbFg)
|
||||
})
|
||||
|
||||
test("should preserve background colors", async () => {
|
||||
const { term, addon } = createTerminal()
|
||||
|
||||
const input = "\x1b[48;2;255;0;0mRED_BG\x1b[48;2;0;255;0mGREEN_BG\x1b[0mNORMAL"
|
||||
await writeAndWait(term, input)
|
||||
|
||||
const origLine = term.buffer.active.getLine(0)
|
||||
const origRedBg = origLine!.getCell(0)!.getBgColor()
|
||||
const origGreenBg = origLine!.getCell(6)!.getBgColor()
|
||||
|
||||
const serialized = addon.serialize({ range: { start: 0, end: 0 } })
|
||||
|
||||
const { term: term2 } = createTerminal()
|
||||
terminals.push(term2)
|
||||
await writeAndWait(term2, serialized)
|
||||
|
||||
const line = term2.buffer.active.getLine(0)
|
||||
|
||||
const redBgCell = line!.getCell(0)
|
||||
expect(redBgCell!.getChars()).toBe("R")
|
||||
expect(redBgCell!.getBgColor()).toBe(origRedBg)
|
||||
|
||||
const greenBgCell = line!.getCell(6)
|
||||
expect(greenBgCell!.getChars()).toBe("G")
|
||||
expect(greenBgCell!.getBgColor()).toBe(origGreenBg)
|
||||
})
|
||||
|
||||
test("should handle combined colors and attributes", async () => {
|
||||
const { term, addon } = createTerminal()
|
||||
|
||||
const input =
|
||||
"\x1b[1;38;2;255;0;0;48;2;255;255;0mCOMBO\x1b[0mNORMAL "
|
||||
await writeAndWait(term, input)
|
||||
|
||||
const origLine = term.buffer.active.getLine(0)
|
||||
const origFg = origLine!.getCell(0)!.getFgColor()
|
||||
const origBg = origLine!.getCell(0)!.getBgColor()
|
||||
expect(origLine!.getCell(0)!.isBold()).toBe(1)
|
||||
|
||||
const serialized = addon.serialize({ range: { start: 0, end: 0 } })
|
||||
const cleanSerialized = serialized.replace(/\x1b\[\d+X/g, "")
|
||||
|
||||
expect(cleanSerialized.startsWith("\x1b[1;")).toBe(true)
|
||||
|
||||
const { term: term2 } = createTerminal()
|
||||
terminals.push(term2)
|
||||
await writeAndWait(term2, cleanSerialized)
|
||||
|
||||
const line = term2.buffer.active.getLine(0)
|
||||
const comboCell = line!.getCell(0)
|
||||
|
||||
expect(comboCell!.getChars()).toBe("C")
|
||||
expect(cleanSerialized).toContain("\x1b[1;38;2;255;0;0;48;2;255;255;0m")
|
||||
})
|
||||
})
|
||||
|
||||
describe("round-trip serialization", () => {
|
||||
test("should not produce ECH sequences", async () => {
|
||||
const { term, addon } = createTerminal()
|
||||
|
||||
await writeAndWait(term, "\x1b[31mHello\x1b[0m World")
|
||||
|
||||
const serialized = addon.serialize()
|
||||
|
||||
const hasECH = /\x1b\[\d+X/.test(serialized)
|
||||
expect(hasECH).toBe(false)
|
||||
})
|
||||
|
||||
test("multi-line content should not have garbage characters", 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()
|
||||
|
||||
expect(/\x1b\[\d+X/.test(serialized)).toBe(false)
|
||||
|
||||
const { term: term2 } = createTerminal()
|
||||
terminals.push(term2)
|
||||
await writeAndWait(term2, serialized)
|
||||
|
||||
for (let row = 0; row < 3; row++) {
|
||||
const line = term2.buffer.active.getLine(row)?.translateToString(true)
|
||||
expect(line?.includes("𑼝")).toBe(false)
|
||||
}
|
||||
|
||||
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("serialized output written to new terminal should match original colors", async () => {
|
||||
const { term, addon } = createTerminal(40, 5)
|
||||
|
||||
const input = "\x1b[38;2;255;0;0mHello\x1b[0m \x1b[38;2;0;255;0mWorld\x1b[0m! "
|
||||
await writeAndWait(term, input)
|
||||
|
||||
const origLine = term.buffer.active.getLine(0)
|
||||
const origHelloFg = origLine!.getCell(0)!.getFgColor()
|
||||
const origWorldFg = origLine!.getCell(6)!.getFgColor()
|
||||
|
||||
const serialized = addon.serialize({ range: { start: 0, end: 0 } })
|
||||
|
||||
const { term: term2 } = createTerminal(40, 5)
|
||||
terminals.push(term2)
|
||||
await writeAndWait(term2, serialized)
|
||||
|
||||
const newLine = term2.buffer.active.getLine(0)
|
||||
|
||||
expect(newLine!.getCell(0)!.getChars()).toBe("H")
|
||||
expect(newLine!.getCell(0)!.getFgColor()).toBe(origHelloFg)
|
||||
|
||||
expect(newLine!.getCell(6)!.getChars()).toBe("W")
|
||||
expect(newLine!.getCell(6)!.getFgColor()).toBe(origWorldFg)
|
||||
|
||||
expect(newLine!.getCell(11)!.getChars()).toBe("!")
|
||||
})
|
||||
})
|
||||
})
|
||||
595
packages/app/src/addons/serialize.ts
Normal file
595
packages/app/src/addons/serialize.ts
Normal file
@@ -0,0 +1,595 @@
|
||||
/**
|
||||
* SerializeAddon - Serialize terminal buffer contents
|
||||
*
|
||||
* Port of xterm.js addon-serialize for ghostty-web.
|
||||
* Enables serialization of terminal contents to a string that can
|
||||
* be written back to restore terminal state.
|
||||
*
|
||||
* Usage:
|
||||
* ```typescript
|
||||
* const serializeAddon = new SerializeAddon();
|
||||
* term.loadAddon(serializeAddon);
|
||||
* const content = serializeAddon.serialize();
|
||||
* ```
|
||||
*/
|
||||
|
||||
import type { ITerminalAddon, ITerminalCore, IBufferRange } from "ghostty-web"
|
||||
|
||||
// ============================================================================
|
||||
// Buffer Types (matching ghostty-web internal interfaces)
|
||||
// ============================================================================
|
||||
|
||||
interface IBuffer {
|
||||
readonly type: "normal" | "alternate"
|
||||
readonly cursorX: number
|
||||
readonly cursorY: number
|
||||
readonly viewportY: number
|
||||
readonly baseY: number
|
||||
readonly length: number
|
||||
getLine(y: number): IBufferLine | undefined
|
||||
getNullCell(): IBufferCell
|
||||
}
|
||||
|
||||
interface IBufferLine {
|
||||
readonly length: number
|
||||
readonly isWrapped: boolean
|
||||
getCell(x: number): IBufferCell | undefined
|
||||
translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): string
|
||||
}
|
||||
|
||||
interface IBufferCell {
|
||||
getChars(): string
|
||||
getCode(): number
|
||||
getWidth(): number
|
||||
getFgColorMode(): number
|
||||
getBgColorMode(): number
|
||||
getFgColor(): number
|
||||
getBgColor(): number
|
||||
isBold(): number
|
||||
isItalic(): number
|
||||
isUnderline(): number
|
||||
isStrikethrough(): number
|
||||
isBlink(): number
|
||||
isInverse(): number
|
||||
isInvisible(): number
|
||||
isFaint(): number
|
||||
isDim(): boolean
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
// ============================================================================
|
||||
|
||||
export interface ISerializeOptions {
|
||||
/**
|
||||
* The row range to serialize. When an explicit range is specified, the cursor
|
||||
* will get its final repositioning.
|
||||
*/
|
||||
range?: ISerializeRange
|
||||
/**
|
||||
* The number of rows in the scrollback buffer to serialize, starting from
|
||||
* the bottom of the scrollback buffer. When not specified, all available
|
||||
* rows in the scrollback buffer will be serialized.
|
||||
*/
|
||||
scrollback?: number
|
||||
/**
|
||||
* Whether to exclude the terminal modes from the serialization.
|
||||
* Default: false
|
||||
*/
|
||||
excludeModes?: boolean
|
||||
/**
|
||||
* Whether to exclude the alt buffer from the serialization.
|
||||
* Default: false
|
||||
*/
|
||||
excludeAltBuffer?: boolean
|
||||
}
|
||||
|
||||
export interface ISerializeRange {
|
||||
/**
|
||||
* The line to start serializing (inclusive).
|
||||
*/
|
||||
start: number
|
||||
/**
|
||||
* The line to end serializing (inclusive).
|
||||
*/
|
||||
end: number
|
||||
}
|
||||
|
||||
export interface IHTMLSerializeOptions {
|
||||
/**
|
||||
* The number of rows in the scrollback buffer to serialize, starting from
|
||||
* the bottom of the scrollback buffer.
|
||||
*/
|
||||
scrollback?: number
|
||||
/**
|
||||
* Whether to only serialize the selection.
|
||||
* Default: false
|
||||
*/
|
||||
onlySelection?: boolean
|
||||
/**
|
||||
* Whether to include the global background of the terminal.
|
||||
* Default: false
|
||||
*/
|
||||
includeGlobalBackground?: boolean
|
||||
/**
|
||||
* The range to serialize. This is prioritized over onlySelection.
|
||||
*/
|
||||
range?: {
|
||||
startLine: number
|
||||
endLine: number
|
||||
startCol: number
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helper Functions
|
||||
// ============================================================================
|
||||
|
||||
function constrain(value: number, low: number, high: number): number {
|
||||
return Math.max(low, Math.min(value, high))
|
||||
}
|
||||
|
||||
function equalFg(cell1: IBufferCell, cell2: IBufferCell): boolean {
|
||||
return cell1.getFgColorMode() === cell2.getFgColorMode() && cell1.getFgColor() === cell2.getFgColor()
|
||||
}
|
||||
|
||||
function equalBg(cell1: IBufferCell, cell2: IBufferCell): boolean {
|
||||
return cell1.getBgColorMode() === cell2.getBgColorMode() && cell1.getBgColor() === cell2.getBgColor()
|
||||
}
|
||||
|
||||
function equalFlags(cell1: IBufferCell, cell2: IBufferCell): boolean {
|
||||
return (
|
||||
!!cell1.isInverse() === !!cell2.isInverse() &&
|
||||
!!cell1.isBold() === !!cell2.isBold() &&
|
||||
!!cell1.isUnderline() === !!cell2.isUnderline() &&
|
||||
!!cell1.isBlink() === !!cell2.isBlink() &&
|
||||
!!cell1.isInvisible() === !!cell2.isInvisible() &&
|
||||
!!cell1.isItalic() === !!cell2.isItalic() &&
|
||||
!!cell1.isDim() === !!cell2.isDim() &&
|
||||
!!cell1.isStrikethrough() === !!cell2.isStrikethrough()
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Base Serialize Handler
|
||||
// ============================================================================
|
||||
|
||||
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()
|
||||
|
||||
const startRow = range.start.y
|
||||
const endRow = range.end.y
|
||||
const startColumn = range.start.x
|
||||
const endColumn = range.end.x
|
||||
|
||||
this._beforeSerialize(endRow - startRow, 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)
|
||||
for (let col = startLineColumn; col < endLineColumn; col++) {
|
||||
const c = line.getCell(col)
|
||||
if (!c) {
|
||||
continue
|
||||
}
|
||||
this._nextCell(c, oldCell, row, col)
|
||||
oldCell = c
|
||||
}
|
||||
}
|
||||
this._rowEnd(row, row === endRow)
|
||||
}
|
||||
|
||||
this._afterSerialize()
|
||||
|
||||
return this._serializeString(excludeFinalCursorPosition)
|
||||
}
|
||||
|
||||
protected _nextCell(_cell: IBufferCell, _oldCell: IBufferCell, _row: number, _col: number): void {}
|
||||
protected _rowEnd(_row: number, _isLastRow: boolean): void {}
|
||||
protected _beforeSerialize(_rows: number, _startRow: number, _endRow: number): void {}
|
||||
protected _afterSerialize(): void {}
|
||||
protected _serializeString(_excludeFinalCursorPosition?: boolean): string {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// String Serialize Handler
|
||||
// ============================================================================
|
||||
|
||||
class StringSerializeHandler extends BaseSerializeHandler {
|
||||
private _rowIndex: number = 0
|
||||
private _allRows: string[] = []
|
||||
private _allRowSeparators: string[] = []
|
||||
private _currentRow: string = ""
|
||||
private _nullCellCount: number = 0
|
||||
private _cursorStyle: IBufferCell
|
||||
private _firstRow: number = 0
|
||||
private _lastCursorRow: number = 0
|
||||
private _lastCursorCol: number = 0
|
||||
private _lastContentCursorRow: number = 0
|
||||
private _lastContentCursorCol: number = 0
|
||||
|
||||
constructor(
|
||||
buffer: IBuffer,
|
||||
private readonly _terminal: ITerminalCore,
|
||||
) {
|
||||
super(buffer)
|
||||
this._cursorStyle = this._buffer.getNullCell()
|
||||
}
|
||||
|
||||
protected _beforeSerialize(rows: number, start: number, _end: number): void {
|
||||
this._allRows = new Array<string>(rows)
|
||||
this._lastContentCursorRow = start
|
||||
this._lastCursorRow = start
|
||||
this._firstRow = start
|
||||
}
|
||||
|
||||
protected _rowEnd(row: number, isLastRow: boolean): void {
|
||||
let rowSeparator = ""
|
||||
|
||||
if (!isLastRow) {
|
||||
const nextLine = this._buffer.getLine(row + 1)
|
||||
|
||||
if (!nextLine?.isWrapped) {
|
||||
rowSeparator = "\r\n"
|
||||
this._lastCursorRow = row + 1
|
||||
this._lastCursorCol = 0
|
||||
}
|
||||
}
|
||||
|
||||
this._allRows[this._rowIndex] = this._currentRow
|
||||
this._allRowSeparators[this._rowIndex++] = rowSeparator
|
||||
this._currentRow = ""
|
||||
this._nullCellCount = 0
|
||||
}
|
||||
|
||||
private _diffStyle(cell: IBufferCell, oldCell: IBufferCell): number[] {
|
||||
const sgrSeq: number[] = []
|
||||
const fgChanged = !equalFg(cell, oldCell)
|
||||
const bgChanged = !equalBg(cell, oldCell)
|
||||
const flagsChanged = !equalFlags(cell, oldCell)
|
||||
|
||||
if (fgChanged || bgChanged || flagsChanged) {
|
||||
if (this._isAttributeDefault(cell)) {
|
||||
if (!this._isAttributeDefault(oldCell)) {
|
||||
sgrSeq.push(0)
|
||||
}
|
||||
} else {
|
||||
if (flagsChanged) {
|
||||
if (!!cell.isInverse() !== !!oldCell.isInverse()) {
|
||||
sgrSeq.push(cell.isInverse() ? 7 : 27)
|
||||
}
|
||||
if (!!cell.isBold() !== !!oldCell.isBold()) {
|
||||
sgrSeq.push(cell.isBold() ? 1 : 22)
|
||||
}
|
||||
if (!!cell.isUnderline() !== !!oldCell.isUnderline()) {
|
||||
sgrSeq.push(cell.isUnderline() ? 4 : 24)
|
||||
}
|
||||
if (!!cell.isBlink() !== !!oldCell.isBlink()) {
|
||||
sgrSeq.push(cell.isBlink() ? 5 : 25)
|
||||
}
|
||||
if (!!cell.isInvisible() !== !!oldCell.isInvisible()) {
|
||||
sgrSeq.push(cell.isInvisible() ? 8 : 28)
|
||||
}
|
||||
if (!!cell.isItalic() !== !!oldCell.isItalic()) {
|
||||
sgrSeq.push(cell.isItalic() ? 3 : 23)
|
||||
}
|
||||
if (!!cell.isDim() !== !!oldCell.isDim()) {
|
||||
sgrSeq.push(cell.isDim() ? 2 : 22)
|
||||
}
|
||||
if (!!cell.isStrikethrough() !== !!oldCell.isStrikethrough()) {
|
||||
sgrSeq.push(cell.isStrikethrough() ? 9 : 29)
|
||||
}
|
||||
}
|
||||
if (fgChanged) {
|
||||
const color = cell.getFgColor()
|
||||
const mode = cell.getFgColorMode()
|
||||
if (mode === 2 || mode === 3 || mode === -1) {
|
||||
sgrSeq.push(38, 2, (color >>> 16) & 0xff, (color >>> 8) & 0xff, color & 0xff)
|
||||
} else if (mode === 1) {
|
||||
// Palette
|
||||
if (color >= 16) {
|
||||
sgrSeq.push(38, 5, color)
|
||||
} else {
|
||||
sgrSeq.push(color & 8 ? 90 + (color & 7) : 30 + (color & 7))
|
||||
}
|
||||
} else {
|
||||
sgrSeq.push(39)
|
||||
}
|
||||
}
|
||||
if (bgChanged) {
|
||||
const color = cell.getBgColor()
|
||||
const mode = cell.getBgColorMode()
|
||||
if (mode === 2 || mode === 3 || mode === -1) {
|
||||
sgrSeq.push(48, 2, (color >>> 16) & 0xff, (color >>> 8) & 0xff, color & 0xff)
|
||||
} else if (mode === 1) {
|
||||
// Palette
|
||||
if (color >= 16) {
|
||||
sgrSeq.push(48, 5, color)
|
||||
} else {
|
||||
sgrSeq.push(color & 8 ? 100 + (color & 7) : 40 + (color & 7))
|
||||
}
|
||||
} else {
|
||||
sgrSeq.push(49)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sgrSeq
|
||||
}
|
||||
|
||||
private _isAttributeDefault(cell: IBufferCell): boolean {
|
||||
const mode = cell.getFgColorMode()
|
||||
const bgMode = cell.getBgColorMode()
|
||||
|
||||
if (mode === 0 && bgMode === 0) {
|
||||
return (
|
||||
!cell.isBold() &&
|
||||
!cell.isItalic() &&
|
||||
!cell.isUnderline() &&
|
||||
!cell.isBlink() &&
|
||||
!cell.isInverse() &&
|
||||
!cell.isInvisible() &&
|
||||
!cell.isDim() &&
|
||||
!cell.isStrikethrough()
|
||||
)
|
||||
}
|
||||
|
||||
const fgColor = cell.getFgColor()
|
||||
const bgColor = cell.getBgColor()
|
||||
const nullCell = this._buffer.getNullCell()
|
||||
const nullFg = nullCell.getFgColor()
|
||||
const nullBg = nullCell.getBgColor()
|
||||
|
||||
return (
|
||||
fgColor === nullFg &&
|
||||
bgColor === nullBg &&
|
||||
!cell.isBold() &&
|
||||
!cell.isItalic() &&
|
||||
!cell.isUnderline() &&
|
||||
!cell.isBlink() &&
|
||||
!cell.isInverse() &&
|
||||
!cell.isInvisible() &&
|
||||
!cell.isDim() &&
|
||||
!cell.isStrikethrough()
|
||||
)
|
||||
}
|
||||
|
||||
protected _nextCell(cell: IBufferCell, _oldCell: IBufferCell, row: number, col: number): void {
|
||||
const isPlaceHolderCell = cell.getWidth() === 0
|
||||
|
||||
if (isPlaceHolderCell) {
|
||||
return
|
||||
}
|
||||
|
||||
const codepoint = cell.getCode()
|
||||
const isGarbage = codepoint >= 0xf000
|
||||
const isEmptyCell = codepoint === 0 || cell.getChars() === "" || isGarbage
|
||||
|
||||
const sgrSeq = this._diffStyle(cell, this._cursorStyle)
|
||||
|
||||
const styleChanged = isEmptyCell ? !equalBg(this._cursorStyle, cell) : sgrSeq.length > 0
|
||||
|
||||
if (styleChanged) {
|
||||
if (this._nullCellCount > 0) {
|
||||
this._currentRow += `\u001b[${this._nullCellCount}C`
|
||||
this._nullCellCount = 0
|
||||
}
|
||||
|
||||
this._lastContentCursorRow = this._lastCursorRow = row
|
||||
this._lastContentCursorCol = this._lastCursorCol = col
|
||||
|
||||
this._currentRow += `\u001b[${sgrSeq.join(";")}m`
|
||||
|
||||
const line = this._buffer.getLine(row)
|
||||
const cellFromLine = line?.getCell(col)
|
||||
if (cellFromLine) {
|
||||
this._cursorStyle = cellFromLine
|
||||
}
|
||||
}
|
||||
|
||||
if (isEmptyCell) {
|
||||
this._nullCellCount += cell.getWidth()
|
||||
} else {
|
||||
if (this._nullCellCount > 0) {
|
||||
this._currentRow += `\u001b[${this._nullCellCount}C`
|
||||
this._nullCellCount = 0
|
||||
}
|
||||
|
||||
this._currentRow += cell.getChars()
|
||||
|
||||
this._lastContentCursorRow = this._lastCursorRow = row
|
||||
this._lastContentCursorCol = this._lastCursorCol = col + cell.getWidth()
|
||||
}
|
||||
}
|
||||
|
||||
protected _serializeString(excludeFinalCursorPosition?: boolean): string {
|
||||
let rowEnd = this._allRows.length
|
||||
|
||||
if (this._buffer.length - this._firstRow <= this._terminal.rows) {
|
||||
rowEnd = this._lastContentCursorRow + 1 - this._firstRow
|
||||
this._lastCursorCol = this._lastContentCursorCol
|
||||
this._lastCursorRow = this._lastContentCursorRow
|
||||
}
|
||||
|
||||
let content = ""
|
||||
|
||||
for (let i = 0; i < rowEnd; i++) {
|
||||
content += this._allRows[i]
|
||||
if (i + 1 < rowEnd) {
|
||||
content += this._allRowSeparators[i]
|
||||
}
|
||||
}
|
||||
|
||||
if (!excludeFinalCursorPosition) {
|
||||
const absoluteCursorRow = (this._buffer.baseY ?? 0) + this._buffer.cursorY
|
||||
const cursorRow = constrain(absoluteCursorRow - this._firstRow + 1, 1, Number.MAX_SAFE_INTEGER)
|
||||
const cursorCol = this._buffer.cursorX + 1
|
||||
content += `\u001b[${cursorRow};${cursorCol}H`
|
||||
}
|
||||
|
||||
return content
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SerializeAddon Class
|
||||
// ============================================================================
|
||||
|
||||
export class SerializeAddon implements ITerminalAddon {
|
||||
private _terminal?: ITerminalCore
|
||||
|
||||
/**
|
||||
* Activate the addon (called by Terminal.loadAddon)
|
||||
*/
|
||||
public activate(terminal: ITerminalCore): void {
|
||||
this._terminal = terminal
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose the addon and clean up resources
|
||||
*/
|
||||
public dispose(): void {
|
||||
this._terminal = undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes terminal rows into a string that can be written back to the
|
||||
* terminal to restore the state. The cursor will also be positioned to the
|
||||
* correct cell.
|
||||
*
|
||||
* @param options Custom options to allow control over what gets serialized.
|
||||
*/
|
||||
public serialize(options?: ISerializeOptions): string {
|
||||
if (!this._terminal) {
|
||||
throw new Error("Cannot use addon until it has been loaded")
|
||||
}
|
||||
|
||||
const terminal = this._terminal as any
|
||||
const buffer = terminal.buffer
|
||||
|
||||
if (!buffer) {
|
||||
return ""
|
||||
}
|
||||
|
||||
const normalBuffer = buffer.normal || buffer.active
|
||||
const altBuffer = buffer.alternate
|
||||
|
||||
if (!normalBuffer) {
|
||||
return ""
|
||||
}
|
||||
|
||||
let content = options?.range
|
||||
? this._serializeBufferByRange(normalBuffer, options.range, true)
|
||||
: this._serializeBufferByScrollback(normalBuffer, options?.scrollback)
|
||||
|
||||
if (!options?.excludeAltBuffer && buffer.active?.type === "alternate" && altBuffer) {
|
||||
const alternateContent = this._serializeBufferByScrollback(altBuffer, undefined)
|
||||
content += `\u001b[?1049h\u001b[H${alternateContent}`
|
||||
}
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes terminal content as plain text (no escape sequences)
|
||||
* @param options Custom options to allow control over what gets serialized.
|
||||
*/
|
||||
public serializeAsText(options?: { scrollback?: number; trimWhitespace?: boolean }): string {
|
||||
if (!this._terminal) {
|
||||
throw new Error("Cannot use addon until it has been loaded")
|
||||
}
|
||||
|
||||
const terminal = this._terminal as any
|
||||
const buffer = terminal.buffer
|
||||
|
||||
if (!buffer) {
|
||||
return ""
|
||||
}
|
||||
|
||||
const activeBuffer = buffer.active || buffer.normal
|
||||
if (!activeBuffer) {
|
||||
return ""
|
||||
}
|
||||
|
||||
const maxRows = activeBuffer.length
|
||||
const scrollback = options?.scrollback
|
||||
const correctRows = scrollback === undefined ? maxRows : constrain(scrollback + this._terminal.rows, 0, maxRows)
|
||||
|
||||
const startRow = maxRows - correctRows
|
||||
const endRow = maxRows - 1
|
||||
const lines: string[] = []
|
||||
|
||||
for (let row = startRow; row <= endRow; row++) {
|
||||
const line = activeBuffer.getLine(row)
|
||||
if (line) {
|
||||
const text = line.translateToString(options?.trimWhitespace ?? true)
|
||||
lines.push(text)
|
||||
}
|
||||
}
|
||||
|
||||
// Trim trailing empty lines if requested
|
||||
if (options?.trimWhitespace) {
|
||||
while (lines.length > 0 && lines[lines.length - 1] === "") {
|
||||
lines.pop()
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join("\n")
|
||||
}
|
||||
|
||||
private _serializeBufferByScrollback(buffer: IBuffer, scrollback?: number): string {
|
||||
const maxRows = buffer.length
|
||||
const rows = this._terminal?.rows ?? 24
|
||||
const correctRows = scrollback === undefined ? maxRows : constrain(scrollback + rows, 0, maxRows)
|
||||
return this._serializeBufferByRange(
|
||||
buffer,
|
||||
{
|
||||
start: maxRows - correctRows,
|
||||
end: maxRows - 1,
|
||||
},
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
private _serializeBufferByRange(
|
||||
buffer: IBuffer,
|
||||
range: ISerializeRange,
|
||||
excludeFinalCursorPosition: boolean,
|
||||
): string {
|
||||
const handler = new StringSerializeHandler(buffer, this._terminal!)
|
||||
const cols = this._terminal?.cols ?? 80
|
||||
return handler.serialize(
|
||||
{
|
||||
start: { x: 0, y: range.start },
|
||||
end: { x: cols, y: range.end },
|
||||
},
|
||||
excludeFinalCursorPosition,
|
||||
)
|
||||
}
|
||||
}
|
||||
115
packages/app/src/app.tsx
Normal file
115
packages/app/src/app.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import "@/index.css"
|
||||
import { ErrorBoundary, Show, 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 Layout from "@/pages/layout"
|
||||
import Home from "@/pages/home"
|
||||
import DirectoryLayout from "@/pages/directory-layout"
|
||||
import Session from "@/pages/session"
|
||||
import { ErrorPage } from "./pages/error"
|
||||
import { iife } from "@opencode-ai/util/iife"
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__OPENCODE__?: { updaterEnabled?: boolean; port?: number }
|
||||
}
|
||||
}
|
||||
|
||||
const defaultServerUrl = iife(() => {
|
||||
const param = new URLSearchParams(document.location.search).get("url")
|
||||
if (param) return param
|
||||
|
||||
if (location.hostname.includes("opencode.ai")) return "http://localhost:4096"
|
||||
if (window.__OPENCODE__) return `http://127.0.0.1:${window.__OPENCODE__.port}`
|
||||
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
|
||||
})
|
||||
|
||||
function ServerKey(props: ParentProps) {
|
||||
const server = useServer()
|
||||
return (
|
||||
<Show when={server.url} keyed>
|
||||
{props.children}
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<MetaProvider>
|
||||
<Font />
|
||||
<ThemeProvider>
|
||||
<ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
|
||||
<DialogProvider>
|
||||
<MarkedProvider>
|
||||
<DiffComponentProvider component={Diff}>
|
||||
<CodeComponentProvider component={Code}>
|
||||
<ServerProvider defaultUrl={defaultServerUrl}>
|
||||
<ServerKey>
|
||||
<GlobalSDKProvider>
|
||||
<GlobalSyncProvider>
|
||||
<Router
|
||||
root={(props) => (
|
||||
<PermissionProvider>
|
||||
<LayoutProvider>
|
||||
<NotificationProvider>
|
||||
<CommandProvider>
|
||||
<Layout>{props.children}</Layout>
|
||||
</CommandProvider>
|
||||
</NotificationProvider>
|
||||
</LayoutProvider>
|
||||
</PermissionProvider>
|
||||
)}
|
||||
>
|
||||
<Route path="/" component={Home} />
|
||||
<Route path="/:dir" component={DirectoryLayout}>
|
||||
<Route path="/" component={() => <Navigate href="session" />} />
|
||||
<Route
|
||||
path="/session/:id?"
|
||||
component={(p) => (
|
||||
<Show when={p.params.id ?? "new"} keyed>
|
||||
<TerminalProvider>
|
||||
<FileProvider>
|
||||
<PromptProvider>
|
||||
<Session />
|
||||
</PromptProvider>
|
||||
</FileProvider>
|
||||
</TerminalProvider>
|
||||
</Show>
|
||||
)}
|
||||
/>
|
||||
</Route>
|
||||
</Router>
|
||||
</GlobalSyncProvider>
|
||||
</GlobalSDKProvider>
|
||||
</ServerKey>
|
||||
</ServerProvider>
|
||||
</CodeComponentProvider>
|
||||
</DiffComponentProvider>
|
||||
</MarkedProvider>
|
||||
</DialogProvider>
|
||||
</ErrorBoundary>
|
||||
</ThemeProvider>
|
||||
</MetaProvider>
|
||||
)
|
||||
}
|
||||
383
packages/app/src/components/dialog-connect-provider.tsx
Normal file
383
packages/app/src/components/dialog-connect-provider.tsx
Normal file
@@ -0,0 +1,383 @@
|
||||
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 { Link } from "@/components/link"
|
||||
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()
|
||||
const globalSync = useGlobalSync()
|
||||
const globalSDK = useGlobalSDK()
|
||||
const platform = usePlatform()
|
||||
const provider = createMemo(() => globalSync.data.provider.all.find((x) => x.id === props.provider)!)
|
||||
const methods = createMemo(
|
||||
() =>
|
||||
globalSync.data.provider_auth[props.provider] ?? [
|
||||
{
|
||||
type: "api",
|
||||
label: "API key",
|
||||
},
|
||||
],
|
||||
)
|
||||
const [store, setStore] = createStore({
|
||||
methodIndex: undefined as undefined | number,
|
||||
authorization: undefined as undefined | ProviderAuthAuthorization,
|
||||
state: "pending" as undefined | "pending" | "complete" | "error",
|
||||
error: undefined as string | undefined,
|
||||
})
|
||||
|
||||
const method = createMemo(() => (store.methodIndex !== undefined ? methods().at(store.methodIndex!) : undefined))
|
||||
|
||||
async function selectMethod(index: number) {
|
||||
const method = methods()[index]
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
draft.methodIndex = index
|
||||
draft.authorization = undefined
|
||||
draft.state = undefined
|
||||
draft.error = undefined
|
||||
}),
|
||||
)
|
||||
|
||||
if (method.type === "oauth") {
|
||||
setStore("state", "pending")
|
||||
const start = Date.now()
|
||||
await globalSDK.client.provider.oauth
|
||||
.authorize(
|
||||
{
|
||||
providerID: props.provider,
|
||||
method: index,
|
||||
},
|
||||
{ throwOnError: true },
|
||||
)
|
||||
.then((x) => {
|
||||
const elapsed = Date.now() - start
|
||||
const delay = 1000 - elapsed
|
||||
|
||||
if (delay > 0) {
|
||||
setTimeout(() => {
|
||||
setStore("state", "complete")
|
||||
setStore("authorization", x.data!)
|
||||
}, delay)
|
||||
return
|
||||
}
|
||||
setStore("state", "complete")
|
||||
setStore("authorization", x.data!)
|
||||
})
|
||||
.catch((e) => {
|
||||
setStore("state", "error")
|
||||
setStore("error", String(e))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let listRef: ListRef | undefined
|
||||
function handleKey(e: KeyboardEvent) {
|
||||
if (e.key === "Enter" && e.target instanceof HTMLInputElement) {
|
||||
return
|
||||
}
|
||||
if (e.key === "Escape") return
|
||||
listRef?.onKeyDown(e)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (methods().length === 1) {
|
||||
selectMethod(0)
|
||||
}
|
||||
document.addEventListener("keydown", handleKey)
|
||||
onCleanup(() => {
|
||||
document.removeEventListener("keydown", handleKey)
|
||||
})
|
||||
})
|
||||
|
||||
async function complete() {
|
||||
await globalSDK.client.global.dispose()
|
||||
dialog.close()
|
||||
showToast({
|
||||
variant: "success",
|
||||
icon: "circle-check",
|
||||
title: `${provider().name} connected`,
|
||||
description: `${provider().name} models are now available to use.`,
|
||||
})
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
if (methods().length === 1) {
|
||||
dialog.show(() => <DialogSelectProvider />)
|
||||
return
|
||||
}
|
||||
if (store.authorization) {
|
||||
setStore("authorization", undefined)
|
||||
setStore("methodIndex", undefined)
|
||||
return
|
||||
}
|
||||
if (store.methodIndex) {
|
||||
setStore("methodIndex", undefined)
|
||||
return
|
||||
}
|
||||
dialog.show(() => <DialogSelectProvider />)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog title={<IconButton tabIndex={-1} icon="arrow-left" variant="ghost" onClick={goBack} />}>
|
||||
<div class="flex flex-col gap-6 px-2.5 pb-3">
|
||||
<div class="px-2.5 flex gap-4 items-center">
|
||||
<ProviderIcon id={props.provider as IconName} class="size-5 shrink-0 icon-strong-base" />
|
||||
<div class="text-16-medium text-text-strong">
|
||||
<Switch>
|
||||
<Match when={props.provider === "anthropic" && method()?.label?.toLowerCase().includes("max")}>
|
||||
Login with Claude Pro/Max
|
||||
</Match>
|
||||
<Match when={true}>Connect {provider().name}</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-2.5 pb-10 flex flex-col gap-6">
|
||||
<Switch>
|
||||
<Match when={store.methodIndex === undefined}>
|
||||
<div class="text-14-regular text-text-base">Select login method for {provider().name}.</div>
|
||||
<div class="">
|
||||
<List
|
||||
ref={(ref) => {
|
||||
listRef = ref
|
||||
}}
|
||||
items={methods}
|
||||
key={(m) => m?.label}
|
||||
onSelect={async (method, index) => {
|
||||
if (!method) return
|
||||
selectMethod(index)
|
||||
}}
|
||||
>
|
||||
{(i) => (
|
||||
<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>
|
||||
<span>{i.label}</span>
|
||||
</div>
|
||||
)}
|
||||
</List>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={store.state === "pending"}>
|
||||
<div class="text-14-regular text-text-base">
|
||||
<div class="flex items-center gap-x-2">
|
||||
<Spinner />
|
||||
<span>Authorization in progress...</span>
|
||||
</div>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={store.state === "error"}>
|
||||
<div class="text-14-regular text-text-base">
|
||||
<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>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={method()?.type === "api"}>
|
||||
{iife(() => {
|
||||
const [formStore, setFormStore] = createStore({
|
||||
value: "",
|
||||
error: undefined as string | undefined,
|
||||
})
|
||||
|
||||
async function handleSubmit(e: SubmitEvent) {
|
||||
e.preventDefault()
|
||||
|
||||
const form = e.currentTarget as HTMLFormElement
|
||||
const formData = new FormData(form)
|
||||
const apiKey = formData.get("apiKey") as string
|
||||
|
||||
if (!apiKey?.trim()) {
|
||||
setFormStore("error", "API key is required")
|
||||
return
|
||||
}
|
||||
|
||||
setFormStore("error", undefined)
|
||||
await globalSDK.client.auth.set({
|
||||
providerID: props.provider,
|
||||
auth: {
|
||||
type: "api",
|
||||
key: apiKey,
|
||||
},
|
||||
})
|
||||
await complete()
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="flex flex-col gap-6">
|
||||
<Switch>
|
||||
<Match when={provider().id === "opencode"}>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="text-14-regular text-text-base">
|
||||
OpenCode Zen gives you access to a curated set of reliable optimized models for coding
|
||||
agents.
|
||||
</div>
|
||||
<div class="text-14-regular text-text-base">
|
||||
With a single API key you'll get access to models such as Claude, GPT, Gemini, GLM and more.
|
||||
</div>
|
||||
<div class="text-14-regular text-text-base">
|
||||
Visit{" "}
|
||||
<Link href="https://opencode.ai/zen" tabIndex={-1}>
|
||||
opencode.ai/zen
|
||||
</Link>{" "}
|
||||
to collect your API key.
|
||||
</div>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<div class="text-14-regular text-text-base">
|
||||
Enter your {provider().name} API key to connect your account and use {provider().name} models
|
||||
in OpenCode.
|
||||
</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
<form onSubmit={handleSubmit} class="flex flex-col items-start gap-4">
|
||||
<TextField
|
||||
autofocus
|
||||
type="text"
|
||||
label={`${provider().name} API key`}
|
||||
placeholder="API key"
|
||||
name="apiKey"
|
||||
value={formStore.value}
|
||||
onChange={setFormStore.bind(null, "value")}
|
||||
validationState={formStore.error ? "invalid" : undefined}
|
||||
error={formStore.error}
|
||||
/>
|
||||
<Button class="w-auto" type="submit" size="large" variant="primary">
|
||||
Submit
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</Match>
|
||||
<Match when={method()?.type === "oauth"}>
|
||||
<Switch>
|
||||
<Match when={store.authorization?.method === "code"}>
|
||||
{iife(() => {
|
||||
const [formStore, setFormStore] = createStore({
|
||||
value: "",
|
||||
error: undefined as string | undefined,
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
if (store.authorization?.method === "code" && store.authorization?.url) {
|
||||
platform.openLink(store.authorization.url)
|
||||
}
|
||||
})
|
||||
|
||||
async function handleSubmit(e: SubmitEvent) {
|
||||
e.preventDefault()
|
||||
|
||||
const form = e.currentTarget as HTMLFormElement
|
||||
const formData = new FormData(form)
|
||||
const code = formData.get("code") as string
|
||||
|
||||
if (!code?.trim()) {
|
||||
setFormStore("error", "Authorization code is required")
|
||||
return
|
||||
}
|
||||
|
||||
setFormStore("error", undefined)
|
||||
const { error } = await globalSDK.client.provider.oauth.callback({
|
||||
providerID: props.provider,
|
||||
method: store.methodIndex,
|
||||
code,
|
||||
})
|
||||
if (!error) {
|
||||
await complete()
|
||||
return
|
||||
}
|
||||
setFormStore("error", "Invalid authorization code")
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="text-14-regular text-text-base">
|
||||
Visit <Link href={store.authorization!.url}>this link</Link> to collect your authorization
|
||||
code to connect your account and use {provider().name} models in OpenCode.
|
||||
</div>
|
||||
<form onSubmit={handleSubmit} class="flex flex-col items-start gap-4">
|
||||
<TextField
|
||||
autofocus
|
||||
type="text"
|
||||
label={`${method()?.label} authorization code`}
|
||||
placeholder="Authorization code"
|
||||
name="code"
|
||||
value={formStore.value}
|
||||
onChange={setFormStore.bind(null, "value")}
|
||||
validationState={formStore.error ? "invalid" : undefined}
|
||||
error={formStore.error}
|
||||
/>
|
||||
<Button class="w-auto" type="submit" size="large" variant="primary">
|
||||
Submit
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</Match>
|
||||
<Match when={store.authorization?.method === "auto"}>
|
||||
{iife(() => {
|
||||
const code = createMemo(() => {
|
||||
const instructions = store.authorization?.instructions
|
||||
if (instructions?.includes(":")) {
|
||||
return instructions?.split(":")[1]?.trim()
|
||||
}
|
||||
return instructions
|
||||
})
|
||||
|
||||
onMount(async () => {
|
||||
const result = await globalSDK.client.provider.oauth.callback({
|
||||
providerID: props.provider,
|
||||
method: store.methodIndex,
|
||||
})
|
||||
if (result.error) {
|
||||
// TODO: show error
|
||||
dialog.close()
|
||||
return
|
||||
}
|
||||
await complete()
|
||||
})
|
||||
|
||||
return (
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="text-14-regular text-text-base">
|
||||
Visit <Link href={store.authorization!.url}>this link</Link> and enter the code below to
|
||||
connect your account and use {provider().name} models in OpenCode.
|
||||
</div>
|
||||
<TextField label="Confirmation code" class="font-mono" value={code()} readOnly copyable />
|
||||
<div class="text-14-regular text-text-base flex items-center gap-4">
|
||||
<Spinner />
|
||||
<span>Waiting for authorization...</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</Match>
|
||||
</Switch>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
180
packages/app/src/components/dialog-edit-project.tsx
Normal file
180
packages/app/src/components/dialog-edit-project.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
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 { Avatar } from "@opencode-ai/ui/avatar"
|
||||
|
||||
const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
|
||||
|
||||
function getFilename(input: string) {
|
||||
const parts = input.split("/")
|
||||
return parts[parts.length - 1] || input
|
||||
}
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
57
packages/app/src/components/dialog-manage-models.tsx
Normal file
57
packages/app/src/components/dialog-manage-models.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
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
|
||||
search={{ placeholder: "Search models", autofocus: true }}
|
||||
emptyMessage="No model results"
|
||||
key={(x) => `${x?.provider?.id}:${x?.id}`}
|
||||
items={local.model.list()}
|
||||
filterKeys={["provider.name", "name", "id"]}
|
||||
sortBy={(a, b) => a.name.localeCompare(b.name)}
|
||||
groupBy={(x) => x.provider.name}
|
||||
sortGroupsBy={(a, b) => {
|
||||
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) => {
|
||||
if (!x) return
|
||||
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-3">
|
||||
<span>{i.name}</span>
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<Switch
|
||||
checked={
|
||||
!!local.model.visible({
|
||||
modelID: i.id,
|
||||
providerID: i.provider.id,
|
||||
})
|
||||
}
|
||||
onChange={(checked) => {
|
||||
local.model.setVisibility({ modelID: i.id, providerID: i.provider.id }, checked)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</List>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
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>
|
||||
)
|
||||
}
|
||||
50
packages/app/src/components/dialog-select-file.tsx
Normal file
50
packages/app/src/components/dialog-select-file.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
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 file = useFile()
|
||||
const dialog = useDialog()
|
||||
const params = useParams()
|
||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||
const tabs = createMemo(() => layout.tabs(sessionKey()))
|
||||
return (
|
||||
<Dialog title="Select file">
|
||||
<List
|
||||
search={{ placeholder: "Search files", autofocus: true }}
|
||||
emptyMessage="No files found"
|
||||
items={file.searchFiles}
|
||||
key={(x) => x}
|
||||
onSelect={(path) => {
|
||||
if (path) {
|
||||
const value = file.tab(path)
|
||||
tabs().open(value)
|
||||
file.load(path)
|
||||
}
|
||||
dialog.close()
|
||||
}}
|
||||
>
|
||||
{(i) => (
|
||||
<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: 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">
|
||||
{getDirectory(i)}
|
||||
</span>
|
||||
<span class="text-text-strong whitespace-nowrap">{getFilename(i)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</List>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
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>
|
||||
)
|
||||
}
|
||||
110
packages/app/src/components/dialog-select-model-unpaid.tsx
Normal file
110
packages/app/src/components/dialog-select-model-unpaid.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
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 { 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()
|
||||
const dialog = useDialog()
|
||||
const providers = useProviders()
|
||||
|
||||
let listRef: ListRef | undefined
|
||||
const handleKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") return
|
||||
listRef?.onKeyDown(e)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
document.addEventListener("keydown", handleKey)
|
||||
onCleanup(() => {
|
||||
document.removeEventListener("keydown", handleKey)
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<Dialog title="Select model">
|
||||
<div class="flex flex-col gap-3 px-2.5">
|
||||
<div class="text-14-medium text-text-base px-2.5">Free models provided by OpenCode</div>
|
||||
<List
|
||||
ref={(ref) => (listRef = ref)}
|
||||
items={local.model.list}
|
||||
current={local.model.current()}
|
||||
key={(x) => `${x.provider.id}:${x.id}`}
|
||||
onSelect={(x) => {
|
||||
local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, {
|
||||
recent: true,
|
||||
})
|
||||
dialog.close()
|
||||
}}
|
||||
>
|
||||
{(i) => (
|
||||
<div class="w-full flex items-center gap-x-2.5">
|
||||
<span>{i.name}</span>
|
||||
<Tag>Free</Tag>
|
||||
<Show when={i.latest}>
|
||||
<Tag>Latest</Tag>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</List>
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
<div class="px-1.5 pb-1.5">
|
||||
<div class="w-full rounded-sm border border-border-weak-base bg-surface-raised-base">
|
||||
<div class="w-full flex flex-col items-start gap-4 px-1.5 pt-4 pb-4">
|
||||
<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 px-0"
|
||||
key={(x) => x?.id}
|
||||
items={providers.popular}
|
||||
activeIcon="plus-small"
|
||||
sortBy={(a, b) => {
|
||||
if (popularProviders.includes(a.id) && popularProviders.includes(b.id))
|
||||
return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)
|
||||
return a.name.localeCompare(b.name)
|
||||
}}
|
||||
onSelect={(x) => {
|
||||
if (!x) return
|
||||
dialog.show(() => <DialogConnectProvider provider={x.id} />)
|
||||
}}
|
||||
>
|
||||
{(i) => (
|
||||
<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>
|
||||
</Show>
|
||||
<Show when={i.id === "anthropic"}>
|
||||
<div class="text-14-regular text-text-weak">Connect with Claude Pro/Max or API key</div>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</List>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="w-full justify-start px-[11px] py-3.5 gap-4.5 text-14-medium"
|
||||
icon="dot-grid"
|
||||
onClick={() => {
|
||||
dialog.show(() => <DialogSelectProvider />)
|
||||
}}
|
||||
>
|
||||
View all providers
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
115
packages/app/src/components/dialog-select-model.tsx
Normal file
115
packages/app/src/components/dialog-select-model.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
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 { 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}
|
||||
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-2 text-13-regular">
|
||||
<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">
|
||||
<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>
|
||||
)
|
||||
}
|
||||
54
packages/app/src/components/dialog-select-provider.tsx
Normal file
54
packages/app/src/components/dialog-select-provider.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Component, Show } from "solid-js"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { popularProviders, useProviders } from "@/hooks/use-providers"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
import { Tag } from "@opencode-ai/ui/tag"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import { IconName } from "@opencode-ai/ui/icons/provider"
|
||||
import { DialogConnectProvider } from "./dialog-connect-provider"
|
||||
|
||||
export const DialogSelectProvider: Component = () => {
|
||||
const dialog = useDialog()
|
||||
const providers = useProviders()
|
||||
|
||||
return (
|
||||
<Dialog title="Connect provider">
|
||||
<List
|
||||
search={{ placeholder: "Search providers", autofocus: true }}
|
||||
activeIcon="plus-small"
|
||||
key={(x) => x?.id}
|
||||
items={providers.all}
|
||||
filterKeys={["id", "name"]}
|
||||
groupBy={(x) => (popularProviders.includes(x.id) ? "Popular" : "Other")}
|
||||
sortBy={(a, b) => {
|
||||
if (popularProviders.includes(a.id) && popularProviders.includes(b.id))
|
||||
return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)
|
||||
return a.name.localeCompare(b.name)
|
||||
}}
|
||||
sortGroupsBy={(a, b) => {
|
||||
if (a.category === "Popular" && b.category !== "Popular") return -1
|
||||
if (b.category === "Popular" && a.category !== "Popular") return 1
|
||||
return 0
|
||||
}}
|
||||
onSelect={(x) => {
|
||||
if (!x) return
|
||||
dialog.show(() => <DialogConnectProvider provider={x.id} />)
|
||||
}}
|
||||
>
|
||||
{(i) => (
|
||||
<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>
|
||||
</Show>
|
||||
<Show when={i.id === "anthropic"}>
|
||||
<div class="text-14-regular text-text-weak">Connect with Claude Pro/Max or API key</div>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</List>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
179
packages/app/src/components/dialog-select-server.tsx
Normal file
179
packages/app/src/components/dialog-select-server.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
import { createEffect, createMemo, onCleanup } 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 { 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 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)
|
||||
}
|
||||
|
||||
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"
|
||||
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>
|
||||
)}
|
||||
</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>
|
||||
</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>
|
||||
)
|
||||
|
||||
17
packages/app/src/components/link.tsx
Normal file
17
packages/app/src/components/link.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { ComponentProps, splitProps } from "solid-js"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
|
||||
export interface LinkProps extends ComponentProps<"button"> {
|
||||
href: string
|
||||
}
|
||||
|
||||
export function Link(props: LinkProps) {
|
||||
const platform = usePlatform()
|
||||
const [local, rest] = splitProps(props, ["href", "children"])
|
||||
|
||||
return (
|
||||
<button class="text-text-strong underline" onClick={() => platform.openLink(local.href)} {...rest}>
|
||||
{local.children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
1825
packages/app/src/components/prompt-input.tsx
Normal file
1825
packages/app/src/components/prompt-input.tsx
Normal file
File diff suppressed because it is too large
Load Diff
102
packages/app/src/components/session-context-usage.tsx
Normal file
102
packages/app/src/components/session-context-usage.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
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 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
|
||||
layout.review.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"
|
||||
419
packages/app/src/components/session/session-context-tab.tsx
Normal file
419
packages/app/src/components/session/session-context-tab.tsx
Normal file
@@ -0,0 +1,419 @@
|
||||
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 = () => {
|
||||
const el = scroll
|
||||
if (!el) return
|
||||
|
||||
const s = props.view()?.scroll("context")
|
||||
if (!s) 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>
|
||||
)
|
||||
}
|
||||
212
packages/app/src/components/session/session-header.tsx
Normal file
212
packages/app/src/components/session/session-header.tsx
Normal file
@@ -0,0 +1,212 @@
|
||||
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 { 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"
|
||||
|
||||
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 sessions = createMemo(() => (sync.data.session ?? []).filter((s) => !s.parentID))
|
||||
const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
|
||||
const shareEnabled = createMemo(() => sync.data.config.share !== "disabled")
|
||||
const branch = createMemo(() => sync.data.vcs?.branch)
|
||||
|
||||
function navigateToProject(directory: string) {
|
||||
navigate(`/${base64Encode(directory)}`)
|
||||
}
|
||||
|
||||
function navigateToSession(session: Session | undefined) {
|
||||
if (!session) return
|
||||
navigate(`/${params.dir}/session/${session.id}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base flex" data-tauri-drag-region>
|
||||
<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={layout.projects.list().map((project) => project.worktree)}
|
||||
current={sync.directory}
|
||||
label={(x) => {
|
||||
const name = getFilename(x)
|
||||
const b = x === sync.directory ? branch() : undefined
|
||||
return b ? `${name}:${b}` : name
|
||||
}}
|
||||
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>
|
||||
<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>
|
||||
<Show when={currentSession()}>
|
||||
<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={layout.review.toggle}>
|
||||
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
||||
<Icon
|
||||
name={layout.review.opened() ? "layout-right" : "layout-left"}
|
||||
size="small"
|
||||
class="group-hover/review-toggle:hidden"
|
||||
/>
|
||||
<Icon
|
||||
name={layout.review.opened() ? "layout-right-partial" : "layout-left-partial"}
|
||||
size="small"
|
||||
class="hidden group-hover/review-toggle:inline-block"
|
||||
/>
|
||||
<Icon
|
||||
name={layout.review.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={layout.terminal.toggle}>
|
||||
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
||||
<Icon
|
||||
size="small"
|
||||
name={layout.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={layout.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: sync.directory })
|
||||
.then((r) => r.data?.share?.url)
|
||||
.catch((e) => {
|
||||
console.error("Failed to share session", e)
|
||||
return undefined
|
||||
})
|
||||
}
|
||||
return shareURL
|
||||
},
|
||||
)
|
||||
return <Show when={url()}>{(url) => <TextField value={url()} readOnly copyable class="w-72" />}</Show>
|
||||
})}
|
||||
</Popover>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
35
packages/app/src/components/session/session-new-view.tsx
Normal file
35
packages/app/src/components/session/session-new-view.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Show } 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"
|
||||
|
||||
export function NewSessionView() {
|
||||
const sync = useSync()
|
||||
|
||||
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">
|
||||
<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(sync.data.path.directory)}
|
||||
<span class="text-text-strong">{getFilename(sync.data.path.directory)}</span>
|
||||
</div>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
48
packages/app/src/components/session/session-sortable-tab.tsx
Normal file
48
packages/app/src/components/session/session-sortable-tab.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
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
|
||||
>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user